@gajae-code/coding-agent 0.4.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/dist/types/cli/fast-help.d.ts +1 -0
- package/dist/types/cli/setup-cli.d.ts +2 -0
- package/dist/types/commands/harness.d.ts +6 -0
- package/dist/types/commands/setup.d.ts +6 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -2
- package/dist/types/config/model-profiles.d.ts +7 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +35 -0
- package/dist/types/config/settings-schema.d.ts +4 -3
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/server.d.ts +8 -2
- package/dist/types/gjc-runtime/team-runtime.d.ts +0 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/harness-control-plane/finalize.d.ts +5 -0
- package/dist/types/harness-control-plane/owner.d.ts +1 -1
- package/dist/types/harness-control-plane/phase-rollup.d.ts +23 -0
- package/dist/types/harness-control-plane/receipt-ingest.d.ts +19 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
- package/dist/types/harness-control-plane/receipts.d.ts +46 -0
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
- package/dist/types/harness-control-plane/types.d.ts +13 -1
- package/dist/types/hindsight/mental-models.d.ts +5 -5
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/components/model-selector.d.ts +1 -12
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
- package/dist/types/modes/utils/abort-message.d.ts +4 -0
- package/dist/types/sdk.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/blob-store.d.ts +20 -1
- package/dist/types/session/session-manager.d.ts +32 -6
- package/dist/types/session/streaming-output.d.ts +3 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -0
- package/dist/types/setup/hermes-setup.d.ts +7 -0
- package/dist/types/task/fork-context-advisory.d.ts +13 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/roi-reconciliation.d.ts +27 -0
- package/dist/types/task/types.d.ts +17 -0
- package/dist/types/thinking-metadata.d.ts +16 -0
- package/dist/types/thinking.d.ts +3 -12
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +0 -10
- package/dist/types/utils/tool-choice.d.ts +14 -1
- package/package.json +8 -7
- package/scripts/build-binary.ts +4 -0
- package/src/cli/fast-help.ts +80 -0
- package/src/cli/setup-cli.ts +12 -3
- package/src/cli.ts +112 -17
- package/src/commands/coordinator.ts +44 -1
- package/src/commands/harness.ts +128 -11
- package/src/commands/launch.ts +2 -2
- package/src/commands/mcp-serve.ts +3 -2
- package/src/commands/session.ts +3 -1
- package/src/commands/setup.ts +4 -0
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +255 -56
- package/src/config/model-resolver.ts +9 -6
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +427 -193
- package/src/cursor.ts +46 -4
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
- package/src/export/html/index.ts +13 -9
- package/src/gjc-runtime/launch-worktree.ts +12 -1
- package/src/gjc-runtime/session-state-sidecar.ts +38 -0
- package/src/gjc-runtime/team-runtime.ts +33 -7
- package/src/gjc-runtime/tmux-common.ts +15 -0
- package/src/gjc-runtime/tmux-sessions.ts +19 -11
- package/src/gjc-runtime/ultragoal-runtime.ts +505 -41
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -1
- package/src/harness-control-plane/finalize.ts +39 -5
- package/src/harness-control-plane/owner.ts +87 -28
- package/src/harness-control-plane/phase-rollup.ts +96 -0
- package/src/harness-control-plane/receipt-ingest.ts +127 -0
- package/src/harness-control-plane/receipt-spool.ts +128 -0
- package/src/harness-control-plane/receipts.ts +229 -1
- package/src/harness-control-plane/rpc-adapter.ts +8 -0
- package/src/harness-control-plane/state-machine.ts +27 -6
- package/src/harness-control-plane/storage.ts +23 -0
- package/src/harness-control-plane/types.ts +33 -1
- package/src/hindsight/mental-models.ts +17 -16
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/main.ts +7 -3
- package/src/modes/components/assistant-message.ts +26 -14
- package/src/modes/components/diff.ts +97 -0
- package/src/modes/components/model-selector.ts +353 -181
- package/src/modes/components/status-line.ts +6 -6
- package/src/modes/components/tool-execution.ts +30 -13
- package/src/modes/controllers/event-controller.ts +5 -4
- package/src/modes/controllers/selector-controller.ts +33 -42
- package/src/modes/interactive-mode.ts +4 -5
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +3 -2
- package/src/modes/rpc/rpc-mode.ts +44 -14
- package/src/modes/rpc/rpc-types.ts +5 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +10 -5
- package/src/modes/shared/agent-wire/command-validation.ts +11 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/utils/abort-message.ts +41 -0
- package/src/modes/utils/context-usage.ts +15 -8
- package/src/modes/utils/ui-helpers.ts +5 -6
- package/src/sdk.ts +38 -6
- package/src/secrets/obfuscator.ts +102 -27
- package/src/session/agent-session.ts +121 -25
- package/src/session/blob-store.ts +89 -3
- package/src/session/session-manager.ts +328 -57
- package/src/session/streaming-output.ts +185 -122
- package/src/session/tool-choice-queue.ts +23 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +3 -2
- package/src/setup/hermes-setup.ts +63 -8
- package/src/task/executor.ts +69 -6
- package/src/task/fork-context-advisory.ts +99 -0
- package/src/task/index.ts +31 -2
- package/src/task/receipt.ts +7 -0
- package/src/task/render.ts +21 -1
- package/src/task/roi-reconciliation.ts +90 -0
- package/src/task/types.ts +15 -0
- package/src/thinking-metadata.ts +51 -0
- package/src/thinking.ts +26 -46
- package/src/tools/bash.ts +1 -1
- package/src/tools/index.ts +4 -2
- package/src/tools/resolve.ts +93 -18
- package/src/tools/subagent-render.ts +10 -1
- package/src/utils/edit-mode.ts +1 -1
- package/src/utils/title-generator.ts +16 -2
- package/src/utils/tool-choice.ts +45 -16
|
@@ -32,6 +32,7 @@ import { ArtifactManager } from "./artifacts";
|
|
|
32
32
|
import {
|
|
33
33
|
type BlobPutResult,
|
|
34
34
|
BlobStore,
|
|
35
|
+
EphemeralBlobStore,
|
|
35
36
|
externalizeImageData,
|
|
36
37
|
externalizeImageDataSync,
|
|
37
38
|
externalizeImageDataUrl,
|
|
@@ -106,6 +107,12 @@ export interface ModelChangeEntry extends SessionEntryBase {
|
|
|
106
107
|
model: string;
|
|
107
108
|
/** Role: "default" or an agent role. Undefined treated as "default" */
|
|
108
109
|
role?: string;
|
|
110
|
+
/** Requested model before a runtime substitution/fallback, in "provider/modelId" format. */
|
|
111
|
+
previousModel?: string;
|
|
112
|
+
/** Machine-readable reason for runtime model substitution/fallback. */
|
|
113
|
+
reason?: string;
|
|
114
|
+
/** Effective thinking level when the change was recorded. */
|
|
115
|
+
thinkingLevel?: string | null;
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
export interface ServiceTierChangeEntry extends SessionEntryBase {
|
|
@@ -565,13 +572,14 @@ export function buildSessionContext(
|
|
|
565
572
|
};
|
|
566
573
|
}
|
|
567
574
|
|
|
568
|
-
// Walk from leaf to root,
|
|
575
|
+
// Walk from leaf to root, then reverse once to avoid repeated front insertions on long branches.
|
|
569
576
|
const path: SessionEntry[] = [];
|
|
570
577
|
let current: SessionEntry | undefined = leaf;
|
|
571
578
|
while (current) {
|
|
572
|
-
path.
|
|
579
|
+
path.push(current);
|
|
573
580
|
current = current.parentId ? byId.get(current.parentId) : undefined;
|
|
574
581
|
}
|
|
582
|
+
path.reverse();
|
|
575
583
|
|
|
576
584
|
// Extract settings and find compaction
|
|
577
585
|
let thinkingLevel: string | undefined = "off";
|
|
@@ -727,6 +735,17 @@ export function buildSessionContext(
|
|
|
727
735
|
};
|
|
728
736
|
}
|
|
729
737
|
|
|
738
|
+
function cloneSessionContext(context: SessionContext): SessionContext {
|
|
739
|
+
return {
|
|
740
|
+
...context,
|
|
741
|
+
messages: cloneJsonSemantic(context.messages),
|
|
742
|
+
models: { ...context.models },
|
|
743
|
+
injectedTtsrRules: [...context.injectedTtsrRules],
|
|
744
|
+
selectedMCPToolNames: [...context.selectedMCPToolNames],
|
|
745
|
+
modeData: cloneJsonSemantic(context.modeData),
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
730
749
|
/**
|
|
731
750
|
* Compute the default session directory for a cwd.
|
|
732
751
|
* Classifies cwd by canonical location so symlink/alias paths resolve to the
|
|
@@ -1175,13 +1194,20 @@ function shouldExternalizeResidentString(key: string | undefined): boolean {
|
|
|
1175
1194
|
return !key || !RESIDENT_EXTERNALIZE_STRING_EXCLUDED_KEYS.has(key);
|
|
1176
1195
|
}
|
|
1177
1196
|
|
|
1178
|
-
|
|
1197
|
+
interface ResidentBlobStores {
|
|
1198
|
+
textStore: BlobStore;
|
|
1199
|
+
imageStore: BlobStore;
|
|
1200
|
+
sessionId?: string;
|
|
1201
|
+
sessionFile?: string;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function externalizeResidentValueSync(obj: unknown, stores: ResidentBlobStores, key?: string): unknown {
|
|
1179
1205
|
if (obj === null || obj === undefined) return obj;
|
|
1180
1206
|
if (typeof obj === "string") {
|
|
1181
1207
|
if (key === "image_url" && isImageDataUrl(obj) && obj.length >= BLOB_EXTERNALIZE_THRESHOLD)
|
|
1182
|
-
return residentBlobSentinel("imageUrl", externalizeImageDataUrlSync(
|
|
1208
|
+
return residentBlobSentinel("imageUrl", externalizeImageDataUrlSync(stores.imageStore, obj));
|
|
1183
1209
|
if (shouldExternalizeResidentString(key) && obj.length >= BLOB_EXTERNALIZE_THRESHOLD)
|
|
1184
|
-
return residentBlobSentinel("text",
|
|
1210
|
+
return residentBlobSentinel("text", stores.textStore.putSync(Buffer.from(obj, "utf8")).ref);
|
|
1185
1211
|
return obj;
|
|
1186
1212
|
}
|
|
1187
1213
|
if (Array.isArray(obj)) {
|
|
@@ -1198,11 +1224,11 @@ function externalizeResidentValueSync(obj: unknown, blobStore: BlobStore, key?:
|
|
|
1198
1224
|
changed = true;
|
|
1199
1225
|
result[i] = {
|
|
1200
1226
|
...item,
|
|
1201
|
-
data: residentBlobSentinel("imageData", externalizeImageDataSync(
|
|
1227
|
+
data: residentBlobSentinel("imageData", externalizeImageDataSync(stores.imageStore, item.data)),
|
|
1202
1228
|
};
|
|
1203
1229
|
continue;
|
|
1204
1230
|
}
|
|
1205
|
-
const newItem = externalizeResidentValueSync(item,
|
|
1231
|
+
const newItem = externalizeResidentValueSync(item, stores, key);
|
|
1206
1232
|
if (newItem !== item) changed = true;
|
|
1207
1233
|
result[i] = newItem;
|
|
1208
1234
|
}
|
|
@@ -1212,7 +1238,7 @@ function externalizeResidentValueSync(obj: unknown, blobStore: BlobStore, key?:
|
|
|
1212
1238
|
let changed = false;
|
|
1213
1239
|
const entries: Array<readonly [string, unknown]> = [];
|
|
1214
1240
|
for (const [childKey, value] of Object.entries(obj)) {
|
|
1215
|
-
const newValue = externalizeResidentValueSync(value,
|
|
1241
|
+
const newValue = externalizeResidentValueSync(value, stores, childKey);
|
|
1216
1242
|
if (newValue !== value) changed = true;
|
|
1217
1243
|
entries.push([childKey, newValue]);
|
|
1218
1244
|
}
|
|
@@ -1221,13 +1247,13 @@ function externalizeResidentValueSync(obj: unknown, blobStore: BlobStore, key?:
|
|
|
1221
1247
|
return obj;
|
|
1222
1248
|
}
|
|
1223
1249
|
|
|
1224
|
-
function prepareEntryForResidentSync(entry: FileEntry,
|
|
1225
|
-
return externalizeResidentValueSync(entry,
|
|
1250
|
+
function prepareEntryForResidentSync(entry: FileEntry, stores: ResidentBlobStores): FileEntry {
|
|
1251
|
+
return externalizeResidentValueSync(entry, stores) as FileEntry;
|
|
1226
1252
|
}
|
|
1227
1253
|
|
|
1228
1254
|
function materializeResidentValueSync(
|
|
1229
1255
|
obj: unknown,
|
|
1230
|
-
|
|
1256
|
+
stores: ResidentBlobStores,
|
|
1231
1257
|
key?: string,
|
|
1232
1258
|
cache = new Map<string, string>(),
|
|
1233
1259
|
): unknown {
|
|
@@ -1239,17 +1265,17 @@ function materializeResidentValueSync(
|
|
|
1239
1265
|
if (cached !== undefined) return cached;
|
|
1240
1266
|
const resolved =
|
|
1241
1267
|
obj.kind === "imageUrl"
|
|
1242
|
-
? resolveImageDataUrlSync(
|
|
1268
|
+
? resolveImageDataUrlSync(stores.imageStore, obj.ref)
|
|
1243
1269
|
: obj.kind === "imageData"
|
|
1244
|
-
? resolveImageDataSync(
|
|
1245
|
-
: resolveTextBlobSync(
|
|
1270
|
+
? resolveImageDataSync(stores.imageStore, obj.ref)
|
|
1271
|
+
: resolveTextBlobSync(stores.textStore, obj.ref, stores);
|
|
1246
1272
|
cache.set(cacheKey, resolved);
|
|
1247
1273
|
return resolved;
|
|
1248
1274
|
}
|
|
1249
1275
|
if (Array.isArray(obj)) {
|
|
1250
1276
|
let changed = false;
|
|
1251
1277
|
const result = obj.map(item => {
|
|
1252
|
-
const newItem = materializeResidentValueSync(item,
|
|
1278
|
+
const newItem = materializeResidentValueSync(item, stores, key, cache);
|
|
1253
1279
|
if (newItem !== item) changed = true;
|
|
1254
1280
|
return newItem;
|
|
1255
1281
|
});
|
|
@@ -1258,7 +1284,7 @@ function materializeResidentValueSync(
|
|
|
1258
1284
|
if (typeof obj === "object") {
|
|
1259
1285
|
let changed = false;
|
|
1260
1286
|
const entries = Object.entries(obj).map(([childKey, value]) => {
|
|
1261
|
-
const newValue = materializeResidentValueSync(value,
|
|
1287
|
+
const newValue = materializeResidentValueSync(value, stores, childKey, cache);
|
|
1262
1288
|
if (newValue !== value) changed = true;
|
|
1263
1289
|
return [childKey, newValue] as const;
|
|
1264
1290
|
});
|
|
@@ -1269,15 +1295,38 @@ function materializeResidentValueSync(
|
|
|
1269
1295
|
|
|
1270
1296
|
function materializeResidentEntrySync<T extends FileEntry | SessionEntry>(
|
|
1271
1297
|
entry: T,
|
|
1272
|
-
|
|
1298
|
+
stores: ResidentBlobStores,
|
|
1273
1299
|
cache: Map<string, string>,
|
|
1274
1300
|
): T {
|
|
1275
|
-
return materializeResidentValueSync(entry,
|
|
1301
|
+
return materializeResidentValueSync(entry, stores, undefined, cache) as T;
|
|
1276
1302
|
}
|
|
1277
1303
|
|
|
1278
|
-
function materializeResidentEntriesSync<T extends FileEntry | SessionEntry>(
|
|
1304
|
+
function materializeResidentEntriesSync<T extends FileEntry | SessionEntry>(
|
|
1305
|
+
entries: T[],
|
|
1306
|
+
stores: ResidentBlobStores,
|
|
1307
|
+
): T[] {
|
|
1279
1308
|
const cache = new Map<string, string>();
|
|
1280
|
-
return entries.map(entry => materializeResidentEntrySync(entry,
|
|
1309
|
+
return entries.map(entry => materializeResidentEntrySync(entry, stores, cache));
|
|
1310
|
+
}
|
|
1311
|
+
function cloneJsonSemantic<T>(value: T): T {
|
|
1312
|
+
if (value === null || value === undefined || typeof value !== "object") return value;
|
|
1313
|
+
if (Array.isArray(value)) return value.map(item => cloneJsonSemantic(item)) as T;
|
|
1314
|
+
const cloned: Record<string, unknown> = {};
|
|
1315
|
+
for (const [key, child] of Object.entries(value)) cloned[key] = cloneJsonSemantic(child);
|
|
1316
|
+
return cloned as T;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function cloneAgentMessage<T extends AgentMessage>(message: T): T {
|
|
1320
|
+
return {
|
|
1321
|
+
...message,
|
|
1322
|
+
...("content" in message ? { content: cloneJsonSemantic(message.content) } : {}),
|
|
1323
|
+
...("providerPayload" in message ? { providerPayload: cloneJsonSemantic(message.providerPayload) } : {}),
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function cloneSessionEntry(entry: SessionEntry): SessionEntry {
|
|
1328
|
+
if (entry.type !== "message") return { ...entry };
|
|
1329
|
+
return { ...entry, message: cloneAgentMessage(entry.message) } as SessionEntry;
|
|
1281
1330
|
}
|
|
1282
1331
|
|
|
1283
1332
|
async function truncateForPersistence(obj: FileEntry, blobStore: BlobStore, key?: string): Promise<FileEntry>;
|
|
@@ -1710,6 +1759,7 @@ const SESSION_LIST_PREFIX_BYTES = 4096;
|
|
|
1710
1759
|
const SESSION_LIST_PARALLEL_THRESHOLD = 64;
|
|
1711
1760
|
const SESSION_LIST_MAX_WORKERS = 16;
|
|
1712
1761
|
const sessionListPrefixDecoder = new TextDecoder("utf-8", { fatal: false });
|
|
1762
|
+
let residentCacheInstanceCounter = 0;
|
|
1713
1763
|
|
|
1714
1764
|
async function readSessionListPrefix(file: string, storage: SessionStorage, buffer: Buffer): Promise<string> {
|
|
1715
1765
|
if (!(storage instanceof FileSessionStorage)) {
|
|
@@ -2010,6 +2060,7 @@ interface SessionManagerStateSnapshot {
|
|
|
2010
2060
|
flushed: boolean;
|
|
2011
2061
|
needsFullRewriteOnNextPersist: boolean;
|
|
2012
2062
|
fileEntries: FileEntry[];
|
|
2063
|
+
materializedFileEntries: FileEntry[];
|
|
2013
2064
|
}
|
|
2014
2065
|
|
|
2015
2066
|
export class SessionManager {
|
|
@@ -2048,7 +2099,21 @@ export class SessionManager {
|
|
|
2048
2099
|
#inMemoryArtifacts: Map<string, string> | null = null;
|
|
2049
2100
|
#inMemoryArtifactCounter = 0;
|
|
2050
2101
|
readonly #blobStore: BlobStore;
|
|
2051
|
-
|
|
2102
|
+
#residentTextBlobStore: BlobStore = new MemoryBlobStore();
|
|
2103
|
+
readonly #residentImageBlobStore: BlobStore;
|
|
2104
|
+
#entryRevision = 0;
|
|
2105
|
+
#leafRevision = 0;
|
|
2106
|
+
/** Export/header cache invalidation contract; consumers may arrive after the revision field. */
|
|
2107
|
+
#headerExportRevision = 0;
|
|
2108
|
+
/** Label-view cache invalidation contract; consumers may arrive after the revision field. */
|
|
2109
|
+
#labelRevision = 0;
|
|
2110
|
+
#replayMetadataRevision = 0;
|
|
2111
|
+
#materializedEntriesRevision = -1;
|
|
2112
|
+
#materializedEntriesCache: WeakRef<SessionEntry[]> | undefined;
|
|
2113
|
+
#sessionContextCache: WeakRef<SessionContext> | undefined;
|
|
2114
|
+
#sessionContextEntryRevision = -1;
|
|
2115
|
+
#sessionContextLeafRevision = -1;
|
|
2116
|
+
#sessionContextReplayMetadataRevision = -1;
|
|
2052
2117
|
|
|
2053
2118
|
private constructor(
|
|
2054
2119
|
private cwd: string,
|
|
@@ -2056,19 +2121,101 @@ export class SessionManager {
|
|
|
2056
2121
|
private readonly persist: boolean,
|
|
2057
2122
|
private readonly storage: SessionStorage,
|
|
2058
2123
|
) {
|
|
2059
|
-
this.#blobStore = persist ? new BlobStore(getBlobsDir()) : this.#
|
|
2124
|
+
this.#blobStore = persist ? new BlobStore(getBlobsDir()) : this.#residentTextBlobStore;
|
|
2125
|
+
this.#residentImageBlobStore = this.#blobStore;
|
|
2060
2126
|
if (persist && sessionDir) {
|
|
2061
2127
|
this.storage.ensureDirSync(sessionDir);
|
|
2062
2128
|
}
|
|
2063
2129
|
// Note: call _initSession() or _initSessionFile() after construction
|
|
2064
2130
|
}
|
|
2065
2131
|
|
|
2132
|
+
#residentBlobStores(): ResidentBlobStores {
|
|
2133
|
+
return {
|
|
2134
|
+
textStore: this.#residentTextBlobStore,
|
|
2135
|
+
imageStore: this.#residentImageBlobStore,
|
|
2136
|
+
sessionId: this.#sessionId || undefined,
|
|
2137
|
+
sessionFile: this.#sessionFile,
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
#residentCacheDir(sessionFile: string): string {
|
|
2142
|
+
const instance = ++residentCacheInstanceCounter;
|
|
2143
|
+
return path.join(
|
|
2144
|
+
sessionFile.slice(0, -6),
|
|
2145
|
+
"resident-cache",
|
|
2146
|
+
`${this.#sessionId || "pending"}-${process.pid}-${instance}`,
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
#reexternalizeFileEntriesForResidentStore(): void {
|
|
2151
|
+
this.#fileEntries = this.#fileEntries.map(entry =>
|
|
2152
|
+
prepareEntryForResidentSync(entry, this.#residentBlobStores()),
|
|
2153
|
+
);
|
|
2154
|
+
this.#buildIndex();
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
#resetMaterializedCaches(): void {
|
|
2158
|
+
this.#materializedEntriesRevision = -1;
|
|
2159
|
+
this.#materializedEntriesCache = undefined;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
#bumpEntryRevision(): void {
|
|
2163
|
+
this.#entryRevision++;
|
|
2164
|
+
this.#resetMaterializedCaches();
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
#bumpAllRevisions(): void {
|
|
2168
|
+
this.#entryRevision++;
|
|
2169
|
+
this.#leafRevision++;
|
|
2170
|
+
this.#headerExportRevision++;
|
|
2171
|
+
this.#labelRevision++;
|
|
2172
|
+
this.#replayMetadataRevision++;
|
|
2173
|
+
this.#resetMaterializedCaches();
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
/**
|
|
2177
|
+
* Snapshot of the five cache-invalidation revision domains (plan: Lane 1
|
|
2178
|
+
* revision contract). Tests assert the invalidation mapping through this;
|
|
2179
|
+
* future export/label-view caches key off their respective domains.
|
|
2180
|
+
*/
|
|
2181
|
+
revisionSnapshot(): {
|
|
2182
|
+
entry: number;
|
|
2183
|
+
leaf: number;
|
|
2184
|
+
headerExport: number;
|
|
2185
|
+
label: number;
|
|
2186
|
+
replayMetadata: number;
|
|
2187
|
+
} {
|
|
2188
|
+
return {
|
|
2189
|
+
entry: this.#entryRevision,
|
|
2190
|
+
leaf: this.#leafRevision,
|
|
2191
|
+
headerExport: this.#headerExportRevision,
|
|
2192
|
+
label: this.#labelRevision,
|
|
2193
|
+
replayMetadata: this.#replayMetadataRevision,
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
#disposeResidentTextBlobStore(): void {
|
|
2198
|
+
if (this.#residentTextBlobStore instanceof EphemeralBlobStore) {
|
|
2199
|
+
this.#residentTextBlobStore.dispose();
|
|
2200
|
+
}
|
|
2201
|
+
this.#residentTextBlobStore = new MemoryBlobStore();
|
|
2202
|
+
this.#resetMaterializedCaches();
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
#resetResidentTextBlobStore(): void {
|
|
2206
|
+
this.#disposeResidentTextBlobStore();
|
|
2207
|
+
if (this.persist && this.#sessionFile && this.storage instanceof FileSessionStorage) {
|
|
2208
|
+
this.#residentTextBlobStore = new EphemeralBlobStore(this.#residentCacheDir(this.#sessionFile));
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2066
2212
|
/** Puts a binary blob into the blob store and returns the blob reference */
|
|
2067
2213
|
async putBlob(data: Buffer): Promise<BlobPutResult> {
|
|
2068
2214
|
return this.#blobStore.put(data);
|
|
2069
2215
|
}
|
|
2070
2216
|
|
|
2071
2217
|
captureState(): SessionManagerStateSnapshot {
|
|
2218
|
+
const materializedFileEntries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores());
|
|
2072
2219
|
return {
|
|
2073
2220
|
sessionId: this.#sessionId,
|
|
2074
2221
|
sessionName: this.#sessionName,
|
|
@@ -2079,17 +2226,21 @@ export class SessionManager {
|
|
|
2079
2226
|
// Snapshot entry objects by reference: switch/reload replaces the active entry array,
|
|
2080
2227
|
// so rollback does not need structured cloning of extension/custom details.
|
|
2081
2228
|
fileEntries: [...this.#fileEntries],
|
|
2229
|
+
// Rollback snapshots must own resident data before another session reset disposes
|
|
2230
|
+
// the ephemeral store backing the resident sentinels above.
|
|
2231
|
+
materializedFileEntries,
|
|
2082
2232
|
};
|
|
2083
2233
|
}
|
|
2084
2234
|
|
|
2085
2235
|
restoreState(snapshot: SessionManagerStateSnapshot): void {
|
|
2236
|
+
const restoredFileEntries = [...snapshot.materializedFileEntries];
|
|
2086
2237
|
this.#sessionId = snapshot.sessionId;
|
|
2087
2238
|
this.#sessionName = snapshot.sessionName;
|
|
2088
2239
|
this.#titleSource = snapshot.titleSource;
|
|
2089
2240
|
this.#sessionFile = snapshot.sessionFile;
|
|
2090
2241
|
this.#flushed = snapshot.flushed;
|
|
2091
2242
|
this.#needsFullRewriteOnNextPersist = snapshot.needsFullRewriteOnNextPersist;
|
|
2092
|
-
this.#fileEntries =
|
|
2243
|
+
this.#fileEntries = restoredFileEntries;
|
|
2093
2244
|
this.#persistWriter = undefined;
|
|
2094
2245
|
this.#persistWriterPath = undefined;
|
|
2095
2246
|
this.#persistChain = Promise.resolve();
|
|
@@ -2098,7 +2249,9 @@ export class SessionManager {
|
|
|
2098
2249
|
this.#artifactManager = null;
|
|
2099
2250
|
this.#artifactManagerSessionFile = null;
|
|
2100
2251
|
this.#adoptedArtifactManager = null;
|
|
2101
|
-
this.#
|
|
2252
|
+
this.#resetResidentTextBlobStore();
|
|
2253
|
+
this.#reexternalizeFileEntriesForResidentStore();
|
|
2254
|
+
this.#bumpAllRevisions();
|
|
2102
2255
|
if (this.#sessionFile) {
|
|
2103
2256
|
writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
|
|
2104
2257
|
}
|
|
@@ -2112,6 +2265,7 @@ export class SessionManager {
|
|
|
2112
2265
|
/** Initialize with a new session (used by factory methods) */
|
|
2113
2266
|
#initNewSession(): void {
|
|
2114
2267
|
this.#newSessionSync();
|
|
2268
|
+
this.#bumpAllRevisions();
|
|
2115
2269
|
}
|
|
2116
2270
|
|
|
2117
2271
|
/** Switch to a different session file (used for resume and branching) */
|
|
@@ -2130,22 +2284,26 @@ export class SessionManager {
|
|
|
2130
2284
|
|
|
2131
2285
|
this.#needsFullRewriteOnNextPersist = migrateToCurrentVersion(this.#fileEntries);
|
|
2132
2286
|
await resolveBlobRefsInEntries(this.#fileEntries, this.#blobStore);
|
|
2287
|
+
this.#resetResidentTextBlobStore();
|
|
2133
2288
|
|
|
2134
2289
|
this.#fileEntries = this.#fileEntries.map(entry =>
|
|
2135
|
-
prepareEntryForResidentSync(entry, this.#
|
|
2290
|
+
prepareEntryForResidentSync(entry, this.#residentBlobStores()),
|
|
2136
2291
|
);
|
|
2137
2292
|
this.sanitizeLoadedOpenAIResponsesReplayMetadata();
|
|
2138
2293
|
|
|
2139
2294
|
this.#buildIndex();
|
|
2295
|
+
this.#bumpAllRevisions();
|
|
2140
2296
|
this.#flushed = true;
|
|
2141
2297
|
this.#ensuredOnDisk = true;
|
|
2142
2298
|
} else {
|
|
2143
2299
|
const explicitPath = this.#sessionFile;
|
|
2144
2300
|
this.#newSessionSync();
|
|
2145
2301
|
this.#sessionFile = explicitPath; // preserve explicit path from --session flag
|
|
2302
|
+
this.#resetResidentTextBlobStore();
|
|
2146
2303
|
await this.#rewriteFile();
|
|
2147
2304
|
this.#flushed = true;
|
|
2148
2305
|
this.#ensuredOnDisk = true;
|
|
2306
|
+
this.#bumpAllRevisions();
|
|
2149
2307
|
return;
|
|
2150
2308
|
}
|
|
2151
2309
|
}
|
|
@@ -2153,7 +2311,9 @@ export class SessionManager {
|
|
|
2153
2311
|
/** Start a new session. Closes any existing writer first. */
|
|
2154
2312
|
async newSession(options?: NewSessionOptions): Promise<string | undefined> {
|
|
2155
2313
|
await this.#closePersistWriter();
|
|
2156
|
-
|
|
2314
|
+
const sessionFile = this.#newSessionSync(options);
|
|
2315
|
+
this.#bumpAllRevisions();
|
|
2316
|
+
return sessionFile;
|
|
2157
2317
|
}
|
|
2158
2318
|
|
|
2159
2319
|
/** Delete a session file and its artifacts. Drains the persist writer first to avoid EPERM on Windows. ENOENT is treated as success. */
|
|
@@ -2179,6 +2339,7 @@ export class SessionManager {
|
|
|
2179
2339
|
|
|
2180
2340
|
const oldSessionFile = this.#sessionFile;
|
|
2181
2341
|
const oldSessionId = this.#sessionId;
|
|
2342
|
+
const materializedEntries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores());
|
|
2182
2343
|
|
|
2183
2344
|
// Close the current writer
|
|
2184
2345
|
await this.#closePersistWriter();
|
|
@@ -2208,8 +2369,11 @@ export class SessionManager {
|
|
|
2208
2369
|
this.#titleSource = newHeader.titleSource;
|
|
2209
2370
|
|
|
2210
2371
|
// Replace the header in fileEntries
|
|
2211
|
-
const entries =
|
|
2372
|
+
const entries = materializedEntries.filter((e): e is SessionEntry => e.type !== "session");
|
|
2212
2373
|
this.#fileEntries = [newHeader, ...entries];
|
|
2374
|
+
this.#resetResidentTextBlobStore();
|
|
2375
|
+
this.#reexternalizeFileEntriesForResidentStore();
|
|
2376
|
+
this.#bumpAllRevisions();
|
|
2213
2377
|
|
|
2214
2378
|
// Write the new session file
|
|
2215
2379
|
this.#flushed = false;
|
|
@@ -2247,6 +2411,24 @@ export class SessionManager {
|
|
|
2247
2411
|
hadSessionFile = this.storage.existsSync(oldSessionFile);
|
|
2248
2412
|
let movedSessionFile = false;
|
|
2249
2413
|
let movedArtifactDir = false;
|
|
2414
|
+
const materializedEntries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores());
|
|
2415
|
+
const restoreResidentStateAfterFailure = (): void => {
|
|
2416
|
+
this.#fileEntries = materializedEntries;
|
|
2417
|
+
this.#resetResidentTextBlobStore();
|
|
2418
|
+
this.#reexternalizeFileEntriesForResidentStore();
|
|
2419
|
+
this.#bumpAllRevisions();
|
|
2420
|
+
};
|
|
2421
|
+
const restoreResidentStateAndThrow = (error: unknown): never => {
|
|
2422
|
+
try {
|
|
2423
|
+
restoreResidentStateAfterFailure();
|
|
2424
|
+
} catch (restoreErr) {
|
|
2425
|
+
throw new Error(
|
|
2426
|
+
`Failed to restore live session resident state after move failure: ${toError(restoreErr).message}; original error: ${toError(error).message}`,
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2429
|
+
throw error;
|
|
2430
|
+
};
|
|
2431
|
+
this.#disposeResidentTextBlobStore();
|
|
2250
2432
|
|
|
2251
2433
|
try {
|
|
2252
2434
|
// Guard: session file may not exist yet (no assistant messages persisted)
|
|
@@ -2269,8 +2451,10 @@ export class SessionManager {
|
|
|
2269
2451
|
try {
|
|
2270
2452
|
await fs.promises.rename(newArtifactDir, oldArtifactDir);
|
|
2271
2453
|
} catch (rollbackErr) {
|
|
2272
|
-
|
|
2273
|
-
|
|
2454
|
+
restoreResidentStateAndThrow(
|
|
2455
|
+
new Error(
|
|
2456
|
+
`Failed to move artifacts and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
|
|
2457
|
+
),
|
|
2274
2458
|
);
|
|
2275
2459
|
}
|
|
2276
2460
|
}
|
|
@@ -2278,14 +2462,20 @@ export class SessionManager {
|
|
|
2278
2462
|
try {
|
|
2279
2463
|
await fs.promises.rename(newSessionFile, oldSessionFile);
|
|
2280
2464
|
} catch (rollbackErr) {
|
|
2281
|
-
|
|
2282
|
-
|
|
2465
|
+
restoreResidentStateAndThrow(
|
|
2466
|
+
new Error(
|
|
2467
|
+
`Failed to move session file and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
|
|
2468
|
+
),
|
|
2283
2469
|
);
|
|
2284
2470
|
}
|
|
2285
2471
|
}
|
|
2286
|
-
|
|
2472
|
+
restoreResidentStateAndThrow(err);
|
|
2287
2473
|
}
|
|
2288
2474
|
this.#sessionFile = newSessionFile;
|
|
2475
|
+
this.#fileEntries = materializedEntries;
|
|
2476
|
+
this.#resetResidentTextBlobStore();
|
|
2477
|
+
this.#reexternalizeFileEntriesForResidentStore();
|
|
2478
|
+
this.#bumpAllRevisions();
|
|
2289
2479
|
}
|
|
2290
2480
|
|
|
2291
2481
|
// Update cwd and sessionDir after the move succeeds.
|
|
@@ -2296,6 +2486,7 @@ export class SessionManager {
|
|
|
2296
2486
|
const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
2297
2487
|
if (header) {
|
|
2298
2488
|
header.cwd = resolvedCwd;
|
|
2489
|
+
this.#headerExportRevision++;
|
|
2299
2490
|
}
|
|
2300
2491
|
|
|
2301
2492
|
// Rewrite the session file at its new location with updated header.
|
|
@@ -2346,6 +2537,7 @@ export class SessionManager {
|
|
|
2346
2537
|
this.#sessionFile = path.join(this.getSessionDir(), `${fileTimestamp}_${this.#sessionId}.jsonl`);
|
|
2347
2538
|
writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
|
|
2348
2539
|
}
|
|
2540
|
+
this.#resetResidentTextBlobStore();
|
|
2349
2541
|
return this.#sessionFile;
|
|
2350
2542
|
}
|
|
2351
2543
|
|
|
@@ -2625,7 +2817,7 @@ export class SessionManager {
|
|
|
2625
2817
|
await this.#queuePersistTask(async () => {
|
|
2626
2818
|
await this.#closePersistWriterInternal();
|
|
2627
2819
|
const entries = await Promise.all(
|
|
2628
|
-
materializeResidentEntriesSync(this.#fileEntries, this.#
|
|
2820
|
+
materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores()).map(entry =>
|
|
2629
2821
|
prepareEntryForPersistence(entry, this.#blobStore),
|
|
2630
2822
|
),
|
|
2631
2823
|
);
|
|
@@ -2639,7 +2831,7 @@ export class SessionManager {
|
|
|
2639
2831
|
#rewriteFileSync(): void {
|
|
2640
2832
|
if (!this.persist || !this.#sessionFile) return;
|
|
2641
2833
|
this.#closePersistWriterInternalSync();
|
|
2642
|
-
const entries = materializeResidentEntriesSync(this.#fileEntries, this.#
|
|
2834
|
+
const entries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores()).map(entry =>
|
|
2643
2835
|
prepareEntryForPersistenceSync(entry, this.#blobStore),
|
|
2644
2836
|
);
|
|
2645
2837
|
this.#writeEntriesAtomicallySync(entries);
|
|
@@ -2676,11 +2868,13 @@ export class SessionManager {
|
|
|
2676
2868
|
|
|
2677
2869
|
/** Close the persistent writer after flushing all pending data. */
|
|
2678
2870
|
async close(): Promise<void> {
|
|
2679
|
-
if (!this.#persistWriter) return;
|
|
2680
2871
|
await this.#queuePersistTask(async () => {
|
|
2681
|
-
|
|
2682
|
-
|
|
2872
|
+
if (this.#persistWriter) {
|
|
2873
|
+
await this.#closePersistWriterInternal();
|
|
2874
|
+
this.#flushed = true;
|
|
2875
|
+
}
|
|
2683
2876
|
});
|
|
2877
|
+
this.#disposeResidentTextBlobStore();
|
|
2684
2878
|
if (this.#persistError) throw this.#persistError;
|
|
2685
2879
|
}
|
|
2686
2880
|
|
|
@@ -2888,6 +3082,7 @@ export class SessionManager {
|
|
|
2888
3082
|
header.title = sanitized;
|
|
2889
3083
|
header.titleSource = source;
|
|
2890
3084
|
}
|
|
3085
|
+
this.#headerExportRevision++;
|
|
2891
3086
|
|
|
2892
3087
|
// Update the session file header with the title (if already flushed)
|
|
2893
3088
|
const sessionFile = this.#sessionFile;
|
|
@@ -2944,7 +3139,7 @@ export class SessionManager {
|
|
|
2944
3139
|
this.#rewriteFile().catch(() => {});
|
|
2945
3140
|
return;
|
|
2946
3141
|
}
|
|
2947
|
-
const materializedEntry = materializeResidentEntrySync(entry, this.#
|
|
3142
|
+
const materializedEntry = materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map());
|
|
2948
3143
|
const persistedEntry = prepareEntryForPersistenceSync(materializedEntry, this.#blobStore);
|
|
2949
3144
|
writer.writeSync(persistedEntry);
|
|
2950
3145
|
} catch (err) {
|
|
@@ -2954,10 +3149,13 @@ export class SessionManager {
|
|
|
2954
3149
|
}
|
|
2955
3150
|
|
|
2956
3151
|
#appendEntry(entry: SessionEntry): void {
|
|
2957
|
-
const residentEntry = prepareEntryForResidentSync(entry, this.#
|
|
3152
|
+
const residentEntry = prepareEntryForResidentSync(entry, this.#residentBlobStores()) as SessionEntry;
|
|
2958
3153
|
this.#fileEntries.push(residentEntry);
|
|
2959
3154
|
this.#byId.set(residentEntry.id, residentEntry);
|
|
2960
3155
|
this.#leafId = residentEntry.id;
|
|
3156
|
+
this.#bumpEntryRevision();
|
|
3157
|
+
this.#leafRevision++;
|
|
3158
|
+
if (entry.type === "label") this.#labelRevision++;
|
|
2961
3159
|
this._persist(residentEntry);
|
|
2962
3160
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
2963
3161
|
const usage = entry.message.usage;
|
|
@@ -3052,7 +3250,11 @@ export class SessionManager {
|
|
|
3052
3250
|
* @param model Model in "provider/modelId" format
|
|
3053
3251
|
* @param role Optional role (default: "default")
|
|
3054
3252
|
*/
|
|
3055
|
-
appendModelChange(
|
|
3253
|
+
appendModelChange(
|
|
3254
|
+
model: string,
|
|
3255
|
+
role?: string,
|
|
3256
|
+
metadata?: { previousModel?: string; reason?: string; thinkingLevel?: string | null },
|
|
3257
|
+
): string {
|
|
3056
3258
|
const entry: ModelChangeEntry = {
|
|
3057
3259
|
type: "model_change",
|
|
3058
3260
|
id: generateId(this.#byId),
|
|
@@ -3060,6 +3262,9 @@ export class SessionManager {
|
|
|
3060
3262
|
timestamp: new Date().toISOString(),
|
|
3061
3263
|
model,
|
|
3062
3264
|
role,
|
|
3265
|
+
previousModel: metadata?.previousModel,
|
|
3266
|
+
reason: metadata?.reason,
|
|
3267
|
+
thinkingLevel: metadata?.thinkingLevel,
|
|
3063
3268
|
};
|
|
3064
3269
|
this.#appendEntry(entry);
|
|
3065
3270
|
return entry.id;
|
|
@@ -3125,6 +3330,28 @@ export class SessionManager {
|
|
|
3125
3330
|
return entry.id;
|
|
3126
3331
|
}
|
|
3127
3332
|
|
|
3333
|
+
/**
|
|
3334
|
+
* Write mutated message entries back into the canonical entry store by id.
|
|
3335
|
+
*
|
|
3336
|
+
* `getBranch()` materializes resident-blob entries into copies, so in-place
|
|
3337
|
+
* mutation of returned entries (e.g. pruning tool outputs) does not affect
|
|
3338
|
+
* the canonical store. This applies such mutations for real.
|
|
3339
|
+
*/
|
|
3340
|
+
applyEntryMessageUpdates(entries: readonly SessionMessageEntry[]): void {
|
|
3341
|
+
for (const updated of entries) {
|
|
3342
|
+
const canonical = this.#byId.get(updated.id);
|
|
3343
|
+
if (canonical?.type !== "message") continue;
|
|
3344
|
+
const residentEntry = prepareEntryForResidentSync(
|
|
3345
|
+
{ ...canonical, message: updated.message },
|
|
3346
|
+
this.#residentBlobStores(),
|
|
3347
|
+
) as SessionMessageEntry;
|
|
3348
|
+
canonical.message = residentEntry.message;
|
|
3349
|
+
}
|
|
3350
|
+
this.#needsFullRewriteOnNextPersist = true;
|
|
3351
|
+
this.#bumpEntryRevision();
|
|
3352
|
+
this.#replayMetadataRevision++;
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3128
3355
|
/**
|
|
3129
3356
|
* Rewrite the session file after in-place entry updates.
|
|
3130
3357
|
* Use sparingly (e.g., pruning old tool outputs).
|
|
@@ -3234,7 +3461,7 @@ export class SessionManager {
|
|
|
3234
3461
|
getLeafEntry(): SessionEntry | undefined {
|
|
3235
3462
|
if (!this.#leafId) return undefined;
|
|
3236
3463
|
const entry = this.#byId.get(this.#leafId);
|
|
3237
|
-
return entry ? materializeResidentEntrySync(entry, this.#
|
|
3464
|
+
return entry ? materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map()) : undefined;
|
|
3238
3465
|
}
|
|
3239
3466
|
|
|
3240
3467
|
/**
|
|
@@ -3254,7 +3481,7 @@ export class SessionManager {
|
|
|
3254
3481
|
|
|
3255
3482
|
getEntry(id: string): SessionEntry | undefined {
|
|
3256
3483
|
const entry = this.#byId.get(id);
|
|
3257
|
-
return entry ? materializeResidentEntrySync(entry, this.#
|
|
3484
|
+
return entry ? materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map()) : undefined;
|
|
3258
3485
|
}
|
|
3259
3486
|
|
|
3260
3487
|
/**
|
|
@@ -3265,7 +3492,7 @@ export class SessionManager {
|
|
|
3265
3492
|
const children: SessionEntry[] = [];
|
|
3266
3493
|
for (const entry of this.#byId.values()) {
|
|
3267
3494
|
if (entry.parentId === parentId) {
|
|
3268
|
-
children.push(materializeResidentEntrySync(entry, this.#
|
|
3495
|
+
children.push(materializeResidentEntrySync(entry, this.#residentBlobStores(), cache));
|
|
3269
3496
|
}
|
|
3270
3497
|
}
|
|
3271
3498
|
return children;
|
|
@@ -3315,9 +3542,10 @@ export class SessionManager {
|
|
|
3315
3542
|
const startId = fromId ?? this.#leafId;
|
|
3316
3543
|
let current = startId ? this.#byId.get(startId) : undefined;
|
|
3317
3544
|
while (current) {
|
|
3318
|
-
path.
|
|
3545
|
+
path.push(materializeResidentEntrySync(current, this.#residentBlobStores(), cache));
|
|
3319
3546
|
current = current.parentId ? this.#byId.get(current.parentId) : undefined;
|
|
3320
3547
|
}
|
|
3548
|
+
path.reverse();
|
|
3321
3549
|
return path;
|
|
3322
3550
|
}
|
|
3323
3551
|
|
|
@@ -3326,9 +3554,22 @@ export class SessionManager {
|
|
|
3326
3554
|
* Uses tree traversal from current leaf.
|
|
3327
3555
|
*/
|
|
3328
3556
|
buildSessionContext(): SessionContext {
|
|
3329
|
-
|
|
3557
|
+
const cached = this.#sessionContextCache?.deref();
|
|
3558
|
+
if (
|
|
3559
|
+
cached &&
|
|
3560
|
+
this.#sessionContextEntryRevision === this.#entryRevision &&
|
|
3561
|
+
this.#sessionContextLeafRevision === this.#leafRevision &&
|
|
3562
|
+
this.#sessionContextReplayMetadataRevision === this.#replayMetadataRevision
|
|
3563
|
+
) {
|
|
3564
|
+
return cloneSessionContext(cached);
|
|
3565
|
+
}
|
|
3566
|
+
const context = buildSessionContext(this.#getMaterializedEntriesInternal(), this.#leafId);
|
|
3567
|
+
this.#sessionContextCache = new WeakRef(context);
|
|
3568
|
+
this.#sessionContextEntryRevision = this.#entryRevision;
|
|
3569
|
+
this.#sessionContextLeafRevision = this.#leafRevision;
|
|
3570
|
+
this.#sessionContextReplayMetadataRevision = this.#replayMetadataRevision;
|
|
3571
|
+
return cloneSessionContext(context);
|
|
3330
3572
|
}
|
|
3331
|
-
|
|
3332
3573
|
/** Strip stale OpenAI Responses assistant replay metadata from loaded in-memory entries. */
|
|
3333
3574
|
sanitizeLoadedOpenAIResponsesReplayMetadata(): boolean {
|
|
3334
3575
|
let didSanitize = false;
|
|
@@ -3345,6 +3586,10 @@ export class SessionManager {
|
|
|
3345
3586
|
entry.message = sanitizedMessage;
|
|
3346
3587
|
didSanitize = true;
|
|
3347
3588
|
}
|
|
3589
|
+
if (didSanitize) {
|
|
3590
|
+
this.#bumpEntryRevision();
|
|
3591
|
+
this.#replayMetadataRevision++;
|
|
3592
|
+
}
|
|
3348
3593
|
|
|
3349
3594
|
return didSanitize;
|
|
3350
3595
|
}
|
|
@@ -3362,11 +3607,22 @@ export class SessionManager {
|
|
|
3362
3607
|
* The session is append-only: use appendXXX() to add entries, branch() to
|
|
3363
3608
|
* change the leaf pointer. Entries cannot be modified or deleted.
|
|
3364
3609
|
*/
|
|
3610
|
+
#getMaterializedEntriesInternal(): SessionEntry[] {
|
|
3611
|
+
if (this.#materializedEntriesRevision === this.#entryRevision) {
|
|
3612
|
+
const cached = this.#materializedEntriesCache?.deref();
|
|
3613
|
+
if (cached) return cached;
|
|
3614
|
+
}
|
|
3615
|
+
const resolvedTextBlobCache = new Map<string, string>();
|
|
3616
|
+
const materializedEntries = this.#fileEntries
|
|
3617
|
+
.filter((e): e is SessionEntry => e.type !== "session")
|
|
3618
|
+
.map(entry => materializeResidentEntrySync(entry, this.#residentBlobStores(), resolvedTextBlobCache));
|
|
3619
|
+
this.#materializedEntriesCache = new WeakRef(materializedEntries);
|
|
3620
|
+
this.#materializedEntriesRevision = this.#entryRevision;
|
|
3621
|
+
return materializedEntries;
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3365
3624
|
getEntries(): SessionEntry[] {
|
|
3366
|
-
return
|
|
3367
|
-
this.#fileEntries.filter((e): e is SessionEntry => e.type !== "session"),
|
|
3368
|
-
this.#residentBlobStore,
|
|
3369
|
-
);
|
|
3625
|
+
return this.#getMaterializedEntriesInternal().map(entry => cloneSessionEntry(entry));
|
|
3370
3626
|
}
|
|
3371
3627
|
|
|
3372
3628
|
/**
|
|
@@ -3428,6 +3684,7 @@ export class SessionManager {
|
|
|
3428
3684
|
throw new Error(`Entry ${branchFromId} not found`);
|
|
3429
3685
|
}
|
|
3430
3686
|
this.#leafId = branchFromId;
|
|
3687
|
+
this.#leafRevision++;
|
|
3431
3688
|
}
|
|
3432
3689
|
|
|
3433
3690
|
/**
|
|
@@ -3437,6 +3694,7 @@ export class SessionManager {
|
|
|
3437
3694
|
*/
|
|
3438
3695
|
resetLeaf(): void {
|
|
3439
3696
|
this.#leafId = null;
|
|
3697
|
+
this.#leafRevision++;
|
|
3440
3698
|
}
|
|
3441
3699
|
|
|
3442
3700
|
/**
|
|
@@ -3526,17 +3784,19 @@ export class SessionManager {
|
|
|
3526
3784
|
parentId = labelEntry.id;
|
|
3527
3785
|
}
|
|
3528
3786
|
this.storage.writeTextSync(newSessionFile, `${lines.join("\n")}\n`);
|
|
3787
|
+
this.#sessionId = newSessionId;
|
|
3788
|
+
this.#sessionFile = newSessionFile;
|
|
3789
|
+
this.#resetResidentTextBlobStore();
|
|
3529
3790
|
this.#fileEntries = [
|
|
3530
3791
|
header,
|
|
3531
3792
|
...pathWithoutLabels.map(
|
|
3532
|
-
entry => prepareEntryForResidentSync(entry, this.#
|
|
3793
|
+
entry => prepareEntryForResidentSync(entry, this.#residentBlobStores()) as SessionEntry,
|
|
3533
3794
|
),
|
|
3534
3795
|
...labelEntries,
|
|
3535
3796
|
];
|
|
3536
|
-
this.#sessionId = newSessionId;
|
|
3537
|
-
this.#sessionFile = newSessionFile;
|
|
3538
3797
|
this.#flushed = true;
|
|
3539
3798
|
this.#buildIndex();
|
|
3799
|
+
this.#bumpAllRevisions();
|
|
3540
3800
|
return newSessionFile;
|
|
3541
3801
|
}
|
|
3542
3802
|
|
|
@@ -3555,13 +3815,17 @@ export class SessionManager {
|
|
|
3555
3815
|
labelEntries.push(labelEntry);
|
|
3556
3816
|
parentId = labelEntry.id;
|
|
3557
3817
|
}
|
|
3818
|
+
this.#sessionId = newSessionId;
|
|
3819
|
+
this.#resetResidentTextBlobStore();
|
|
3558
3820
|
this.#fileEntries = [
|
|
3559
3821
|
header,
|
|
3560
|
-
...pathWithoutLabels.map(
|
|
3822
|
+
...pathWithoutLabels.map(
|
|
3823
|
+
entry => prepareEntryForResidentSync(entry, this.#residentBlobStores()) as SessionEntry,
|
|
3824
|
+
),
|
|
3561
3825
|
...labelEntries,
|
|
3562
3826
|
];
|
|
3563
|
-
this.#sessionId = newSessionId;
|
|
3564
3827
|
this.#buildIndex();
|
|
3828
|
+
this.#bumpAllRevisions();
|
|
3565
3829
|
return undefined;
|
|
3566
3830
|
}
|
|
3567
3831
|
|
|
@@ -3603,18 +3867,25 @@ export class SessionManager {
|
|
|
3603
3867
|
const forkEntries = structuredClone(await loadEntriesFromFile(sourcePath, storage)) as FileEntry[];
|
|
3604
3868
|
migrateToCurrentVersion(forkEntries);
|
|
3605
3869
|
await resolveBlobRefsInEntries(forkEntries, manager.#blobStore);
|
|
3606
|
-
manager.#fileEntries = forkEntries
|
|
3870
|
+
manager.#fileEntries = forkEntries;
|
|
3607
3871
|
const sourceHeader = manager.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
3608
3872
|
const historyEntries = manager.#fileEntries.filter(entry => entry.type !== "session") as SessionEntry[];
|
|
3609
3873
|
manager.#newSessionSync({ parentSession: sourceHeader?.id });
|
|
3874
|
+
manager.#resetResidentTextBlobStore();
|
|
3610
3875
|
const newHeader = manager.#fileEntries[0] as SessionHeader;
|
|
3611
3876
|
newHeader.title = sourceHeader?.title;
|
|
3612
3877
|
newHeader.titleSource = sourceHeader?.titleSource;
|
|
3613
|
-
manager.#fileEntries = [
|
|
3878
|
+
manager.#fileEntries = [
|
|
3879
|
+
newHeader,
|
|
3880
|
+
...historyEntries.map(
|
|
3881
|
+
entry => prepareEntryForResidentSync(entry, manager.#residentBlobStores()) as SessionEntry,
|
|
3882
|
+
),
|
|
3883
|
+
];
|
|
3614
3884
|
manager.#sessionName = newHeader.title;
|
|
3615
3885
|
manager.#titleSource = newHeader.titleSource;
|
|
3616
3886
|
manager.sanitizeLoadedOpenAIResponsesReplayMetadata();
|
|
3617
3887
|
manager.#buildIndex();
|
|
3888
|
+
manager.#bumpAllRevisions();
|
|
3618
3889
|
await manager.#rewriteFile();
|
|
3619
3890
|
return manager;
|
|
3620
3891
|
}
|