Repository navigation
fix(tree): drop a removed Bot's open-turn records from the load tracker (#732) - #741
Conversation
|
Verdict: INC at e74b999 One item. Everything else in this PR is right: The fix stops below the "not a tree bot" guard, so the invariant it claims is still false. Proof at this tip, run as a case added to test('PROBE #732: a removed non-tree Bot keeps its load records', async () => {
const m = await setup()
const o = await outsider(m)
m.feed('turn.start', { botId: o, turnId: 'o-1' })
assert.equal(m.tree.loadOf(o), 1, 'the outsider is tracked while it runs')
m.removeBot(o)
assert.equal(m.tree.loadOf(o), 0, 'PROBE: a removed non-tree Bot is still counted')
})→ Fix: move Ran:
Not run: root |
) `onBotRemoved` returned early for a Bot the tree never held, and the `load.clear(botId)` call sat under that return. The tracker is fed from the core activity feed for every Bot, so a Bot outside the tree (never placed, or created straight through core `create_bot`) kept counting turns that can no longer end after it was deleted. Clear first, then guard: the tracker holds turn records only, never the tree shape, so the cleanup is safe for a non-tree removal too. The non-tree case in `tree.test.js` now starts a turn, removes the Bot and asserts `loadOf` and `openForOf` are 0.
e74b999 to
df5d6f7
Compare
|
Verdict: PASS at df5d6f7 Both halves of #732 are closed and each one is held by a test. Checks: Red, three cuts on
Ran:
Not run: |
|
Re-enqueueing after the queue ejected this PR at 10:54:11Z. The It is a flake, not this change: the same test passed on this PR's own Worth a separate issue if it recurs: that case renders a handoff message built from the installed DSH package, so a copy change in the harness can turn it red for a PR that never touched it. |
Written-by: Dev2
Fixes #732
Tip SHA: df5d6f7
INC answered
The INC (
6338326988) was right: the fix stopped belowonBotRemoved'sif (parent === undefined) return, so a Bot the tree never held kept its records — andindex.jsfeeds the tracker for every Bot (fb.onFeed('turn.start', …)), with no tree-membership test. The leak this issue is about had a second half, and the doc sentence I added ("so every record belongs to a live Bot") was false as written.load.clear(botId)now runs before the guard: the tracker holds turn records only, never the tree shape, so clearing on a non-tree removal is safe.bot.removed for a non-tree bot is ignored, and still drops its load records: start a turn for the outsider, assertloadOf(o) === 1, remove it, assertloadOf(o) === 0andopenForOf(o) === 0, and theAchild is still re-attached to the root.d5fefe01(wasa718f345). Nothing on main touchedplugins/tree/ordocs/subsystems/tree.mdin between (a718f345..d5fefe01changes onlyplugins/timeline/, client, docs and labels), so the six files keep main's content and this change sits on top of it.Reproduce
plugins/tree/src/tree.jsonBotRemoved()re-attaches the deleted Bot's children, repairs the organizer, refreshes prompts and runs adoption — it never touched the load tracker, and after the first revision it touched it only for a Bot the tree held.createLoadTrackerkeeps one record list per Bot id in a process-localMap, so a deleted id stays forever:snapshot()still counts a Bot that no longer exists andopenFor()still answers the age of a turn nobody can end.Two cases fail on main — they are the reproduction: a tree Bot with two open turns (
expected 2 to equal 0), and an outsider, created through corecreate_bot, that the tracker records and then never forgets.What changed
plugins/tree/src/load.js:createLoadTrackergainsclear(id)— removes that Bot's records, an unknown or empty id is a no-op, pure in the style ofstart/end/of.plugins/tree/src/tree.js:onBotRemoved()callsload.clear(botId)first, before the "not a tree bot" guard, so both halves of the leak are closed.plugins/tree/test/load.test.js:cleardrops one Bot's records, leaves another Bot's alone, is a no-op for an unknown id and a second clear, andof/openForread 0 for the cleared id.plugins/tree/test/tree.test.js: two tree-level cases — two open turns forWorkand one forDev,removeBot(Work)leavesloadOf(Work) === 0/openForOf(Work) === 0whileDevstill counts; and the outsider case above.docs/subsystems/tree.mdsays the removal drops the Bot's turn records;changelog.d/732-load-tracker-clear.fixed.mdadded.What I ran
At this tip
df5d6f7e9c3854a17d02bf4767819e735e53f74e(based5fefe01, 6 files, +105/−4):cd plugins/tree && TZ=UTC DSH_HARNESS=/opt/flybot FLYBOT_CORE=<repo>/core node --import ./test/resolve.mjs --test --test-concurrency=2 "test/*.test.js"→ 105 tests, 105 pass, 0 fail. This is the INC's command: the resolve hook mapsds-botto the core checkout, which is what made the twohost.test.js/Cordis failures environmental in my earlier runs, not real.node --test test/load.test.js→ 6/6.node scripts/lint-plugins.mjs(harness set) → 0 errors, 1 pre-existing warning (for…ofoverObject.entriesinclassify, untouched here), packed@flybot/treeimported.arch-docs: OK (6 changed file(s));changelog.mjs check→ 19 fragments well formed;patches/ds-bot/untouched by this diff (plugins only), sods-bot-patches checkis unaffected.Red without the source change
load.clear(botId)back below the guard (the shape the INC caught) → 104/105, and the single red is exactlybot.removed for a non-tree bot is ignored, and still drops its load records. The tree-Bot case stays green, which is why the first revision looked right.load.clear(botId)call entirely → 103/105: both tree-level cases fail.clear()method fromcreateLoadTracker→test/load.test.js5/6 on the missing function.Not run
npm run verifyas a whole (core lint + core suite + the other two plugin suites) and thepackage/typecheck jobs — this diff touchesplugins/tree, its tests, one doc and one changelog fragment.Map, exercised through the plugin's own harness.Risk
T2 (
plugins/tree/src/**is plugin code in.github/agent/risk-rules.json). The blast radius is oneMapdelete on a path that already disposes of the Bot, and it now also runs for a Bot the tree did not hold — which is the fix, not a widening: the tracker is keyed by Bot id and holds nothing the tree reads. A live Bot's records are asserted untouched in both tree-level cases,clearof an unknown or empty id is a no-op, and nothing persists, so a restart behaves as before.