@gajae-code/coding-agent 0.7.2 → 0.7.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/CHANGELOG.md +86 -0
- package/bin/gjc.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/deep-interview/plaintext-gate-guard.d.ts +11 -0
- package/dist/types/extensibility/gjc-plugins/compiler.d.ts +19 -0
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +29 -0
- package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/injection.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/installer.d.ts +13 -0
- package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +26 -0
- package/dist/types/extensibility/gjc-plugins/observability.d.ts +27 -0
- package/dist/types/extensibility/gjc-plugins/prompt-appendix.d.ts +16 -0
- package/dist/types/extensibility/gjc-plugins/registry.d.ts +32 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +42 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +158 -2
- package/dist/types/extensibility/gjc-plugins/validation.d.ts +8 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/psmux-detect.d.ts +78 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +20 -1
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +18 -0
- 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/status-line/git-utils.d.ts +6 -0
- package/dist/types/modes/theme/defaults/index.d.ts +99 -0
- 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 +72 -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/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/composer-bash-policy.d.ts +14 -0
- package/dist/types/tools/index.d.ts +3 -1
- 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/package.json +11 -9
- package/scripts/g004-tmux-smoke.ts +100 -0
- package/scripts/g005-daemon-smoke.ts +181 -0
- package/scripts/g011-daemon-path-smoke.ts +153 -0
- package/src/cli/mcp-cli.ts +272 -0
- package/src/cli/plugin-cli.ts +66 -3
- package/src/cli.ts +27 -6
- 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 +55 -7
- package/src/deep-interview/plaintext-gate-guard.ts +94 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +1 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +7 -6
- package/src/defaults/gjc/skills/team/SKILL.md +5 -3
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +41 -13
- package/src/export/html/index.ts +2 -2
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/gjc-plugins/compiler.ts +351 -0
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +170 -0
- package/src/extensibility/gjc-plugins/index.ts +9 -0
- package/src/extensibility/gjc-plugins/injection.ts +109 -0
- package/src/extensibility/gjc-plugins/installer.ts +434 -0
- package/src/extensibility/gjc-plugins/loader.ts +3 -1
- package/src/extensibility/gjc-plugins/mcp-policy.ts +239 -0
- package/src/extensibility/gjc-plugins/observability.ts +84 -0
- package/src/extensibility/gjc-plugins/paths.ts +1 -1
- package/src/extensibility/gjc-plugins/prompt-appendix.ts +109 -0
- package/src/extensibility/gjc-plugins/registry.ts +180 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +234 -0
- package/src/extensibility/gjc-plugins/schema.ts +250 -20
- package/src/extensibility/gjc-plugins/session-validation.ts +147 -0
- package/src/extensibility/gjc-plugins/types.ts +199 -3
- package/src/extensibility/gjc-plugins/validation.ts +80 -0
- package/src/extensibility/skills.ts +15 -0
- package/src/gjc-runtime/launch-tmux.ts +61 -7
- package/src/gjc-runtime/psmux-detect.ts +239 -0
- package/src/gjc-runtime/team-runtime.ts +56 -23
- package/src/gjc-runtime/tmux-common.ts +30 -3
- package/src/gjc-runtime/tmux-sessions.ts +51 -1
- package/src/gjc-runtime/ultragoal-guard.ts +25 -8
- package/src/gjc-runtime/ultragoal-runtime.ts +75 -15
- package/src/hooks/skill-state.ts +57 -0
- package/src/internal-urls/docs-index.generated.ts +12 -8
- package/src/main.ts +14 -3
- package/src/modes/bridge/bridge-mode.ts +11 -0
- 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 +56 -11
- 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 +53 -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/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 +228 -0
- package/src/notifications/lifecycle-control-runtime.ts +400 -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/runtime-mcp/transports/stdio.ts +38 -4
- package/src/runtime-mcp/types.ts +7 -0
- package/src/sdk.ts +157 -10
- package/src/session/agent-session.ts +166 -74
- package/src/session/blob-store.ts +196 -8
- package/src/session/session-manager.ts +678 -7
- 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/composer-bash-policy.ts +96 -0
- package/src/tools/fetch.ts +18 -2
- package/src/tools/index.ts +3 -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
|
@@ -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 `GJC_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.GJC_LIFECYCLE_REQUEST_ID) return undefined;
|
|
381
|
+
const id = process.env.GJC_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++) {
|
|
@@ -1199,6 +1263,17 @@ function isResidentBlobSentinel(value: unknown): value is ResidentBlobSentinel {
|
|
|
1199
1263
|
isBlobRef((value as { ref: string }).ref)
|
|
1200
1264
|
);
|
|
1201
1265
|
}
|
|
1266
|
+
function containsResidentSentinel(value: unknown, seen = new WeakSet<object>()): boolean {
|
|
1267
|
+
if (value === null || value === undefined || typeof value !== "object") return false;
|
|
1268
|
+
if ((value as { [RESIDENT_BLOB_SENTINEL_KEY]?: unknown })[RESIDENT_BLOB_SENTINEL_KEY] === true) return true;
|
|
1269
|
+
if (seen.has(value)) return false;
|
|
1270
|
+
seen.add(value);
|
|
1271
|
+
if (Array.isArray(value)) return value.some(item => containsResidentSentinel(item, seen));
|
|
1272
|
+
for (const child of Object.values(value)) {
|
|
1273
|
+
if (containsResidentSentinel(child, seen)) return true;
|
|
1274
|
+
}
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1202
1277
|
|
|
1203
1278
|
/**
|
|
1204
1279
|
* Recursively truncate large strings in an object for session persistence.
|
|
@@ -1257,6 +1332,7 @@ interface ResidentBlobStores {
|
|
|
1257
1332
|
imageStore: BlobStore;
|
|
1258
1333
|
sessionId?: string;
|
|
1259
1334
|
sessionFile?: string;
|
|
1335
|
+
onResidentBlobRead?: (kind: ResidentBlobKind) => void;
|
|
1260
1336
|
}
|
|
1261
1337
|
|
|
1262
1338
|
type ResidentBlobMissingPolicy = "throw" | "placeholder";
|
|
@@ -1344,6 +1420,7 @@ function materializeResidentValueSync(
|
|
|
1344
1420
|
}
|
|
1345
1421
|
}
|
|
1346
1422
|
cache.set(cacheKey, resolved);
|
|
1423
|
+
stores.onResidentBlobRead?.(obj.kind);
|
|
1347
1424
|
return resolved;
|
|
1348
1425
|
}
|
|
1349
1426
|
if (Array.isArray(obj)) {
|
|
@@ -1435,6 +1512,308 @@ function cloneSessionEntry(entry: SessionEntry): SessionEntry {
|
|
|
1435
1512
|
return { ...entry, message: cloneAgentMessage(entry.message) } as SessionEntry;
|
|
1436
1513
|
}
|
|
1437
1514
|
|
|
1515
|
+
const COLD_SPILL_NOTICE = "[Compacted history content evicted to durable cold storage]";
|
|
1516
|
+
const COLD_SPILL_ARGUMENTS_SENTINEL_KEY = "__gjcColdSpillArguments";
|
|
1517
|
+
const COLD_SPILL_MIN_CHARS = 1024;
|
|
1518
|
+
|
|
1519
|
+
type ColdSpillWrite = {
|
|
1520
|
+
path: string;
|
|
1521
|
+
encoding: "utf8" | "json";
|
|
1522
|
+
data: Buffer;
|
|
1523
|
+
originalChars: number;
|
|
1524
|
+
};
|
|
1525
|
+
|
|
1526
|
+
type ColdSpillResidentPromotion = {
|
|
1527
|
+
stores: ResidentBlobStores;
|
|
1528
|
+
};
|
|
1529
|
+
|
|
1530
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1531
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
function isColdSpillRef(value: unknown): value is ColdSpillRef {
|
|
1535
|
+
return (
|
|
1536
|
+
isRecord(value) &&
|
|
1537
|
+
value.kind === "cold_spill" &&
|
|
1538
|
+
typeof value.ref === "string" &&
|
|
1539
|
+
(value.encoding === "utf8" || value.encoding === "json") &&
|
|
1540
|
+
typeof value.originalChars === "number" &&
|
|
1541
|
+
typeof value.sha256 === "string" &&
|
|
1542
|
+
typeof value.bytes === "number"
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
function isColdSpillArgumentsSentinel(value: unknown): value is Record<string, unknown> {
|
|
1547
|
+
return isRecord(value) && value[COLD_SPILL_ARGUMENTS_SENTINEL_KEY] === true;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function residentBlobBytesForColdSpill(value: ResidentBlobSentinel, promotion: ColdSpillResidentPromotion): Buffer {
|
|
1551
|
+
const hash = parseBlobRef(value.ref);
|
|
1552
|
+
if (!hash)
|
|
1553
|
+
throw new ResidentBlobMissingError(
|
|
1554
|
+
value.ref,
|
|
1555
|
+
value.kind,
|
|
1556
|
+
promotion.stores.sessionId,
|
|
1557
|
+
promotion.stores.sessionFile,
|
|
1558
|
+
);
|
|
1559
|
+
const store = value.kind === "text" ? promotion.stores.textStore : promotion.stores.imageStore;
|
|
1560
|
+
const data = store.getSync(hash);
|
|
1561
|
+
if (!data)
|
|
1562
|
+
throw new ResidentBlobMissingError(hash, value.kind, promotion.stores.sessionId, promotion.stores.sessionFile);
|
|
1563
|
+
promotion.stores.onResidentBlobRead?.(value.kind);
|
|
1564
|
+
if (value.kind === "imageData") return Buffer.from(data.toString("base64"), "utf8");
|
|
1565
|
+
return Buffer.from(data);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function coldSpillResidentValue(
|
|
1569
|
+
value: ResidentBlobSentinel,
|
|
1570
|
+
basePath: string,
|
|
1571
|
+
writes: ColdSpillWrite[],
|
|
1572
|
+
promotion: ColdSpillResidentPromotion,
|
|
1573
|
+
): string {
|
|
1574
|
+
const data = residentBlobBytesForColdSpill(value, promotion);
|
|
1575
|
+
writes.push({ path: basePath, encoding: "utf8", data, originalChars: data.byteLength });
|
|
1576
|
+
return COLD_SPILL_NOTICE;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
function coldSpillTextValue(value: string, basePath: string, writes: ColdSpillWrite[]): string {
|
|
1580
|
+
writes.push({ path: basePath, encoding: "utf8", data: Buffer.from(value, "utf8"), originalChars: value.length });
|
|
1581
|
+
return COLD_SPILL_NOTICE;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
function coldSpillJsonValue(value: unknown, basePath: string, writes: ColdSpillWrite[]): Record<string, unknown> {
|
|
1585
|
+
const json = JSON.stringify(value);
|
|
1586
|
+
writes.push({ path: basePath, encoding: "json", data: Buffer.from(json, "utf8"), originalChars: json.length });
|
|
1587
|
+
return {
|
|
1588
|
+
[COLD_SPILL_ARGUMENTS_SENTINEL_KEY]: true,
|
|
1589
|
+
refPath: basePath,
|
|
1590
|
+
notice: COLD_SPILL_NOTICE,
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function coldSpillSubtreeValue(
|
|
1595
|
+
value: unknown,
|
|
1596
|
+
basePath: string,
|
|
1597
|
+
writes: ColdSpillWrite[],
|
|
1598
|
+
promotion: ColdSpillResidentPromotion,
|
|
1599
|
+
): unknown {
|
|
1600
|
+
if (isResidentBlobSentinel(value)) return coldSpillResidentValue(value, basePath, writes, promotion);
|
|
1601
|
+
if (isColdSpillArgumentsSentinel(value)) return value;
|
|
1602
|
+
if (typeof value === "string") {
|
|
1603
|
+
return value.length >= COLD_SPILL_MIN_CHARS ? coldSpillTextValue(value, basePath, writes) : value;
|
|
1604
|
+
}
|
|
1605
|
+
if (Array.isArray(value)) {
|
|
1606
|
+
if (!containsResidentSentinel(value)) {
|
|
1607
|
+
const json = JSON.stringify(value);
|
|
1608
|
+
return json.length >= COLD_SPILL_MIN_CHARS ? coldSpillJsonValue(value, basePath, writes) : value;
|
|
1609
|
+
}
|
|
1610
|
+
let changed = false;
|
|
1611
|
+
const next = value.map((child, index) => {
|
|
1612
|
+
const replaced = coldSpillSubtreeValue(child, `${basePath}.${index}`, writes, promotion);
|
|
1613
|
+
if (replaced !== child) changed = true;
|
|
1614
|
+
return replaced;
|
|
1615
|
+
});
|
|
1616
|
+
return changed ? next : value;
|
|
1617
|
+
}
|
|
1618
|
+
if (!isRecord(value)) return value;
|
|
1619
|
+
if (!containsResidentSentinel(value)) {
|
|
1620
|
+
const json = JSON.stringify(value);
|
|
1621
|
+
return json.length >= COLD_SPILL_MIN_CHARS ? coldSpillJsonValue(value, basePath, writes) : value;
|
|
1622
|
+
}
|
|
1623
|
+
let changed = false;
|
|
1624
|
+
const entries = Object.entries(value).map(([key, child]) => {
|
|
1625
|
+
const replaced = coldSpillSubtreeValue(child, `${basePath}.${key}`, writes, promotion);
|
|
1626
|
+
if (replaced !== child) changed = true;
|
|
1627
|
+
return [key, replaced] as const;
|
|
1628
|
+
});
|
|
1629
|
+
return changed ? Object.fromEntries(entries) : value;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
function coldSpillArgumentsValue(
|
|
1633
|
+
value: unknown,
|
|
1634
|
+
basePath: string,
|
|
1635
|
+
writes: ColdSpillWrite[],
|
|
1636
|
+
promotion: ColdSpillResidentPromotion,
|
|
1637
|
+
): unknown {
|
|
1638
|
+
return coldSpillSubtreeValue(value, basePath, writes, promotion);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function coldSpillContentBlock(
|
|
1642
|
+
block: unknown,
|
|
1643
|
+
basePath: string,
|
|
1644
|
+
writes: ColdSpillWrite[],
|
|
1645
|
+
promotion: ColdSpillResidentPromotion,
|
|
1646
|
+
): unknown {
|
|
1647
|
+
if (!isRecord(block) || typeof block.type !== "string") return block;
|
|
1648
|
+
if (isResidentBlobSentinel(block)) return coldSpillResidentValue(block, basePath, writes, promotion);
|
|
1649
|
+
if (block.type === "image") return block;
|
|
1650
|
+
if (block.type === "text") {
|
|
1651
|
+
const text = block.text;
|
|
1652
|
+
if (isResidentBlobSentinel(text))
|
|
1653
|
+
return { ...block, text: coldSpillResidentValue(text, `${basePath}.text`, writes, promotion) };
|
|
1654
|
+
if (typeof text !== "string" || text.length < COLD_SPILL_MIN_CHARS) return block;
|
|
1655
|
+
return { ...block, text: coldSpillTextValue(text, `${basePath}.text`, writes) };
|
|
1656
|
+
}
|
|
1657
|
+
if (block.type === "thinking") {
|
|
1658
|
+
const thinking = block.thinking;
|
|
1659
|
+
if (typeof thinking !== "string" || thinking.length < COLD_SPILL_MIN_CHARS) return block;
|
|
1660
|
+
return { ...block, thinking: coldSpillTextValue(thinking, `${basePath}.thinking`, writes) };
|
|
1661
|
+
}
|
|
1662
|
+
if (block.type === "redactedThinking") {
|
|
1663
|
+
const data = block.data;
|
|
1664
|
+
if (typeof data !== "string" || data.length < COLD_SPILL_MIN_CHARS) return block;
|
|
1665
|
+
return { ...block, data: coldSpillTextValue(data, `${basePath}.data`, writes) };
|
|
1666
|
+
}
|
|
1667
|
+
if (block.type === "toolCall") {
|
|
1668
|
+
const args = block.arguments;
|
|
1669
|
+
if (isColdSpillArgumentsSentinel(args)) return block;
|
|
1670
|
+
const json = JSON.stringify(args);
|
|
1671
|
+
if (json.length < COLD_SPILL_MIN_CHARS && !containsResidentSentinel(args)) return block;
|
|
1672
|
+
const nextArgs = coldSpillArgumentsValue(args, `${basePath}.arguments`, writes, promotion);
|
|
1673
|
+
return nextArgs === args ? block : { ...block, arguments: nextArgs };
|
|
1674
|
+
}
|
|
1675
|
+
let changed = false;
|
|
1676
|
+
const entries = Object.entries(block).map(([key, child]) => {
|
|
1677
|
+
const replaced = key === "type" ? child : coldSpillSubtreeValue(child, `${basePath}.${key}`, writes, promotion);
|
|
1678
|
+
if (replaced !== child) changed = true;
|
|
1679
|
+
return [key, replaced] as const;
|
|
1680
|
+
});
|
|
1681
|
+
return changed ? Object.fromEntries(entries) : block;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
function coldSpillContentBlocks(
|
|
1685
|
+
value: unknown[],
|
|
1686
|
+
basePath: string,
|
|
1687
|
+
writes: ColdSpillWrite[],
|
|
1688
|
+
promotion: ColdSpillResidentPromotion,
|
|
1689
|
+
): unknown {
|
|
1690
|
+
if (!containsResidentSentinel(value)) {
|
|
1691
|
+
let changedRuns = false;
|
|
1692
|
+
const merged: unknown[] = [];
|
|
1693
|
+
for (let index = 0; index < value.length; index++) {
|
|
1694
|
+
const block = value[index];
|
|
1695
|
+
if (isRecord(block) && block.type === "text" && typeof block.text === "string") {
|
|
1696
|
+
const start = index;
|
|
1697
|
+
const texts: string[] = [];
|
|
1698
|
+
while (index < value.length) {
|
|
1699
|
+
const runBlock = value[index];
|
|
1700
|
+
if (!isRecord(runBlock) || runBlock.type !== "text" || typeof runBlock.text !== "string") break;
|
|
1701
|
+
texts.push(runBlock.text);
|
|
1702
|
+
index++;
|
|
1703
|
+
}
|
|
1704
|
+
index--;
|
|
1705
|
+
const text = texts.join("");
|
|
1706
|
+
if (text.length >= COLD_SPILL_MIN_CHARS) {
|
|
1707
|
+
changedRuns = true;
|
|
1708
|
+
merged.push({ ...block, text: coldSpillTextValue(text, `${basePath}.${start}.text`, writes) });
|
|
1709
|
+
} else {
|
|
1710
|
+
merged.push(...value.slice(start, index + 1));
|
|
1711
|
+
}
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1714
|
+
const replaced = coldSpillContentBlock(block, `${basePath}.${index}`, writes, promotion);
|
|
1715
|
+
if (replaced !== block) changedRuns = true;
|
|
1716
|
+
merged.push(replaced);
|
|
1717
|
+
}
|
|
1718
|
+
if (changedRuns) return merged;
|
|
1719
|
+
}
|
|
1720
|
+
let changed = false;
|
|
1721
|
+
const next = value.map((block, index) => {
|
|
1722
|
+
const replaced = coldSpillContentBlock(block, `${basePath}.${index}`, writes, promotion);
|
|
1723
|
+
if (replaced !== block) changed = true;
|
|
1724
|
+
return replaced;
|
|
1725
|
+
});
|
|
1726
|
+
return changed ? next : value;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function coldSpillCustomMessageContent(
|
|
1730
|
+
content: CustomMessageEntry["content"],
|
|
1731
|
+
writes: ColdSpillWrite[],
|
|
1732
|
+
promotion: ColdSpillResidentPromotion,
|
|
1733
|
+
): CustomMessageEntry["content"] {
|
|
1734
|
+
if (typeof content === "string") {
|
|
1735
|
+
return content.length >= COLD_SPILL_MIN_CHARS
|
|
1736
|
+
? coldSpillTextValue(content, "custom_message.content", writes)
|
|
1737
|
+
: content;
|
|
1738
|
+
}
|
|
1739
|
+
if (Array.isArray(content))
|
|
1740
|
+
return coldSpillContentBlocks(
|
|
1741
|
+
content,
|
|
1742
|
+
"custom_message.content",
|
|
1743
|
+
writes,
|
|
1744
|
+
promotion,
|
|
1745
|
+
) as CustomMessageEntry["content"];
|
|
1746
|
+
return content;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
function coldSpillUnavailable(ref: ColdSpillRef): string {
|
|
1750
|
+
return `[Cold-spill blob unavailable: ${ref.ref}; original ${ref.originalChars} chars unavailable]`;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
function rehydrateColdSpillRef(ref: ColdSpillRef, blobStore: BlobStore, residentStores?: ResidentBlobStores): unknown {
|
|
1754
|
+
const hash = ref.ref.startsWith("blob:sha256:") ? ref.ref.slice("blob:sha256:".length) : ref.sha256;
|
|
1755
|
+
const data = blobStore.getCheckedSync(hash);
|
|
1756
|
+
if (!data || hash !== ref.sha256) return coldSpillUnavailable(ref);
|
|
1757
|
+
const text = data.toString("utf8");
|
|
1758
|
+
if (ref.encoding === "json") {
|
|
1759
|
+
try {
|
|
1760
|
+
const parsed = JSON.parse(text) as unknown;
|
|
1761
|
+
return residentStores ? materializeResidentValueSync(parsed, residentStores) : parsed;
|
|
1762
|
+
} catch {
|
|
1763
|
+
return coldSpillUnavailable(ref);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
return text;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
function rehydrateColdSpillValue(
|
|
1770
|
+
value: unknown,
|
|
1771
|
+
marker: EvictedContentMarker | undefined,
|
|
1772
|
+
blobStore: BlobStore,
|
|
1773
|
+
basePath: string,
|
|
1774
|
+
residentStores?: ResidentBlobStores,
|
|
1775
|
+
): unknown {
|
|
1776
|
+
const directRef = marker?.payloads[basePath];
|
|
1777
|
+
if (directRef) return rehydrateColdSpillRef(directRef, blobStore, residentStores);
|
|
1778
|
+
if (isColdSpillArgumentsSentinel(value) && typeof value.refPath === "string") {
|
|
1779
|
+
const ref = marker?.payloads[value.refPath];
|
|
1780
|
+
return ref ? rehydrateColdSpillRef(ref, blobStore, residentStores) : value;
|
|
1781
|
+
}
|
|
1782
|
+
if (Array.isArray(value))
|
|
1783
|
+
return value.map((item, index) =>
|
|
1784
|
+
rehydrateColdSpillValue(item, marker, blobStore, `${basePath}.${index}`, residentStores),
|
|
1785
|
+
);
|
|
1786
|
+
if (!isRecord(value)) return value;
|
|
1787
|
+
const entries = Object.entries(value).map(([key, child]) => {
|
|
1788
|
+
if (key === "evictedContent") return [key, child] as const;
|
|
1789
|
+
return [key, rehydrateColdSpillValue(child, marker, blobStore, `${basePath}.${key}`, residentStores)] as const;
|
|
1790
|
+
});
|
|
1791
|
+
return Object.fromEntries(entries);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
function rehydrateColdSpillEntry(
|
|
1795
|
+
entry: SessionEntry,
|
|
1796
|
+
blobStore: BlobStore,
|
|
1797
|
+
residentStores?: ResidentBlobStores,
|
|
1798
|
+
): SessionEntry {
|
|
1799
|
+
if (entry.type === "message") {
|
|
1800
|
+
const marker = entry.evictedContent;
|
|
1801
|
+
const message = rehydrateColdSpillValue(
|
|
1802
|
+
entry.message,
|
|
1803
|
+
marker,
|
|
1804
|
+
blobStore,
|
|
1805
|
+
"message",
|
|
1806
|
+
residentStores,
|
|
1807
|
+
) as AgentMessage;
|
|
1808
|
+
return { ...entry, message };
|
|
1809
|
+
}
|
|
1810
|
+
if (entry.type === "custom_message") {
|
|
1811
|
+
const marker = entry.evictedContent;
|
|
1812
|
+
return rehydrateColdSpillValue(entry, marker, blobStore, "custom_message", residentStores) as CustomMessageEntry;
|
|
1813
|
+
}
|
|
1814
|
+
return cloneSessionEntry(entry);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1438
1817
|
async function truncateForPersistence(obj: FileEntry, blobStore: BlobStore, key?: string): Promise<FileEntry>;
|
|
1439
1818
|
async function truncateForPersistence(obj: string, blobStore: BlobStore, key?: string): Promise<string>;
|
|
1440
1819
|
async function truncateForPersistence(obj: unknown[], blobStore: BlobStore, key?: string): Promise<unknown[]>;
|
|
@@ -2171,6 +2550,8 @@ interface SessionManagerStateSnapshot {
|
|
|
2171
2550
|
|
|
2172
2551
|
export class SessionManager {
|
|
2173
2552
|
#sessionId: string = "";
|
|
2553
|
+
/** True once a lifecycle pre-allocated id has been adopted (consume-once). */
|
|
2554
|
+
#lifecycleIdAdopted: boolean = false;
|
|
2174
2555
|
#sessionName: string | undefined;
|
|
2175
2556
|
#titleSource: "auto" | "user" | undefined;
|
|
2176
2557
|
#sessionFile: string | undefined;
|
|
@@ -2220,6 +2601,16 @@ export class SessionManager {
|
|
|
2220
2601
|
#sessionContextEntryRevision = -1;
|
|
2221
2602
|
#sessionContextLeafRevision = -1;
|
|
2222
2603
|
#sessionContextReplayMetadataRevision = -1;
|
|
2604
|
+
#coldSpillWriteCount = 0;
|
|
2605
|
+
#coldSpillReadCount = 0;
|
|
2606
|
+
#residentTextReadCount = 0;
|
|
2607
|
+
#residentImageReadCount = 0;
|
|
2608
|
+
#publicMaterializerCallCount = 0;
|
|
2609
|
+
#getEntryMaterializerCallCount = 0;
|
|
2610
|
+
#getBranchMaterializerCallCount = 0;
|
|
2611
|
+
#getEntriesMaterializerCallCount = 0;
|
|
2612
|
+
#materializedEntriesCachePopulateCount = 0;
|
|
2613
|
+
#pathOnlyContextBuildCount = 0;
|
|
2223
2614
|
|
|
2224
2615
|
private constructor(
|
|
2225
2616
|
private cwd: string,
|
|
@@ -2244,6 +2635,19 @@ export class SessionManager {
|
|
|
2244
2635
|
};
|
|
2245
2636
|
}
|
|
2246
2637
|
|
|
2638
|
+
#residentBlobStoresForColdRehydrate(): ResidentBlobStores {
|
|
2639
|
+
return {
|
|
2640
|
+
...this.#residentBlobStores(),
|
|
2641
|
+
onResidentBlobRead: kind => {
|
|
2642
|
+
if (kind === "text") {
|
|
2643
|
+
this.#residentTextReadCount++;
|
|
2644
|
+
} else {
|
|
2645
|
+
this.#residentImageReadCount++;
|
|
2646
|
+
}
|
|
2647
|
+
},
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2247
2651
|
#residentCacheDir(sessionFile: string): string {
|
|
2248
2652
|
const instance = ++residentCacheInstanceCounter;
|
|
2249
2653
|
return path.join(
|
|
@@ -2615,7 +3019,12 @@ export class SessionManager {
|
|
|
2615
3019
|
this.#persistChain = Promise.resolve();
|
|
2616
3020
|
this.#persistError = undefined;
|
|
2617
3021
|
this.#persistErrorReported = false;
|
|
2618
|
-
|
|
3022
|
+
// Adopt a lifecycle pre-allocated id exactly once (the initial session of a
|
|
3023
|
+
// /session_create child); later new-session paths (/new, fork, branch) get
|
|
3024
|
+
// fresh ids so they cannot reuse the original GJC_SESSION_ID.
|
|
3025
|
+
const preallocated = this.#lifecycleIdAdopted ? undefined : lifecyclePreallocatedSessionId();
|
|
3026
|
+
if (preallocated) this.#lifecycleIdAdopted = true;
|
|
3027
|
+
this.#sessionId = preallocated ?? createSessionId();
|
|
2619
3028
|
this.#sessionName = undefined;
|
|
2620
3029
|
this.#titleSource = undefined;
|
|
2621
3030
|
const timestamp = new Date().toISOString();
|
|
@@ -3589,7 +3998,214 @@ export class SessionManager {
|
|
|
3589
3998
|
return undefined;
|
|
3590
3999
|
}
|
|
3591
4000
|
|
|
4001
|
+
evictCompactedContent(firstKeptEntryId: string, compactionEntryId: string): EvictCompactedContentResult {
|
|
4002
|
+
const firstKept = this.#byId.get(firstKeptEntryId);
|
|
4003
|
+
const compaction = this.#byId.get(compactionEntryId);
|
|
4004
|
+
if (!firstKept) throw new Error(`Entry ${firstKeptEntryId} not found`);
|
|
4005
|
+
if (!compaction || compaction.type !== "compaction")
|
|
4006
|
+
throw new Error(`Compaction entry ${compactionEntryId} not found`);
|
|
4007
|
+
const ids: string[] = [];
|
|
4008
|
+
let current: SessionEntry | undefined = compaction;
|
|
4009
|
+
while (current) {
|
|
4010
|
+
ids.push(current.id);
|
|
4011
|
+
current = current.parentId ? this.#byId.get(current.parentId) : undefined;
|
|
4012
|
+
}
|
|
4013
|
+
ids.reverse();
|
|
4014
|
+
let evictedEntries = 0;
|
|
4015
|
+
let hotCharsRemoved = 0;
|
|
4016
|
+
let coldBlobBytes = 0;
|
|
4017
|
+
let payloadRefs = 0;
|
|
4018
|
+
let alreadyEvictedEntries = 0;
|
|
4019
|
+
let mutated = false;
|
|
4020
|
+
try {
|
|
4021
|
+
for (const id of ids) {
|
|
4022
|
+
if (id === firstKeptEntryId) break;
|
|
4023
|
+
const entry = this.#byId.get(id);
|
|
4024
|
+
if (!entry || entry.type === "compaction") continue;
|
|
4025
|
+
if (entry.type !== "message" && entry.type !== "custom_message") continue;
|
|
4026
|
+
if (entry.evictedContent?.reason === "compacted_history") {
|
|
4027
|
+
alreadyEvictedEntries++;
|
|
4028
|
+
continue;
|
|
4029
|
+
}
|
|
4030
|
+
const beforeChars = JSON.stringify(entry).length;
|
|
4031
|
+
const writes: ColdSpillWrite[] = [];
|
|
4032
|
+
const nextEntry = this.#coldSpillClone(entry, writes);
|
|
4033
|
+
if (writes.length === 0 || nextEntry === entry) continue;
|
|
4034
|
+
const payloads: Record<string, ColdSpillRef> = {};
|
|
4035
|
+
for (const write of writes) {
|
|
4036
|
+
const put = this.#blobStore.putImmutableSync(write.data);
|
|
4037
|
+
this.#coldSpillWriteCount++;
|
|
4038
|
+
payloads[write.path] = {
|
|
4039
|
+
kind: "cold_spill",
|
|
4040
|
+
ref: put.ref,
|
|
4041
|
+
encoding: write.encoding,
|
|
4042
|
+
originalChars: write.originalChars,
|
|
4043
|
+
sha256: put.hash,
|
|
4044
|
+
bytes: put.bytes,
|
|
4045
|
+
};
|
|
4046
|
+
coldBlobBytes += put.bytes;
|
|
4047
|
+
}
|
|
4048
|
+
const marker: EvictedContentMarker = {
|
|
4049
|
+
evictedAt: Date.now(),
|
|
4050
|
+
reason: "compacted_history",
|
|
4051
|
+
compactionEntryId,
|
|
4052
|
+
firstKeptEntryId,
|
|
4053
|
+
payloads,
|
|
4054
|
+
};
|
|
4055
|
+
// Store the marker at the ENTRY level (session metadata), not on the
|
|
4056
|
+
// strict message type, so message shapes stay type-clean.
|
|
4057
|
+
if (nextEntry.type === "message" || nextEntry.type === "custom_message") {
|
|
4058
|
+
nextEntry.evictedContent = marker;
|
|
4059
|
+
}
|
|
4060
|
+
this.#replaceCanonicalEntry(nextEntry);
|
|
4061
|
+
mutated = true;
|
|
4062
|
+
evictedEntries++;
|
|
4063
|
+
payloadRefs += writes.length;
|
|
4064
|
+
hotCharsRemoved += Math.max(0, beforeChars - JSON.stringify(nextEntry).length);
|
|
4065
|
+
}
|
|
4066
|
+
} finally {
|
|
4067
|
+
if (mutated) {
|
|
4068
|
+
this.#needsFullRewriteOnNextPersist = true;
|
|
4069
|
+
this.#bumpEntryRevision();
|
|
4070
|
+
this.#replayMetadataRevision++;
|
|
4071
|
+
this.#materializedEntriesCache = undefined;
|
|
4072
|
+
this.#materializedEntriesRevision = -1;
|
|
4073
|
+
this.#sessionContextCache = undefined;
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
return {
|
|
4077
|
+
evictedEntries,
|
|
4078
|
+
hotCharsRemoved,
|
|
4079
|
+
coldBlobBytes,
|
|
4080
|
+
payloadRefs,
|
|
4081
|
+
alreadyEvictedEntries,
|
|
4082
|
+
coldSpillWriteCount: this.#coldSpillWriteCount,
|
|
4083
|
+
coldSpillReadCount: this.#coldSpillReadCount,
|
|
4084
|
+
residentTextReadCount: this.#residentTextReadCount,
|
|
4085
|
+
residentImageReadCount: this.#residentImageReadCount,
|
|
4086
|
+
};
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4089
|
+
#coldSpillClone(entry: SessionEntry, writes: ColdSpillWrite[]): SessionEntry {
|
|
4090
|
+
if (entry.type === "message") {
|
|
4091
|
+
const content = "content" in entry.message ? entry.message.content : undefined;
|
|
4092
|
+
if (!Array.isArray(content)) return entry;
|
|
4093
|
+
const nextContent = coldSpillContentBlocks(content, "message.content", writes, {
|
|
4094
|
+
stores: this.#residentBlobStoresForColdRehydrate(),
|
|
4095
|
+
});
|
|
4096
|
+
return nextContent === content
|
|
4097
|
+
? entry
|
|
4098
|
+
: { ...entry, message: { ...entry.message, content: nextContent } as AgentMessage };
|
|
4099
|
+
}
|
|
4100
|
+
if (entry.type === "custom_message") {
|
|
4101
|
+
const content = coldSpillCustomMessageContent(entry.content, writes, {
|
|
4102
|
+
stores: this.#residentBlobStoresForColdRehydrate(),
|
|
4103
|
+
});
|
|
4104
|
+
return content === entry.content ? entry : { ...entry, content };
|
|
4105
|
+
}
|
|
4106
|
+
return entry;
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
#replaceCanonicalEntry(entry: SessionEntry): void {
|
|
4110
|
+
this.#byId.set(entry.id, entry);
|
|
4111
|
+
const index = this.#fileEntries.findIndex(candidate => candidate.type !== "session" && candidate.id === entry.id);
|
|
4112
|
+
if (index >= 0) this.#fileEntries[index] = entry;
|
|
4113
|
+
}
|
|
4114
|
+
|
|
4115
|
+
getObservabilityStatsForTests(): SessionManagerObservabilityStats {
|
|
4116
|
+
return {
|
|
4117
|
+
coldSpillWriteCount: this.#coldSpillWriteCount,
|
|
4118
|
+
coldSpillReadCount: this.#coldSpillReadCount,
|
|
4119
|
+
residentTextReadCount: this.#residentTextReadCount,
|
|
4120
|
+
residentImageReadCount: this.#residentImageReadCount,
|
|
4121
|
+
publicMaterializerCallCount: this.#publicMaterializerCallCount,
|
|
4122
|
+
getEntryMaterializerCallCount: this.#getEntryMaterializerCallCount,
|
|
4123
|
+
getBranchMaterializerCallCount: this.#getBranchMaterializerCallCount,
|
|
4124
|
+
getEntriesMaterializerCallCount: this.#getEntriesMaterializerCallCount,
|
|
4125
|
+
materializedEntriesCachePopulateCount: this.#materializedEntriesCachePopulateCount,
|
|
4126
|
+
pathOnlyContextBuildCount: this.#pathOnlyContextBuildCount,
|
|
4127
|
+
};
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
hotRetainedMessageCharsForTests(): number {
|
|
4131
|
+
let total = 0;
|
|
4132
|
+
for (const entry of this.#fileEntries) {
|
|
4133
|
+
if (entry.type !== "message" && entry.type !== "custom_message") continue;
|
|
4134
|
+
total += JSON.stringify(entry).length;
|
|
4135
|
+
}
|
|
4136
|
+
return total;
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
getCanonicalEntryForTests(id: string): SessionEntry | undefined {
|
|
4140
|
+
const entry = this.#byId.get(id);
|
|
4141
|
+
return entry ? cloneSessionEntry(entry) : undefined;
|
|
4142
|
+
}
|
|
4143
|
+
|
|
4144
|
+
getEntryForFidelity(id: string): SessionEntry | undefined {
|
|
4145
|
+
const entry = this.#byId.get(id);
|
|
4146
|
+
return entry
|
|
4147
|
+
? rehydrateColdSpillEntry(
|
|
4148
|
+
materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map()),
|
|
4149
|
+
this.#blobStore,
|
|
4150
|
+
this.#residentBlobStoresForColdRehydrate(),
|
|
4151
|
+
)
|
|
4152
|
+
: undefined;
|
|
4153
|
+
}
|
|
4154
|
+
|
|
4155
|
+
getBranchForFidelity(fromId?: string): SessionEntry[] {
|
|
4156
|
+
const cache = new Map<string, string>();
|
|
4157
|
+
const path: SessionEntry[] = [];
|
|
4158
|
+
let current = (fromId ?? this.#leafId) ? this.#byId.get(fromId ?? this.#leafId ?? "") : undefined;
|
|
4159
|
+
while (current) {
|
|
4160
|
+
path.push(
|
|
4161
|
+
rehydrateColdSpillEntry(
|
|
4162
|
+
materializeResidentEntrySync(current, this.#residentBlobStores(), cache),
|
|
4163
|
+
this.#blobStore,
|
|
4164
|
+
this.#residentBlobStoresForColdRehydrate(),
|
|
4165
|
+
),
|
|
4166
|
+
);
|
|
4167
|
+
current = current.parentId ? this.#byId.get(current.parentId) : undefined;
|
|
4168
|
+
}
|
|
4169
|
+
path.reverse();
|
|
4170
|
+
return path;
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4173
|
+
#getCanonicalBranchClones(fromId?: string): SessionEntry[] {
|
|
4174
|
+
const path: SessionEntry[] = [];
|
|
4175
|
+
let current = (fromId ?? this.#leafId) ? this.#byId.get(fromId ?? this.#leafId ?? "") : undefined;
|
|
4176
|
+
while (current) {
|
|
4177
|
+
path.push(cloneSessionEntry(current));
|
|
4178
|
+
current = current.parentId ? this.#byId.get(current.parentId) : undefined;
|
|
4179
|
+
}
|
|
4180
|
+
path.reverse();
|
|
4181
|
+
return path;
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
/**
|
|
4185
|
+
* Walk the active branch without materializing resident blobs or rehydrating
|
|
4186
|
+
* cold-spill payloads. Intended for metadata-only scans such as todo-phase
|
|
4187
|
+
* sync; callers must not mutate returned entries.
|
|
4188
|
+
*/
|
|
4189
|
+
getActivePathEntriesCanonical(fromId?: string): SessionEntry[] {
|
|
4190
|
+
return this.#getCanonicalBranchClones(fromId);
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
getEntriesForExport(): SessionEntry[] {
|
|
4194
|
+
const cache = new Map<string, string>();
|
|
4195
|
+
return this.#fileEntries
|
|
4196
|
+
.filter((entry): entry is SessionEntry => entry.type !== "session")
|
|
4197
|
+
.map(entry =>
|
|
4198
|
+
rehydrateColdSpillEntry(
|
|
4199
|
+
materializeResidentEntrySync(entry, this.#residentBlobStores(), cache),
|
|
4200
|
+
this.#blobStore,
|
|
4201
|
+
this.#residentBlobStoresForColdRehydrate(),
|
|
4202
|
+
),
|
|
4203
|
+
);
|
|
4204
|
+
}
|
|
4205
|
+
|
|
3592
4206
|
getEntry(id: string): SessionEntry | undefined {
|
|
4207
|
+
this.#publicMaterializerCallCount++;
|
|
4208
|
+
this.#getEntryMaterializerCallCount++;
|
|
3593
4209
|
const entry = this.#byId.get(id);
|
|
3594
4210
|
return entry ? materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map()) : undefined;
|
|
3595
4211
|
}
|
|
@@ -3647,6 +4263,8 @@ export class SessionManager {
|
|
|
3647
4263
|
* Use buildSessionContext() to get the resolved messages for the LLM.
|
|
3648
4264
|
*/
|
|
3649
4265
|
getBranch(fromId?: string): SessionEntry[] {
|
|
4266
|
+
this.#publicMaterializerCallCount++;
|
|
4267
|
+
this.#getBranchMaterializerCallCount++;
|
|
3650
4268
|
const cache = new Map<string, string>();
|
|
3651
4269
|
const path: SessionEntry[] = [];
|
|
3652
4270
|
const startId = fromId ?? this.#leafId;
|
|
@@ -3673,13 +4291,60 @@ export class SessionManager {
|
|
|
3673
4291
|
) {
|
|
3674
4292
|
return cloneSessionContext(cached);
|
|
3675
4293
|
}
|
|
3676
|
-
|
|
4294
|
+
this.#pathOnlyContextBuildCount++;
|
|
4295
|
+
const context = buildSessionContext(this.#getActivePathEntriesForProviderContext(), this.#leafId);
|
|
3677
4296
|
this.#sessionContextCache = new WeakRef(context);
|
|
3678
4297
|
this.#sessionContextEntryRevision = this.#entryRevision;
|
|
3679
4298
|
this.#sessionContextLeafRevision = this.#leafRevision;
|
|
3680
4299
|
this.#sessionContextReplayMetadataRevision = this.#replayMetadataRevision;
|
|
3681
4300
|
return cloneSessionContext(context);
|
|
3682
4301
|
}
|
|
4302
|
+
|
|
4303
|
+
#getActivePathEntriesForProviderContext(fromId?: string | null): SessionEntry[] {
|
|
4304
|
+
if (fromId === null || (fromId === undefined && this.#leafId === null)) return [];
|
|
4305
|
+
const ids: string[] = [];
|
|
4306
|
+
let current = this.#byId.get(fromId ?? this.#leafId ?? "");
|
|
4307
|
+
while (current) {
|
|
4308
|
+
ids.push(current.id);
|
|
4309
|
+
current = current.parentId ? this.#byId.get(current.parentId) : undefined;
|
|
4310
|
+
}
|
|
4311
|
+
ids.reverse();
|
|
4312
|
+
const pathEntries = ids
|
|
4313
|
+
.map(id => this.#byId.get(id))
|
|
4314
|
+
.filter((entry): entry is SessionEntry => entry !== undefined);
|
|
4315
|
+
let compaction: CompactionEntry | undefined;
|
|
4316
|
+
for (const entry of pathEntries) if (entry.type === "compaction") compaction = entry;
|
|
4317
|
+
if (!compaction) return pathEntries.map(entry => this.#entryForProviderContext(entry, undefined));
|
|
4318
|
+
const compactionIndex = pathEntries.findIndex(entry => entry.id === compaction.id);
|
|
4319
|
+
const firstKeptIndex = pathEntries.findIndex(entry => entry.id === compaction.firstKeptEntryId);
|
|
4320
|
+
const remote = compaction.preserveData?.openaiRemoteCompaction;
|
|
4321
|
+
const hasRemoteReplacement = isRecord(remote) && Array.isArray(remote.replacementHistory);
|
|
4322
|
+
return pathEntries.map((entry, index) => {
|
|
4323
|
+
const covered =
|
|
4324
|
+
index < compactionIndex && (hasRemoteReplacement || (firstKeptIndex >= 0 && index < firstKeptIndex));
|
|
4325
|
+
return this.#entryForProviderContext(entry, covered ? "covered" : undefined);
|
|
4326
|
+
});
|
|
4327
|
+
}
|
|
4328
|
+
|
|
4329
|
+
#entryForProviderContext(entry: SessionEntry, coldSpillPolicy: "covered" | undefined): SessionEntry {
|
|
4330
|
+
if (coldSpillPolicy === "covered" && (entry.type === "message" || entry.type === "custom_message")) {
|
|
4331
|
+
return cloneSessionEntry(entry);
|
|
4332
|
+
}
|
|
4333
|
+
if (entry.type !== "message" && entry.type !== "custom_message") return cloneSessionEntry(entry);
|
|
4334
|
+
const materialized = materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map());
|
|
4335
|
+
const rehydrated = rehydrateColdSpillEntry(
|
|
4336
|
+
materialized,
|
|
4337
|
+
this.#blobStore,
|
|
4338
|
+
this.#residentBlobStoresForColdRehydrate(),
|
|
4339
|
+
);
|
|
4340
|
+
if (rehydrated !== materialized) this.#coldSpillReadCount += this.#countColdSpillPayloads(entry);
|
|
4341
|
+
return rehydrated;
|
|
4342
|
+
}
|
|
4343
|
+
|
|
4344
|
+
#countColdSpillPayloads(entry: SessionEntry): number {
|
|
4345
|
+
const marker = entry.type === "message" || entry.type === "custom_message" ? entry.evictedContent : undefined;
|
|
4346
|
+
return marker ? Object.keys(marker.payloads ?? {}).length : 0;
|
|
4347
|
+
}
|
|
3683
4348
|
/** Strip stale OpenAI Responses assistant replay metadata from loaded in-memory entries. */
|
|
3684
4349
|
sanitizeLoadedOpenAIResponsesReplayMetadata(): boolean {
|
|
3685
4350
|
let didSanitize = false;
|
|
@@ -3721,6 +4386,7 @@ export class SessionManager {
|
|
|
3721
4386
|
if (this.#materializedEntriesRevision === this.#entryRevision && this.#materializedEntriesCache) {
|
|
3722
4387
|
return this.#materializedEntriesCache;
|
|
3723
4388
|
}
|
|
4389
|
+
this.#materializedEntriesCachePopulateCount++;
|
|
3724
4390
|
const resolvedTextBlobCache = new Map<string, string>();
|
|
3725
4391
|
const materializedEntries = this.#fileEntries
|
|
3726
4392
|
.filter((e): e is SessionEntry => e.type !== "session")
|
|
@@ -3731,6 +4397,8 @@ export class SessionManager {
|
|
|
3731
4397
|
}
|
|
3732
4398
|
|
|
3733
4399
|
getEntries(): SessionEntry[] {
|
|
4400
|
+
this.#publicMaterializerCallCount++;
|
|
4401
|
+
this.#getEntriesMaterializerCallCount++;
|
|
3734
4402
|
return this.#getMaterializedEntriesInternal().map(entry => cloneSessionEntry(entry));
|
|
3735
4403
|
}
|
|
3736
4404
|
|
|
@@ -3837,14 +4505,17 @@ export class SessionManager {
|
|
|
3837
4505
|
*/
|
|
3838
4506
|
createBranchedSession(leafId: string): string | undefined {
|
|
3839
4507
|
const previousSessionFile = this.#sessionFile;
|
|
3840
|
-
const branchPath = this
|
|
4508
|
+
const branchPath = this.#getCanonicalBranchClones(leafId);
|
|
3841
4509
|
if (branchPath.length === 0) {
|
|
3842
4510
|
throw new Error(`Entry ${leafId} not found`);
|
|
3843
4511
|
}
|
|
3844
4512
|
|
|
3845
4513
|
// Filter out LabelEntry from path - we'll recreate them from the resolved map
|
|
3846
4514
|
const pathWithoutLabels = branchPath.filter(e => e.type !== "label");
|
|
3847
|
-
|
|
4515
|
+
const materializedPathWithoutLabels = materializeResidentEntriesSync(
|
|
4516
|
+
pathWithoutLabels,
|
|
4517
|
+
this.#residentBlobStores(),
|
|
4518
|
+
);
|
|
3848
4519
|
const newSessionId = createSessionId();
|
|
3849
4520
|
const timestamp = new Date().toISOString();
|
|
3850
4521
|
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
@@ -3871,7 +4542,7 @@ export class SessionManager {
|
|
|
3871
4542
|
if (this.persist) {
|
|
3872
4543
|
const lines: string[] = [];
|
|
3873
4544
|
lines.push(JSON.stringify(header));
|
|
3874
|
-
for (const entry of
|
|
4545
|
+
for (const entry of materializedPathWithoutLabels) {
|
|
3875
4546
|
lines.push(JSON.stringify(prepareEntryForPersistenceSync(entry, this.#blobStore)));
|
|
3876
4547
|
}
|
|
3877
4548
|
// Write fresh label entries at the end
|
|
@@ -3898,7 +4569,7 @@ export class SessionManager {
|
|
|
3898
4569
|
this.#resetResidentTextBlobStore();
|
|
3899
4570
|
this.#fileEntries = [
|
|
3900
4571
|
header,
|
|
3901
|
-
...
|
|
4572
|
+
...materializedPathWithoutLabels.map(
|
|
3902
4573
|
entry => prepareEntryForResidentSync(entry, this.#residentBlobStores()) as SessionEntry,
|
|
3903
4574
|
),
|
|
3904
4575
|
...labelEntries,
|
|
@@ -3928,7 +4599,7 @@ export class SessionManager {
|
|
|
3928
4599
|
this.#resetResidentTextBlobStore();
|
|
3929
4600
|
this.#fileEntries = [
|
|
3930
4601
|
header,
|
|
3931
|
-
...
|
|
4602
|
+
...materializedPathWithoutLabels.map(
|
|
3932
4603
|
entry => prepareEntryForResidentSync(entry, this.#residentBlobStores()) as SessionEntry,
|
|
3933
4604
|
),
|
|
3934
4605
|
...labelEntries,
|