From f0182cdc9eead4d6c5894fb342bcbb64f603a208 Mon Sep 17 00:00:00 2001 From: David Brailovsky Date: Fri, 14 Aug 2026 10:54:00 +0200 Subject: [PATCH] fix(agent): make fork parent source-derived Move optional fork destination id into ForkOptions and stop allowing fork callers to pass a parentSessionId override. Update the harness spec to state that fork destination parentSessionId is always the source session id. --- packages/agent/docs/harness.md | 9 +++++---- packages/agent/src/harness/session/memory.ts | 2 +- packages/agent/src/harness/session/types.ts | 6 ++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/agent/docs/harness.md b/packages/agent/docs/harness.md index 80858b5ee..bad807dc0 100644 --- a/packages/agent/docs/harness.md +++ b/packages/agent/docs/harness.md @@ -802,8 +802,8 @@ A fork is a repository operation over one coherent source-session snapshot. It c ```ts type ForkOptions = - | { scope?: "branch"; entryId?: string; position?: "before" | "at" } - | { scope: "tree" }; + | { scope?: "branch"; entryId?: string; position?: "before" | "at"; id?: string } + | { scope: "tree"; id?: string }; ``` - Memory and JSONL obtain the snapshot as one job on the source storage queue. SQLite uses one read transaction. @@ -812,7 +812,8 @@ type ForkOptions = - Facts follow the selected scope: name/custom facts always copy; labels copy only when their target copies unless tree scope copies all targets. - Any message may be the fork point. Request construction heals orphaned tool calls. - Copied entries keep their ids. -- The destination metadata records `parentSessionId`. +- `id` optionally supplies the destination session id. +- The destination metadata records `parentSessionId` as the source session id; callers cannot override it through fork options. A source with only fresh/unconfigured `main`—new format 4 or read-only normalized v3—may have no configuration. Either fork scope then creates one unconfigured destination `main`, which first harness attachment seeds normally. Every configured format-4 lane copied by a fork keeps its current total configuration. @@ -840,7 +841,7 @@ interface SessionRepo>; list(options?: L): Promise; delete(metadata: M): Promise; - fork(source: M, options: ForkOptions & C): Promise>; + fork(source: M, options: ForkOptions): Promise>; } interface SessionReader { diff --git a/packages/agent/src/harness/session/memory.ts b/packages/agent/src/harness/session/memory.ts index 3cc560332..aebf2d378 100644 --- a/packages/agent/src/harness/session/memory.ts +++ b/packages/agent/src/harness/session/memory.ts @@ -394,7 +394,7 @@ export class MemorySessionRepo implements SessionRepo { this.sessions.delete(metadata.id); } - async fork(source: SessionMetadata, options: ForkOptions & SessionCreateOptions): Promise { + async fork(source: SessionMetadata, options: ForkOptions): Promise { this.assertOpen(); const sourceRecord = this.sessions.get(source.id); if (sourceRecord === undefined) throw new Error(`Unknown session: ${source.id}`); diff --git a/packages/agent/src/harness/session/types.ts b/packages/agent/src/harness/session/types.ts index a7bf2116c..92237c383 100644 --- a/packages/agent/src/harness/session/types.ts +++ b/packages/agent/src/harness/session/types.ts @@ -533,7 +533,9 @@ export interface SessionCreateOptions { parentSessionId?: string; } -export type ForkOptions = { scope?: "branch"; entryId?: string; position?: "before" | "at" } | { scope: "tree" }; +export type ForkOptions = + | { scope?: "branch"; entryId?: string; position?: "before" | "at"; id?: string } + | { scope: "tree"; id?: string }; export interface SessionRepo< TMetadata extends SessionMetadata = SessionMetadata, @@ -544,5 +546,5 @@ export interface SessionRepo< open(metadata: TMetadata): Promise>; list(options?: TListOptions): Promise; delete(metadata: TMetadata): Promise; - fork(source: TMetadata, options: ForkOptions & TCreateOptions): Promise>; + fork(source: TMetadata, options: ForkOptions): Promise>; }