feat(agent): require explicit named branches for session forks

Require fork scope and source branch selection, preserve the branch name in the destination, and cover named-branch behavior across backends.
This commit is contained in:
David Brailovsky 2026-09-01 12:35:14 +02:00
parent 86bac52f94
commit f8da63be59
9 changed files with 98 additions and 61 deletions

View file

@ -101,6 +101,7 @@ function buildForkSnapshot(source: SqliteStorageSnapshot, options: ForkOptions):
};
}
// TODO(WP08): Remove this snapshot path when SQLite forks use streaming staging.
function readForkSourceEntries(
db: SqliteDatabase,
sessionId: string,
@ -108,12 +109,12 @@ function readForkSourceEntries(
options: ForkOptions,
): Entry[] {
if (options.scope === "tree") return readSourceEntries(db, sessionId);
const mainAddress = branchTip("main");
const mainTip = scalarValues.find(
(stored) => stored.address.namespace === mainAddress.namespace && stored.address.key === mainAddress.key,
const sourceAddress = branchTip(options.branch);
const sourceTip = scalarValues.find(
(stored) => stored.address.namespace === sourceAddress.namespace && stored.address.key === sourceAddress.key,
) as StoredValue<string | null> | undefined;
if (mainTip === undefined) throw new Error("Source session is missing main branch");
const requested = options.entryId ?? mainTip.value;
if (sourceTip === undefined) throw new Error(`Unknown source branch: ${options.branch}`);
const requested = options.entryId ?? sourceTip.value;
return requested === null ? [] : scanBranchEntries(db, sessionId, { start: requested, order: "oldestFirst" });
}

View file

@ -129,8 +129,7 @@ export class SqliteStorage implements Storage {
return Promise.resolve(readSessionStats(this.db, this.sessionId));
}
snapshot(options: ForkOptions | undefined, _context: Context): Promise<SqliteStorageSnapshot> {
options ??= {};
snapshot(options: ForkOptions, _context: Context): Promise<SqliteStorageSnapshot> {
if (this.state !== "open") return Promise.reject(new Error("SqliteStorage is closed"));
const result = this.commitQueue.then(() => this.readSnapshot(options));
this.commitQueue = result.then(
@ -151,12 +150,12 @@ export class SqliteStorage implements Storage {
private readSnapshotEntries(options: ForkOptions, scalarValues: readonly StoredValue<unknown>[]): Entry[] {
if (options.scope === "tree") return readAllEntryRows(this.db, this.sessionId).map(decodeEntryRow);
const mainAddress = branchTip("main");
const mainTip = scalarValues.find(
(stored) => stored.address.namespace === mainAddress.namespace && stored.address.key === mainAddress.key,
const sourceAddress = branchTip(options.branch);
const sourceTip = scalarValues.find(
(stored) => stored.address.namespace === sourceAddress.namespace && stored.address.key === sourceAddress.key,
) as StoredValue<string | null> | undefined;
if (mainTip === undefined) throw new Error("Source session is missing main branch");
const requested = options.entryId ?? mainTip.value;
if (sourceTip === undefined) throw new Error(`Unknown source branch: ${options.branch}`);
const requested = options.entryId ?? sourceTip.value;
return requested === null
? []
: scanBranchEntries(this.db, this.sessionId, { start: requested, order: "oldestFirst" });