Repository navigation
Files Changed: roll up subagent (task_spawn) edits into the parent session's diff #1136
Description
Important
Problem — "Files Changed" in an Amicode session shows only edits made by the main agent of the viewed session. Edits made by Task-tool subagents — which run in separate child sessions but mutate the same worktree — are silently excluded. A user who dispatches a subagent to edit files sees an incomplete diff, undermining the panel's promise of reflecting what the session did.
Approach — Widen the agent-touched file set in Session.diff to include the tool-touched files of every subagent reachable from the viewed session through task_spawn edges, resolved through the existing session-lineage engine. The net diff itself is unchanged: it stays the session-start snapshot vs. the live worktree, so deletions/reversions fall out for free. Only which files are eligible changes.
Scope — in: subagent (task_spawn) edit rollup into the viewed session's Files Changed, transitively to any depth. out: session_spawn tabs (they keep their own Files Changed); background subagents (async, from/to cannot bracket them — see Key Decisions); legacy sessions with no lineage row (unchanged, no backfill); the client/UI (renders whatever the server returns).
Assumptions — Foreground Task subagents run synchronously within a parent LLM step (so the parent's from/to snapshot range brackets subagent execution and already contains their byte changes); the lineage engine records task_spawn edges at spawn time for all sessions created since it landed.
Acceptance Criteria
- A parent session with a
task_spawnchild, where the child editssub.tsand the parent editsmain.ts, returns both files fromSession.diff(parent), once each. - A depth-2 subagent edit (child spawns grandchild that edits
deep.ts) appears inSession.diff(root)— full-tree, not direct-children-only. - A mid-tree subagent that has a sibling subtree returns its own + its own
task_spawn-descendants' files only — the sibling subtree's files are excluded (subtree walk, not root-flat union). - A file touched at two different depths (e.g. child and grandchild) appears exactly once (dedup across depth).
- A
session_spawnchild that editstab.tsdoes NOT appear in the parent's diff. - A
task_spawnsubagent spawned under asession_spawntab is rolled into that tab's diff (reachable via task_spawn from the tab), not into the tab's parent's diff (reachability is task_spawn-only, transitively from the viewed session). - A subagent edit that is later reverted to its session-start content is absent from the parent's diff (net-zero → filtered by the existing
additions+deletions > 0guard), including when thediffFromDiskFallback A path runs — the fallback applies the same net-zero filter to rolled-up files. - A file created then deleted by a subagent is absent from the parent's diff (no net change on disk).
- A legacy session (no lineage row) with a
parent_id-only child edit returns only its own files — no regression, no rollup. - An external worktree change touched by no session's tools stays excluded even when a subagent is present (the Slice 1: Contamination fix — derive agent-file filter from tool filediff only #742 contamination guarantee holds).
- A file edited by a subagent whose session is later deleted (its lineage row cascade-removed), where no surviving lineage session claims it, is removed from Files Changed even if the change remains on disk (honest, drop-on-delete;
SessionLineageOriginTableretention is deliberately NOT consulted).
Testing Decisions
Extend the existing scoping suite session-diff-scoped.test.ts (the file that already pins #742/#733/#744). It carries the harness patterns needed — Snapshot.track(), synthetic step-start/tool parts with filediff metadata, HTTP diff assertions. Add one new session-creation helper for task_spawn/session_spawn children reusing Session.create({ parentID, lineageEdgeKind }). The mid-tree and sibling-exclusion criteria are the load-bearing ones — a suite that only tests Session.diff(root) would pass while the mechanism is wrong for interior sessions, so at least one test MUST view a mid-tree subagent that has a sibling subtree. No new suite — this is the same surface, more cases.
Key Decisions
- Option A (lineage-only), not a
parent_idfallback. Aggregate subagents only when a lineage row exists (mode∈full/partial). Legacy sessions (no row) show main-agent edits only. This matches the lineage engine's explicit no-backfill stance and avoids folding spawned tabs in with true subagents (the rawparent_idwalk carries noedge_kindto distinguish them). For apartial-mode session, only edits from its epoch (epoch_started_at) forward have lineage rows — a subagent spawned before the epoch is not rolled up. - Subtree walk from the viewed session — NOT a root-flat union.
SessionLineage.get(sessionID)returns the entire tree keyed onroot_idregardless of which session it was called on (lineage.tsdescendants query filters onroot_idonly). Filtering that flat list totask_spawnwould fold sibling subtrees into a mid-tree session's diff — reopening exactly the cross-session contamination Slice 1: Contamination fix — derive agent-file filter from tool filediff only #742 guards. Instead: from the viewed session, walk transitively viadescendants[].parentIDfollowing onlytask_spawnedges, collecting reachable subagent sessions. Depth is bounded in practice bysubagent_depth(default 1) but the walk must be correct for any depth. - Foreground only; background subagents are out of scope. A
background: trueTask (gated byexperimentalBackgroundSubagents) runs asynchronously and can still be writing files after the parent'sSession.diffcomputesto = snapshot.track(). Thefrom/tobracket does not contain its post-towrites, so its rollup would be partial/racy. Explicitly out of scope: background-subagent rollup is best-effort/eventually-consistent, not guaranteed by this change. - Widen the eligible-file set, never the diff computation. The parent's session-start snapshot → live-worktree diff already contains foreground-subagent bytes and already emits
added/modified/deleted/net-zero correctly. The only change is unioning reachable subagents'filedifffiles into the agent-touched set the diff is filtered against. from/tostay the parent's. No consultation of child snapshots.- Honest drop-on-delete — via cascade, not archive.
Session.removecascade-deletes the child's lineage row (it does not settime_archived), so the deleted subagent leaves the tree and its files leave the eligible set.SessionLineageOriginTableretention rows (which exist for historical receipt rendering) are deliberately not consulted by the diff path. A still-on-disk change authored only by a now-deleted subagent disappears from Files Changed — deliberate, consistent with "Files Changed reflects this session's net diff."
Constraints & Invariants
- The Slice 1: Contamination fix — derive agent-file filter from tool filediff only #742 filter invariant holds: the eligible-file set is derived only from completed edit/write tool
filediffmetadata (of the parent ANDtask_spawndescendants) — never from patch-part file lists. - The fix(app): remove per-message Changed files section from chat output #733 plan-mode guarantee holds: a session (and its subagents) that touched no files returns
[], never a contaminated fallback. - No new snapshotting, no cross-repo git work, no client change.
- Legacy summary/tool-part fallback paths are untouched (they fire only for snapshot-less sessions, which have no lineage rows — consistent with Option A).
- Deduplication: a file edited by the parent and one or more reachable subagents, at any depth, appears exactly once.
- The
diffFromDiskFallback A path (which runs when the primarydiffFull-then-filter yields empty) must apply the same net-zero eligibility to rolled-up subagent files as it does to the parent's — the reversion/deletion criteria must hold through the fallback, not only the primary path.
Prior Art
- The core diff function
Session.diffand its existing agent-touched-file collection + snapshot filter (the function to modify). - The session-lineage engine:
SessionLineage.get/ edge kindstask_spawnvssession_spawn, and the "Durable Files Changed lineage" SQL table — the infrastructure this rides. - The Task tool's child-session creation with
lineageEdgeKind: "task_spawn". - The shadow-snapshot engine's
diffFull(from, to)— the net-diff semantics (added/modified/deleted, net-zero elision) we depend on unchanged. - The scoping test suite
session-diff-scoped.test.ts— the harness and the contamination/plan-mode regression tests to extend and keep green.
Source
Design deliberated in-session (plan mode) prior to this issue. Depends on the session-lineage engine already present in the absorbed overlay.
Notes
- Tooling note:
amico spec reviewis not available on this machine — the adversarial review of this issue was performed by hand against the deliberate skill's checklists, which is a weaker claim than a tooled review and should be read as one. A deliberating subagent then reviewed this issue against the actual code (three independent lenses) and returned approved-by-hand, no blocking contradictions; the severe advisories it raised (mid-tree sibling contamination, background-subagent async, drop-on-delete-via-cascade-not-archive, the fallback path) have been folded into the criteria and Key Decisions above. - Rebuild the engine binary (
pnpm --filter amicode run build:binary) and reload the Extension Dev Host to exercise end-to-end; the client (session.tsxreviewDiffs / accumulate-diffs) needs no change. - Non-goals, stated so they aren't re-litigated: no
parent_idfallback for legacy sessions; no backfill migration synthesizing lineage rows; no origin-table retention of deleted-subagent contributions; no background-subagent rollup guarantee.
Activity
jeonghun-jj-lee commentedon Sep 14, 2026
Deliberation record (by hand —
amico spec reviewunavailable on this machine)A deliberating subagent reviewed this issue against the actual engine code through three independent lenses (correctness-of-mechanism / contamination-scoping / boundary-lifecycle).
Verdict: approved by hand, no blocking contradictions — a strictly weaker claim than a tooled
approved(no independent critic tooling ran). The severe advisories below were verified against source and are now folded into the issue body:descendantslist keyed onroot_id, no recursion'. Verified againstlineage.ts:132-138— that query returns the entire tree regardless of the viewed session, so a mid-tree session would fold in sibling subtrees, reopening Slice 1: Contamination fix — derive agent-file filter from tool filediff only #742-class contamination. Fixed: the approach now specifies a transitive subtree walk viadescendants[].parentIDfollowingtask_spawnedges only, with a mandatory mid-tree/sibling-exclusion acceptance criterion.background: trueTasks (task.ts:97-102, experimental flag). Fixed: background subagents explicitly scoped out; assumption narrowed to the foreground path.Session.removecascade-deletes the lineage row (it does not archive). Fixed: the mechanism and the AC now say cascade-delete, and noteSessionLineageOriginTableretention is deliberately not consulted.diffFromDiskFallback A path was unaddressed. Fixed: an invariant + AC now require the net-zero filter to hold through the fallback for rolled-up files.partial-mode epoch boundary.Acceptance criteria grew from 8 → 11; the mid-tree/sibling test is called out as load-bearing (a root-only suite would pass while the mechanism was wrong).
chore(#1136): refresh overlay manifest hashes
chore(#1136): refresh overlay manifest hashes for the two edited files
Files Changed: roll up subagent (task_spawn) edits into the viewed se…
fix(lineage): repair orphaned entries + propagate diff_version to par…
fix(lineage): repair orphaned entries + propagate diff_version to par…
docs: ADR 0021 — research layout is a resolved contract (#1220)