From 98b372e8c10451a6a5d808b40ffb1810bb3d0105 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 14 Sep 2026 17:30:47 -0400 Subject: [PATCH] fix(lineage): repair orphaned entries + propagate diff_version to parents (#1136) --- packages/app-bundle/manifest.json | 22 ++--- .../src/context/global-sync/bootstrap.test.ts | 1 + .../src/context/global-sync/child-store.ts | 1 + .../context/global-sync/event-reducer.test.ts | 71 +++++++++++++++ .../src/context/global-sync/event-reducer.ts | 13 +++ .../context/global-sync/session-cache.test.ts | 4 + .../src/context/global-sync/session-cache.ts | 2 + .../app/src/context/global-sync/types.ts | 3 + .../app/src/context/server-session.ts | 12 +++ .../packages/opencode/src/session/lineage.ts | 88 ++++++++++++++++++- .../packages/opencode/src/session/session.ts | 3 + .../test/server/session-diff-scoped.test.ts | 40 +++++++++ 12 files changed, 246 insertions(+), 14 deletions(-) diff --git a/packages/app-bundle/manifest.json b/packages/app-bundle/manifest.json index e631bedd1..1a7fba3dc 100644 --- a/packages/app-bundle/manifest.json +++ b/packages/app-bundle/manifest.json @@ -158,7 +158,7 @@ "packages/app/src/context/platform.tsx": "edd9b1773feb5c084b922a57b6914f6a473eed558b44f9de0eeb1c4d4e2b49da", "packages/app/src/context/server-sdk.test.ts": "5cd22962c8ea1e4374915508a765281ceb9f808ad8bbf241c06b00b48621c816", "packages/app/src/context/server-sdk.tsx": "b3450d9bdafde6ed1a95d7dc970fc9f33bef81531b05245c36b789878d09d186", - "packages/app/src/context/server-session.ts": "a9e1f089ad328a705848d11ba25e890c5e374bb3a33066ffe67b6f09bd48ed16", + "packages/app/src/context/server-session.ts": "e1ccf71508ece66409cfb7b26942c290978f1dac5619960472a8bd60801f15af", "packages/app/src/context/server-sync.tsx": "43154ffe6227203d778378d190af3c46d7f49098619721fea5e6263e8794dc84", "packages/app/src/context/server.tsx": "08e978e4b4b3094221a2a0b92e069f39cbedb6d6cf95935881e21cde52265ee2", "packages/app/src/context/settings.tsx": "19e078e81620ffa21a92797335009b84d4311485a37c5ede2f76906b734292ad", @@ -268,7 +268,7 @@ "packages/opencode/src/session/evidence.ts": "f1b11aa0dcfb345f074e408ab95986ea9d932249e82d6f2cc6594d8d4ab65a1e", "packages/opencode/src/session/external-diff.ts": "2e1955ac23f5a50f572d61152a14cc994aee9573c59d6bd261a060513b4140b2", "packages/opencode/src/session/instruction.ts": "19640d02c96b8ec764edd49b0ce74879432fd2d2cbb4d2ad0076caa200dd4797", - "packages/opencode/src/session/lineage.ts": "f76b6f7a580e17155e878f0989c6ce0f3e3bdf35bfba32c503f96352cf1e2250", + "packages/opencode/src/session/lineage.ts": "713c65e3a59a192cff0fb9154be235b6ae8f547e04aaf24265e73a784009b949", "packages/opencode/src/session/message-v2.ts": "7c5bcf4f68ffd8b5e7f637b7a7550dd154d79e69c8939e4a172446ea2cce2240", "packages/opencode/src/session/mutation.ts": "31f29e8865be9dd62eb8148d240811f2344db4c440be2739dc113764749d3e88", "packages/opencode/src/session/prompt.ts": "65f53031ee84f8e16e6295b5c8aaf9f63f9411f30799a4323f53e2c19eebe05a", @@ -276,7 +276,7 @@ "packages/opencode/src/session/receipt-privacy.ts": "92ba8a92d4ddc0d18249efa5efdf600a32ebc0453d605b8c747cd4bcbd6d19a9", "packages/opencode/src/session/receipt.ts": "06fd30326424112b8dacd756e4bba5424b3a7a2d10318464a5670aeabb0c4808", "packages/opencode/src/session/rollout.ts": "55518da37d6d4d1298eeab1c7ee208f59b80b7246f3370438de98ad774e4b02e", - "packages/opencode/src/session/session.ts": "a0534c1767b3c50ae58ceb71efdfb4629f85ce96720b684c61c905293ed77815", + "packages/opencode/src/session/session.ts": "1d94c364a55c3124ba6ab762f599cc4194905d7e23ec91ba79552671cd4d86c7", "packages/opencode/src/session/summary.ts": "09f546db362a992156bdb4901cce58e495bd38bab6ba41b4386cca77bebab7f3", "packages/opencode/src/session/system.ts": "c91d00de07d6d4e3910561d59a598579eeafb725583a9d6475a158abdd4a17c4", "packages/opencode/src/session/turn-output.ts": "889624ca54a8149d35f200c7efa732771dfca36984e8a322a9781b1a25434762", @@ -321,7 +321,7 @@ "packages/opencode/test/server/httpapi-ui.test.ts": "d029e959cfd9347fff4cf17c5686dd125b0bd3a41afb3408a23b31418f12e3b0", "packages/opencode/test/server/pasqal-secret.test.ts": "54be8c666160dad22cd37bbf041f5bd40aec120271818f3c56c94107d987b9fb", "packages/opencode/test/server/session-diff-missing-patch.test.ts": "70526ca7fb86741843fd167f8f2f9c6f6d7e5e415a20a5448d17d24229bdd65b", - "packages/opencode/test/server/session-diff-scoped.test.ts": "dec76dd378c48c20ed395f0a2b9c792446f1a3611d1d3b018b7182d58fce52af", + "packages/opencode/test/server/session-diff-scoped.test.ts": "20ef80b92ad872573b371540575f028be8f1ad3c9f34d72f814b621e14e7a59e", "packages/opencode/test/server/workspace-routing.test.ts": "2574731ac379c540f450edc15f8750ac5360e8c33b4a0dad7c61e8eb686137bb", "packages/opencode/test/session/adversarial-e2e.test.ts": "1b471a69223d040c5d5f4f2ee99c2daa6df56bdf11679b65d902c15bedac3e80", "packages/opencode/test/session/business-record.test.ts": "7857094479408bcfd6ea8d9f5b425c6384bcf8490f930674bc1c203e82782d75", @@ -626,13 +626,13 @@ "packages/app/src/components/settings-v2/skills-controller.ts": "2c7344d144da0eec1c25083ead45f5afb2a8c6fd759541ad719cae9a09fcd19b", "packages/app/src/components/settings-v2/skills.tsx": "42deb0c11fcba158abad857b9644ffb551aed5ba9e00dbfd1584a7ffaaba5aa0", "packages/app/src/components/ui/drawer.tsx": "76574ca4c6997209f37a766a61000d707332a9a857f6ea8100a3cee1be58d65c", - "packages/app/src/context/global-sync/bootstrap.test.ts": "149c0241b378b6ce837aef45e1ba257dbfed88de37c70affa13f513e9c72338d", - "packages/app/src/context/global-sync/child-store.ts": "b13a513c93b8fec7b656593d842e9cfa30f346a0911dc0f516783ccc5fa4c07d", - "packages/app/src/context/global-sync/event-reducer.test.ts": "600cd7654854b910f25c61b20d8ae69f043f52ed544e6bf9be5ee9e5bbd6c96e", - "packages/app/src/context/global-sync/event-reducer.ts": "667fbae1259f6deed77573edd2b742beee4f825b4306b6d861670443e381a55f", - "packages/app/src/context/global-sync/session-cache.test.ts": "c66d790c6336794b2b1d707e2739fe0992ccad7ed3b96187b124b5c783ef332a", - "packages/app/src/context/global-sync/session-cache.ts": "1849408caba0f114809f37153e7d531bf2776572433880a96c5ebd31d7b30aa3", - "packages/app/src/context/global-sync/types.ts": "901c9bd1fda63dab676d33dc1a8582a29f8ee00aa20458e843aed5f93414fa84", + "packages/app/src/context/global-sync/bootstrap.test.ts": "41dc3f7726d382829c76ff0f9b8bf4525411c5287e529ddda909ee8b789630a7", + "packages/app/src/context/global-sync/child-store.ts": "972315dcc47a0fa2828d045127d5e6d667f98b8342f19884043af736a1b6705e", + "packages/app/src/context/global-sync/event-reducer.test.ts": "55cd27292735d1a1c5e6bfa745283eb282c72a99b91dca299a6870376927b187", + "packages/app/src/context/global-sync/event-reducer.ts": "7259b95c2b282791b3548bc2291b78e0e955a6bfde0dda420fbc6af31c47f0e8", + "packages/app/src/context/global-sync/session-cache.test.ts": "8441b26707fe0d231e24f3cfbd88edfa13c6c6ad9f01c18a5df8d749beb5c071", + "packages/app/src/context/global-sync/session-cache.ts": "9eca016b8214ca657aae293cbb9920f30b6db85c6a6c982e13850bd262cc91f9", + "packages/app/src/context/global-sync/types.ts": "d290ae16d257273d831f4ed23dd32f622d73ede54371f7c85a7e218ac155fcf9", "packages/app/src/pages/home/home-projects-view.tsx": "074f1cdd7926db9574164824666cd63f1f5bd0f8e8dfb65453f6fe8f2dca0ec8", "packages/app/src/pages/home/home-sessions-view.tsx": "5b73b18e4349a628a15e09a3810ecf2319bde222e18fe8de5d08373ac91fb37c", "packages/app/src/pages/layout/helpers.test.ts": "3a77cfe018ce2867cccded2c726f556e53a9895a77baa944a4878714b9b2b59e", diff --git a/packages/app-bundle/overlay/packages/app/src/context/global-sync/bootstrap.test.ts b/packages/app-bundle/overlay/packages/app/src/context/global-sync/bootstrap.test.ts index dcbb20885..1b9f39a39 100644 --- a/packages/app-bundle/overlay/packages/app/src/context/global-sync/bootstrap.test.ts +++ b/packages/app-bundle/overlay/packages/app/src/context/global-sync/bootstrap.test.ts @@ -59,6 +59,7 @@ function directoryState() { }, session_diff: {}, diff_version: {}, + parent_of: {}, todo: {}, permission: {}, question: {}, diff --git a/packages/app-bundle/overlay/packages/app/src/context/global-sync/child-store.ts b/packages/app-bundle/overlay/packages/app/src/context/global-sync/child-store.ts index 9ebd27d98..574ed46aa 100644 --- a/packages/app-bundle/overlay/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app-bundle/overlay/packages/app/src/context/global-sync/child-store.ts @@ -235,6 +235,7 @@ export function createChildStoreManager(input: { }, session_diff: {}, diff_version: {}, + parent_of: {}, todo: {}, permission: {}, question: {}, diff --git a/packages/app-bundle/overlay/packages/app/src/context/global-sync/event-reducer.test.ts b/packages/app-bundle/overlay/packages/app/src/context/global-sync/event-reducer.test.ts index 265fe8082..3d5cf6e14 100644 --- a/packages/app-bundle/overlay/packages/app/src/context/global-sync/event-reducer.test.ts +++ b/packages/app-bundle/overlay/packages/app/src/context/global-sync/event-reducer.test.ts @@ -73,6 +73,7 @@ const baseState = (input: Partial = {}) => session_status: {}, session_diff: {}, diff_version: {}, + parent_of: {}, todo: {}, permission: {}, question: {}, @@ -614,4 +615,74 @@ describe("applyDirectoryEvent", () => { expect(pushes).toEqual(["/tmp"]) expect(lspLoads).toBe(1) }) + + test("message.part.updated with filediff bumps diff_version of parent session too", () => { + const [store, setStore] = createStore({ + ...baseState(), + session: [rootSession({ id: "parent" }), rootSession({ id: "child", parentID: "parent" })], + parent_of: { child: "parent" }, + diff_version: {}, + }) + + applyDirectoryEvent({ + event: { + type: "message.part.updated", + properties: { + part: { + id: "prt_1", + sessionID: "child", + messageID: "msg_1", + type: "tool", + tool: "edit", + state: { + status: "completed", + metadata: { filediff: { file: "test.ts", status: "modified" } }, + }, + } as Part, + }, + }, + store, + setStore, + push: () => {}, + directory: "/tmp", + loadLsp: () => {}, + }) + + expect(store.diff_version.child).toBe(1) + expect(store.diff_version.parent).toBe(1) + }) + + test("session.created with parentID populates parent_of mapping", () => { + const [store, setStore] = createStore(baseState()) + + applyDirectoryEvent({ + event: { type: "session.created", properties: { info: rootSession({ id: "child", parentID: "parent" }) } }, + store, + setStore, + push: () => {}, + directory: "/tmp", + loadLsp: () => {}, + }) + + expect(store.parent_of.child).toBe("parent") + }) + + test("session.deleted cleans up parent_of mapping", () => { + const [store, setStore] = createStore({ + ...baseState(), + session: [rootSession({ id: "parent" }), rootSession({ id: "child", parentID: "parent" })], + parent_of: { child: "parent" }, + }) + + applyDirectoryEvent({ + event: { type: "session.deleted", properties: { info: rootSession({ id: "child", parentID: "parent" }) } }, + store, + setStore, + push: () => {}, + directory: "/tmp", + loadLsp: () => {}, + }) + + expect(store.parent_of.child).toBeUndefined() + }) }) diff --git a/packages/app-bundle/overlay/packages/app/src/context/global-sync/event-reducer.ts b/packages/app-bundle/overlay/packages/app/src/context/global-sync/event-reducer.ts index 8a56d05ad..f9af6490a 100644 --- a/packages/app-bundle/overlay/packages/app/src/context/global-sync/event-reducer.ts +++ b/packages/app-bundle/overlay/packages/app/src/context/global-sync/event-reducer.ts @@ -141,6 +141,7 @@ export function applyDirectoryEvent(input: { input.setStore("session", reconcile(trimmed, { key: "id" })) cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo) if (!info.parentID) input.setStore("sessionTotal", (value) => value + 1) + if (info.parentID) input.setStore("parent_of", info.id, info.parentID) break } case "session.updated": { @@ -186,6 +187,12 @@ export function applyDirectoryEvent(input: { ) } cleanupSessionCaches(input.setStore, sessionID, input.setSessionTodo) + input.setStore( + "parent_of", + produce((draft) => { + delete draft[sessionID] + }), + ) if (info?.parentID) break input.setStore("sessionTotal", (value) => Math.max(0, value - 1)) break @@ -321,6 +328,12 @@ export function applyDirectoryEvent(input: { (part.state as { metadata?: Record }).metadata?.filediff ) { input.setStore("diff_version", part.sessionID, (v) => (v ?? 0) + 1) + // Propagate up the parent chain so parent sessions refetch their diff + let ancestor = input.store.parent_of[part.sessionID] + while (ancestor) { + input.setStore("diff_version", ancestor, (v) => (v ?? 0) + 1) + ancestor = input.store.parent_of[ancestor] + } } input.setStore( produce((draft) => { diff --git a/packages/app-bundle/overlay/packages/app/src/context/global-sync/session-cache.test.ts b/packages/app-bundle/overlay/packages/app/src/context/global-sync/session-cache.test.ts index d8c9d91bb..fbdfac1f9 100644 --- a/packages/app-bundle/overlay/packages/app/src/context/global-sync/session-cache.test.ts +++ b/packages/app-bundle/overlay/packages/app/src/context/global-sync/session-cache.test.ts @@ -28,6 +28,7 @@ describe("app session cache", () => { session_status: Record session_diff: Record diff_version: Record + parent_of: Record todo: Record message: Record session_message: Record @@ -39,6 +40,7 @@ describe("app session cache", () => { session_status: { ses_1: { type: "busy" } as SessionStatus }, session_diff: { ses_1: [] }, diff_version: { ses_1: 0 }, + parent_of: {}, todo: { ses_1: [] as Todo[] }, message: {}, session_message: {}, @@ -66,6 +68,7 @@ describe("app session cache", () => { session_status: Record session_diff: Record diff_version: Record + parent_of: Record todo: Record message: Record session_message: Record @@ -77,6 +80,7 @@ describe("app session cache", () => { session_status: {}, session_diff: {}, diff_version: {}, + parent_of: {}, todo: {}, message: { ses_1: [m] }, session_message: {}, diff --git a/packages/app-bundle/overlay/packages/app/src/context/global-sync/session-cache.ts b/packages/app-bundle/overlay/packages/app/src/context/global-sync/session-cache.ts index 3fb2d82a8..4318149be 100644 --- a/packages/app-bundle/overlay/packages/app/src/context/global-sync/session-cache.ts +++ b/packages/app-bundle/overlay/packages/app/src/context/global-sync/session-cache.ts @@ -8,6 +8,7 @@ type SessionCache = { session_status: Record session_diff: Record diff_version: Record + parent_of: Record todo: Record message: Record session_message: Record @@ -36,6 +37,7 @@ export function dropSessionCaches(store: SessionCache, sessionIDs: Iterable>() const removedMessages = new Map>() const deltaBases = new Map() + const parentOf = new Map() const deleteMessageParts = ( cache: { part: Record; part_text_accum_delta: Record }, messageID: string, @@ -1001,6 +1002,10 @@ export function createServerSession( switch (event.type) { case "session.created": remember((event.properties as { info: Session }).info) + { + const info = (event.properties as { info: Session }).info + if (info.parentID) parentOf.set(info.id, info.parentID) + } return case "session.updated": { const info = (event.properties as { info: Session }).info @@ -1012,6 +1017,7 @@ export function createServerSession( const properties = event.properties as { sessionID?: string; info?: Session } const sessionID = properties.info?.id ?? properties.sessionID if (!sessionID) return + parentOf.delete(sessionID) infoSeen.delete(sessionID) setData( "info", @@ -1160,6 +1166,12 @@ export function createServerSession( (part.state as { metadata?: Record }).metadata?.filediff ) { setData("diff_version", part.sessionID, (v = 0) => v + 1) + // Propagate up the parent chain + let ancestor = parentOf.get(part.sessionID) + while (ancestor) { + setData("diff_version", ancestor, (v = 0) => v + 1) + ancestor = parentOf.get(ancestor) + } } return } diff --git a/packages/app-bundle/overlay/packages/opencode/src/session/lineage.ts b/packages/app-bundle/overlay/packages/opencode/src/session/lineage.ts index 3caaafdfb..5efedf47b 100644 --- a/packages/app-bundle/overlay/packages/opencode/src/session/lineage.ts +++ b/packages/app-bundle/overlay/packages/opencode/src/session/lineage.ts @@ -1,6 +1,6 @@ import { Database } from "@opencode-ai/core/database/database" import { SessionLineageOriginTable, SessionLineageTable, SessionTable } from "@opencode-ai/core/session/sql" -import { and, eq, isNull } from "drizzle-orm" +import { and, eq, isNull, isNotNull, sql } from "drizzle-orm" import { Effect } from "effect" import { SessionID } from "./schema" @@ -52,16 +52,44 @@ export namespace SessionLineage { .get() .pipe(Effect.orDie) if (!parent) { + // Auto-create a lineage row for the legacy parent (idempotent) + yield* database.db + .insert(SessionLineageTable) + .values({ session_id: input.parentID, root_id: input.parentID, mode: "full" }) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie) + + // Register child normally — edge_kind preserved yield* database.db .insert(SessionLineageTable) .values({ session_id: input.sessionID, - root_id: input.sessionID, + root_id: input.parentID, + parent_id: input.parentID, + edge_kind: input.edgeKind ?? "session_spawn", mode: "full", - legacy_parent_id: input.parentID, }) .run() .pipe(Effect.orDie) + + // Self-heal existing orphaned siblings under the same legacy parent + yield* database.db + .update(SessionLineageTable) + .set({ + root_id: input.parentID, + parent_id: input.parentID, + edge_kind: sql`COALESCE(${SessionLineageTable.edge_kind}, 'task_spawn')`, + }) + .where( + and( + eq(SessionLineageTable.legacy_parent_id, input.parentID), + isNull(SessionLineageTable.parent_id), + ), + ) + .run() + .pipe(Effect.orDie) + return } if (parent.mode === "legacy") @@ -209,4 +237,58 @@ export namespace SessionLineage { .pipe(Effect.orDie) }) } + + /** One-time repair for orphaned lineage entries created before the Bug A fix. */ + export function repairOrphanedEntries(database: Database.Interface) { + return Effect.gen(function* () { + // Find all orphaned entries (have legacy_parent_id but no parent_id) + const orphans = yield* database.db + .select({ + legacy_parent_id: SessionLineageTable.legacy_parent_id, + }) + .from(SessionLineageTable) + .where( + and( + isNull(SessionLineageTable.parent_id), + isNotNull(SessionLineageTable.legacy_parent_id), + ), + ) + .all() + .pipe(Effect.orDie) + + // Get unique parent IDs + const parentIDs = [...new Set( + orphans + .map((row) => row.legacy_parent_id) + .filter((id): id is string => id != null), + )] + + for (const parentID of parentIDs) { + // Ensure the parent has a lineage row + yield* database.db + .insert(SessionLineageTable) + .values({ session_id: parentID, root_id: parentID, mode: "full" }) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie) + + // Repair orphaned children + yield* database.db + .update(SessionLineageTable) + .set({ + root_id: parentID, + parent_id: parentID, + edge_kind: sql`COALESCE(${SessionLineageTable.edge_kind}, 'task_spawn')`, + }) + .where( + and( + eq(SessionLineageTable.legacy_parent_id, parentID), + isNull(SessionLineageTable.parent_id), + ), + ) + .run() + .pipe(Effect.orDie) + } + }) + } } diff --git a/packages/app-bundle/overlay/packages/opencode/src/session/session.ts b/packages/app-bundle/overlay/packages/opencode/src/session/session.ts index 9c85b0031..58f5de579 100644 --- a/packages/app-bundle/overlay/packages/opencode/src/session/session.ts +++ b/packages/app-bundle/overlay/packages/opencode/src/session/session.ts @@ -1179,6 +1179,9 @@ const layer: Layer.Layer< return Option.none() }) + // One-time repair of orphaned lineage entries from the legacy→full transition + yield* SessionLineage.repairOrphanedEntries(database).pipe(Effect.orDie) + return Service.of({ list, listGlobal, diff --git a/packages/app-bundle/overlay/packages/opencode/test/server/session-diff-scoped.test.ts b/packages/app-bundle/overlay/packages/opencode/test/server/session-diff-scoped.test.ts index 759b21a61..9c8e613ea 100644 --- a/packages/app-bundle/overlay/packages/opencode/test/server/session-diff-scoped.test.ts +++ b/packages/app-bundle/overlay/packages/opencode/test/server/session-diff-scoped.test.ts @@ -1458,6 +1458,46 @@ describe("Session.diff — subagent (task_spawn) edit rollup (#1136)", () => { { git: true, config: { formatter: false, lsp: false } }, ) + it.instance( + "register() auto-creates lineage for a legacy parent and preserves edgeKind (Bug A fix)", + () => + Effect.gen(function* () { + const test = yield* TestInstance + const fs = yield* FSUtil.Service + const database = yield* Database.Service + + // Create a parent, then delete its lineage row to simulate legacy + const parent = yield* withSession({ title: "legacy-parent-fix" }) + const { SessionLineageTable } = yield* Effect.promise(() => import("@opencode-ai/core/session/sql")) + const { eq } = yield* Effect.promise(() => import("drizzle-orm")) + yield* database.db + .delete(SessionLineageTable) + .where(eq(SessionLineageTable.session_id, parent.id)) + .run() + .pipe(Effect.orDie) + + // Now create a task_spawn child — this should trigger Bug A fix + yield* fs.writeWithDirs(path.join(test.directory, "parent.ts"), "orig") + yield* fs.writeWithDirs(path.join(test.directory, "child.ts"), "orig") + const { userMsgID } = yield* recordStart(parent.id) + yield* fs.writeWithDirs(path.join(test.directory, "parent.ts"), "changed") + yield* recordToolEdit(parent.id, userMsgID, path.join(test.directory, "parent.ts"), "parent.ts") + + const child = yield* Session.use.create({ parentID: parent.id, lineageEdgeKind: "task_spawn", title: "child" }) + const cStart = yield* recordStart(child.id) + yield* fs.writeWithDirs(path.join(test.directory, "child.ts"), "changed") + yield* recordToolEdit(child.id, cStart.userMsgID, path.join(test.directory, "child.ts"), "child.ts") + + // The parent should now have a proper lineage row, and the child's + // edit should roll up into the parent's diff + const diffs = yield* diffFiles(parent.id, test.directory) + const files = diffs.map((d) => d.file) + expect(files).toContain("parent.ts") + expect(files).toContain("child.ts") + }), + { git: true, config: { formatter: false, lsp: false } }, + ) + it.instance( "a file edited only by a since-deleted subagent is dropped from Files Changed (lineage cascade)", () =>