HOMELAB-1544: fix(subagents): make async dispatch the enforceable default and stop teardown eating sibling results #14
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "plane/HOMELAB-1544-async-only-dispatch"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes HOMELAB-1544.
Problem
Three paths could freeze the orchestrator's session loop on subagent work. All three live in
pi-swarm-suite, not pi core:core/index.tsawait manager.spawnAndWait(...)— no timeoutget_subagent_result wait:truecore/index.tsawait record.promise— unboundedagent_swarmfan-outswarm/swarm-tool.tsawait collector.allTerminal()(unchanged here)The foreground path was the one that bit on 2026-09-13 (3425s, 34 tool uses on a single scout, chat frozen, user killed it and lost two siblings' results): its effective default was hardcoded
falseininvocation-config.ts, so any LLM call that omittedrun_in_backgroundblocked.Changes
defaultRunInBackground(new,subagents.json) — defaults tofalse, so behavior is unchanged until explicitly enabled. Whentrue,Agentreturns an agent id immediately. Precedence stays frontmatter → explicit tool param → setting, sorun_in_background: falseremains an escape hatch for callers that need the result inline.toolWaitCapSec(new, default120) — capsget_subagent_result's wait. On expiry the tool returns the agent's status snapshot, and re-arms the completion notification (resultConsumedreset) so the result is still announced when the agent actually finishes rather than being silently swallowed.runBudgetMinutes(new, default10,0disables) — soft wall-clock budget. The existing 60s manager sweep now also flags agents past budget, once each, via asetBudgetHookcallback. The agent keeps running — the budget makes a runaway visible, it does not kill work.dispose()no longer callsagents.clear()— it discarded every finished agent's result along with the live ones, which is how killing one agent took two completed siblings with it. Only records that could still run are removed.Latent bug fixed in passing
notifyApplied()persistedsnapshotSettings(), which carries only the keys the/agentsmenu manages — so any settings change silently dropped every other knob in the project file (including the pre-existingtaskMaxAttempts). It now merges over the existing project settings, matching what the suite settings page already did.extractCoreSettingsgained matching range checks for the same reason: a value it drops is silently erased on save.All three knobs are surfaced on
/agents → Settingsand the unifiedpi-swarm-suitesettings page.Verification
Baseline on
mainwas 48 files / 389 tests, so the new suite adds 16 cases and nothing regressed.New tests in
tests/core/async-dispatch.test.tscover the four ticket requirements: background-by-default precedence (including both escape-hatch directions), wait-cap clamping, the budget firing exactly once and never stopping the agent, and sibling results surviving bothdispose()andabort(id).Not covered here: scope item 1 asked for an audit of the
agent_swarmsynchronous fan-out. It does block by design (allTerminal()), but it's a synchronous tool whose contract is "run this fan-out and report" — making it async would need a job-id + notification model of its own, so it's left as-is rather than half-converted. Flagging in case that should become its own ticket.Note for review
~/.pi/agent/extensionsis the live extensions directory, and it had 20 uncommitted files (HOMELAB-1534/1542 result-contract work) plus 3 failing tests when this work started. Those are parked onrescue/HOMELAB-1534-1542-inflight(with the failingtiers.test.tsfixed — it still expecteddeepseek/deepseek-v4-flashafter the rename todeepseek-flash) and are not part of this PR. This branch is based cleanly onmain.A subagent could freeze the orchestrator's session loop indefinitely, and killing one discarded its siblings' finished work. Blocking paths found (all in this extension, not pi core): - Agent's foreground branch awaited spawnAndWait() with no timeout (core/index.ts). It was reached whenever an LLM call omitted run_in_background, because the effective default was hardcoded `false`. - get_subagent_result({ wait: true }) awaited record.promise unbounded. - agent_swarm always awaits collector.allTerminal() (unchanged here). Changes: 1. `defaultRunInBackground` (new, subagents.json) — default false, so behavior is unchanged until explicitly enabled. When true, Agent returns an agent id immediately instead of awaiting spawnAndWait(). Precedence stays frontmatter -> explicit tool param -> this setting, so run_in_background: false remains an escape hatch for callers that need the result inline. 2. `toolWaitCapSec` (new, default 120s) — caps get_subagent_result's wait. On expiry the tool returns the agent's current status snapshot and re-arms the completion notification (resultConsumed is reset) so the orchestrator is still told when the agent actually finishes rather than having that notification silently suppressed. 3. `runBudgetMinutes` (new, default 10, 0 disables) — soft wall-clock budget for a running agent. The existing 60s manager sweep now also flags agents past budget, once each, via a setBudgetHook callback wired in core/index.ts. The agent keeps running: the budget makes a runaway visible, it does not kill work. 4. dispose() no longer calls agents.clear(). It dropped every finished agent's result along with the live ones, which is how killing one agent took two completed siblings with it. Only records that could still be running are removed now. Also fixes a latent settings-clobber: notifyApplied() persisted snapshotSettings(), which only carries the keys the /agents menu manages, so any settings change silently dropped every other knob in the project file (including taskMaxAttempts). It now merges over the existing project settings, matching what the suite page already did. All three knobs are surfaced on /agents -> Settings and the unified pi-swarm-suite settings page (extractCoreSettings gained matching range checks, since a value it drops would otherwise be silently erased on save). New tests (tests/core/async-dispatch.test.ts, 16 cases) cover the four ticket requirements: background-by-default precedence, wait-cap clamping, the budget firing once and never stopping the agent, and sibling results surviving both dispose() and abort(id). Verified: tsc --noEmit clean; vitest 49 files / 405 tests passing (was 48/389 on main).Heads-up from HOMELAB-1548 (merged as
142a866):mainmoved while this PR was open, so Forgejo now reports the branch as not mergeable.I ran a merge of
maininto this branch locally to check the damage:pi-swarm-suite/README.md— both branches append rows to the same settings table. Take both sets: keepdefaultRunInBackground/toolWaitCapSec/runBudgetMinutes(this PR), keep thedefaultModelrow and the updatedtier-*rows (main).tsc --noEmitclean,vitest run→ 51 files / 419 tests passing.deepseek/deepseek-flash(HOMELAB-1548). If you carry test fixtures that pin tier model strings (e.g. from therescue/HOMELAB-1534-1542-inflightstash), update those to the new default.Recommended:
git merge forgejo/mainon the branch, resolve the README table as above, and re-run typecheck + tests before merging.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.