feat(subagents): child-safe nested fanout and MCP tool allowlists - #1029
Conversation
Review — PR #1029 (sync subagents upstream)Reviewed against Findings1. Unbounded const seen = new Set<string>();
…
seen.add(request.requestId);The listener's The sibling 2. Timer disposal on extension reload — const timer = setInterval(() => { … }, 200);
timer.unref?.();
return timer;The returned timer is dropped at the registration site ( 3. Confusing const userDir = getEnvValue('ATOMIC_CODING_AGENT_DIR') ? userDirOld[0]! : fs.existsSync(userDirNew) ? userDirNew : userDirOld[0]!;Two ternaries on one line with the env-var branch yielding the variable named 4. Completion-guard bypass surface — 5. Test coverage gap on new files — 6. Nits
Done well
Verification I did
LGTM after addressing the unbounded-set / timer-disposal items (1, 2) and at least a short comment on the |
Sync packages/subagents with upstream pi-subagents v0.25.0 while preserving Atomic package/runtime adaptations. Includes nested child-safe fanout/status/control support, Atomic config and env compatibility, documentation updates, and test alignment with upstream render behavior. Assistant-model: GPT-5.5
19c3c43 to
d25da31
Compare
Review: feat(subagents): sync upstream fanout updatesReviewed the diff as a large upstream sync (~5.2k+/3.7k-, 39 files). The Atomic-specific reapply work looks careful and consistent — Things I found while reading. Most are minor; one or two are worth a follow-up. Bugs / correctness1. 2. Consider either (a) only creating a route when a child can actually emit (i.e. when 3. 4. Silent 5. 6. Style / convention notes
Things that look good
TestingI'd add lightweight coverage for:
Overall the sync looks coherent and the Atomic-specific adaptations are applied uniformly. The route-cleanup gap (#2) and the unbounded retry map (#3) are the items I'd most want addressed before merge or in a quick follow-up. The rest is polish. |
Assistant-model: GPT-5.5
Assistant-model: GPT-5.5
Code Review — Upstream subagents syncReviewed against A few items worth addressing before merge: Correctness / Robustness
Test coverage
Style / Maintainability
Observations (no action needed)
VerdictApprove with the test-coverage gap (#6) being the most important item to address before this lands, since the new code is a parent/child IPC trust boundary. Items 1–5 are bug-fix nits or hardening that can be done in follow-ups. The Atomic-side reapplications all look intact. 🤖 Generated with Claude Code |
|
ok |
|
Review part 1/4 - overview and bugs/correctness Reviewed the diff on top of e671f3c. The sync is large (~5k +, ~3.7k -) but well-structured: nested events, capability tokens, path containment, atomic writes, and 0o700/0o600 permissions are applied consistently across the new runs/shared/nested-events.ts, extension/fanout-child.ts, and mcp-direct-tool-allowlist.ts. A few things worth addressing before merge. Bugs / correctness:
|
|
Review part 2/4 - maintainability and tests Maintainability:
Tests:
|
|
Review part 3/4 - performance and security Performance / resource use:
Security:
|
|
Review part 4/4 - nits and summary Nits:
Summary: Overall this looks like a careful sync. The big-ticket items I would want before merge are (1) nested temp cleanup, (2) pendingResults eviction, and (7) some targeted unit tests around the new validation logic - the rest are nits or polish. (Note: please ignore the earlier single-word "ok" comment - that was a test of stdin posting since the sandbox blocked posting the full review in one shot.) |
Assistant-model: GPT-5.5
Review: feat(subagents): add nested fanout support with child-safe execution and MCP allowlistsThanks for the meticulous upstream sync — this is a large change (5,697 / 3,717) but the Atomic-specific seams (env prefix derived from Strengths
Bugs / things worth a second look
Test coverage gapsThe four new tests are well-targeted, but several substantial new modules have no direct coverage:
Style / conventions
VerificationI was unable to run `bun run typecheck` or `bun run test:unit` from this review sandbox, so I'm relying on the PR's stated `1385 passing` and clean typecheck. Recommend confirming CI is green before merging given the surface area. Overall: solid sync with thoughtful Atomic-side preservation and good security primitives. The `spawnRunner` mode fix is the only thing I'd consider close to a blocker; the rest are nice-to-haves. 🤖 Generated with Claude Code |
Assistant-model: GPT-5.5
|
test body |
Code Review — PR #1029Thanks for the careful upstream sync. The new nested-fanout machinery is well-defended (capability tokens, path containment, bounded sizes, symlink rejection) and the unit tests for the data layer are thorough. A few items worth a second look: Bugs / correctness
Style / consistency
Performance
Test coverage
Security -- looks goodThe defensive primitives are solid:
Nice work overall -- this is a defensible foundation. The race in (1) and event-replay churn in (2) are the only items I would want to see addressed before merging; the rest are quality-of-life suggestions. Note: a stray "test body" comment (#issuecomment-4529407616) was posted earlier while validating the gh pr comment --body-file - stdin path. Feel free to delete it. |
Assistant-model: GPT-5.5
Code Review — Subagents upstream sync (child-safe nested fanout + MCP allowlists)Reviewed the new files (fanout-child.ts, nested-events.ts, nested-path.ts, nested-render.ts, mcp-direct-tool-allowlist.ts, run-id-resolver.ts, result-intercom.ts additions) and the wiring in pi-args.ts, subagent-executor.ts, async-execution.ts, and completion-guard.ts. Overall the security boundary is well thought through (capability tokens, route containment, depth/breadth clamping, gated mutating actions, fanout-only nested env injection). A few findings, ordered roughly by severity. 1. fs.writeFileSync mode does not retighten existing files — capability-token leak windowIn packages/subagents/src/runs/background/async-execution.ts:175 and packages/subagents/src/runs/shared/nested-events.ts:672 both write secrets with the mode 0o600 option: Node's mode option only applies when the file is created — if a stale file at the same path exists from a previous run (or from before this fix landed) with 0o644, the new write keeps the old permissions. For writeRouteRecord this is a non-issue (filename includes randomUUID), but for writeAsyncRunnerConfig the path is deterministic: TEMP_ROOT_DIR/async-cfg-SUFFIX.json. The new test test/unit/subagents-async-config.test.ts:17 uses a Date.now-derived suffix so it never exercises the "stale file" case. Suggested fix — explicitly create with truncation+mode, or unlink first: And ideally a regression test that pre-writes the path with 0o644 and asserts the mode is 0o600 after writeAsyncRunnerConfig. 2. sleepSync blocks the entire event loop under registry-lock contentionpackages/subagents/src/runs/shared/nested-events.ts:434-436 uses Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms) to spin-block in 10 ms increments for up to 2 s (REGISTRY_LOCK_TIMEOUT_MS). With multiple fanout children projecting the registry concurrently, this can stall the host event loop noticeably (status polling is already ~250 ms cadence). projectNestedEvents is sync because callers want immediate consistency, but the contention case is the worst time to block. Options:
At minimum, a code comment at the sleepSync definition spelling out that this intentionally blocks the loop and pointing to the bounded REGISTRY_LOCK_TIMEOUT_MS would help future maintainers. 3. Capability tokens appear in error logspackages/subagents/src/extension/fanout-child.ts:168,170,183 and packages/subagents/src/runs/shared/nested-events.ts:494,574 log route.controlInbox / routeRoot paths on failure. Those paths embed the per-route capability token (the routeRoot is rootRunId-capabilityToken from createNestedRoute at line 127). If logs are aggregated to a shared sink the token is exfiltrated for the remaining lifetime of the route. Tokens are per-run and short-lived, so this is low-severity, but consider redacting to just route.rootRunId (or hashing the token) in error strings. 4. collectNestedRuns walks steps[].children and run.children separately without deduppackages/subagents/src/runs/shared/nested-events.ts:511,531 flatten child.steps?.flatMap(step => step.children ?? []) to find/collect runs, but attachNestedChildrenToResultChildren in result-intercom.ts:138-144 also re-merges children from both the run-level and step-level lists. If the same child id appears under both run.children and a step's children (can happen during parallel chain steps), collectNestedRuns will yield duplicates and findNestedRunMatchesById may double-count. Worth a dedup pass on run.id in collectNestedRuns, or a unit test asserting "child appearing under both run.children and step.children is resolved once." 5. MAX_PROCESSED_NESTED_EVENTS = 20_000 can replay events if cleanup lagsprojectNestedEvents (nested-events.ts:653) caps processedEvents at 20k via [...seen].slice(-MAX_PROCESSED_NESTED_EVENTS). Filenames are timestamp-prefixed and cleanupOldNestedRuntimeDirs removes them by mtime, so in normal use it self-balances. But a long-running root with a high-fanout burst could exceed 20k before cleanup runs — the oldest filenames would be dropped from the seen set and, if still on disk, re-applied. applyNestedEvent is idempotent for completed states (mergeSummary honors terminal), but "updated" states could regress visible run state to an older snapshot. The comment on line 652 ("worst-case bounded fanout") is reassuring; a guard like "skip events older than registry.updatedAt minus replayWindow" would make this water-tight. 6. Minor: expandTilde is ~/-onlyfanout-child.ts:28-30 and the executor's expandTilde do not handle ~user/... or Windows-style backslash tilde paths. This is consistent with the rest of the codebase, so just noting it; if a user sets defaultSessionDir: "~me/subagents" in config they'd get a directory literally named ~me. Probably fine, but worth a TODO. 7. parseNestedEventRecords accepts a trailing trimmed single-line record without newlinenested-events.ts:367-368 has a fallback branch that parses single-line content. writeRouteRecord always appends a newline (line 666), so this branch only fires for malformed/legacy files. Fine, but worth a one-line comment so future readers don't assume single-record files are an expected format. 8. Test coverage observations (positive)
Gaps:
9. Style / convention
TL;DRSolid sync — the new boundaries (capability token, route containment, depth/breadth clamps, allowMutatingManagementActions: false, fanout-only env injection) are coherent and well-tested. The biggest follow-up is finding 1 (deterministic config path + writeFileSync mode semantics) since it directly weakens the documented owner-only-permissions fix. Findings 2-3 are also worth addressing before this hits a high-fanout production session. |
* fix(subagents): animate running spinner smoothly without flicker The subagent running glyph was derived purely from progress data, so it froze/stuttered between updates, and no steady re-render ticker was scheduled while a subagent ran (regression from the v0.25.0 upstream sync in #1029). Drive the running glyph from a wall-clock frame (currentRunningFrame) so every active spinner advances smoothly and in lockstep, and restore the steady re-render tickers for live result cards, slash result cards, and the async-agents widget (now a live component). Per-frame diffs stay limited to the spinner glyph cell, so the differential renderer keeps doing partial redraws (no full-screen clear / flicker). Tickers are unref'd, stale-context-safe, and torn down on completion, reload, and session shutdown. Verified in a real TUI session (smooth animation, zero full-screen clears before and after) and with updated render-stability unit tests. Closes #1084 Assistant-model: Claude Opus 4.8 * refactor(subagents): address spinner-animation review feedback - Drop the unreachable static-glyph branch in runningGlyph and the now-unused STATIC_RUNNING_GLYPH constant (the wall-clock frame is always finite). - Refresh the captured invalidate on a re-sync so the result ticker always calls the latest render context's callback, with a regression test. - Keep the async widget animating while any nested step is still running, not just the top-level job. - Document the single-widget module-singleton state, add the wall-clock-only test assumption note, and make the third describe block tear down result timers uniformly. Assistant-model: Claude Opus 4.8 * refactor(subagents): harden spinner animation tickers per review - Animation interval callbacks (result + widget) now swallow any error and tear their own timer down instead of rethrowing, so a cosmetic spinner tick can never surface as an uncaughtException. This also lets us drop the fragile stale-extension-context string match entirely. - Split stopWidgetTicker (stop only the ticker) from stopWidgetAnimation (full teardown); renderWidget keeps the last-rendered ctx/jobs when jobs are visible but idle instead of clearing state it just set. - Keep the async widget animating while any nested step is running; collapse duplicate animation-state types; document the requestRender cast (needed due to hasUI narrowing). - Tests: assert the spinner advances in RUNNING_FRAMES order, add throwing-invalidate teardown coverage, and add async-widget ticker start/stop lifecycle coverage. Assistant-model: Claude Opus 4.8
) * feat(subagents): sync upstream fanout updates Sync packages/subagents with upstream pi-subagents v0.25.0 while preserving Atomic package/runtime adaptations. Includes nested child-safe fanout/status/control support, Atomic config and env compatibility, documentation updates, and test alignment with upstream render behavior. Assistant-model: GPT-5.5 * fix(subagents): honor nested control interrupt responses Assistant-model: GPT-5.5 * style(subagents): normalize ctrl+o hint casing Assistant-model: GPT-5.5 * fix(subagents): harden nested fanout runtime Assistant-model: GPT-5.5 * fix(subagents): protect nested async config tokens Assistant-model: GPT-5.5 * fix(subagents): serialize nested registry projection Assistant-model: GPT-5.5
* fix(subagents): animate running spinner smoothly without flicker The subagent running glyph was derived purely from progress data, so it froze/stuttered between updates, and no steady re-render ticker was scheduled while a subagent ran (regression from the v0.25.0 upstream sync in #1029). Drive the running glyph from a wall-clock frame (currentRunningFrame) so every active spinner advances smoothly and in lockstep, and restore the steady re-render tickers for live result cards, slash result cards, and the async-agents widget (now a live component). Per-frame diffs stay limited to the spinner glyph cell, so the differential renderer keeps doing partial redraws (no full-screen clear / flicker). Tickers are unref'd, stale-context-safe, and torn down on completion, reload, and session shutdown. Verified in a real TUI session (smooth animation, zero full-screen clears before and after) and with updated render-stability unit tests. Closes #1084 Assistant-model: Claude Opus 4.8 * refactor(subagents): address spinner-animation review feedback - Drop the unreachable static-glyph branch in runningGlyph and the now-unused STATIC_RUNNING_GLYPH constant (the wall-clock frame is always finite). - Refresh the captured invalidate on a re-sync so the result ticker always calls the latest render context's callback, with a regression test. - Keep the async widget animating while any nested step is still running, not just the top-level job. - Document the single-widget module-singleton state, add the wall-clock-only test assumption note, and make the third describe block tear down result timers uniformly. Assistant-model: Claude Opus 4.8 * refactor(subagents): harden spinner animation tickers per review - Animation interval callbacks (result + widget) now swallow any error and tear their own timer down instead of rethrowing, so a cosmetic spinner tick can never surface as an uncaughtException. This also lets us drop the fragile stale-extension-context string match entirely. - Split stopWidgetTicker (stop only the ticker) from stopWidgetAnimation (full teardown); renderWidget keeps the last-rendered ctx/jobs when jobs are visible but idle instead of clearing state it just set. - Keep the async widget animating while any nested step is running; collapse duplicate animation-state types; document the requestRender cast (needed due to hasUI narrowing). - Tests: assert the spinner advances in RUNNING_FRAMES order, add throwing-invalidate teardown coverage, and add async-widget ticker start/stop lifecycle coverage. Assistant-model: Claude Opus 4.8
Summary
Syncs
packages/subagentswith upstreamnicobailon/pi-subagentsv0.25.0, introducing nested child-safe fanout execution, parent-visible nested status/control rendering, and MCP tool allowlist handling — while preserving all Atomic-specific import, config, and runtime adaptations.Closes #1019
Added
fanout-child.ts): registers a scopedsubagenttool in child processes running underSUBAGENT_FANOUT_CHILD_ENV, enabling explicitly authorized agents to spawn their own subagents with a read-only management surface and bidirectional control (interrupt/resume) via a polling inbox.nested-events.ts): file-based event bus for nested run lifecycle (started/updated/completed) and bidirectional control (interrupt/resume) between parent and child fanout processes, with depth-bounded route inheritance and registry state.nested-path.ts): depth-bounded, sanitized path IDs for routing nested control messages safely across process boundaries.nested-render.ts): formats nested subagent step summaries as inline status lines with glyphs for display in parent TUI widgets.mcp-direct-tool-allowlist.ts): resolves and caches configured MCP direct-tool names from global/project configs across all supported clients (Claude Code, Cursor, Windsurf, Codex, VS Code), injecting them into explicit child tool allowlists at spawn time.run-id-resolver.ts): resolves canonical run IDs from environment variables or filesystem state for accurate async job targeting across foreground, async, and nested run kinds.result-intercom.ts): delivers intercom message events to live nested child agents by address, and attaches nested children to result intercom payloads.NestedRunState,NestedOwnerState,NestedRunAddress,NestedStepSummary,NestedRunSummary,PublicNestedRunSummary, andPublicNestedStepSummaryinshared/types.ts.Fixed
output: "false"the same as booleanfalse, preventing literal"false"output files.0o600) so nested route capability tokens are not exposed via permissive umasks.windowsHide: true).Changed
render.ts; upstream rendering is now used directly with only required Atomic import/type adaptations retained.Validation
bun run typecheck— cleanbun run test:unit— 1385 passing (6 new test files:subagents-async-config,subagents-mcp-direct-tool-allowlist,subagents-nested-events,subagents-nested-render,subagents-pi-args,subagents-result-intercom,subagents-run-id-resolver)git diff --check— no whitespace errorsbun packages/coding-agent/src/cli.ts):subagentlist smoke test foundcodebase-analyzerQA_NESTED_PARENT_OKandQA_NESTED_CHILD_OK