Skip to content

Files Changed: roll up subagent (task_spawn) edits into the parent session's diff #1136

Closed

Description

@jeonghun-jj-lee

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_spawn child, where the child edits sub.ts and the parent edits main.ts, returns both files from Session.diff(parent), once each.
  • A depth-2 subagent edit (child spawns grandchild that edits deep.ts) appears in Session.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_spawn child that edits tab.ts does NOT appear in the parent's diff.
  • A task_spawn subagent spawned under a session_spawn tab 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 > 0 guard), including when the diffFromDisk Fallback 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; SessionLineageOriginTable retention 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_id fallback. 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 raw parent_id walk carries no edge_kind to distinguish them). For a partial-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 on root_id regardless of which session it was called on (lineage.ts descendants query filters on root_id only). Filtering that flat list to task_spawn would 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 via descendants[].parentID following only task_spawn edges, collecting reachable subagent sessions. Depth is bounded in practice by subagent_depth (default 1) but the walk must be correct for any depth.
  • Foreground only; background subagents are out of scope. A background: true Task (gated by experimentalBackgroundSubagents) runs asynchronously and can still be writing files after the parent's Session.diff computes to = snapshot.track(). The from/to bracket does not contain its post-to writes, 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' filediff files into the agent-touched set the diff is filtered against.
  • from/to stay the parent's. No consultation of child snapshots.
  • Honest drop-on-delete — via cascade, not archive. Session.remove cascade-deletes the child's lineage row (it does not set time_archived), so the deleted subagent leaves the tree and its files leave the eligible set. SessionLineageOriginTable retention 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 filediff metadata (of the parent AND task_spawn descendants) — 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 diffFromDisk Fallback A path (which runs when the primary diffFull-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.diff and its existing agent-touched-file collection + snapshot filter (the function to modify).
  • The session-lineage engine: SessionLineage.get / edge kinds task_spawn vs session_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 review is 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.tsx reviewDiffs / accumulate-diffs) needs no change.
  • Non-goals, stated so they aren't re-litigated: no parent_id fallback for legacy sessions; no backfill migration synthesizing lineage rows; no origin-table retention of deleted-subagent contributions; no background-subagent rollup guarantee.

Activity

  1. jeonghun-jj-lee commented on Sep 14, 2026

    @jeonghun-jj-lee
    ContributorAuthor

    Deliberation record (by hand — amico spec review unavailable 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:

    • C1/S1 (was the core bug): the original mechanism said 'flat descendants list keyed on root_id, no recursion'. Verified against lineage.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 via descendants[].parentID following task_spawn edges only, with a mandatory mid-tree/sibling-exclusion acceptance criterion.
    • B2: the 'subagents run synchronously' assumption is false for background: true Tasks (task.ts:97-102, experimental flag). Fixed: background subagents explicitly scoped out; assumption narrowed to the foreground path.
    • B1: drop-on-delete reasoning cited the non-archive filter, but Session.remove cascade-deletes the lineage row (it does not archive). Fixed: the mechanism and the AC now say cascade-delete, and note SessionLineageOriginTable retention is deliberately not consulted.
    • C2: the diffFromDisk Fallback A path was unaddressed. Fixed: an invariant + AC now require the net-zero filter to hold through the fallback for rolled-up files.
    • Added criteria for dedup-across-depth and the task_spawn-under-session_spawn reachability case; noted the 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).

  2. added 3 commits that reference this issue on Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

afkImplementable without human interaction

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions