mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 03:15:46 -06:00
4273ca9dbd
* refactor(sessions): keep helper transcripts in memory * refactor(sessions): remove file-era transcript storage * test(sessions): use SQLite identity in attempt persistence * test(codex): isolate legacy transcript fixtures * fix(sessions): preserve SQLite transcript identity * fix(sessions): harden transcript lifecycle invariants * fix(sessions): validate transcript identities * fix(sessions): close identity compatibility gaps * fix(sessions): preserve leaf and plugin identities * fix(sessions): retain dispatch transcript targets * fix(sessions): preserve active transcript context * fix(sessions): isolate artifact accounting * fix(sessions): bound SQLite usage accounting * fix(sessions): retain bounded latest usage * fix(sessions): align rebased transcript targets * test(sessions): align accessor scope fixture * fix(telegram): derive SQLite transcript identity * refactor(sessions): remove file-era compaction residue * chore(sessions): lower max-lines baseline * fix(sessions): preserve structured transcript identity * test(sessions): align doctor identity assertions * fix(sessions): isolate default SDK database * refactor(sessions): remove dead file-era exports * fix(sessions): reconcile SQLite transcript identity * fix(sessions): pass checkpoint identity explicitly * test(sessions): make entry field probe explicit * test(sessions): satisfy transcript cleanup lint * test(sessions): align diagnostics identity proof * fix(sessions): finish transcript runtime teardown * fix(sessions): preserve transcript identity invariants * fix(sessions): harden transcript compatibility edges * fix(sessions): preserve checkpoint transcript anchors * fix(sessions): preserve SQLite lifecycle invariants * fix(sessions): retarget compaction successors * test(sessions): preserve transcript fixture semantics * feat(plugin-sdk): add command transcript targets * fix(sessions): serialize transcript rewrites * fix(sessions): validate legacy successor identity * fix(sessions): normalize compaction ownership * fix(sessions): validate successor identity before adoption * fix(sessions): preserve plugin transcript ownership * fix(sessions): carry transcript identity through commands * fix(sessions): import legacy checkpoint artifacts into SQLite * fix(sessions): preserve successor transcript ownership * fix(sessions): align transcript consumers with target identity * fix(sessions): scope transcript token estimates * fix(sessions): retain agent identity across lifecycle hooks * fix(sessions): resolve scoped SQLite targets * fix(sessions): isolate lifecycle transcript targets * fix(sessions): validate compaction agent ownership * fix(sessions): preserve reset and cleanup lifecycle * fix(sessions): serialize prompt cleanup lifecycle * fix(sessions): remove stale lock import * fix(sessions): preserve reset target context * fix(sessions): fence prompt reload takeover * fix(sessions): unblock abort and default lifecycle reads * fix(sessions): validate legacy successor scope * fix(sessions): reject metadata-only runtime rows * fix(sessions): propagate custom transcript stores * fix(sessions): preserve adopted retry targets * fix(sessions): allow unkeyed usage reads * fix(sessions): harden runtime target boundaries * fix(sessions): serialize retry transcript writes * fix(sessions): bound prompt reload disposal * fix(sessions): complete retry marker identity * fix(sessions): keep legacy marker identity minimal * test(sessions): tighten teardown fixture types * fix(sessions): preserve compatibility target identity * test(sessions): persist post-checkpoint boundary turn * test(sessions): align runtime store mock contracts * style(sessions): simplify persisted identity guard * fix(sessions): prefer complete typed targets * fix(sessions): recover legacy marker targets * test(sessions): align marker lookup fixture scope * fix(sessions): validate partial transcript targets * fix(sessions): reconcile partial transcript identities * fix(sessions): canonicalize compatibility identities * test(sessions): cover compatibility aliases * fix(sessions): adopt legacy successor identity * fix(sessions): preserve usage read identity * fix(sessions): preserve partial marker compatibility * fix(sessions): validate legacy successor mappings * fix(sessions): reconcile marker store mappings * fix(sessions): preserve legacy fallback identity * fix(sessions): harden marker alias resolution * fix(sessions): prefer verified successor aliases * fix(sessions): resolve preferred marker aliases * fix(sessions): serialize cleanup admission * fix(sessions): align marker lookup scopes * fix(codex): type marker alias summaries * style(sessions): satisfy changed lint * test(sessions): align structured target assertions * fix(sessions): reconcile latest identity contracts * fix(sessions): validate transcript identity boundaries * docs(sessions): explain stable registry keys * fix(sessions): harden compatibility target round trips * fix(sessions): port usage identity to split modules * test(sessions): align subagent transcript identity * fix(sessions): finish transcript identity migration * fix(agents): route subagent completion capture through transcript targets * fix(agents): settle SQLite prompt handoff during cleanup * chore: shrink max-lines baseline after teardown * fix(sessions): port teardown across split runtime owners * fix(sessions): carry transcript targets through split owners * test(agents): use SQLite compaction target in abort coverage * chore: retain unrelated max-lines suppressions * chore: shrink max-lines baseline after main splits * style(agents): const compaction checkpoint locals * fix(sessions): harden SQLite teardown boundaries * test(sessions): use typed metadata in predicate isolation fixture * test(agents): cover malformed settlement rejections lint-safely * fix(sessions): close remaining SQLite identity races * fix(agents): fail closed on incomplete successor targets * fix(sessions): preserve transcript identity fallbacks * fix(agents): preserve session-key abort admission * fix(trajectory): validate incomplete export targets * test(sessions): drop retired pricing cache imports * fix(sessions): validate partial transcript identities * fix(sessions): close transcript identity edge cases * fix(plugins): reserve retired transcript locator slot * fix(sessions): scope transcript locks by target * style(sessions): simplify SDK initialization error * fix(sessions): preserve initialized transcript state * fix(codex): verify mirrored history session keys * fix(sessions): reject stale transcript ownership * fix(sessions): anchor asynchronous transcript ownership * fix(sessions): measure active transcript state * fix(sessions): preserve scoped transcript compaction * fix(sessions): harden transcript identity and lifecycle * fix(sessions): resolve scoped command transcript stores * fix(sessions): make transcript appends failure-atomic * fix(sessions): enforce scoped transcript ownership * fix(sessions): reject cross-owner transcript handoffs * fix(sessions): fence cleanup transcript ownership * fix(sessions): retire stale write ownership contexts * fix(sessions): preserve pending session migration state * fix(sessions): validate migrated transcript ownership * fix(sessions): validate usage transcript targets * fix(sessions): clear predecessor transcript metadata * fix(sessions): align durable session event targets * fix(sessions): fence late prompt handoffs * fix(sessions): fence lifecycle transcript fallbacks * fix(sessions): bound zero-length memory capture * fix(sessions): preserve transcript teardown ownership * fix(sessions): reject duplicate cleanup ownership * fix(sessions): serialize runtime writes with sqlite leases * fix(sessions): close sqlite teardown concurrency gaps * fix(sessions): preserve nested lifecycle failures * fix(sessions): canonicalize sqlite transcript ownership * fix(sessions): settle disposed prompt handoffs * fix(sessions): resolve canonical attempt lock targets * test(sessions): align canonical target fixtures * test(sessions): retire redundant jsonl parser coverage * refactor(sessions): split active transcript cursors * test(memory): retire legacy marker fixture * fix(sessions): preserve canonical transcript access after rebase * fix(sessions): fence prompt lease and return transcript targets * fix(sessions): colocate transcript leases with target store * fix(sessions): canonicalize transcript lease and worker targets * fix(sessions): preserve plugin and fork identity markers * fix(sessions): complete sqlite transcript target migration * fix(sessions): integrate canonical followup identity * fix(sessions): preserve bounded transcript topology * fix(sessions): validate transcript identity boundaries * fix(context): separate caller and successor targets * test(sessions): split persistence compatibility coverage * test(sessions): preserve fixture topology efficiently * chore(sdk): refresh plugin api baseline * test(agents): align compaction lock target mocks * test(sessions): seed malformed transcript fixtures directly * fix(agents): canonicalize transcript compatibility inputs * fix(agents): type optional tool result ids * test(ci): stabilize loaded process timing * test(tui): wait for collect queue admission
404 lines
15 KiB
TypeScript
404 lines
15 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import {
|
|
openOpenClawAgentDatabase,
|
|
runOpenClawAgentWriteTransaction,
|
|
type OpenClawAgentDatabase,
|
|
} from "../../state/openclaw-agent-db.js";
|
|
import type {
|
|
ForkSessionEntryFromParentTargetParams,
|
|
ForkSessionEntryFromParentTargetResult,
|
|
ForkSessionFromParentTranscriptParams,
|
|
ForkSessionFromParentTranscriptResult,
|
|
SessionParentForkDecision,
|
|
} from "./session-accessor.sqlite-contract.js";
|
|
import {
|
|
deleteLegacySessionEntryRows,
|
|
normalizeSqliteLifecycleTarget,
|
|
readSqliteSessionIdentitySnapshot,
|
|
rehomeSqliteSessionWindows,
|
|
resolveSqliteLifecyclePrimaryEntry,
|
|
writeSessionEntry,
|
|
} from "./session-accessor.sqlite-entry-store.js";
|
|
import { emitCommittedSessionIdentityDiff } from "./session-accessor.sqlite-identity.js";
|
|
import type { SqliteSessionEntryMaintenancePlan } from "./session-accessor.sqlite-lifecycle-types.js";
|
|
import {
|
|
applySqliteSessionEntryMaintenance,
|
|
finalizeSqliteSessionEntryMaintenancePlansBestEffort,
|
|
} from "./session-accessor.sqlite-maintenance.js";
|
|
import {
|
|
buildSqliteForkedChildTranscriptEvents,
|
|
estimateSqliteTranscriptPromptTokens,
|
|
resolveSqliteParentForkDecision,
|
|
resolveSqliteParentForkSourceTranscript,
|
|
type SqliteParentForkSourceTranscript,
|
|
} from "./session-accessor.sqlite-parent-fork.js";
|
|
import { loadSqliteTranscriptEventsFromDatabase } from "./session-accessor.sqlite-read.js";
|
|
import {
|
|
cloneSessionEntry,
|
|
formatLegacySqliteSessionMarkerForScope,
|
|
formatSqliteSessionReferenceForScope,
|
|
normalizeSqliteSessionKey,
|
|
resolveSqliteScope,
|
|
resolveSqliteStoreScope,
|
|
resolveSqliteTranscriptArchiveDirectory,
|
|
runExclusiveSqliteSessionWrite,
|
|
toDatabaseOptions,
|
|
type ResolvedSqliteScope,
|
|
type ResolvedTranscriptScope,
|
|
} from "./session-accessor.sqlite-scope.js";
|
|
import { appendTranscriptEventsInTransaction } from "./session-accessor.sqlite-transcript-store.js";
|
|
import { preserveSqliteSameKeySessionRolloverLineage } from "./session-entry-lineage.js";
|
|
import type { SessionEntry } from "./types.js";
|
|
import { mergeSessionEntry, resolveFreshSessionTotalTokens } from "./types.js";
|
|
|
|
// Parent-session fork owner: decision, transcript copy, and child entry commit.
|
|
|
|
export async function forkSqliteSessionTranscriptFromParent(
|
|
params: ForkSessionFromParentTranscriptParams,
|
|
): Promise<ForkSessionFromParentTranscriptResult> {
|
|
const resolved = resolveSqliteScope({
|
|
...(params.agentId ? { agentId: params.agentId } : {}),
|
|
sessionKey: params.sessionKey,
|
|
storePath: params.storePath,
|
|
});
|
|
const target = params.targetStorePath
|
|
? resolveSqliteScope({ sessionKey: params.sessionKey, storePath: params.targetStorePath })
|
|
: resolved;
|
|
const crossDatabase =
|
|
target.agentId !== resolved.agentId || (target.path ?? "") !== (resolved.path ?? "");
|
|
if (!crossDatabase) {
|
|
return await runExclusiveSqliteSessionWrite(resolved, async () => {
|
|
let result: ForkSessionFromParentTranscriptResult = { status: "failed" };
|
|
runOpenClawAgentWriteTransaction((database) => {
|
|
result = forkSqliteParentTranscriptInTransaction(database, resolved, {
|
|
parentEntry: params.parentEntry,
|
|
parentSessionKey: params.parentSessionKey,
|
|
targetSessionId: params.targetSessionId,
|
|
targetSessionKey: params.sessionKey,
|
|
});
|
|
}, toDatabaseOptions(resolved));
|
|
return result;
|
|
});
|
|
}
|
|
// Cross-agent fork (worktree/cross-agent sessions.create): parent rows live
|
|
// in the source agent database while the child transcript must be owned by
|
|
// the target agent's database. Two databases cannot share one transaction,
|
|
// so read the parent branch first, then write the child under the target's
|
|
// exclusive session write lock.
|
|
if (!params.parentEntry.sessionId) {
|
|
return { status: "missing-parent" };
|
|
}
|
|
const sourceDatabase = openOpenClawAgentDatabase(toDatabaseOptions(resolved));
|
|
const source = resolveSqliteParentForkSourceTranscript(
|
|
loadSqliteTranscriptEventsFromDatabase(sourceDatabase, params.parentEntry.sessionId),
|
|
);
|
|
if (!source) {
|
|
return { status: "failed" };
|
|
}
|
|
const parentSessionFile = formatLegacySqliteSessionMarkerForScope({
|
|
...resolved,
|
|
sessionId: params.parentEntry.sessionId,
|
|
sessionKey: normalizeSqliteSessionKey(params.parentSessionKey),
|
|
});
|
|
return await runExclusiveSqliteSessionWrite(target, async () => {
|
|
const sessionId = params.targetSessionId ?? randomUUID();
|
|
const targetScope = {
|
|
...target,
|
|
sessionId,
|
|
sessionKey: normalizeSqliteSessionKey(params.sessionKey),
|
|
};
|
|
const sessionFile = formatSqliteSessionReferenceForScope(targetScope);
|
|
runOpenClawAgentWriteTransaction((database) => {
|
|
writeSqliteForkedChildTranscriptInTransaction(database, targetScope, {
|
|
parentSessionFile,
|
|
source,
|
|
});
|
|
}, toDatabaseOptions(target));
|
|
return { status: "created", transcript: { sessionFile, sessionId } };
|
|
});
|
|
}
|
|
|
|
/** Forks parent context into a child session entry using SQLite rows only. */
|
|
export async function forkSqliteSessionEntryFromParentTarget(
|
|
params: ForkSessionEntryFromParentTargetParams,
|
|
): Promise<ForkSessionEntryFromParentTargetResult> {
|
|
const resolved = resolveSqliteStoreScope(params.storePath, { agentId: params.agentId });
|
|
const parentTarget = normalizeSqliteLifecycleTarget(params.parentTarget);
|
|
const sessionTarget = normalizeSqliteLifecycleTarget(params.sessionTarget);
|
|
return await runExclusiveSqliteSessionWrite(resolved, async () => {
|
|
const database = openOpenClawAgentDatabase(toDatabaseOptions(resolved));
|
|
const parent = resolveSqliteLifecyclePrimaryEntry(database, parentTarget);
|
|
if (!parent?.entry.sessionId) {
|
|
return { status: "missing-parent" };
|
|
}
|
|
|
|
const existing = resolveSqliteLifecyclePrimaryEntry(database, sessionTarget);
|
|
const base = existing?.entry ?? params.fallbackEntry;
|
|
if (!base) {
|
|
return { status: "missing-entry" };
|
|
}
|
|
|
|
if (params.skipForkWhen?.(cloneSessionEntry(base))) {
|
|
const sessionEntry = await persistSqliteParentForkSkipPatch({
|
|
entry: base,
|
|
params,
|
|
sessionTarget,
|
|
patch: params.skipPatch?.(cloneSessionEntry(base)),
|
|
resolved,
|
|
});
|
|
return {
|
|
status: "skipped",
|
|
reason: "existing-entry",
|
|
parentEntry: cloneSessionEntry(parent.entry),
|
|
sessionEntry,
|
|
};
|
|
}
|
|
|
|
const needsTranscriptTokenEstimate =
|
|
typeof resolveFreshSessionTotalTokens(parent.entry) !== "number" &&
|
|
typeof parent.entry.sessionId === "string" &&
|
|
parent.entry.sessionId.length > 0;
|
|
const transcriptParentTokens = needsTranscriptTokenEstimate
|
|
? estimateSqliteTranscriptPromptTokens(
|
|
loadSqliteTranscriptEventsFromDatabase(database, parent.entry.sessionId),
|
|
)
|
|
: undefined;
|
|
const decision = resolveSqliteParentForkDecision(parent.entry, transcriptParentTokens);
|
|
if (decision.status === "skip") {
|
|
const patch = params.decisionSkipPatch?.({
|
|
decision,
|
|
entry: cloneSessionEntry(base),
|
|
parentEntry: cloneSessionEntry(parent.entry),
|
|
});
|
|
const sessionEntry = await persistSqliteParentForkSkipPatch({
|
|
entry: base,
|
|
params,
|
|
sessionTarget,
|
|
patch,
|
|
resolved,
|
|
});
|
|
return {
|
|
status: "skipped",
|
|
reason: "decision-skip",
|
|
parentEntry: cloneSessionEntry(parent.entry),
|
|
sessionEntry,
|
|
decision,
|
|
};
|
|
}
|
|
|
|
let result: ForkSessionEntryFromParentTargetResult = { status: "failed" };
|
|
const maintenancePlans: SqliteSessionEntryMaintenancePlan[] = [];
|
|
let previousIdentity = new Map<string, SessionEntry>();
|
|
let currentIdentity = new Map<string, SessionEntry>();
|
|
runOpenClawAgentWriteTransaction((writeDatabase) => {
|
|
const freshParent = resolveSqliteLifecyclePrimaryEntry(writeDatabase, parentTarget)?.entry;
|
|
if (!freshParent?.sessionId) {
|
|
result = { status: "missing-parent" };
|
|
return;
|
|
}
|
|
const freshExisting = resolveSqliteLifecyclePrimaryEntry(writeDatabase, sessionTarget);
|
|
const freshBase = freshExisting?.entry ?? params.fallbackEntry;
|
|
if (!freshBase) {
|
|
result = { status: "missing-entry" };
|
|
return;
|
|
}
|
|
const fork = forkSqliteParentTranscriptInTransaction(writeDatabase, resolved, {
|
|
parentEntry: freshParent,
|
|
parentSessionKey: parentTarget.canonicalKey,
|
|
targetSessionKey: sessionTarget.canonicalKey,
|
|
});
|
|
if (fork.status !== "created") {
|
|
result =
|
|
fork.status === "missing-parent" ? { status: "missing-parent" } : { status: "failed" };
|
|
return;
|
|
}
|
|
const patch = params.patch?.({
|
|
decision,
|
|
entry: cloneSessionEntry(freshBase),
|
|
fork: fork.transcript,
|
|
parentEntry: cloneSessionEntry(freshParent),
|
|
});
|
|
const next = mergeSessionEntry(freshBase, {
|
|
...patch,
|
|
forkSource: {
|
|
sessionKey: parentTarget.canonicalKey,
|
|
sessionId: freshParent.sessionId,
|
|
},
|
|
forkedFromParent: true,
|
|
sessionId: fork.transcript.sessionId,
|
|
});
|
|
previousIdentity = readSqliteSessionIdentitySnapshot(writeDatabase, sessionTarget.storeKeys);
|
|
writeSessionEntry(writeDatabase, sessionTarget.canonicalKey, next, {
|
|
previousEntry: freshBase,
|
|
});
|
|
rehomeSqliteSessionWindows(
|
|
writeDatabase,
|
|
sessionTarget.canonicalKey,
|
|
sessionTarget.storeKeys,
|
|
);
|
|
deleteLegacySessionEntryRows(
|
|
writeDatabase,
|
|
sessionTarget.storeKeys,
|
|
sessionTarget.canonicalKey,
|
|
{ rehomeMembers: freshBase.sessionId === next.sessionId },
|
|
);
|
|
maintenancePlans.push(
|
|
applySqliteSessionEntryMaintenance(writeDatabase, {
|
|
activeSessionKey: sessionTarget.canonicalKey,
|
|
archiveDirectory: resolveSqliteTranscriptArchiveDirectory(resolved),
|
|
skipMaintenance: true,
|
|
storePath: params.storePath,
|
|
}),
|
|
);
|
|
currentIdentity = readSqliteSessionIdentitySnapshot(writeDatabase, sessionTarget.storeKeys);
|
|
result = {
|
|
status: "forked",
|
|
decision,
|
|
fork: fork.transcript,
|
|
parentEntry: cloneSessionEntry(freshParent),
|
|
sessionEntry: cloneSessionEntry(next),
|
|
};
|
|
}, toDatabaseOptions(resolved));
|
|
emitCommittedSessionIdentityDiff(previousIdentity, currentIdentity);
|
|
finalizeSqliteSessionEntryMaintenancePlansBestEffort(resolved, maintenancePlans);
|
|
return result;
|
|
});
|
|
}
|
|
|
|
async function persistSqliteParentForkSkipPatch(params: {
|
|
entry: SessionEntry;
|
|
params: ForkSessionEntryFromParentTargetParams;
|
|
sessionTarget: { canonicalKey: string; storeKeys: string[] };
|
|
patch: Partial<SessionEntry> | null | undefined;
|
|
resolved: ResolvedSqliteScope;
|
|
}): Promise<SessionEntry> {
|
|
if (!params.patch) {
|
|
return cloneSessionEntry(params.entry);
|
|
}
|
|
const merged = mergeSessionEntry(params.entry, params.patch);
|
|
const next = preserveSqliteSameKeySessionRolloverLineage({
|
|
next: merged,
|
|
previous: params.entry,
|
|
sessionKey: params.sessionTarget.canonicalKey,
|
|
});
|
|
const maintenancePlans: SqliteSessionEntryMaintenancePlan[] = [];
|
|
let previousIdentity = new Map<string, SessionEntry>();
|
|
let currentIdentity = new Map<string, SessionEntry>();
|
|
runOpenClawAgentWriteTransaction((database) => {
|
|
previousIdentity = readSqliteSessionIdentitySnapshot(database, params.sessionTarget.storeKeys);
|
|
writeSessionEntry(database, params.sessionTarget.canonicalKey, next, {
|
|
previousEntry: params.entry,
|
|
});
|
|
rehomeSqliteSessionWindows(
|
|
database,
|
|
params.sessionTarget.canonicalKey,
|
|
params.sessionTarget.storeKeys,
|
|
);
|
|
deleteLegacySessionEntryRows(
|
|
database,
|
|
params.sessionTarget.storeKeys,
|
|
params.sessionTarget.canonicalKey,
|
|
{ rehomeMembers: params.entry.sessionId === next.sessionId },
|
|
);
|
|
maintenancePlans.push(
|
|
applySqliteSessionEntryMaintenance(database, {
|
|
activeSessionKey: params.sessionTarget.canonicalKey,
|
|
archiveDirectory: resolveSqliteTranscriptArchiveDirectory(params.resolved),
|
|
skipMaintenance: true,
|
|
storePath: params.params.storePath,
|
|
}),
|
|
);
|
|
currentIdentity = readSqliteSessionIdentitySnapshot(database, params.sessionTarget.storeKeys);
|
|
}, toDatabaseOptions(params.resolved));
|
|
emitCommittedSessionIdentityDiff(previousIdentity, currentIdentity);
|
|
finalizeSqliteSessionEntryMaintenancePlansBestEffort(params.resolved, maintenancePlans);
|
|
return cloneSessionEntry(next);
|
|
}
|
|
|
|
/** Cleans scoped session lifecycle rows and associated SQLite transcript state. */
|
|
|
|
export async function resolveSqliteSessionParentForkDecision(params: {
|
|
parentEntry: SessionEntry;
|
|
storePath: string;
|
|
}): Promise<SessionParentForkDecision> {
|
|
const parentSessionId =
|
|
typeof params.parentEntry.sessionId === "string" ? params.parentEntry.sessionId : "";
|
|
const needsTranscriptTokenEstimate =
|
|
typeof resolveFreshSessionTotalTokens(params.parentEntry) !== "number" &&
|
|
parentSessionId.length > 0;
|
|
if (!needsTranscriptTokenEstimate) {
|
|
return resolveSqliteParentForkDecision(params.parentEntry);
|
|
}
|
|
const resolved = resolveSqliteStoreScope(params.storePath);
|
|
const database = openOpenClawAgentDatabase(toDatabaseOptions(resolved));
|
|
return resolveSqliteParentForkDecision(
|
|
params.parentEntry,
|
|
estimateSqliteTranscriptPromptTokens(
|
|
loadSqliteTranscriptEventsFromDatabase(database, parentSessionId),
|
|
),
|
|
);
|
|
}
|
|
|
|
function forkSqliteParentTranscriptInTransaction(
|
|
database: OpenClawAgentDatabase,
|
|
resolved: ResolvedSqliteScope,
|
|
params: {
|
|
parentEntry: SessionEntry;
|
|
parentSessionKey: string;
|
|
targetSessionId?: string;
|
|
targetSessionKey: string;
|
|
},
|
|
): ForkSessionFromParentTranscriptResult {
|
|
if (!params.parentEntry.sessionId) {
|
|
return { status: "missing-parent" };
|
|
}
|
|
const source = resolveSqliteParentForkSourceTranscript(
|
|
loadSqliteTranscriptEventsFromDatabase(database, params.parentEntry.sessionId),
|
|
);
|
|
if (!source) {
|
|
return { status: "failed" };
|
|
}
|
|
const sessionId = params.targetSessionId ?? randomUUID();
|
|
const targetScope = {
|
|
...resolved,
|
|
sessionId,
|
|
sessionKey: normalizeSqliteSessionKey(params.targetSessionKey),
|
|
};
|
|
const parentSessionFile = formatLegacySqliteSessionMarkerForScope({
|
|
...resolved,
|
|
sessionId: params.parentEntry.sessionId,
|
|
sessionKey: normalizeSqliteSessionKey(params.parentSessionKey),
|
|
});
|
|
const sessionFile = formatSqliteSessionReferenceForScope(targetScope);
|
|
writeSqliteForkedChildTranscriptInTransaction(database, targetScope, {
|
|
parentSessionFile,
|
|
source,
|
|
});
|
|
return {
|
|
status: "created",
|
|
transcript: {
|
|
sessionFile,
|
|
sessionId,
|
|
},
|
|
};
|
|
}
|
|
|
|
function writeSqliteForkedChildTranscriptInTransaction(
|
|
database: OpenClawAgentDatabase,
|
|
targetScope: ResolvedTranscriptScope,
|
|
params: {
|
|
parentSessionFile: string;
|
|
source: SqliteParentForkSourceTranscript;
|
|
},
|
|
): void {
|
|
appendTranscriptEventsInTransaction(
|
|
database,
|
|
targetScope,
|
|
buildSqliteForkedChildTranscriptEvents({
|
|
parentSessionFile: params.parentSessionFile,
|
|
source: params.source,
|
|
targetSessionId: targetScope.sessionId,
|
|
}),
|
|
);
|
|
}
|