@tea-agent/loop-agent 0.40.0-next.11 → 0.40.0-next.13
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 +45 -0
- package/README.md +1 -1
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +9 -5
- package/dist/cli/program.js +38 -3
- package/dist/commands/client-recovery.js +8 -36
- package/dist/commands/task-advance.js +23 -1
- package/dist/commands/task-source-prepare.js +5 -0
- package/dist/commands/task-status.js +1 -0
- package/dist/executors/pi-sdk-executor.js +33 -5
- package/dist/shared/operator/capabilities.js +9 -1
- package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
- package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
- package/dist/shared/pi-context-pressure/env.js +58 -0
- package/dist/shared/pi-context-pressure/extension.js +100 -0
- package/dist/shared/pi-context-pressure/index.js +7 -0
- package/dist/shared/pi-context-pressure/overflow.js +252 -0
- package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
- package/dist/shared/pi-context-pressure/telemetry.js +51 -0
- package/dist/worker/console/chat/assistant-content.js +7 -0
- package/dist/worker/console/chat/chat-event-store.js +82 -11
- package/dist/worker/console/chat/goal-round-driver.js +72 -0
- package/dist/worker/console/chat/goals.js +300 -0
- package/dist/worker/console/chat/pi-runtime.js +285 -8
- package/dist/worker/console/chat/provider-error.js +2 -1
- package/dist/worker/console/chat/repo-browser.js +94 -12
- package/dist/worker/console/chat/routes.js +485 -29
- package/dist/worker/console/chat/session-catalog.js +48 -0
- package/dist/worker/console/chat/session-store.js +27 -18
- package/dist/worker/console/chat/shortcuts.js +7 -0
- package/dist/worker/console/chat/sift-bridge.js +1 -0
- package/dist/worker/console/chat/turn-process.js +32 -3
- package/dist/worker/console/operator-actions.js +16 -2
- package/dist/worker/console/operator-selection.js +6 -2
- package/dist/worker/console/pi-plugins.catalog.json +73658 -0
- package/dist/worker/console/pi-plugins.js +100 -0
- package/dist/worker/console/routes.js +221 -6
- package/dist/worker/console/server.js +172 -42
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bs-CDAXM.js → abnfDiagram-N423BO3Z-Wj-fsQlk.js} +1 -1
- package/dist/worker/console/static/assets/{arc-CAkA3We3.js → arc-CkXT-vzQ.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DEmI_zqr.js → architectureDiagram-T3A2C74G-xlfnojWU.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-uDHANebE.js → blockDiagram-VBNYF7ZC-BilahvuB.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Bv97b4_B.js → c4Diagram-5PPSVZJV-DqtAVhjz.js} +1 -1
- package/dist/worker/console/static/assets/channel-SnQ_u-Td.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-shmQKsFz.js → chunk-2GRJ4B5K-pAqQXZ5y.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BAraNvcN.js → chunk-2Q5K7J3B-BEWxeq2r.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BirhxUop.js → chunk-5RXB4S5H-CvlbGDTp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C9LgUJpR.js → chunk-5VM5RSS4-D0uGd9-f.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DrU-s-t7.js → chunk-6Q2QTUOP-DSdWIteg.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Tx-H_FA2.js → chunk-GF5L2VYU-Dweg7f0v.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BjLtNFrR.js → chunk-JWPE2WC7-BpZKI_k7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-UNLVwSsy.js → chunk-KBJHAD2P-IPLknC4m.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CBEUX8XD.js → chunk-RYQCIY6F-E93k9lVI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-WTL7IZOq.js → chunk-XXDRQBXY-BrESwkPq.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-SGM51NVY.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-SGM51NVY.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CkHG6W3s.js → cose-bilkent-JH36ORCC-DmFHvJkB.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BRzFqTaL.js → cynefin-VYW2F7L2-CDigXiSL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2_CFp_i7.js → cynefinDiagram-MW4NZA55-B0zhrbaA.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJtsmdkE.js → dagre-VZM6K2ZE-DrKybVxm.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DNEFFo6w.js → diagram-7IWD3JNH-CeUrnbFk.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B_IWV7eo.js → diagram-B4RE2ZJO-BJyunUMi.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DvvRx2dz.js → diagram-LBJQPF4R-a5TBP73u.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CsibsWLg.js → diagram-Q27KOJAE-ChRgBURJ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-les_djBQ.js → diagram-UB23O5K3-CqCnPRSk.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BEm2n7qJ.js → ebnfDiagram-BXEA7PRR-BI-UD9_S.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BI7vHHVm.js → erDiagram-JOGREHBK-CDLvvkYZ.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CUFCERrB.js → flowDiagram-UKHOOZJN-Heoo4afj.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-wxyLiGWt.js → ganttDiagram-PKOTCBZU-kFpOitHd.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bt897K8Z.js → gitGraphDiagram-DS77QQ5N-FvyZ9WAY.js} +1 -1
- package/dist/worker/console/static/assets/index-C80SO8y0.js +437 -0
- package/dist/worker/console/static/assets/index-CmJCHmJP.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DYNx_IPv.js → infoDiagram-6WML65LV-HBzlwtgI.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CzpLNEu6.js → ishikawaDiagram-WSZJBQD7-C9Iim5sm.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-97f9owNU.js → journeyDiagram-NVQOT4AX-CJwJTg0S.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BxM9hyBn.js → kanban-definition-27J2QSJJ-CZNJ7S4q.js} +1 -1
- package/dist/worker/console/static/assets/{linear-ByuxcHvp.js → linear-RZ0AnGi8.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DwPGvpCv.js → mermaid.core-EYSdm-nd.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DCoqG9RT.js → mindmap-definition-FAOFIHXS-D0OLECFN.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BLSBmBMx.js → pegDiagram-VL7TDLO6-CPTvGbh7.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B0t_0oJW.js → pieDiagram-7S7Q4E2Y-Ddc1aDea.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js → quadrantDiagram-CIZ2JOQS-CWCTCdAt.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-B3jRdATC.js → railroadDiagram-AXF67PYL-CfsaBL8E.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-cTtXEN.js → requirementDiagram-LRYGKXZP-BH2rqGf6.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B25igJRE.js → sankeyDiagram-W5VNT64P-BZgJ1Til.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-jW_lM1_D.js → sequenceDiagram-SI44F4Z6-DmPr-o-y.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CrzTgsHE.js → sizeCapture-X5ZJPWSS-CF6HzDh_.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-B01MvlqE.js → stateDiagram-OKZ733FA-DhS8N_B9.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-N56zxwsk.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-YcF12FiU.js → swimlanes-SLNWSIFB-CYNzoiaD.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-o47kGgVt.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ClnRcg2s.js → timeline-definition-Z64GVDOM-QBICF5qH.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-B6gr_z-q.js → vennDiagram-T6HMQDX7-fquBvWcf.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CvSgguGH.js → wardleyDiagram-T6FBY63Y-BYaJLlSd.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-23OtSZCb.js → xychartDiagram-ELKLHX3M--ePe_JCJ.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +95 -25
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +35 -1
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +3 -1
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +24 -19
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +8 -1
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
- package/dist/worker/console/static-src/operator-chat/workspace-reorder.js +18 -0
- package/dist/worker/console/static-src/shell/useWorkspaces.js +150 -34
- package/dist/worker/console/workspace-context.js +28 -3
- package/dist/worker/console/workspace-registry.js +333 -60
- package/dist/worker/observe/static/inspect-bootstrap.d.ts +8 -0
- package/dist/worker/observe/static/inspect-bootstrap.js +57 -0
- package/dist/worker/observe/static/inspect-workspace.js +54 -10
- package/dist/worker/observe/static/operator-chrome.css +23 -4
- package/dist/worker/observe/static/operator-chrome.js +6 -2
- package/dist/worker/observe/static/styles.css +87 -25
- package/dist/worker/observe/static/views/dag-inspector.js +42 -1
- package/dist/worker/observe/static/views/dags.js +7 -5
- package/dist/worker/observe/static/views/session-timeline.js +135 -0
- package/dist/workflows/dag/init-hybrid.js +3 -3
- package/docs/architecture/runtime-boundaries.md +2 -1
- package/docs/templates/backend-test-dag.json +3 -3
- package/package.json +3 -2
- package/skills/loop-agent/references/command-reference.md +14 -0
- package/dist/worker/console/static/assets/channel-amxUpk7o.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +0 -1
- package/dist/worker/console/static/assets/index-BHhUOTri.css +0 -1
- package/dist/worker/console/static/assets/index-DqJbO3-p.js +0 -407
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +0 -8
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseBoundedNumber, parseEnabledFlag } from "./env.js";
|
|
5
|
+
import { emptyContextPressureCounters, incrementCounter, } from "./telemetry.js";
|
|
6
|
+
export const SIFT_POLICY_VERSION = "sift-v1-query-undefined";
|
|
7
|
+
export const SIFT_RETRIEVE_TOOL_NAME = "sift_retrieve";
|
|
8
|
+
export const SIFT_STASH_KEY_PATTERN = /^[0-9a-f]{24}$/;
|
|
9
|
+
export const SIFT_RETRIEVE_DEFAULT_BYTES = 24 * 1024;
|
|
10
|
+
export const SIFT_RETRIEVE_MAX_BYTES = 48 * 1024;
|
|
11
|
+
export const DEFAULT_MEMO_MAX_ENTRIES = 128;
|
|
12
|
+
export const DEFAULT_MEMO_MAX_BYTES = 32 * 1024 * 1024;
|
|
13
|
+
export const DEFAULT_STASH_RETENTION_DAYS = 14;
|
|
14
|
+
class BoundedMemo {
|
|
15
|
+
maxEntries;
|
|
16
|
+
maxBytes;
|
|
17
|
+
entries = new Map();
|
|
18
|
+
bytes = 0;
|
|
19
|
+
constructor(maxEntries, maxBytes) {
|
|
20
|
+
this.maxEntries = maxEntries;
|
|
21
|
+
this.maxBytes = maxBytes;
|
|
22
|
+
}
|
|
23
|
+
get(key) {
|
|
24
|
+
const hit = this.entries.get(key);
|
|
25
|
+
if (!hit)
|
|
26
|
+
return undefined;
|
|
27
|
+
this.entries.delete(key);
|
|
28
|
+
this.entries.set(key, hit);
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(hit.json);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
set(key, value) {
|
|
37
|
+
const json = JSON.stringify(value);
|
|
38
|
+
const size = Buffer.byteLength(json);
|
|
39
|
+
if (size > this.maxBytes)
|
|
40
|
+
return;
|
|
41
|
+
this.delete(key);
|
|
42
|
+
while (this.entries.size >= this.maxEntries ||
|
|
43
|
+
this.bytes + size > this.maxBytes) {
|
|
44
|
+
const oldest = this.entries.keys().next().value;
|
|
45
|
+
if (oldest === undefined)
|
|
46
|
+
break;
|
|
47
|
+
this.delete(oldest);
|
|
48
|
+
}
|
|
49
|
+
this.entries.set(key, { json, bytes: size });
|
|
50
|
+
this.bytes += size;
|
|
51
|
+
}
|
|
52
|
+
delete(key) {
|
|
53
|
+
const current = this.entries.get(key);
|
|
54
|
+
if (!current)
|
|
55
|
+
return;
|
|
56
|
+
this.entries.delete(key);
|
|
57
|
+
this.bytes -= current.bytes;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function parseSiftBridgeConfig(input) {
|
|
61
|
+
const env = input.env ?? process.env;
|
|
62
|
+
const override = env.HARNESS_CONSOLE_SIFT_STASH_DIR?.trim();
|
|
63
|
+
const root = override || input.defaultStashRoot;
|
|
64
|
+
const stashDir = path.join(root, input.sessionId);
|
|
65
|
+
const entries = parseBoundedNumber(env.HARNESS_CONSOLE_SIFT_MEMO_MAX_ENTRIES, DEFAULT_MEMO_MAX_ENTRIES, 8, 4096);
|
|
66
|
+
const bytes = parseBoundedNumber(env.HARNESS_CONSOLE_SIFT_MEMO_MAX_BYTES, DEFAULT_MEMO_MAX_BYTES, 1024, 256 * 1024 * 1024);
|
|
67
|
+
const retention = parseBoundedNumber(env.HARNESS_CONSOLE_SIFT_STASH_RETENTION_DAYS, DEFAULT_STASH_RETENTION_DAYS, 1, 365);
|
|
68
|
+
return {
|
|
69
|
+
sessionId: input.sessionId,
|
|
70
|
+
stashDir,
|
|
71
|
+
memoMaxEntries: entries.value,
|
|
72
|
+
memoMaxBytes: bytes.value,
|
|
73
|
+
retentionDays: retention.value,
|
|
74
|
+
...(input.siftFactory ? { siftFactory: input.siftFactory } : {}),
|
|
75
|
+
...(input.importSift ? { importSift: input.importSift } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function createSiftBridgeState(config) {
|
|
79
|
+
return {
|
|
80
|
+
sessionId: config.sessionId,
|
|
81
|
+
stashDir: config.stashDir,
|
|
82
|
+
config,
|
|
83
|
+
counters: emptyContextPressureCounters(),
|
|
84
|
+
retrieveBypassIds: new Set(),
|
|
85
|
+
diagnostics: new Set(),
|
|
86
|
+
memo: new BoundedMemo(config.memoMaxEntries, config.memoMaxBytes),
|
|
87
|
+
importFailed: false,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function noteOnce(state, code) {
|
|
91
|
+
if (state.diagnostics.has(code))
|
|
92
|
+
return;
|
|
93
|
+
state.diagnostics.add(code);
|
|
94
|
+
incrementCounter(state.counters, "fail-open");
|
|
95
|
+
}
|
|
96
|
+
function memoKey(payload) {
|
|
97
|
+
return createHash("sha256")
|
|
98
|
+
.update(SIFT_POLICY_VERSION)
|
|
99
|
+
.update("\0")
|
|
100
|
+
.update(JSON.stringify(payload))
|
|
101
|
+
.digest("hex");
|
|
102
|
+
}
|
|
103
|
+
function ensureStashDir(stashDir) {
|
|
104
|
+
mkdirSync(stashDir, { recursive: true, mode: 0o700 });
|
|
105
|
+
}
|
|
106
|
+
async function ensureSift(state) {
|
|
107
|
+
if (state.sift)
|
|
108
|
+
return state.sift;
|
|
109
|
+
if (state.importFailed)
|
|
110
|
+
return undefined;
|
|
111
|
+
try {
|
|
112
|
+
ensureStashDir(state.stashDir);
|
|
113
|
+
if (state.config.siftFactory) {
|
|
114
|
+
state.sift = state.config.siftFactory({ stashDir: state.stashDir });
|
|
115
|
+
return state.sift;
|
|
116
|
+
}
|
|
117
|
+
const importer = state.config.importSift ??
|
|
118
|
+
(async () => (await import("@agent-context/sift")));
|
|
119
|
+
const mod = await importer();
|
|
120
|
+
if (typeof mod.createSift !== "function") {
|
|
121
|
+
state.importFailed = true;
|
|
122
|
+
noteOnce(state, "sift-import-invalid");
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
state.sift = mod.createSift({ stashDir: state.stashDir });
|
|
126
|
+
return state.sift;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
state.importFailed = true;
|
|
130
|
+
noteOnce(state, "sift-import-failed");
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function isRecord(value) {
|
|
135
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
136
|
+
}
|
|
137
|
+
function hasTrailingAnthropicCacheAnchor(payload) {
|
|
138
|
+
if (!isRecord(payload) || !Array.isArray(payload.messages))
|
|
139
|
+
return false;
|
|
140
|
+
const last = payload.messages[payload.messages.length - 1];
|
|
141
|
+
if (!isRecord(last))
|
|
142
|
+
return false;
|
|
143
|
+
const content = last.content;
|
|
144
|
+
if (!Array.isArray(content) || content.length === 0)
|
|
145
|
+
return false;
|
|
146
|
+
const lastBlock = content[content.length - 1];
|
|
147
|
+
return isRecord(lastBlock) && lastBlock.cache_control !== undefined;
|
|
148
|
+
}
|
|
149
|
+
function restoreProtectedToolResults(originalPayload, compressed, protectedIds) {
|
|
150
|
+
if (protectedIds.size === 0)
|
|
151
|
+
return compressed;
|
|
152
|
+
const original = structuredClone(originalPayload);
|
|
153
|
+
const next = structuredClone(compressed);
|
|
154
|
+
restoreInMessages(original, next, protectedIds);
|
|
155
|
+
restoreInResponsesInput(original, next, protectedIds);
|
|
156
|
+
return next;
|
|
157
|
+
}
|
|
158
|
+
function restoreInMessages(original, next, protectedIds) {
|
|
159
|
+
if (!isRecord(original) || !isRecord(next))
|
|
160
|
+
return;
|
|
161
|
+
if (!Array.isArray(original.messages) || !Array.isArray(next.messages))
|
|
162
|
+
return;
|
|
163
|
+
for (let index = 0; index < next.messages.length; index += 1) {
|
|
164
|
+
const origMsg = original.messages[index];
|
|
165
|
+
const nextMsg = next.messages[index];
|
|
166
|
+
if (!isRecord(origMsg) || !isRecord(nextMsg))
|
|
167
|
+
continue;
|
|
168
|
+
const origId = typeof origMsg.tool_call_id === "string"
|
|
169
|
+
? origMsg.tool_call_id
|
|
170
|
+
: undefined;
|
|
171
|
+
if (origId && protectedIds.has(origId)) {
|
|
172
|
+
next.messages[index] = origMsg;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (Array.isArray(origMsg.content) && Array.isArray(nextMsg.content)) {
|
|
176
|
+
for (let blockIndex = 0; blockIndex < nextMsg.content.length; blockIndex += 1) {
|
|
177
|
+
const origBlock = origMsg.content[blockIndex];
|
|
178
|
+
if (!isRecord(origBlock))
|
|
179
|
+
continue;
|
|
180
|
+
const toolUseId = typeof origBlock.tool_use_id === "string"
|
|
181
|
+
? origBlock.tool_use_id
|
|
182
|
+
: undefined;
|
|
183
|
+
if (toolUseId && protectedIds.has(toolUseId)) {
|
|
184
|
+
nextMsg.content[blockIndex] = origBlock;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function restoreInResponsesInput(original, next, protectedIds) {
|
|
191
|
+
if (!isRecord(original) || !isRecord(next))
|
|
192
|
+
return;
|
|
193
|
+
if (!Array.isArray(original.input) || !Array.isArray(next.input))
|
|
194
|
+
return;
|
|
195
|
+
for (let index = 0; index < next.input.length; index += 1) {
|
|
196
|
+
const origItem = original.input[index];
|
|
197
|
+
if (!isRecord(origItem))
|
|
198
|
+
continue;
|
|
199
|
+
const callId = typeof origItem.call_id === "string" ? origItem.call_id : undefined;
|
|
200
|
+
if (callId && protectedIds.has(callId)) {
|
|
201
|
+
next.input[index] = origItem;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export async function compressProviderPayload(state, payload) {
|
|
206
|
+
if (payload === undefined)
|
|
207
|
+
return undefined;
|
|
208
|
+
if (!isRecord(payload)) {
|
|
209
|
+
noteOnce(state, "unknown-format");
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
const key = memoKey(payload);
|
|
213
|
+
const cached = state.memo.get(key);
|
|
214
|
+
if (cached !== undefined)
|
|
215
|
+
return cached;
|
|
216
|
+
if (hasTrailingAnthropicCacheAnchor(payload)) {
|
|
217
|
+
const sift = await ensureSift(state);
|
|
218
|
+
if (!sift)
|
|
219
|
+
return undefined;
|
|
220
|
+
try {
|
|
221
|
+
const result = sift.siftRequest(payload, undefined);
|
|
222
|
+
if (result.changed === false &&
|
|
223
|
+
result.blocksExamined === 0 &&
|
|
224
|
+
result.frozenMessages ===
|
|
225
|
+
(Array.isArray(payload.messages)
|
|
226
|
+
? (payload.messages.length)
|
|
227
|
+
: 0)) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
if (!result.changed)
|
|
231
|
+
return undefined;
|
|
232
|
+
noteOnce(state, "anthropic-unexpected-change");
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
noteOnce(state, "sift-handler-error");
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const sift = await ensureSift(state);
|
|
241
|
+
if (!sift)
|
|
242
|
+
return undefined;
|
|
243
|
+
try {
|
|
244
|
+
const result = sift.siftRequest(payload, undefined);
|
|
245
|
+
if (!result || result.changed !== true)
|
|
246
|
+
return undefined;
|
|
247
|
+
const restored = restoreProtectedToolResults(payload, result.body, state.retrieveBypassIds);
|
|
248
|
+
state.counters.tokensSavedEstimate += Math.max(0, result.tokensSaved ?? 0);
|
|
249
|
+
state.counters.blocksCompressed += Math.max(0, result.blocksCompressed ?? 0);
|
|
250
|
+
incrementCounter(state.counters, "wire-compression");
|
|
251
|
+
state.memo.set(key, restored);
|
|
252
|
+
return restored;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
noteOnce(state, "sift-handler-error");
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
export function clearRetrieveBypass(state) {
|
|
260
|
+
state.retrieveBypassIds.clear();
|
|
261
|
+
}
|
|
262
|
+
export function sliceUtf8Range(text, offset, maxBytes) {
|
|
263
|
+
const totalBytes = Buffer.byteLength(text, "utf8");
|
|
264
|
+
const start = Math.max(0, Math.floor(offset));
|
|
265
|
+
const limit = Math.min(SIFT_RETRIEVE_MAX_BYTES, Math.max(1, Math.floor(maxBytes)));
|
|
266
|
+
if (start >= totalBytes) {
|
|
267
|
+
return { slice: "", nextOffset: null, totalBytes, complete: true };
|
|
268
|
+
}
|
|
269
|
+
const buffer = Buffer.from(text, "utf8");
|
|
270
|
+
let end = Math.min(totalBytes, start + limit);
|
|
271
|
+
while (end > start && (buffer[end] & 0b1100_0000) === 0b1000_0000) {
|
|
272
|
+
end -= 1;
|
|
273
|
+
}
|
|
274
|
+
const slice = buffer.subarray(start, end).toString("utf8");
|
|
275
|
+
const complete = end >= totalBytes;
|
|
276
|
+
return {
|
|
277
|
+
slice,
|
|
278
|
+
nextOffset: complete ? null : end,
|
|
279
|
+
totalBytes,
|
|
280
|
+
complete,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
export async function executeSiftRetrieve(state, toolCallId, params) {
|
|
284
|
+
const key = typeof params.key === "string" ? params.key.trim() : "";
|
|
285
|
+
if (!SIFT_STASH_KEY_PATTERN.test(key)) {
|
|
286
|
+
return {
|
|
287
|
+
content: [
|
|
288
|
+
{
|
|
289
|
+
type: "text",
|
|
290
|
+
text: JSON.stringify({
|
|
291
|
+
error: "STASH_NOT_FOUND_OR_EXPIRED",
|
|
292
|
+
complete: true,
|
|
293
|
+
}),
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const sift = await ensureSift(state);
|
|
299
|
+
const original = sift?.retrieve(key) ?? null;
|
|
300
|
+
if (original === null) {
|
|
301
|
+
return {
|
|
302
|
+
content: [
|
|
303
|
+
{
|
|
304
|
+
type: "text",
|
|
305
|
+
text: JSON.stringify({
|
|
306
|
+
error: "STASH_NOT_FOUND_OR_EXPIRED",
|
|
307
|
+
complete: true,
|
|
308
|
+
}),
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const offset = typeof params.offset === "number" && Number.isFinite(params.offset)
|
|
314
|
+
? Math.max(0, Math.floor(params.offset))
|
|
315
|
+
: 0;
|
|
316
|
+
const maxBytes = typeof params.maxBytes === "number" && Number.isFinite(params.maxBytes)
|
|
317
|
+
? params.maxBytes
|
|
318
|
+
: SIFT_RETRIEVE_DEFAULT_BYTES;
|
|
319
|
+
const page = sliceUtf8Range(original, offset, maxBytes);
|
|
320
|
+
if (toolCallId)
|
|
321
|
+
state.retrieveBypassIds.add(toolCallId);
|
|
322
|
+
return {
|
|
323
|
+
content: [
|
|
324
|
+
{
|
|
325
|
+
type: "text",
|
|
326
|
+
text: JSON.stringify({
|
|
327
|
+
key,
|
|
328
|
+
offset,
|
|
329
|
+
maxBytes: Math.min(SIFT_RETRIEVE_MAX_BYTES, Math.max(1, Math.floor(maxBytes))),
|
|
330
|
+
totalBytes: page.totalBytes,
|
|
331
|
+
nextOffset: page.nextOffset,
|
|
332
|
+
complete: page.complete,
|
|
333
|
+
text: page.slice,
|
|
334
|
+
}),
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
export function housekeepSiftStash(input) {
|
|
340
|
+
let removed = 0;
|
|
341
|
+
try {
|
|
342
|
+
const entries = readdirSync(input.root, { withFileTypes: true });
|
|
343
|
+
const cutoff = (input.now ?? Date.now()) - input.retentionDays * 24 * 60 * 60 * 1000;
|
|
344
|
+
for (const entry of entries) {
|
|
345
|
+
if (!entry.isDirectory())
|
|
346
|
+
continue;
|
|
347
|
+
if (input.activeSessionId && entry.name === input.activeSessionId)
|
|
348
|
+
continue;
|
|
349
|
+
const full = path.join(input.root, entry.name);
|
|
350
|
+
let mtime = 0;
|
|
351
|
+
try {
|
|
352
|
+
mtime = statSync(full).mtimeMs;
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (mtime <= cutoff) {
|
|
358
|
+
rmSync(full, { recursive: true, force: true });
|
|
359
|
+
removed += 1;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return { removed, failed: false };
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return { removed, failed: true };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
export function createSiftRetrieveToolDefinition(state) {
|
|
369
|
+
return {
|
|
370
|
+
name: SIFT_RETRIEVE_TOOL_NAME,
|
|
371
|
+
label: "Sift Retrieve",
|
|
372
|
+
description: "Retrieve a previously compressed tool-output page by 24-hex stash key. Use offset/maxBytes to page; default 24KiB, max 48KiB.",
|
|
373
|
+
parameters: {
|
|
374
|
+
type: "object",
|
|
375
|
+
additionalProperties: false,
|
|
376
|
+
properties: {
|
|
377
|
+
key: { type: "string", description: "24-hex stash key" },
|
|
378
|
+
offset: { type: "number", description: "UTF-8 byte offset" },
|
|
379
|
+
maxBytes: { type: "number", description: "Page size in bytes" },
|
|
380
|
+
},
|
|
381
|
+
required: ["key"],
|
|
382
|
+
},
|
|
383
|
+
execute: (toolCallId, params) => executeSiftRetrieve(state, toolCallId, params),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
export { parseEnabledFlag };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function emptyContextPressureCounters() {
|
|
2
|
+
return {
|
|
3
|
+
prePromptThreshold: 0,
|
|
4
|
+
overflow: 0,
|
|
5
|
+
manual: 0,
|
|
6
|
+
wireCompression: 0,
|
|
7
|
+
failOpen: 0,
|
|
8
|
+
usageUnavailable: 0,
|
|
9
|
+
l0CompactFailed: 0,
|
|
10
|
+
tokensSavedEstimate: 0,
|
|
11
|
+
blocksCompressed: 0,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function incrementCounter(counters, reason, amount = 1) {
|
|
15
|
+
switch (reason) {
|
|
16
|
+
case "pre-prompt-threshold":
|
|
17
|
+
counters.prePromptThreshold += amount;
|
|
18
|
+
break;
|
|
19
|
+
case "overflow":
|
|
20
|
+
counters.overflow += amount;
|
|
21
|
+
break;
|
|
22
|
+
case "manual":
|
|
23
|
+
counters.manual += amount;
|
|
24
|
+
break;
|
|
25
|
+
case "wire-compression":
|
|
26
|
+
counters.wireCompression += amount;
|
|
27
|
+
break;
|
|
28
|
+
case "fail-open":
|
|
29
|
+
counters.failOpen += amount;
|
|
30
|
+
break;
|
|
31
|
+
case "usage-unavailable":
|
|
32
|
+
counters.usageUnavailable += amount;
|
|
33
|
+
break;
|
|
34
|
+
case "l0-compact-failed":
|
|
35
|
+
counters.l0CompactFailed += amount;
|
|
36
|
+
break;
|
|
37
|
+
default:
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function projectSafeTelemetry(counters) {
|
|
42
|
+
return {
|
|
43
|
+
prePromptThreshold: counters.prePromptThreshold,
|
|
44
|
+
overflow: counters.overflow,
|
|
45
|
+
manual: counters.manual,
|
|
46
|
+
wireCompression: counters.wireCompression,
|
|
47
|
+
failOpen: counters.failOpen,
|
|
48
|
+
tokensSavedEstimate: counters.tokensSavedEstimate,
|
|
49
|
+
blocksCompressed: counters.blocksCompressed,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -98,6 +98,13 @@ function isFinalAnswerBlock(block) {
|
|
|
98
98
|
}
|
|
99
99
|
export function splitFinalAssistantBlocks(blocks, options = {}) {
|
|
100
100
|
const displayable = getDisplayableAssistantBlocks(blocks, options);
|
|
101
|
+
// A streaming Pi message is cumulative: a later update may append a toolCall
|
|
102
|
+
// after text that is already visible. Keep the whole live message in the
|
|
103
|
+
// chronological process subtree so that update appends the tool in place
|
|
104
|
+
// instead of reparenting the earlier text from answer -> process.
|
|
105
|
+
if (options.isStreaming) {
|
|
106
|
+
return { answerBlocks: [], processBlocks: displayable };
|
|
107
|
+
}
|
|
101
108
|
let lastProcessIndex = -1;
|
|
102
109
|
for (let i = displayable.length - 1; i >= 0; i--) {
|
|
103
110
|
if (!isFinalAnswerBlock(displayable[i])) {
|
|
@@ -136,7 +136,7 @@ export function projectOperationForChat(operation) {
|
|
|
136
136
|
state: operation.state,
|
|
137
137
|
createdAt: operation.createdAt,
|
|
138
138
|
updatedAt: operation.finishedAt ?? operation.startedAt ?? operation.createdAt,
|
|
139
|
-
inspectHref:
|
|
139
|
+
inspectHref: `/#/inspect?operationId=${encodeURIComponent(operation.operationId)}`,
|
|
140
140
|
...(taskId ? { taskId } : {}),
|
|
141
141
|
...(dagRunId ? { dagRunId } : {}),
|
|
142
142
|
...(featureId ? { featureId } : {}),
|
|
@@ -381,12 +381,26 @@ function dropOneByPriority(ring) {
|
|
|
381
381
|
const events = ring.events;
|
|
382
382
|
if (events.length === 0)
|
|
383
383
|
return undefined;
|
|
384
|
-
|
|
384
|
+
// Keep the newest Goal snapshot/tombstone as the session's durable Goal
|
|
385
|
+
// source of truth. Otherwise a long thread could forget an active Goal or
|
|
386
|
+
// resurrect a cleared one after ring eviction.
|
|
387
|
+
let protectedGoalIndex = -1;
|
|
388
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
389
|
+
if (events[index].kind === "goal_changed" ||
|
|
390
|
+
events[index].kind === "goal_cleared") {
|
|
391
|
+
protectedGoalIndex = index;
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const textIndex = events.findIndex((event, index) => index !== protectedGoalIndex && event.kind === "message_update");
|
|
385
396
|
if (textIndex !== -1)
|
|
386
397
|
return events.splice(textIndex, 1)[0];
|
|
387
|
-
const structuralIndex = events.findIndex((event) => event.kind !== "message_end");
|
|
398
|
+
const structuralIndex = events.findIndex((event, index) => index !== protectedGoalIndex && event.kind !== "message_end");
|
|
388
399
|
if (structuralIndex !== -1)
|
|
389
400
|
return events.splice(structuralIndex, 1)[0];
|
|
401
|
+
const anyUnprotected = events.findIndex((_event, index) => index !== protectedGoalIndex);
|
|
402
|
+
if (anyUnprotected !== -1)
|
|
403
|
+
return events.splice(anyUnprotected, 1)[0];
|
|
390
404
|
return events.shift();
|
|
391
405
|
}
|
|
392
406
|
/**
|
|
@@ -430,6 +444,13 @@ const DEFAULT_MAX_EVENTS = 2000;
|
|
|
430
444
|
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
|
431
445
|
/** Same-turn `message_update` events inside this window coalesce into one persisted event. */
|
|
432
446
|
const DEFAULT_COALESCE_WINDOW_MS = 100;
|
|
447
|
+
/** B5-1: `message_update` appends persist at most once per this window (driven
|
|
448
|
+
* by the event `at` clock, same as coalescing — no timers involved). */
|
|
449
|
+
const DEFAULT_PERSIST_DEBOUNCE_MS = 250;
|
|
450
|
+
/** B5-1: hard cap on pending debounced `message_update` appends before an
|
|
451
|
+
* interim flush is forced, so a fast stream never widens the loss window
|
|
452
|
+
* unboundedly even if the `at` clock stalls. */
|
|
453
|
+
const DEFAULT_PERSIST_MAX_PENDING_UPDATES = 250;
|
|
433
454
|
/** Conservative per-event scale for the byte pre-check gate (UTF-8 multibyte ≤ 4 bytes/char × pretty-print expansion). */
|
|
434
455
|
const BYTE_GATE_SCALE = 12;
|
|
435
456
|
/** Fixed envelope slack (schema keys, turnCount, nextSeq, minSeq, turns) for the byte pre-check gate. */
|
|
@@ -632,6 +653,9 @@ export function createChatEventStore(options) {
|
|
|
632
653
|
const maxBytes = options?.maxBytesPerSession ?? DEFAULT_MAX_BYTES;
|
|
633
654
|
const coalesceWindowMs = options?.coalesceWindowMs ?? DEFAULT_COALESCE_WINDOW_MS;
|
|
634
655
|
const eventEpoch = options?.epoch?.trim() || LEGACY_EPOCH;
|
|
656
|
+
const persistWriter = options?.persistWriter ?? writeSecureJsonSync;
|
|
657
|
+
const persistDebounceMs = options?.persistDebounceMs ?? DEFAULT_PERSIST_DEBOUNCE_MS;
|
|
658
|
+
const persistMaxPendingUpdates = options?.persistMaxPendingUpdates ?? DEFAULT_PERSIST_MAX_PENDING_UPDATES;
|
|
635
659
|
const persistenceDir = options?.appData?.chatEvents
|
|
636
660
|
? path.resolve(options.appData.chatEvents)
|
|
637
661
|
: undefined;
|
|
@@ -642,6 +666,9 @@ export function createChatEventStore(options) {
|
|
|
642
666
|
const turnCounts = new Map();
|
|
643
667
|
/** Live subscribers per session. */
|
|
644
668
|
const subscribers = new Map();
|
|
669
|
+
/** B5-1: sessions whose latest `message_update` appends are still waiting
|
|
670
|
+
* for the next persist (debounced; structural events / turn changes flush). */
|
|
671
|
+
const pendingPersists = new Map();
|
|
645
672
|
function persistedPath(sessionId) {
|
|
646
673
|
return persistenceDir
|
|
647
674
|
? path.join(persistenceDir, `${safeSessionId(sessionId)}.json`)
|
|
@@ -709,7 +736,7 @@ export function createChatEventStore(options) {
|
|
|
709
736
|
// current limits if it was written by an older config or hand-edited.
|
|
710
737
|
enforceBounds(sessionId, ring);
|
|
711
738
|
if (orphaned)
|
|
712
|
-
|
|
739
|
+
persistNow(sessionId, ring);
|
|
713
740
|
return ring;
|
|
714
741
|
}
|
|
715
742
|
catch (error) {
|
|
@@ -763,11 +790,36 @@ export function createChatEventStore(options) {
|
|
|
763
790
|
ring.minSeq = ring.events[0].seq;
|
|
764
791
|
}
|
|
765
792
|
}
|
|
766
|
-
|
|
793
|
+
/** Persist the full ring synchronously (atomic tmp+rename) and clear any
|
|
794
|
+
* B5-1 pending-debounce marker for this session. */
|
|
795
|
+
function persistNow(sessionId, ring) {
|
|
767
796
|
const file = persistedPath(sessionId);
|
|
797
|
+
pendingPersists.delete(sessionId);
|
|
768
798
|
if (!file)
|
|
769
799
|
return;
|
|
770
|
-
|
|
800
|
+
persistWriter(file, ringPayload(sessionId, ring));
|
|
801
|
+
}
|
|
802
|
+
/** B5-1: defer the persist of a `message_update` append. The window is driven
|
|
803
|
+
* by the append `at` clock (mirroring `shouldCoalesce`) — a structural event,
|
|
804
|
+
* turn change, elapsed window or the pending-count cap each force a real
|
|
805
|
+
* flush, so a streaming turn no longer serializes the whole ring per token. */
|
|
806
|
+
function persistDebounced(sessionId, ring, atMs) {
|
|
807
|
+
const file = persistedPath(sessionId);
|
|
808
|
+
if (!file)
|
|
809
|
+
return;
|
|
810
|
+
const pending = pendingPersists.get(sessionId);
|
|
811
|
+
if (!pending) {
|
|
812
|
+
pendingPersists.set(sessionId, {
|
|
813
|
+
firstPendingAtMs: atMs,
|
|
814
|
+
pendingUpdates: 1,
|
|
815
|
+
});
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
pending.pendingUpdates += 1;
|
|
819
|
+
if (atMs - pending.firstPendingAtMs >= persistDebounceMs ||
|
|
820
|
+
pending.pendingUpdates >= persistMaxPendingUpdates) {
|
|
821
|
+
persistNow(sessionId, ring);
|
|
822
|
+
}
|
|
771
823
|
}
|
|
772
824
|
function ringFor(sessionId) {
|
|
773
825
|
let ring = rings.get(sessionId);
|
|
@@ -870,7 +922,7 @@ export function createChatEventStore(options) {
|
|
|
870
922
|
};
|
|
871
923
|
ring.turns.push(turn);
|
|
872
924
|
ring.approxBytes += byteHint(turn);
|
|
873
|
-
|
|
925
|
+
persistNow(sessionId, ring);
|
|
874
926
|
return { ok: true, turn: { ...turn }, created: true, idempotent: false };
|
|
875
927
|
},
|
|
876
928
|
setTurnState(sessionId, turnId, state, error) {
|
|
@@ -889,7 +941,7 @@ export function createChatEventStore(options) {
|
|
|
889
941
|
turn.error = error;
|
|
890
942
|
ring.approxBytes +=
|
|
891
943
|
(JSON.stringify(turn).length - before) * BYTE_GATE_SCALE;
|
|
892
|
-
|
|
944
|
+
persistNow(sessionId, ring);
|
|
893
945
|
return { ...turn };
|
|
894
946
|
},
|
|
895
947
|
markAbortRequested(sessionId, turnId, at) {
|
|
@@ -902,7 +954,7 @@ export function createChatEventStore(options) {
|
|
|
902
954
|
turn.abortRequestedAt = at ?? new Date().toISOString();
|
|
903
955
|
ring.approxBytes +=
|
|
904
956
|
(JSON.stringify(turn).length - before) * BYTE_GATE_SCALE;
|
|
905
|
-
|
|
957
|
+
persistNow(sessionId, ring);
|
|
906
958
|
return { ...turn };
|
|
907
959
|
},
|
|
908
960
|
findTurnByClientRequestId(sessionId, clientRequestId) {
|
|
@@ -962,7 +1014,23 @@ export function createChatEventStore(options) {
|
|
|
962
1014
|
ring.approxBytes += byteHint(live);
|
|
963
1015
|
}
|
|
964
1016
|
enforceBounds(sessionId, ring);
|
|
965
|
-
persist
|
|
1017
|
+
// B5-1: only high-frequency `message_update` appends defer the persist;
|
|
1018
|
+
// every other kind (message_end / tool_call / error / turn signals …)
|
|
1019
|
+
// still persists synchronously. Because persistNow writes the FULL
|
|
1020
|
+
// in-memory ring, one flush also lands any earlier pending updates in
|
|
1021
|
+
// order — no two-phase flush is needed.
|
|
1022
|
+
if (partial.kind === "message_update") {
|
|
1023
|
+
const atMs = partial.at ? Date.parse(partial.at) : Date.now();
|
|
1024
|
+
if (!Number.isNaN(atMs)) {
|
|
1025
|
+
persistDebounced(sessionId, ring, atMs);
|
|
1026
|
+
}
|
|
1027
|
+
else {
|
|
1028
|
+
persistNow(sessionId, ring);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
else {
|
|
1032
|
+
persistNow(sessionId, ring);
|
|
1033
|
+
}
|
|
966
1034
|
const subs = subscribers.get(sessionId);
|
|
967
1035
|
if (subs) {
|
|
968
1036
|
for (const fn of subs) {
|
|
@@ -1017,7 +1085,7 @@ export function createChatEventStore(options) {
|
|
|
1017
1085
|
const ring = ringFor(sessionId);
|
|
1018
1086
|
const next = (turnCounts.get(sessionId) ?? 0) + 1;
|
|
1019
1087
|
turnCounts.set(sessionId, next);
|
|
1020
|
-
|
|
1088
|
+
persistNow(sessionId, ring);
|
|
1021
1089
|
return next;
|
|
1022
1090
|
},
|
|
1023
1091
|
latestTurnId(sessionId) {
|
|
@@ -1043,6 +1111,9 @@ export function createChatEventStore(options) {
|
|
|
1043
1111
|
rings.delete(sessionId);
|
|
1044
1112
|
turnCounts.delete(sessionId);
|
|
1045
1113
|
subscribers.delete(sessionId);
|
|
1114
|
+
// B5-1: drop any debounced (not yet written) updates with the ring itself —
|
|
1115
|
+
// the file is being deleted, so flushing first would only resurrect it.
|
|
1116
|
+
pendingPersists.delete(sessionId);
|
|
1046
1117
|
const file = persistedPath(sessionId);
|
|
1047
1118
|
if (file)
|
|
1048
1119
|
rmSync(file, { force: true });
|