@sayknow-cli/coding-agent 0.3.1 → 0.3.4
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/bin/skc.js +4 -0
- package/dist/types/cli/mcp-cli.d.ts +25 -0
- package/dist/types/cli/plugin-cli.d.ts +2 -0
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/commands/mcp.d.ts +70 -0
- package/dist/types/commands/plugin.d.ts +6 -0
- package/dist/types/commands/session.d.ts +6 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +8 -1
- package/dist/types/config/model-profiles.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config-schema.d.ts +0 -5
- package/dist/types/config/settings-schema.d.ts +79 -68
- package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/skc-plugins/compiler.d.ts +19 -0
- package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +29 -0
- package/dist/types/extensibility/skc-plugins/index.d.ts +9 -0
- package/dist/types/extensibility/skc-plugins/injection.d.ts +9 -0
- package/dist/types/extensibility/skc-plugins/installer.d.ts +13 -0
- package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +26 -0
- package/dist/types/extensibility/skc-plugins/observability.d.ts +27 -0
- package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +16 -0
- package/dist/types/extensibility/skc-plugins/registry.d.ts +32 -0
- package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +64 -0
- package/dist/types/extensibility/skc-plugins/session-validation.d.ts +42 -0
- package/dist/types/extensibility/skc-plugins/types.d.ts +158 -2
- package/dist/types/extensibility/skc-plugins/validation.d.ts +8 -1
- package/dist/types/i18n/messages/en.d.ts +0 -1
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +8 -0
- package/dist/types/modes/components/settings-defs.d.ts +0 -1
- package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
- package/dist/types/modes/theme/defaults/index.d.ts +99 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/notifications/html-format.d.ts +11 -0
- package/dist/types/notifications/index.d.ts +149 -1
- package/dist/types/notifications/lifecycle-commands.d.ts +60 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
- package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
- package/dist/types/notifications/operator-runtime.d.ts +52 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +35 -0
- package/dist/types/notifications/telegram-daemon.d.ts +114 -16
- package/dist/types/notifications/telegram-reference.d.ts +3 -1
- package/dist/types/notifications/topic-registry.d.ts +12 -9
- package/dist/types/runtime-mcp/types.d.ts +7 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +14 -4
- package/dist/types/session/blob-store.d.ts +25 -0
- package/dist/types/session/session-manager.d.ts +57 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/skc-runtime/psmux-detect.d.ts +78 -0
- package/dist/types/skc-runtime/state-renderer.d.ts +5 -0
- package/dist/types/skc-runtime/team-runtime.d.ts +2 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +30 -2
- package/dist/types/skc-runtime/tmux-sessions.d.ts +18 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +37 -1
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +80 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +9 -1
- package/dist/types/tools/browser/tab-supervisor.d.ts +31 -0
- package/dist/types/tools/composer-bash-policy.d.ts +14 -0
- package/dist/types/tools/computer-gc.d.ts +23 -0
- package/dist/types/tools/cron.d.ts +36 -61
- package/dist/types/tools/index.d.ts +3 -2
- package/dist/types/tools/resource-gc.d.ts +54 -0
- package/dist/types/utils/changelog.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +6 -3
- package/dist/types/web/scrapers/types.d.ts +5 -0
- package/dist/types/web/scrapers/utils.d.ts +7 -1
- package/dist/types/web/search/index.d.ts +1 -0
- package/dist/types/web/search/providers/utils.d.ts +11 -4
- package/package.json +11 -9
- package/scripts/g004-tmux-smoke.ts +100 -0
- package/scripts/g005-daemon-smoke.ts +180 -0
- package/scripts/g011-daemon-path-smoke.ts +153 -0
- package/src/cli/args.ts +0 -1
- package/src/cli/fast-help.ts +29 -2
- package/src/cli/mcp-cli.ts +272 -0
- package/src/cli/plugin-cli.ts +66 -3
- package/src/cli/web-search-cli.ts +5 -0
- package/src/cli.ts +30 -12
- package/src/commands/mcp.ts +117 -0
- package/src/commands/plugin.ts +4 -0
- package/src/commands/session.ts +18 -0
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-profile-activation.ts +62 -8
- package/src/config/model-profiles.ts +3 -4
- package/src/config/model-registry.ts +3 -6
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +88 -103
- package/src/deep-interview/plaintext-gate-guard.ts +94 -0
- package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +1 -1
- package/src/defaults/skc/skills/deep-interview/SKILL.md +7 -6
- package/src/defaults/skc/skills/team/SKILL.md +5 -3
- package/src/defaults/skc/skills/ultragoal/SKILL.md +41 -13
- package/src/export/html/index.ts +2 -2
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -12
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/skc-plugins/compiler.ts +351 -0
- package/src/extensibility/skc-plugins/constrained-hooks.ts +170 -0
- package/src/extensibility/skc-plugins/index.ts +9 -0
- package/src/extensibility/skc-plugins/injection.ts +109 -0
- package/src/extensibility/skc-plugins/installer.ts +434 -0
- package/src/extensibility/skc-plugins/loader.ts +3 -1
- package/src/extensibility/skc-plugins/mcp-policy.ts +239 -0
- package/src/extensibility/skc-plugins/observability.ts +84 -0
- package/src/extensibility/skc-plugins/paths.ts +1 -1
- package/src/extensibility/skc-plugins/prompt-appendix.ts +109 -0
- package/src/extensibility/skc-plugins/registry.ts +180 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +234 -0
- package/src/extensibility/skc-plugins/schema.ts +250 -20
- package/src/extensibility/skc-plugins/session-validation.ts +147 -0
- package/src/extensibility/skc-plugins/types.ts +199 -3
- package/src/extensibility/skc-plugins/validation.ts +80 -0
- package/src/extensibility/skills.ts +15 -0
- package/src/goals/tools/goal-tool.ts +14 -1
- package/src/hooks/skill-state.ts +57 -0
- package/src/i18n/messages/de.ts +0 -1
- package/src/i18n/messages/en.ts +0 -1
- package/src/i18n/messages/es.ts +0 -1
- package/src/i18n/messages/fr.ts +0 -1
- package/src/i18n/messages/ja.ts +0 -1
- package/src/i18n/messages/ko.ts +0 -1
- package/src/i18n/messages/zh.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +14 -11
- package/src/main.ts +14 -3
- package/src/modes/bridge/bridge-mode.ts +11 -0
- package/src/modes/components/assistant-message.ts +49 -1
- package/src/modes/components/custom-editor.ts +2 -0
- package/src/modes/components/footer.ts +2 -3
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/hook-selector.ts +67 -43
- package/src/modes/components/model-selector.ts +65 -12
- package/src/modes/components/settings-defs.ts +1 -2
- package/src/modes/components/settings-selector.ts +2 -7
- package/src/modes/components/status-line/git-utils.ts +25 -0
- package/src/modes/components/status-line.ts +10 -11
- package/src/modes/components/welcome.ts +2 -3
- package/src/modes/controllers/extension-ui-controller.ts +0 -27
- package/src/modes/controllers/selector-controller.ts +59 -11
- package/src/modes/interactive-mode.ts +4 -1
- package/src/modes/shared/agent-wire/scopes.ts +1 -1
- package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
- package/src/modes/theme/defaults/index.ts +2 -0
- package/src/modes/theme/theme.ts +0 -4
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/notifications/html-format.ts +38 -0
- package/src/notifications/index.ts +242 -12
- package/src/notifications/lifecycle-commands.ts +238 -0
- package/src/notifications/lifecycle-control-runtime.ts +405 -0
- package/src/notifications/lifecycle-orchestrator.ts +358 -0
- package/src/notifications/operator-runtime.ts +171 -0
- package/src/notifications/rate-limit-pool.ts +19 -0
- package/src/notifications/recent-activity.ts +132 -0
- package/src/notifications/telegram-daemon.ts +778 -257
- package/src/notifications/telegram-reference.ts +25 -7
- package/src/notifications/topic-registry.ts +23 -9
- package/src/prompts/agents/executor.md +2 -2
- package/src/prompts/system/system-prompt.md +2 -2
- package/src/prompts/tools/cron.md +5 -3
- package/src/prompts/tools/read.md +1 -1
- package/src/runtime-mcp/transports/stdio.ts +38 -4
- package/src/runtime-mcp/types.ts +7 -0
- package/src/sdk.ts +162 -10
- package/src/session/agent-session.ts +210 -74
- package/src/session/blob-store.ts +196 -8
- package/src/session/session-manager.ts +762 -12
- package/src/skc-runtime/launch-tmux.ts +68 -19
- package/src/skc-runtime/psmux-detect.ts +239 -0
- package/src/skc-runtime/state-renderer.ts +13 -0
- package/src/skc-runtime/team-runtime.ts +56 -23
- package/src/skc-runtime/tmux-common.ts +88 -4
- package/src/skc-runtime/tmux-sessions.ts +111 -9
- package/src/skc-runtime/ultragoal-guard.ts +192 -8
- package/src/skc-runtime/ultragoal-runtime.ts +296 -16
- package/src/slash-commands/builtin-registry.ts +23 -3
- package/src/slash-commands/helpers/fast-status-report.ts +13 -3
- package/src/slash-commands/helpers/parse.ts +2 -1
- package/src/system-prompt.ts +9 -0
- package/src/task/executor.ts +31 -7
- package/src/task/index.ts +2 -0
- package/src/tools/ask.ts +5 -1
- package/src/tools/bash.ts +9 -0
- package/src/tools/browser/tab-supervisor.ts +86 -2
- package/src/tools/composer-bash-policy.ts +96 -0
- package/src/tools/computer-gc.ts +66 -0
- package/src/tools/computer.ts +2 -0
- package/src/tools/cron.ts +75 -112
- package/src/tools/fetch.ts +18 -2
- package/src/tools/index.ts +5 -9
- package/src/tools/read.ts +25 -55
- package/src/tools/renderers.ts +0 -2
- package/src/tools/resource-gc.ts +291 -0
- package/src/tools/ultragoal-ask-guard.ts +7 -1
- package/src/utils/changelog.ts +8 -0
- package/src/web/insane/url-guard.ts +18 -14
- package/src/web/scrapers/types.ts +143 -45
- package/src/web/scrapers/utils.ts +70 -19
- package/src/web/search/index.ts +1 -0
- package/src/web/search/providers/utils.ts +22 -5
- package/vendor/insane-search/MANIFEST.json +3 -1
- package/vendor/insane-search/engine/__init__.py +14 -0
- package/vendor/insane-search/engine/content_safety.py +151 -0
- package/vendor/insane-search/engine/fetch_chain.py +32 -0
- package/vendor/insane-search/engine/tests/test_u8.py +216 -0
- package/dist/types/tools/inspect-image-renderer.d.ts +0 -26
- package/dist/types/tools/inspect-image.d.ts +0 -31
- package/src/prompts/tools/inspect-image-system.md +0 -20
- package/src/prompts/tools/inspect-image.md +0 -32
- package/src/tools/inspect-image-renderer.ts +0 -103
- package/src/tools/inspect-image.ts +0 -172
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
isBlobRef,
|
|
42
42
|
isImageDataUrl,
|
|
43
43
|
MemoryBlobStore,
|
|
44
|
+
parseBlobRef,
|
|
44
45
|
ResidentBlobMissingError,
|
|
45
46
|
resolveImageData,
|
|
46
47
|
resolveImageDataUrl,
|
|
@@ -93,9 +94,55 @@ export interface SessionEntryBase {
|
|
|
93
94
|
timestamp: string;
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
export interface ColdSpillRef {
|
|
98
|
+
kind: "cold_spill";
|
|
99
|
+
ref: string;
|
|
100
|
+
encoding: "utf8" | "json";
|
|
101
|
+
originalChars: number;
|
|
102
|
+
sha256: string;
|
|
103
|
+
bytes: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface EvictedContentMarker {
|
|
107
|
+
evictedAt: number;
|
|
108
|
+
reason: "compacted_history";
|
|
109
|
+
compactionEntryId: string;
|
|
110
|
+
firstKeptEntryId: string;
|
|
111
|
+
payloads: Record<string, ColdSpillRef>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface EvictCompactedContentResult {
|
|
115
|
+
evictedEntries: number;
|
|
116
|
+
hotCharsRemoved: number;
|
|
117
|
+
coldBlobBytes: number;
|
|
118
|
+
payloadRefs: number;
|
|
119
|
+
alreadyEvictedEntries: number;
|
|
120
|
+
coldSpillWriteCount: number;
|
|
121
|
+
coldSpillReadCount: number;
|
|
122
|
+
residentTextReadCount: number;
|
|
123
|
+
residentImageReadCount: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface SessionManagerObservabilityStats {
|
|
127
|
+
coldSpillWriteCount: number;
|
|
128
|
+
coldSpillReadCount: number;
|
|
129
|
+
residentTextReadCount: number;
|
|
130
|
+
residentImageReadCount: number;
|
|
131
|
+
publicMaterializerCallCount: number;
|
|
132
|
+
getEntryMaterializerCallCount: number;
|
|
133
|
+
getBranchMaterializerCallCount: number;
|
|
134
|
+
getEntriesMaterializerCallCount: number;
|
|
135
|
+
materializedEntriesCachePopulateCount: number;
|
|
136
|
+
pathOnlyContextBuildCount: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
96
139
|
export interface SessionMessageEntry extends SessionEntryBase {
|
|
97
140
|
type: "message";
|
|
98
141
|
message: AgentMessage;
|
|
142
|
+
/** Cold-spill marker: when present, heavy message content was moved to durable
|
|
143
|
+
* content-addressed blobs after compaction. The marker is entry-level session
|
|
144
|
+
* metadata (not a message field) so strict message types stay intact. */
|
|
145
|
+
evictedContent?: EvictedContentMarker;
|
|
99
146
|
}
|
|
100
147
|
|
|
101
148
|
export interface ThinkingLevelChangeEntry extends SessionEntryBase {
|
|
@@ -227,6 +274,8 @@ export interface CustomMessageEntry<T = unknown> extends SessionEntryBase {
|
|
|
227
274
|
display: boolean;
|
|
228
275
|
/** Who initiated this message for billing/attribution semantics. */
|
|
229
276
|
attribution?: MessageAttribution;
|
|
277
|
+
/** Cold-spill marker for custom-message content evicted after compaction. */
|
|
278
|
+
evictedContent?: EvictedContentMarker;
|
|
230
279
|
}
|
|
231
280
|
|
|
232
281
|
/** Session entry - has id/parentId for tree structure (returned by "read" methods in SessionManager) */
|
|
@@ -319,6 +368,21 @@ function createSessionId(): string {
|
|
|
319
368
|
return Bun.randomUUIDv7();
|
|
320
369
|
}
|
|
321
370
|
|
|
371
|
+
/**
|
|
372
|
+
* A session id pre-allocated by the notifications lifecycle subsystem, when this
|
|
373
|
+
* process was spawned by `/session_create`. Gated by `SKC_LIFECYCLE_REQUEST_ID`
|
|
374
|
+
* so it ONLY applies to lifecycle-launched sessions (never normal launches): the
|
|
375
|
+
* daemon tags the tmux session, endpoint discovery, and its `/session_recent`
|
|
376
|
+
* id with this value, so the agent MUST adopt it as its header id or those ids
|
|
377
|
+
* diverge (breaking close/resume-by-id after the session is gone).
|
|
378
|
+
*/
|
|
379
|
+
function lifecyclePreallocatedSessionId(): string | undefined {
|
|
380
|
+
if (!process.env.SKC_LIFECYCLE_REQUEST_ID) return undefined;
|
|
381
|
+
const id = process.env.SKC_SESSION_ID?.trim();
|
|
382
|
+
if (!id || !/^[A-Za-z0-9._-]{1,128}$/.test(id)) return undefined;
|
|
383
|
+
return id;
|
|
384
|
+
}
|
|
385
|
+
|
|
322
386
|
/** Generate a unique short ID (8 hex chars, collision-checked) */
|
|
323
387
|
function generateId(byId: { has(id: string): boolean }): string {
|
|
324
388
|
for (let i = 0; i < 100; i++) {
|
|
@@ -576,8 +640,11 @@ export function buildSessionContext(
|
|
|
576
640
|
|
|
577
641
|
// Walk from leaf to root, then reverse once to avoid repeated front insertions on long branches.
|
|
578
642
|
const path: SessionEntry[] = [];
|
|
643
|
+
const visited = new Set<string>();
|
|
579
644
|
let current: SessionEntry | undefined = leaf;
|
|
580
645
|
while (current) {
|
|
646
|
+
if (visited.has(current.id)) break;
|
|
647
|
+
visited.add(current.id);
|
|
581
648
|
path.push(current);
|
|
582
649
|
current = current.parentId ? byId.get(current.parentId) : undefined;
|
|
583
650
|
}
|
|
@@ -1199,6 +1266,17 @@ function isResidentBlobSentinel(value: unknown): value is ResidentBlobSentinel {
|
|
|
1199
1266
|
isBlobRef((value as { ref: string }).ref)
|
|
1200
1267
|
);
|
|
1201
1268
|
}
|
|
1269
|
+
function containsResidentSentinel(value: unknown, seen = new WeakSet<object>()): boolean {
|
|
1270
|
+
if (value === null || value === undefined || typeof value !== "object") return false;
|
|
1271
|
+
if ((value as { [RESIDENT_BLOB_SENTINEL_KEY]?: unknown })[RESIDENT_BLOB_SENTINEL_KEY] === true) return true;
|
|
1272
|
+
if (seen.has(value)) return false;
|
|
1273
|
+
seen.add(value);
|
|
1274
|
+
if (Array.isArray(value)) return value.some(item => containsResidentSentinel(item, seen));
|
|
1275
|
+
for (const child of Object.values(value)) {
|
|
1276
|
+
if (containsResidentSentinel(child, seen)) return true;
|
|
1277
|
+
}
|
|
1278
|
+
return false;
|
|
1279
|
+
}
|
|
1202
1280
|
|
|
1203
1281
|
/**
|
|
1204
1282
|
* Recursively truncate large strings in an object for session persistence.
|
|
@@ -1246,6 +1324,8 @@ const RESIDENT_EXTERNALIZE_STRING_EXCLUDED_KEYS = new Set([
|
|
|
1246
1324
|
"toolName",
|
|
1247
1325
|
"targetId",
|
|
1248
1326
|
"firstKeptEntryId",
|
|
1327
|
+
"encrypted_content",
|
|
1328
|
+
"reasoning_encrypted_content",
|
|
1249
1329
|
]);
|
|
1250
1330
|
|
|
1251
1331
|
function shouldExternalizeResidentString(key: string | undefined): boolean {
|
|
@@ -1257,6 +1337,7 @@ interface ResidentBlobStores {
|
|
|
1257
1337
|
imageStore: BlobStore;
|
|
1258
1338
|
sessionId?: string;
|
|
1259
1339
|
sessionFile?: string;
|
|
1340
|
+
onResidentBlobRead?: (kind: ResidentBlobKind) => void;
|
|
1260
1341
|
}
|
|
1261
1342
|
|
|
1262
1343
|
type ResidentBlobMissingPolicy = "throw" | "placeholder";
|
|
@@ -1344,6 +1425,7 @@ function materializeResidentValueSync(
|
|
|
1344
1425
|
}
|
|
1345
1426
|
}
|
|
1346
1427
|
cache.set(cacheKey, resolved);
|
|
1428
|
+
stores.onResidentBlobRead?.(obj.kind);
|
|
1347
1429
|
return resolved;
|
|
1348
1430
|
}
|
|
1349
1431
|
if (Array.isArray(obj)) {
|
|
@@ -1435,6 +1517,329 @@ function cloneSessionEntry(entry: SessionEntry): SessionEntry {
|
|
|
1435
1517
|
return { ...entry, message: cloneAgentMessage(entry.message) } as SessionEntry;
|
|
1436
1518
|
}
|
|
1437
1519
|
|
|
1520
|
+
function materializeProviderVisibleEntrySync(entry: SessionEntry, stores: ResidentBlobStores): SessionEntry {
|
|
1521
|
+
if (entry.type === "compaction") {
|
|
1522
|
+
const cache = new Map<string, string>();
|
|
1523
|
+
const summary = materializeResidentValueSync(entry.summary, stores, "summary", cache);
|
|
1524
|
+
const shortSummary = materializeResidentValueSync(entry.shortSummary, stores, "shortSummary", cache);
|
|
1525
|
+
const remote = entry.preserveData?.openaiRemoteCompaction;
|
|
1526
|
+
const remoteRecord = isRecord(remote) ? remote : undefined;
|
|
1527
|
+
const replacementHistory = remoteRecord
|
|
1528
|
+
? materializeResidentValueSync(remoteRecord.replacementHistory, stores, "replacementHistory", cache)
|
|
1529
|
+
: undefined;
|
|
1530
|
+
const preserveData =
|
|
1531
|
+
remoteRecord && replacementHistory !== undefined && replacementHistory !== remoteRecord.replacementHistory
|
|
1532
|
+
? {
|
|
1533
|
+
...entry.preserveData,
|
|
1534
|
+
openaiRemoteCompaction: {
|
|
1535
|
+
...remoteRecord,
|
|
1536
|
+
replacementHistory,
|
|
1537
|
+
},
|
|
1538
|
+
}
|
|
1539
|
+
: entry.preserveData;
|
|
1540
|
+
return {
|
|
1541
|
+
...entry,
|
|
1542
|
+
summary: typeof summary === "string" ? summary : entry.summary,
|
|
1543
|
+
shortSummary: typeof shortSummary === "string" ? shortSummary : entry.shortSummary,
|
|
1544
|
+
preserveData,
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
if (entry.type === "branch_summary") {
|
|
1548
|
+
const summary = materializeResidentValueSync(entry.summary, stores, "summary", new Map<string, string>());
|
|
1549
|
+
return typeof summary === "string" ? { ...entry, summary } : { ...entry };
|
|
1550
|
+
}
|
|
1551
|
+
return cloneSessionEntry(entry);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
const COLD_SPILL_NOTICE = "[Compacted history content evicted to durable cold storage]";
|
|
1555
|
+
const COLD_SPILL_ARGUMENTS_SENTINEL_KEY = "__skcColdSpillArguments";
|
|
1556
|
+
const COLD_SPILL_MIN_CHARS = 1024;
|
|
1557
|
+
|
|
1558
|
+
type ColdSpillWrite = {
|
|
1559
|
+
path: string;
|
|
1560
|
+
encoding: "utf8" | "json";
|
|
1561
|
+
data: Buffer;
|
|
1562
|
+
originalChars: number;
|
|
1563
|
+
};
|
|
1564
|
+
|
|
1565
|
+
type ColdSpillResidentPromotion = {
|
|
1566
|
+
stores: ResidentBlobStores;
|
|
1567
|
+
};
|
|
1568
|
+
|
|
1569
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1570
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1571
|
+
}
|
|
1572
|
+
function isColdSpillArgumentsSentinel(value: unknown): value is Record<string, unknown> {
|
|
1573
|
+
return isRecord(value) && value[COLD_SPILL_ARGUMENTS_SENTINEL_KEY] === true;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function residentBlobBytesForColdSpill(value: ResidentBlobSentinel, promotion: ColdSpillResidentPromotion): Buffer {
|
|
1577
|
+
const hash = parseBlobRef(value.ref);
|
|
1578
|
+
if (!hash)
|
|
1579
|
+
throw new ResidentBlobMissingError(
|
|
1580
|
+
value.ref,
|
|
1581
|
+
value.kind,
|
|
1582
|
+
promotion.stores.sessionId,
|
|
1583
|
+
promotion.stores.sessionFile,
|
|
1584
|
+
);
|
|
1585
|
+
const store = value.kind === "text" ? promotion.stores.textStore : promotion.stores.imageStore;
|
|
1586
|
+
const data = store.getSync(hash);
|
|
1587
|
+
if (!data)
|
|
1588
|
+
throw new ResidentBlobMissingError(hash, value.kind, promotion.stores.sessionId, promotion.stores.sessionFile);
|
|
1589
|
+
promotion.stores.onResidentBlobRead?.(value.kind);
|
|
1590
|
+
if (value.kind === "imageData") return Buffer.from(data.toString("base64"), "utf8");
|
|
1591
|
+
return Buffer.from(data);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function coldSpillResidentValue(
|
|
1595
|
+
value: ResidentBlobSentinel,
|
|
1596
|
+
basePath: string,
|
|
1597
|
+
writes: ColdSpillWrite[],
|
|
1598
|
+
promotion: ColdSpillResidentPromotion,
|
|
1599
|
+
): string {
|
|
1600
|
+
const data = residentBlobBytesForColdSpill(value, promotion);
|
|
1601
|
+
writes.push({ path: basePath, encoding: "utf8", data, originalChars: data.byteLength });
|
|
1602
|
+
return COLD_SPILL_NOTICE;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function coldSpillTextValue(value: string, basePath: string, writes: ColdSpillWrite[]): string {
|
|
1606
|
+
writes.push({ path: basePath, encoding: "utf8", data: Buffer.from(value, "utf8"), originalChars: value.length });
|
|
1607
|
+
return COLD_SPILL_NOTICE;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
function coldSpillJsonValue(value: unknown, basePath: string, writes: ColdSpillWrite[]): Record<string, unknown> {
|
|
1611
|
+
const json = JSON.stringify(value);
|
|
1612
|
+
writes.push({ path: basePath, encoding: "json", data: Buffer.from(json, "utf8"), originalChars: json.length });
|
|
1613
|
+
return {
|
|
1614
|
+
[COLD_SPILL_ARGUMENTS_SENTINEL_KEY]: true,
|
|
1615
|
+
refPath: basePath,
|
|
1616
|
+
notice: COLD_SPILL_NOTICE,
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function coldSpillSubtreeValue(
|
|
1621
|
+
value: unknown,
|
|
1622
|
+
basePath: string,
|
|
1623
|
+
writes: ColdSpillWrite[],
|
|
1624
|
+
promotion: ColdSpillResidentPromotion,
|
|
1625
|
+
): unknown {
|
|
1626
|
+
if (isResidentBlobSentinel(value)) return coldSpillResidentValue(value, basePath, writes, promotion);
|
|
1627
|
+
if (isColdSpillArgumentsSentinel(value)) return value;
|
|
1628
|
+
if (typeof value === "string") {
|
|
1629
|
+
return value.length >= COLD_SPILL_MIN_CHARS ? coldSpillTextValue(value, basePath, writes) : value;
|
|
1630
|
+
}
|
|
1631
|
+
if (Array.isArray(value)) {
|
|
1632
|
+
if (!containsResidentSentinel(value)) {
|
|
1633
|
+
const json = JSON.stringify(value);
|
|
1634
|
+
return json.length >= COLD_SPILL_MIN_CHARS ? coldSpillJsonValue(value, basePath, writes) : value;
|
|
1635
|
+
}
|
|
1636
|
+
let changed = false;
|
|
1637
|
+
const next = value.map((child, index) => {
|
|
1638
|
+
const replaced = coldSpillSubtreeValue(child, `${basePath}.${index}`, writes, promotion);
|
|
1639
|
+
if (replaced !== child) changed = true;
|
|
1640
|
+
return replaced;
|
|
1641
|
+
});
|
|
1642
|
+
return changed ? next : value;
|
|
1643
|
+
}
|
|
1644
|
+
if (!isRecord(value)) return value;
|
|
1645
|
+
if (!containsResidentSentinel(value)) {
|
|
1646
|
+
const json = JSON.stringify(value);
|
|
1647
|
+
return json.length >= COLD_SPILL_MIN_CHARS ? coldSpillJsonValue(value, basePath, writes) : value;
|
|
1648
|
+
}
|
|
1649
|
+
let changed = false;
|
|
1650
|
+
const entries = Object.entries(value).map(([key, child]) => {
|
|
1651
|
+
const replaced = coldSpillSubtreeValue(child, `${basePath}.${key}`, writes, promotion);
|
|
1652
|
+
if (replaced !== child) changed = true;
|
|
1653
|
+
return [key, replaced] as const;
|
|
1654
|
+
});
|
|
1655
|
+
return changed ? Object.fromEntries(entries) : value;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function coldSpillArgumentsValue(
|
|
1659
|
+
value: unknown,
|
|
1660
|
+
basePath: string,
|
|
1661
|
+
writes: ColdSpillWrite[],
|
|
1662
|
+
promotion: ColdSpillResidentPromotion,
|
|
1663
|
+
): unknown {
|
|
1664
|
+
return coldSpillSubtreeValue(value, basePath, writes, promotion);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function coldSpillContentBlock(
|
|
1668
|
+
block: unknown,
|
|
1669
|
+
basePath: string,
|
|
1670
|
+
writes: ColdSpillWrite[],
|
|
1671
|
+
promotion: ColdSpillResidentPromotion,
|
|
1672
|
+
): unknown {
|
|
1673
|
+
if (!isRecord(block) || typeof block.type !== "string") return block;
|
|
1674
|
+
if (isResidentBlobSentinel(block)) return coldSpillResidentValue(block, basePath, writes, promotion);
|
|
1675
|
+
if (block.type === "image") return block;
|
|
1676
|
+
if (block.type === "text") {
|
|
1677
|
+
const text = block.text;
|
|
1678
|
+
if (isResidentBlobSentinel(text))
|
|
1679
|
+
return { ...block, text: coldSpillResidentValue(text, `${basePath}.text`, writes, promotion) };
|
|
1680
|
+
if (typeof text !== "string" || text.length < COLD_SPILL_MIN_CHARS) return block;
|
|
1681
|
+
return { ...block, text: coldSpillTextValue(text, `${basePath}.text`, writes) };
|
|
1682
|
+
}
|
|
1683
|
+
if (block.type === "thinking") {
|
|
1684
|
+
const thinking = block.thinking;
|
|
1685
|
+
if (typeof thinking !== "string" || thinking.length < COLD_SPILL_MIN_CHARS) return block;
|
|
1686
|
+
return { ...block, thinking: coldSpillTextValue(thinking, `${basePath}.thinking`, writes) };
|
|
1687
|
+
}
|
|
1688
|
+
if (block.type === "redactedThinking") {
|
|
1689
|
+
const data = block.data;
|
|
1690
|
+
if (typeof data !== "string" || data.length < COLD_SPILL_MIN_CHARS) return block;
|
|
1691
|
+
return { ...block, data: coldSpillTextValue(data, `${basePath}.data`, writes) };
|
|
1692
|
+
}
|
|
1693
|
+
if (block.type === "toolCall") {
|
|
1694
|
+
const args = block.arguments;
|
|
1695
|
+
if (isColdSpillArgumentsSentinel(args)) return block;
|
|
1696
|
+
const json = JSON.stringify(args);
|
|
1697
|
+
if (json.length < COLD_SPILL_MIN_CHARS && !containsResidentSentinel(args)) return block;
|
|
1698
|
+
const nextArgs = coldSpillArgumentsValue(args, `${basePath}.arguments`, writes, promotion);
|
|
1699
|
+
return nextArgs === args ? block : { ...block, arguments: nextArgs };
|
|
1700
|
+
}
|
|
1701
|
+
let changed = false;
|
|
1702
|
+
const entries = Object.entries(block).map(([key, child]) => {
|
|
1703
|
+
const replaced = key === "type" ? child : coldSpillSubtreeValue(child, `${basePath}.${key}`, writes, promotion);
|
|
1704
|
+
if (replaced !== child) changed = true;
|
|
1705
|
+
return [key, replaced] as const;
|
|
1706
|
+
});
|
|
1707
|
+
return changed ? Object.fromEntries(entries) : block;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function coldSpillContentBlocks(
|
|
1711
|
+
value: unknown[],
|
|
1712
|
+
basePath: string,
|
|
1713
|
+
writes: ColdSpillWrite[],
|
|
1714
|
+
promotion: ColdSpillResidentPromotion,
|
|
1715
|
+
): unknown {
|
|
1716
|
+
if (!containsResidentSentinel(value)) {
|
|
1717
|
+
let changedRuns = false;
|
|
1718
|
+
const merged: unknown[] = [];
|
|
1719
|
+
for (let index = 0; index < value.length; index++) {
|
|
1720
|
+
const block = value[index];
|
|
1721
|
+
if (isRecord(block) && block.type === "text" && typeof block.text === "string") {
|
|
1722
|
+
const start = index;
|
|
1723
|
+
const texts: string[] = [];
|
|
1724
|
+
while (index < value.length) {
|
|
1725
|
+
const runBlock = value[index];
|
|
1726
|
+
if (!isRecord(runBlock) || runBlock.type !== "text" || typeof runBlock.text !== "string") break;
|
|
1727
|
+
texts.push(runBlock.text);
|
|
1728
|
+
index++;
|
|
1729
|
+
}
|
|
1730
|
+
index--;
|
|
1731
|
+
const text = texts.join("");
|
|
1732
|
+
if (text.length >= COLD_SPILL_MIN_CHARS) {
|
|
1733
|
+
changedRuns = true;
|
|
1734
|
+
merged.push({ ...block, text: coldSpillTextValue(text, `${basePath}.${start}.text`, writes) });
|
|
1735
|
+
} else {
|
|
1736
|
+
merged.push(...value.slice(start, index + 1));
|
|
1737
|
+
}
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1740
|
+
const replaced = coldSpillContentBlock(block, `${basePath}.${index}`, writes, promotion);
|
|
1741
|
+
if (replaced !== block) changedRuns = true;
|
|
1742
|
+
merged.push(replaced);
|
|
1743
|
+
}
|
|
1744
|
+
if (changedRuns) return merged;
|
|
1745
|
+
}
|
|
1746
|
+
let changed = false;
|
|
1747
|
+
const next = value.map((block, index) => {
|
|
1748
|
+
const replaced = coldSpillContentBlock(block, `${basePath}.${index}`, writes, promotion);
|
|
1749
|
+
if (replaced !== block) changed = true;
|
|
1750
|
+
return replaced;
|
|
1751
|
+
});
|
|
1752
|
+
return changed ? next : value;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
function coldSpillCustomMessageContent(
|
|
1756
|
+
content: CustomMessageEntry["content"],
|
|
1757
|
+
writes: ColdSpillWrite[],
|
|
1758
|
+
promotion: ColdSpillResidentPromotion,
|
|
1759
|
+
): CustomMessageEntry["content"] {
|
|
1760
|
+
if (typeof content === "string") {
|
|
1761
|
+
return content.length >= COLD_SPILL_MIN_CHARS
|
|
1762
|
+
? coldSpillTextValue(content, "custom_message.content", writes)
|
|
1763
|
+
: content;
|
|
1764
|
+
}
|
|
1765
|
+
if (Array.isArray(content))
|
|
1766
|
+
return coldSpillContentBlocks(
|
|
1767
|
+
content,
|
|
1768
|
+
"custom_message.content",
|
|
1769
|
+
writes,
|
|
1770
|
+
promotion,
|
|
1771
|
+
) as CustomMessageEntry["content"];
|
|
1772
|
+
return content;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
function coldSpillUnavailable(ref: ColdSpillRef): string {
|
|
1776
|
+
return `[Cold-spill blob unavailable: ${ref.ref}; original ${ref.originalChars} chars unavailable]`;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function rehydrateColdSpillRef(ref: ColdSpillRef, blobStore: BlobStore, residentStores?: ResidentBlobStores): unknown {
|
|
1780
|
+
const hash = ref.ref.startsWith("blob:sha256:") ? ref.ref.slice("blob:sha256:".length) : ref.sha256;
|
|
1781
|
+
const data = blobStore.getCheckedSync(hash);
|
|
1782
|
+
if (!data || hash !== ref.sha256) return coldSpillUnavailable(ref);
|
|
1783
|
+
const text = data.toString("utf8");
|
|
1784
|
+
if (ref.encoding === "json") {
|
|
1785
|
+
try {
|
|
1786
|
+
const parsed = JSON.parse(text) as unknown;
|
|
1787
|
+
return residentStores ? materializeResidentValueSync(parsed, residentStores) : parsed;
|
|
1788
|
+
} catch {
|
|
1789
|
+
return coldSpillUnavailable(ref);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
return text;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function rehydrateColdSpillValue(
|
|
1796
|
+
value: unknown,
|
|
1797
|
+
marker: EvictedContentMarker | undefined,
|
|
1798
|
+
blobStore: BlobStore,
|
|
1799
|
+
basePath: string,
|
|
1800
|
+
residentStores?: ResidentBlobStores,
|
|
1801
|
+
): unknown {
|
|
1802
|
+
const directRef = marker?.payloads[basePath];
|
|
1803
|
+
if (directRef) return rehydrateColdSpillRef(directRef, blobStore, residentStores);
|
|
1804
|
+
if (isColdSpillArgumentsSentinel(value) && typeof value.refPath === "string") {
|
|
1805
|
+
const ref = marker?.payloads[value.refPath];
|
|
1806
|
+
return ref ? rehydrateColdSpillRef(ref, blobStore, residentStores) : value;
|
|
1807
|
+
}
|
|
1808
|
+
if (Array.isArray(value))
|
|
1809
|
+
return value.map((item, index) =>
|
|
1810
|
+
rehydrateColdSpillValue(item, marker, blobStore, `${basePath}.${index}`, residentStores),
|
|
1811
|
+
);
|
|
1812
|
+
if (!isRecord(value)) return value;
|
|
1813
|
+
const entries = Object.entries(value).map(([key, child]) => {
|
|
1814
|
+
if (key === "evictedContent") return [key, child] as const;
|
|
1815
|
+
return [key, rehydrateColdSpillValue(child, marker, blobStore, `${basePath}.${key}`, residentStores)] as const;
|
|
1816
|
+
});
|
|
1817
|
+
return Object.fromEntries(entries);
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
function rehydrateColdSpillEntry(
|
|
1821
|
+
entry: SessionEntry,
|
|
1822
|
+
blobStore: BlobStore,
|
|
1823
|
+
residentStores?: ResidentBlobStores,
|
|
1824
|
+
): SessionEntry {
|
|
1825
|
+
if (entry.type === "message") {
|
|
1826
|
+
const marker = entry.evictedContent;
|
|
1827
|
+
const message = rehydrateColdSpillValue(
|
|
1828
|
+
entry.message,
|
|
1829
|
+
marker,
|
|
1830
|
+
blobStore,
|
|
1831
|
+
"message",
|
|
1832
|
+
residentStores,
|
|
1833
|
+
) as AgentMessage;
|
|
1834
|
+
return { ...entry, message };
|
|
1835
|
+
}
|
|
1836
|
+
if (entry.type === "custom_message") {
|
|
1837
|
+
const marker = entry.evictedContent;
|
|
1838
|
+
return rehydrateColdSpillValue(entry, marker, blobStore, "custom_message", residentStores) as CustomMessageEntry;
|
|
1839
|
+
}
|
|
1840
|
+
return cloneSessionEntry(entry);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1438
1843
|
async function truncateForPersistence(obj: FileEntry, blobStore: BlobStore, key?: string): Promise<FileEntry>;
|
|
1439
1844
|
async function truncateForPersistence(obj: string, blobStore: BlobStore, key?: string): Promise<string>;
|
|
1440
1845
|
async function truncateForPersistence(obj: unknown[], blobStore: BlobStore, key?: string): Promise<unknown[]>;
|
|
@@ -2171,6 +2576,8 @@ interface SessionManagerStateSnapshot {
|
|
|
2171
2576
|
|
|
2172
2577
|
export class SessionManager {
|
|
2173
2578
|
#sessionId: string = "";
|
|
2579
|
+
/** True once a lifecycle pre-allocated id has been adopted (consume-once). */
|
|
2580
|
+
#lifecycleIdAdopted: boolean = false;
|
|
2174
2581
|
#sessionName: string | undefined;
|
|
2175
2582
|
#titleSource: "auto" | "user" | undefined;
|
|
2176
2583
|
#sessionFile: string | undefined;
|
|
@@ -2220,6 +2627,16 @@ export class SessionManager {
|
|
|
2220
2627
|
#sessionContextEntryRevision = -1;
|
|
2221
2628
|
#sessionContextLeafRevision = -1;
|
|
2222
2629
|
#sessionContextReplayMetadataRevision = -1;
|
|
2630
|
+
#coldSpillWriteCount = 0;
|
|
2631
|
+
#coldSpillReadCount = 0;
|
|
2632
|
+
#residentTextReadCount = 0;
|
|
2633
|
+
#residentImageReadCount = 0;
|
|
2634
|
+
#publicMaterializerCallCount = 0;
|
|
2635
|
+
#getEntryMaterializerCallCount = 0;
|
|
2636
|
+
#getBranchMaterializerCallCount = 0;
|
|
2637
|
+
#getEntriesMaterializerCallCount = 0;
|
|
2638
|
+
#materializedEntriesCachePopulateCount = 0;
|
|
2639
|
+
#pathOnlyContextBuildCount = 0;
|
|
2223
2640
|
|
|
2224
2641
|
private constructor(
|
|
2225
2642
|
private cwd: string,
|
|
@@ -2244,6 +2661,19 @@ export class SessionManager {
|
|
|
2244
2661
|
};
|
|
2245
2662
|
}
|
|
2246
2663
|
|
|
2664
|
+
#residentBlobStoresForColdRehydrate(): ResidentBlobStores {
|
|
2665
|
+
return {
|
|
2666
|
+
...this.#residentBlobStores(),
|
|
2667
|
+
onResidentBlobRead: kind => {
|
|
2668
|
+
if (kind === "text") {
|
|
2669
|
+
this.#residentTextReadCount++;
|
|
2670
|
+
} else {
|
|
2671
|
+
this.#residentImageReadCount++;
|
|
2672
|
+
}
|
|
2673
|
+
},
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2247
2677
|
#residentCacheDir(sessionFile: string): string {
|
|
2248
2678
|
const instance = ++residentCacheInstanceCounter;
|
|
2249
2679
|
return path.join(
|
|
@@ -2615,7 +3045,12 @@ export class SessionManager {
|
|
|
2615
3045
|
this.#persistChain = Promise.resolve();
|
|
2616
3046
|
this.#persistError = undefined;
|
|
2617
3047
|
this.#persistErrorReported = false;
|
|
2618
|
-
|
|
3048
|
+
// Adopt a lifecycle pre-allocated id exactly once (the initial session of a
|
|
3049
|
+
// /session_create child); later new-session paths (/new, fork, branch) get
|
|
3050
|
+
// fresh ids so they cannot reuse the original SKC_SESSION_ID.
|
|
3051
|
+
const preallocated = this.#lifecycleIdAdopted ? undefined : lifecyclePreallocatedSessionId();
|
|
3052
|
+
if (preallocated) this.#lifecycleIdAdopted = true;
|
|
3053
|
+
this.#sessionId = preallocated ?? createSessionId();
|
|
2619
3054
|
this.#sessionName = undefined;
|
|
2620
3055
|
this.#titleSource = undefined;
|
|
2621
3056
|
const timestamp = new Date().toISOString();
|
|
@@ -3579,8 +4014,11 @@ export class SessionManager {
|
|
|
3579
4014
|
* Returns undefined if no model change has been recorded.
|
|
3580
4015
|
*/
|
|
3581
4016
|
getLastModelChangeRole(): string | undefined {
|
|
4017
|
+
const visited = new Set<string>();
|
|
3582
4018
|
let current = this.getLeafEntry();
|
|
3583
4019
|
while (current) {
|
|
4020
|
+
if (visited.has(current.id)) break;
|
|
4021
|
+
visited.add(current.id);
|
|
3584
4022
|
if (current.type === "model_change") {
|
|
3585
4023
|
return current.role ?? "default";
|
|
3586
4024
|
}
|
|
@@ -3589,7 +4027,222 @@ export class SessionManager {
|
|
|
3589
4027
|
return undefined;
|
|
3590
4028
|
}
|
|
3591
4029
|
|
|
4030
|
+
evictCompactedContent(firstKeptEntryId: string, compactionEntryId: string): EvictCompactedContentResult {
|
|
4031
|
+
const firstKept = this.#byId.get(firstKeptEntryId);
|
|
4032
|
+
const compaction = this.#byId.get(compactionEntryId);
|
|
4033
|
+
if (!firstKept) throw new Error(`Entry ${firstKeptEntryId} not found`);
|
|
4034
|
+
if (compaction?.type !== "compaction") throw new Error(`Compaction entry ${compactionEntryId} not found`);
|
|
4035
|
+
const ids: string[] = [];
|
|
4036
|
+
const visited = new Set<string>();
|
|
4037
|
+
let current: SessionEntry | undefined = compaction;
|
|
4038
|
+
while (current) {
|
|
4039
|
+
if (visited.has(current.id)) break;
|
|
4040
|
+
visited.add(current.id);
|
|
4041
|
+
ids.push(current.id);
|
|
4042
|
+
current = current.parentId ? this.#byId.get(current.parentId) : undefined;
|
|
4043
|
+
}
|
|
4044
|
+
ids.reverse();
|
|
4045
|
+
let evictedEntries = 0;
|
|
4046
|
+
let hotCharsRemoved = 0;
|
|
4047
|
+
let coldBlobBytes = 0;
|
|
4048
|
+
let payloadRefs = 0;
|
|
4049
|
+
let alreadyEvictedEntries = 0;
|
|
4050
|
+
let mutated = false;
|
|
4051
|
+
try {
|
|
4052
|
+
for (const id of ids) {
|
|
4053
|
+
if (id === firstKeptEntryId) break;
|
|
4054
|
+
const entry = this.#byId.get(id);
|
|
4055
|
+
if (!entry || entry.type === "compaction") continue;
|
|
4056
|
+
if (entry.type !== "message" && entry.type !== "custom_message") continue;
|
|
4057
|
+
if (entry.evictedContent?.reason === "compacted_history") {
|
|
4058
|
+
alreadyEvictedEntries++;
|
|
4059
|
+
continue;
|
|
4060
|
+
}
|
|
4061
|
+
const beforeChars = JSON.stringify(entry).length;
|
|
4062
|
+
const writes: ColdSpillWrite[] = [];
|
|
4063
|
+
const nextEntry = this.#coldSpillClone(entry, writes);
|
|
4064
|
+
if (writes.length === 0 || nextEntry === entry) continue;
|
|
4065
|
+
const payloads: Record<string, ColdSpillRef> = {};
|
|
4066
|
+
for (const write of writes) {
|
|
4067
|
+
const put = this.#blobStore.putImmutableSync(write.data);
|
|
4068
|
+
this.#coldSpillWriteCount++;
|
|
4069
|
+
payloads[write.path] = {
|
|
4070
|
+
kind: "cold_spill",
|
|
4071
|
+
ref: put.ref,
|
|
4072
|
+
encoding: write.encoding,
|
|
4073
|
+
originalChars: write.originalChars,
|
|
4074
|
+
sha256: put.hash,
|
|
4075
|
+
bytes: put.bytes,
|
|
4076
|
+
};
|
|
4077
|
+
coldBlobBytes += put.bytes;
|
|
4078
|
+
}
|
|
4079
|
+
const marker: EvictedContentMarker = {
|
|
4080
|
+
evictedAt: Date.now(),
|
|
4081
|
+
reason: "compacted_history",
|
|
4082
|
+
compactionEntryId,
|
|
4083
|
+
firstKeptEntryId,
|
|
4084
|
+
payloads,
|
|
4085
|
+
};
|
|
4086
|
+
// Store the marker at the ENTRY level (session metadata), not on the
|
|
4087
|
+
// strict message type, so message shapes stay type-clean.
|
|
4088
|
+
if (nextEntry.type === "message" || nextEntry.type === "custom_message") {
|
|
4089
|
+
nextEntry.evictedContent = marker;
|
|
4090
|
+
}
|
|
4091
|
+
this.#replaceCanonicalEntry(nextEntry);
|
|
4092
|
+
mutated = true;
|
|
4093
|
+
evictedEntries++;
|
|
4094
|
+
payloadRefs += writes.length;
|
|
4095
|
+
hotCharsRemoved += Math.max(0, beforeChars - JSON.stringify(nextEntry).length);
|
|
4096
|
+
}
|
|
4097
|
+
} finally {
|
|
4098
|
+
if (mutated) {
|
|
4099
|
+
this.#needsFullRewriteOnNextPersist = true;
|
|
4100
|
+
this.#bumpEntryRevision();
|
|
4101
|
+
this.#replayMetadataRevision++;
|
|
4102
|
+
this.#materializedEntriesCache = undefined;
|
|
4103
|
+
this.#materializedEntriesRevision = -1;
|
|
4104
|
+
this.#sessionContextCache = undefined;
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
return {
|
|
4108
|
+
evictedEntries,
|
|
4109
|
+
hotCharsRemoved,
|
|
4110
|
+
coldBlobBytes,
|
|
4111
|
+
payloadRefs,
|
|
4112
|
+
alreadyEvictedEntries,
|
|
4113
|
+
coldSpillWriteCount: this.#coldSpillWriteCount,
|
|
4114
|
+
coldSpillReadCount: this.#coldSpillReadCount,
|
|
4115
|
+
residentTextReadCount: this.#residentTextReadCount,
|
|
4116
|
+
residentImageReadCount: this.#residentImageReadCount,
|
|
4117
|
+
};
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
#coldSpillClone(entry: SessionEntry, writes: ColdSpillWrite[]): SessionEntry {
|
|
4121
|
+
if (entry.type === "message") {
|
|
4122
|
+
const content = "content" in entry.message ? entry.message.content : undefined;
|
|
4123
|
+
if (!Array.isArray(content)) return entry;
|
|
4124
|
+
const nextContent = coldSpillContentBlocks(content, "message.content", writes, {
|
|
4125
|
+
stores: this.#residentBlobStoresForColdRehydrate(),
|
|
4126
|
+
});
|
|
4127
|
+
return nextContent === content
|
|
4128
|
+
? entry
|
|
4129
|
+
: { ...entry, message: { ...entry.message, content: nextContent } as AgentMessage };
|
|
4130
|
+
}
|
|
4131
|
+
if (entry.type === "custom_message") {
|
|
4132
|
+
const content = coldSpillCustomMessageContent(entry.content, writes, {
|
|
4133
|
+
stores: this.#residentBlobStoresForColdRehydrate(),
|
|
4134
|
+
});
|
|
4135
|
+
return content === entry.content ? entry : { ...entry, content };
|
|
4136
|
+
}
|
|
4137
|
+
return entry;
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
#replaceCanonicalEntry(entry: SessionEntry): void {
|
|
4141
|
+
this.#byId.set(entry.id, entry);
|
|
4142
|
+
const index = this.#fileEntries.findIndex(candidate => candidate.type !== "session" && candidate.id === entry.id);
|
|
4143
|
+
if (index >= 0) this.#fileEntries[index] = entry;
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
getObservabilityStatsForTests(): SessionManagerObservabilityStats {
|
|
4147
|
+
return {
|
|
4148
|
+
coldSpillWriteCount: this.#coldSpillWriteCount,
|
|
4149
|
+
coldSpillReadCount: this.#coldSpillReadCount,
|
|
4150
|
+
residentTextReadCount: this.#residentTextReadCount,
|
|
4151
|
+
residentImageReadCount: this.#residentImageReadCount,
|
|
4152
|
+
publicMaterializerCallCount: this.#publicMaterializerCallCount,
|
|
4153
|
+
getEntryMaterializerCallCount: this.#getEntryMaterializerCallCount,
|
|
4154
|
+
getBranchMaterializerCallCount: this.#getBranchMaterializerCallCount,
|
|
4155
|
+
getEntriesMaterializerCallCount: this.#getEntriesMaterializerCallCount,
|
|
4156
|
+
materializedEntriesCachePopulateCount: this.#materializedEntriesCachePopulateCount,
|
|
4157
|
+
pathOnlyContextBuildCount: this.#pathOnlyContextBuildCount,
|
|
4158
|
+
};
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4161
|
+
hotRetainedMessageCharsForTests(): number {
|
|
4162
|
+
let total = 0;
|
|
4163
|
+
for (const entry of this.#fileEntries) {
|
|
4164
|
+
if (entry.type !== "message" && entry.type !== "custom_message") continue;
|
|
4165
|
+
total += JSON.stringify(entry).length;
|
|
4166
|
+
}
|
|
4167
|
+
return total;
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
getCanonicalEntryForTests(id: string): SessionEntry | undefined {
|
|
4171
|
+
const entry = this.#byId.get(id);
|
|
4172
|
+
return entry ? cloneSessionEntry(entry) : undefined;
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
getEntryForFidelity(id: string): SessionEntry | undefined {
|
|
4176
|
+
const entry = this.#byId.get(id);
|
|
4177
|
+
return entry
|
|
4178
|
+
? rehydrateColdSpillEntry(
|
|
4179
|
+
materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map()),
|
|
4180
|
+
this.#blobStore,
|
|
4181
|
+
this.#residentBlobStoresForColdRehydrate(),
|
|
4182
|
+
)
|
|
4183
|
+
: undefined;
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
getBranchForFidelity(fromId?: string): SessionEntry[] {
|
|
4187
|
+
const cache = new Map<string, string>();
|
|
4188
|
+
const path: SessionEntry[] = [];
|
|
4189
|
+
const visited = new Set<string>();
|
|
4190
|
+
let current = (fromId ?? this.#leafId) ? this.#byId.get(fromId ?? this.#leafId ?? "") : undefined;
|
|
4191
|
+
while (current) {
|
|
4192
|
+
if (visited.has(current.id)) break;
|
|
4193
|
+
visited.add(current.id);
|
|
4194
|
+
path.push(
|
|
4195
|
+
rehydrateColdSpillEntry(
|
|
4196
|
+
materializeResidentEntrySync(current, this.#residentBlobStores(), cache),
|
|
4197
|
+
this.#blobStore,
|
|
4198
|
+
this.#residentBlobStoresForColdRehydrate(),
|
|
4199
|
+
),
|
|
4200
|
+
);
|
|
4201
|
+
current = current.parentId ? this.#byId.get(current.parentId) : undefined;
|
|
4202
|
+
}
|
|
4203
|
+
path.reverse();
|
|
4204
|
+
return path;
|
|
4205
|
+
}
|
|
4206
|
+
|
|
4207
|
+
#getCanonicalBranchClones(fromId?: string): SessionEntry[] {
|
|
4208
|
+
const path: SessionEntry[] = [];
|
|
4209
|
+
const visited = new Set<string>();
|
|
4210
|
+
let current = (fromId ?? this.#leafId) ? this.#byId.get(fromId ?? this.#leafId ?? "") : undefined;
|
|
4211
|
+
while (current) {
|
|
4212
|
+
if (visited.has(current.id)) break;
|
|
4213
|
+
visited.add(current.id);
|
|
4214
|
+
path.push(cloneSessionEntry(current));
|
|
4215
|
+
current = current.parentId ? this.#byId.get(current.parentId) : undefined;
|
|
4216
|
+
}
|
|
4217
|
+
path.reverse();
|
|
4218
|
+
return path;
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
/**
|
|
4222
|
+
* Walk the active branch without materializing resident blobs or rehydrating
|
|
4223
|
+
* cold-spill payloads. Intended for metadata-only scans such as todo-phase
|
|
4224
|
+
* sync; callers must not mutate returned entries.
|
|
4225
|
+
*/
|
|
4226
|
+
getActivePathEntriesCanonical(fromId?: string): SessionEntry[] {
|
|
4227
|
+
return this.#getCanonicalBranchClones(fromId);
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
getEntriesForExport(): SessionEntry[] {
|
|
4231
|
+
const cache = new Map<string, string>();
|
|
4232
|
+
return this.#fileEntries
|
|
4233
|
+
.filter((entry): entry is SessionEntry => entry.type !== "session")
|
|
4234
|
+
.map(entry =>
|
|
4235
|
+
rehydrateColdSpillEntry(
|
|
4236
|
+
materializeResidentEntrySync(entry, this.#residentBlobStores(), cache),
|
|
4237
|
+
this.#blobStore,
|
|
4238
|
+
this.#residentBlobStoresForColdRehydrate(),
|
|
4239
|
+
),
|
|
4240
|
+
);
|
|
4241
|
+
}
|
|
4242
|
+
|
|
3592
4243
|
getEntry(id: string): SessionEntry | undefined {
|
|
4244
|
+
this.#publicMaterializerCallCount++;
|
|
4245
|
+
this.#getEntryMaterializerCallCount++;
|
|
3593
4246
|
const entry = this.#byId.get(id);
|
|
3594
4247
|
return entry ? materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map()) : undefined;
|
|
3595
4248
|
}
|
|
@@ -3647,11 +4300,16 @@ export class SessionManager {
|
|
|
3647
4300
|
* Use buildSessionContext() to get the resolved messages for the LLM.
|
|
3648
4301
|
*/
|
|
3649
4302
|
getBranch(fromId?: string): SessionEntry[] {
|
|
4303
|
+
this.#publicMaterializerCallCount++;
|
|
4304
|
+
this.#getBranchMaterializerCallCount++;
|
|
3650
4305
|
const cache = new Map<string, string>();
|
|
3651
4306
|
const path: SessionEntry[] = [];
|
|
4307
|
+
const visited = new Set<string>();
|
|
3652
4308
|
const startId = fromId ?? this.#leafId;
|
|
3653
4309
|
let current = startId ? this.#byId.get(startId) : undefined;
|
|
3654
4310
|
while (current) {
|
|
4311
|
+
if (visited.has(current.id)) break;
|
|
4312
|
+
visited.add(current.id);
|
|
3655
4313
|
path.push(materializeResidentEntrySync(current, this.#residentBlobStores(), cache));
|
|
3656
4314
|
current = current.parentId ? this.#byId.get(current.parentId) : undefined;
|
|
3657
4315
|
}
|
|
@@ -3673,13 +4331,64 @@ export class SessionManager {
|
|
|
3673
4331
|
) {
|
|
3674
4332
|
return cloneSessionContext(cached);
|
|
3675
4333
|
}
|
|
3676
|
-
|
|
4334
|
+
this.#pathOnlyContextBuildCount++;
|
|
4335
|
+
const context = buildSessionContext(this.#getActivePathEntriesForProviderContext(), this.#leafId);
|
|
3677
4336
|
this.#sessionContextCache = new WeakRef(context);
|
|
3678
4337
|
this.#sessionContextEntryRevision = this.#entryRevision;
|
|
3679
4338
|
this.#sessionContextLeafRevision = this.#leafRevision;
|
|
3680
4339
|
this.#sessionContextReplayMetadataRevision = this.#replayMetadataRevision;
|
|
3681
4340
|
return cloneSessionContext(context);
|
|
3682
4341
|
}
|
|
4342
|
+
|
|
4343
|
+
#getActivePathEntriesForProviderContext(fromId?: string | null): SessionEntry[] {
|
|
4344
|
+
if (fromId === null || (fromId === undefined && this.#leafId === null)) return [];
|
|
4345
|
+
const ids: string[] = [];
|
|
4346
|
+
const visited = new Set<string>();
|
|
4347
|
+
let current = this.#byId.get(fromId ?? this.#leafId ?? "");
|
|
4348
|
+
while (current) {
|
|
4349
|
+
if (visited.has(current.id)) break;
|
|
4350
|
+
visited.add(current.id);
|
|
4351
|
+
ids.push(current.id);
|
|
4352
|
+
current = current.parentId ? this.#byId.get(current.parentId) : undefined;
|
|
4353
|
+
}
|
|
4354
|
+
ids.reverse();
|
|
4355
|
+
const pathEntries = ids
|
|
4356
|
+
.map(id => this.#byId.get(id))
|
|
4357
|
+
.filter((entry): entry is SessionEntry => entry !== undefined);
|
|
4358
|
+
let compaction: CompactionEntry | undefined;
|
|
4359
|
+
for (const entry of pathEntries) if (entry.type === "compaction") compaction = entry;
|
|
4360
|
+
if (!compaction) return pathEntries.map(entry => this.#entryForProviderContext(entry, undefined));
|
|
4361
|
+
const compactionIndex = pathEntries.findIndex(entry => entry.id === compaction.id);
|
|
4362
|
+
const firstKeptIndex = pathEntries.findIndex(entry => entry.id === compaction.firstKeptEntryId);
|
|
4363
|
+
const remote = compaction.preserveData?.openaiRemoteCompaction;
|
|
4364
|
+
const hasRemoteReplacement = isRecord(remote) && Array.isArray(remote.replacementHistory);
|
|
4365
|
+
return pathEntries.map((entry, index) => {
|
|
4366
|
+
const covered =
|
|
4367
|
+
index < compactionIndex && (hasRemoteReplacement || (firstKeptIndex >= 0 && index < firstKeptIndex));
|
|
4368
|
+
return this.#entryForProviderContext(entry, covered ? "covered" : undefined);
|
|
4369
|
+
});
|
|
4370
|
+
}
|
|
4371
|
+
|
|
4372
|
+
#entryForProviderContext(entry: SessionEntry, coldSpillPolicy: "covered" | undefined): SessionEntry {
|
|
4373
|
+
if (coldSpillPolicy === "covered" && (entry.type === "message" || entry.type === "custom_message")) {
|
|
4374
|
+
return cloneSessionEntry(entry);
|
|
4375
|
+
}
|
|
4376
|
+
if (entry.type !== "message" && entry.type !== "custom_message")
|
|
4377
|
+
return materializeProviderVisibleEntrySync(entry, this.#residentBlobStores());
|
|
4378
|
+
const materialized = materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map());
|
|
4379
|
+
const rehydrated = rehydrateColdSpillEntry(
|
|
4380
|
+
materialized,
|
|
4381
|
+
this.#blobStore,
|
|
4382
|
+
this.#residentBlobStoresForColdRehydrate(),
|
|
4383
|
+
);
|
|
4384
|
+
if (rehydrated !== materialized) this.#coldSpillReadCount += this.#countColdSpillPayloads(entry);
|
|
4385
|
+
return rehydrated;
|
|
4386
|
+
}
|
|
4387
|
+
|
|
4388
|
+
#countColdSpillPayloads(entry: SessionEntry): number {
|
|
4389
|
+
const marker = entry.type === "message" || entry.type === "custom_message" ? entry.evictedContent : undefined;
|
|
4390
|
+
return marker ? Object.keys(marker.payloads ?? {}).length : 0;
|
|
4391
|
+
}
|
|
3683
4392
|
/** Strip stale OpenAI Responses assistant replay metadata from loaded in-memory entries. */
|
|
3684
4393
|
sanitizeLoadedOpenAIResponsesReplayMetadata(): boolean {
|
|
3685
4394
|
let didSanitize = false;
|
|
@@ -3721,6 +4430,7 @@ export class SessionManager {
|
|
|
3721
4430
|
if (this.#materializedEntriesRevision === this.#entryRevision && this.#materializedEntriesCache) {
|
|
3722
4431
|
return this.#materializedEntriesCache;
|
|
3723
4432
|
}
|
|
4433
|
+
this.#materializedEntriesCachePopulateCount++;
|
|
3724
4434
|
const resolvedTextBlobCache = new Map<string, string>();
|
|
3725
4435
|
const materializedEntries = this.#fileEntries
|
|
3726
4436
|
.filter((e): e is SessionEntry => e.type !== "session")
|
|
@@ -3731,6 +4441,8 @@ export class SessionManager {
|
|
|
3731
4441
|
}
|
|
3732
4442
|
|
|
3733
4443
|
getEntries(): SessionEntry[] {
|
|
4444
|
+
this.#publicMaterializerCallCount++;
|
|
4445
|
+
this.#getEntriesMaterializerCallCount++;
|
|
3734
4446
|
return this.#getMaterializedEntriesInternal().map(entry => cloneSessionEntry(entry));
|
|
3735
4447
|
}
|
|
3736
4448
|
|
|
@@ -3750,18 +4462,48 @@ export class SessionManager {
|
|
|
3750
4462
|
nodeMap.set(entry.id, { entry, children: [], label });
|
|
3751
4463
|
}
|
|
3752
4464
|
|
|
3753
|
-
|
|
4465
|
+
const addRoot = (node: SessionTreeNode): void => {
|
|
4466
|
+
if (!roots.includes(node)) {
|
|
4467
|
+
roots.push(node);
|
|
4468
|
+
}
|
|
4469
|
+
};
|
|
4470
|
+
const removeRoot = (node: SessionTreeNode): void => {
|
|
4471
|
+
const index = roots.indexOf(node);
|
|
4472
|
+
if (index !== -1) {
|
|
4473
|
+
roots.splice(index, 1);
|
|
4474
|
+
}
|
|
4475
|
+
};
|
|
4476
|
+
const wouldCreateChildCycle = (parent: SessionTreeNode, child: SessionTreeNode): boolean => {
|
|
4477
|
+
const stack: SessionTreeNode[] = [child];
|
|
4478
|
+
const visited = new Set<SessionTreeNode>();
|
|
4479
|
+
while (stack.length > 0) {
|
|
4480
|
+
const current = stack.pop()!;
|
|
4481
|
+
if (current === parent) {
|
|
4482
|
+
return true;
|
|
4483
|
+
}
|
|
4484
|
+
if (visited.has(current)) {
|
|
4485
|
+
continue;
|
|
4486
|
+
}
|
|
4487
|
+
visited.add(current);
|
|
4488
|
+
stack.push(...current.children);
|
|
4489
|
+
}
|
|
4490
|
+
return false;
|
|
4491
|
+
};
|
|
4492
|
+
|
|
4493
|
+
// Build tree. Corrupt session files can contain duplicate IDs or parentId
|
|
4494
|
+
// cycles; reject only the edge that would make the returned tree cyclic.
|
|
3754
4495
|
for (const entry of entries) {
|
|
3755
4496
|
const node = nodeMap.get(entry.id)!;
|
|
3756
4497
|
if (entry.parentId === null || entry.parentId === entry.id) {
|
|
3757
|
-
|
|
4498
|
+
addRoot(node);
|
|
3758
4499
|
} else {
|
|
3759
4500
|
const parent = nodeMap.get(entry.parentId);
|
|
3760
|
-
if (parent) {
|
|
4501
|
+
if (parent && !wouldCreateChildCycle(parent, node)) {
|
|
3761
4502
|
parent.children.push(node);
|
|
4503
|
+
removeRoot(node);
|
|
3762
4504
|
} else {
|
|
3763
|
-
// Orphan - treat as root
|
|
3764
|
-
|
|
4505
|
+
// Orphan or cycle-closing edge - treat as root
|
|
4506
|
+
addRoot(node);
|
|
3765
4507
|
}
|
|
3766
4508
|
}
|
|
3767
4509
|
}
|
|
@@ -3769,8 +4511,13 @@ export class SessionManager {
|
|
|
3769
4511
|
// Sort children by timestamp (oldest first, newest at bottom)
|
|
3770
4512
|
// Use iterative approach to avoid stack overflow on deep trees
|
|
3771
4513
|
const stack: SessionTreeNode[] = [...roots];
|
|
4514
|
+
const sorted = new Set<SessionTreeNode>();
|
|
3772
4515
|
while (stack.length > 0) {
|
|
3773
4516
|
const node = stack.pop()!;
|
|
4517
|
+
if (sorted.has(node)) {
|
|
4518
|
+
continue;
|
|
4519
|
+
}
|
|
4520
|
+
sorted.add(node);
|
|
3774
4521
|
node.children.sort((a, b) => new Date(a.entry.timestamp).getTime() - new Date(b.entry.timestamp).getTime());
|
|
3775
4522
|
stack.push(...node.children);
|
|
3776
4523
|
}
|
|
@@ -3837,14 +4584,17 @@ export class SessionManager {
|
|
|
3837
4584
|
*/
|
|
3838
4585
|
createBranchedSession(leafId: string): string | undefined {
|
|
3839
4586
|
const previousSessionFile = this.#sessionFile;
|
|
3840
|
-
const branchPath = this
|
|
4587
|
+
const branchPath = this.#getCanonicalBranchClones(leafId);
|
|
3841
4588
|
if (branchPath.length === 0) {
|
|
3842
4589
|
throw new Error(`Entry ${leafId} not found`);
|
|
3843
4590
|
}
|
|
3844
4591
|
|
|
3845
4592
|
// Filter out LabelEntry from path - we'll recreate them from the resolved map
|
|
3846
4593
|
const pathWithoutLabels = branchPath.filter(e => e.type !== "label");
|
|
3847
|
-
|
|
4594
|
+
const materializedPathWithoutLabels = materializeResidentEntriesSync(
|
|
4595
|
+
pathWithoutLabels,
|
|
4596
|
+
this.#residentBlobStores(),
|
|
4597
|
+
);
|
|
3848
4598
|
const newSessionId = createSessionId();
|
|
3849
4599
|
const timestamp = new Date().toISOString();
|
|
3850
4600
|
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
@@ -3871,7 +4621,7 @@ export class SessionManager {
|
|
|
3871
4621
|
if (this.persist) {
|
|
3872
4622
|
const lines: string[] = [];
|
|
3873
4623
|
lines.push(JSON.stringify(header));
|
|
3874
|
-
for (const entry of
|
|
4624
|
+
for (const entry of materializedPathWithoutLabels) {
|
|
3875
4625
|
lines.push(JSON.stringify(prepareEntryForPersistenceSync(entry, this.#blobStore)));
|
|
3876
4626
|
}
|
|
3877
4627
|
// Write fresh label entries at the end
|
|
@@ -3898,7 +4648,7 @@ export class SessionManager {
|
|
|
3898
4648
|
this.#resetResidentTextBlobStore();
|
|
3899
4649
|
this.#fileEntries = [
|
|
3900
4650
|
header,
|
|
3901
|
-
...
|
|
4651
|
+
...materializedPathWithoutLabels.map(
|
|
3902
4652
|
entry => prepareEntryForResidentSync(entry, this.#residentBlobStores()) as SessionEntry,
|
|
3903
4653
|
),
|
|
3904
4654
|
...labelEntries,
|
|
@@ -3928,7 +4678,7 @@ export class SessionManager {
|
|
|
3928
4678
|
this.#resetResidentTextBlobStore();
|
|
3929
4679
|
this.#fileEntries = [
|
|
3930
4680
|
header,
|
|
3931
|
-
...
|
|
4681
|
+
...materializedPathWithoutLabels.map(
|
|
3932
4682
|
entry => prepareEntryForResidentSync(entry, this.#residentBlobStores()) as SessionEntry,
|
|
3933
4683
|
),
|
|
3934
4684
|
...labelEntries,
|