@tea-agent/loop-agent 0.40.0-next.10 → 0.40.0-next.12

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.
Files changed (97) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/application/dag/generate-task-dag.js +6 -2
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/commands/client-recovery.js +8 -36
  5. package/dist/executors/pi-sdk-executor.js +33 -5
  6. package/dist/executors/shell-executor.js +64 -17
  7. package/dist/governance/checks.js +1 -0
  8. package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
  9. package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
  10. package/dist/shared/pi-context-pressure/env.js +58 -0
  11. package/dist/shared/pi-context-pressure/extension.js +100 -0
  12. package/dist/shared/pi-context-pressure/index.js +7 -0
  13. package/dist/shared/pi-context-pressure/overflow.js +252 -0
  14. package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
  15. package/dist/shared/pi-context-pressure/telemetry.js +51 -0
  16. package/dist/worker/console/chat/pi-runtime.js +146 -5
  17. package/dist/worker/console/chat/provider-error.js +2 -1
  18. package/dist/worker/console/chat/routes.js +3 -0
  19. package/dist/worker/console/chat/sift-bridge.js +1 -0
  20. package/dist/worker/console/dag-execution-receipt.js +20 -2
  21. package/dist/worker/console/operator-actions.js +4 -3
  22. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bs-CDAXM.js → abnfDiagram-N423BO3Z-C_hkwt2J.js} +1 -1
  23. package/dist/worker/console/static/assets/{arc-CAkA3We3.js → arc-s2Q6r96B.js} +1 -1
  24. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DEmI_zqr.js → architectureDiagram-T3A2C74G-CtgsrwU1.js} +1 -1
  25. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-uDHANebE.js → blockDiagram-VBNYF7ZC-DZ-A7EiJ.js} +1 -1
  26. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Bv97b4_B.js → c4Diagram-5PPSVZJV-D2qv0Nyo.js} +1 -1
  27. package/dist/worker/console/static/assets/channel-BluaMT3-.js +1 -0
  28. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-shmQKsFz.js → chunk-2GRJ4B5K-D-UYpSaW.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BAraNvcN.js → chunk-2Q5K7J3B-DAS3jrrt.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BirhxUop.js → chunk-5RXB4S5H-CSbuRcGB.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C9LgUJpR.js → chunk-5VM5RSS4-BDH1ZO40.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DrU-s-t7.js → chunk-6Q2QTUOP-Dc2cqQIb.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Tx-H_FA2.js → chunk-GF5L2VYU-CkZuemIW.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BjLtNFrR.js → chunk-JWPE2WC7-URwjQnte.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-UNLVwSsy.js → chunk-KBJHAD2P-04RHylJK.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CBEUX8XD.js → chunk-RYQCIY6F-CvQX56Hh.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-WTL7IZOq.js → chunk-XXDRQBXY-dUF7qura.js} +1 -1
  38. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-H-ZpSafr.js +1 -0
  39. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-H-ZpSafr.js +1 -0
  40. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CkHG6W3s.js → cose-bilkent-JH36ORCC-COwbfxJE.js} +1 -1
  41. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BRzFqTaL.js → cynefin-VYW2F7L2-CaBEJekQ.js} +1 -1
  42. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2_CFp_i7.js → cynefinDiagram-MW4NZA55-COWUBdet.js} +1 -1
  43. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJtsmdkE.js → dagre-VZM6K2ZE-BLxVqTn4.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DNEFFo6w.js → diagram-7IWD3JNH-WL0yKfEQ.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B_IWV7eo.js → diagram-B4RE2ZJO-DaVkKCle.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DvvRx2dz.js → diagram-LBJQPF4R-DdTgnRGf.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CsibsWLg.js → diagram-Q27KOJAE-PG2Axo6A.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-UB23O5K3-les_djBQ.js → diagram-UB23O5K3-6vuQzlzI.js} +1 -1
  49. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BEm2n7qJ.js → ebnfDiagram-BXEA7PRR-BpXeBz-g.js} +1 -1
  50. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BI7vHHVm.js → erDiagram-JOGREHBK-PxtUnaa1.js} +1 -1
  51. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CUFCERrB.js → flowDiagram-UKHOOZJN-B--Qgvs1.js} +1 -1
  52. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-wxyLiGWt.js → ganttDiagram-PKOTCBZU-BIfQCgtx.js} +1 -1
  53. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bt897K8Z.js → gitGraphDiagram-DS77QQ5N-BRaX_E4j.js} +1 -1
  54. package/dist/worker/console/static/assets/index-Crrgl8Mx.css +1 -0
  55. package/dist/worker/console/static/assets/{index-DqJbO3-p.js → index-vPsp-idE.js} +102 -72
  56. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DYNx_IPv.js → infoDiagram-6WML65LV-Cd93r4Ab.js} +1 -1
  57. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CzpLNEu6.js → ishikawaDiagram-WSZJBQD7-C4c80iHt.js} +1 -1
  58. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-97f9owNU.js → journeyDiagram-NVQOT4AX-Ch5ajf3s.js} +1 -1
  59. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BxM9hyBn.js → kanban-definition-27J2QSJJ-DbpyW1Ut.js} +1 -1
  60. package/dist/worker/console/static/assets/{linear-ByuxcHvp.js → linear-BHVjBnTW.js} +1 -1
  61. package/dist/worker/console/static/assets/{mermaid.core-DwPGvpCv.js → mermaid.core-DO70kJ34.js} +5 -5
  62. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DCoqG9RT.js → mindmap-definition-FAOFIHXS-_P44Zpvs.js} +1 -1
  63. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BLSBmBMx.js → pegDiagram-VL7TDLO6-Bgi9FmEd.js} +1 -1
  64. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B0t_0oJW.js → pieDiagram-7S7Q4E2Y-C9InDfJn.js} +1 -1
  65. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js → quadrantDiagram-CIZ2JOQS-DtKrUouv.js} +1 -1
  66. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-B3jRdATC.js → railroadDiagram-AXF67PYL-zgAD_6Yo.js} +1 -1
  67. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-cTtXEN.js → requirementDiagram-LRYGKXZP-CQNO6kj4.js} +1 -1
  68. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B25igJRE.js → sankeyDiagram-W5VNT64P-knKu111P.js} +1 -1
  69. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-jW_lM1_D.js → sequenceDiagram-SI44F4Z6-D6tshiQ6.js} +1 -1
  70. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CrzTgsHE.js → sizeCapture-X5ZJPWSS-BMIWJcir.js} +1 -1
  71. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-B01MvlqE.js → stateDiagram-OKZ733FA-BP9wRDqc.js} +1 -1
  72. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-ClEhytQG.js +1 -0
  73. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-YcF12FiU.js → swimlanes-SLNWSIFB-DEkM5Dhe.js} +2 -2
  74. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPg5IMuu.js +8 -0
  75. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ClnRcg2s.js → timeline-definition-Z64GVDOM-ByBKsL35.js} +1 -1
  76. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-B6gr_z-q.js → vennDiagram-T6HMQDX7-waFbNCUy.js} +1 -1
  77. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CvSgguGH.js → wardleyDiagram-T6FBY63Y-1ljiY0eZ.js} +1 -1
  78. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-23OtSZCb.js → xychartDiagram-ELKLHX3M-B4ZLHr_L.js} +1 -1
  79. package/dist/worker/console/static/index.html +2 -2
  80. package/dist/worker/observe/static/styles.css +9 -0
  81. package/dist/worker/observe/static/views/dag-inspector.js +40 -0
  82. package/dist/worker/observe/static/views/session-timeline.js +135 -0
  83. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +157 -7
  84. package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
  85. package/dist/workflows/dag/backend-test-result-contract.js +4 -0
  86. package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
  87. package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
  88. package/dist/workflows/dag/init-hybrid.js +2 -2
  89. package/docs/architecture/runtime-boundaries.md +2 -1
  90. package/docs/templates/backend-test-dag.json +4 -3
  91. package/package.json +3 -2
  92. package/dist/worker/console/static/assets/channel-amxUpk7o.js +0 -1
  93. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +0 -1
  94. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +0 -1
  95. package/dist/worker/console/static/assets/index-BHhUOTri.css +0 -1
  96. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +0 -1
  97. 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
+ }
@@ -36,6 +36,8 @@ import { OPERATOR_CHAT_ALLOWED_TOOLS, authorizeOperatorChatTool, assertNoWriteTo
36
36
  import { createOperatorChatResourceLoader, } from "./resource-loader.js";
37
37
  import { applySkillAutoInvocationPreferences, isAutoInvocationEnabled, loadResourcePreferences, skillResourceId, } from "./resource-preferences-store.js";
38
38
  import { buildModelCallableToolSchemas } from "./tool-adapter.js";
39
+ import { applyProcessCompactionOverride, createCompactionController, createContextPressureExtension, createSiftBridgeState, housekeepSiftStash, isConsoleSiftEnabled, parseCompactionPolicy, parseSiftBridgeConfig, projectPromptPressure, deriveEffectiveWindow, projectSafeTelemetry, emptyContextPressureCounters, } from "../../../shared/pi-context-pressure/index.js";
40
+ import { incrementCounter as incrementPressureCounter } from "../../../shared/pi-context-pressure/telemetry.js";
39
41
  import { resolveDefaultChatModel, resolveLowChatModel, } from "./model-resolver.js";
40
42
  import { filterActiveInterviewTools } from "../interview/tools.js";
41
43
  import { RUNTIME_CONTEXT_TEXT_MAX, redactRuntimeText, } from "./runtime-context.js";
@@ -845,6 +847,9 @@ export class ConsolePiRuntime {
845
847
  sessionModels = new Map();
846
848
  /** SessionIds disposed while materialization was still in flight. */
847
849
  disposedSessions = new Set();
850
+ compactionControllers = new Map();
851
+ siftStates = new Map();
852
+ /** Session-less tool definition caches (no AgentSession state). */
848
853
  /** Session-less tool definition caches (no AgentSession state). */
849
854
  operatorCustomToolsPromise;
850
855
  exploreCustomToolsPromise;
@@ -892,14 +897,15 @@ export class ConsolePiRuntime {
892
897
  registry: this.options.todoRegistry,
893
898
  })
894
899
  : undefined;
900
+ const pressureExtension = sessionId
901
+ ? this.createContextPressureInlineExtension(sessionId)
902
+ : undefined;
903
+ const extraFactories = [todoContextExtension, pressureExtension].filter((item) => Boolean(item));
895
904
  return {
896
905
  ...base,
897
- ...(todoContextExtension
906
+ ...(extraFactories.length > 0
898
907
  ? {
899
- extensionFactories: [
900
- ...inheritedFactories,
901
- todoContextExtension,
902
- ],
908
+ extensionFactories: [...inheritedFactories, ...extraFactories],
903
909
  }
904
910
  : {}),
905
911
  skillsOverride: (current) => {
@@ -924,6 +930,126 @@ export class ConsolePiRuntime {
924
930
  this.cachedSkillPrefs = prefs;
925
931
  return prefs;
926
932
  }
933
+ getCompactionController(sessionId) {
934
+ let controller = this.compactionControllers.get(sessionId);
935
+ if (!controller) {
936
+ controller = createCompactionController();
937
+ this.compactionControllers.set(sessionId, controller);
938
+ }
939
+ if (this.options.todoRegistry) {
940
+ controller.todos = this.options.todoRegistry.get(sessionId);
941
+ }
942
+ return controller;
943
+ }
944
+ createContextPressureInlineExtension(sessionId) {
945
+ const policy = parseCompactionPolicy();
946
+ const siftEnabled = isConsoleSiftEnabled();
947
+ let sift;
948
+ if (siftEnabled.enabled) {
949
+ sift = this.siftStates.get(sessionId);
950
+ if (!sift) {
951
+ const config = parseSiftBridgeConfig({
952
+ sessionId,
953
+ defaultStashRoot: path.join(this.options.sessionDir, "sift-stash"),
954
+ });
955
+ sift = createSiftBridgeState(config);
956
+ sift.counters = this.getCompactionController(sessionId).counters;
957
+ this.siftStates.set(sessionId, sift);
958
+ const housekeep = housekeepSiftStash({
959
+ root: path.dirname(config.stashDir),
960
+ activeSessionId: sessionId,
961
+ retentionDays: config.retentionDays,
962
+ });
963
+ if (housekeep.failed)
964
+ sift.diagnostics.add("housekeep-failed");
965
+ }
966
+ }
967
+ return createContextPressureExtension({
968
+ name: "operator-chat-context-pressure",
969
+ policy,
970
+ controller: this.getCompactionController(sessionId),
971
+ sift,
972
+ });
973
+ }
974
+ applySessionCompactionOverride(sessionId, services, session) {
975
+ const manager = services?.settingsManager;
976
+ const window = session?.getSessionStats?.()?.contextUsage?.contextWindow;
977
+ applyProcessCompactionOverride(manager, {
978
+ policy: parseCompactionPolicy(),
979
+ ...(typeof window === "number" ? { contextWindow: window } : {}),
980
+ });
981
+ void sessionId;
982
+ }
983
+ async runPrePromptCompaction(sessionId, session, promptText, imageCount, onEvent) {
984
+ const policy = parseCompactionPolicy();
985
+ if (policy.mode === "off")
986
+ return;
987
+ const controller = this.getCompactionController(sessionId);
988
+ const stats = session.getSessionStats?.();
989
+ const window = stats?.contextUsage?.contextWindow;
990
+ const tokens = stats?.contextUsage?.tokens;
991
+ if (typeof window === "number" && window > 0) {
992
+ this.applySessionCompactionOverride(sessionId, this.serviceScopes.get(sessionId) ?? {}, session);
993
+ }
994
+ const projection = projectPromptPressure({
995
+ currentContextTokens: tokens,
996
+ promptText,
997
+ imageCount,
998
+ });
999
+ if (projection.usageUnavailable) {
1000
+ incrementPressureCounter(controller.counters, "usage-unavailable");
1001
+ return;
1002
+ }
1003
+ const effective = typeof window === "number" && window > 0
1004
+ ? deriveEffectiveWindow(window, policy)
1005
+ : undefined;
1006
+ if (!effective || projection.projected < effective.triggerTokens)
1007
+ return;
1008
+ if (typeof session.compact !== "function")
1009
+ return;
1010
+ const compactionId = newCompactionId(sessionId);
1011
+ onEvent({
1012
+ type: "compaction_start",
1013
+ sessionId,
1014
+ compactionId,
1015
+ reason: "threshold",
1016
+ });
1017
+ controller.proactiveCompaction = true;
1018
+ try {
1019
+ incrementPressureCounter(controller.counters, "pre-prompt-threshold");
1020
+ const result = await session.compact();
1021
+ const snapshot = projectCompactSnapshot(result ?? {});
1022
+ onEvent({
1023
+ type: "compaction_end",
1024
+ sessionId,
1025
+ compactionId,
1026
+ reason: "threshold",
1027
+ status: "completed",
1028
+ willRetry: false,
1029
+ summary: snapshot.summary,
1030
+ ...(snapshot.firstKeptEntryId
1031
+ ? { firstKeptEntryId: snapshot.firstKeptEntryId }
1032
+ : {}),
1033
+ ...(snapshot.tokensBefore === undefined
1034
+ ? {}
1035
+ : { tokensBefore: snapshot.tokensBefore }),
1036
+ });
1037
+ }
1038
+ catch {
1039
+ incrementPressureCounter(controller.counters, "l0-compact-failed");
1040
+ onEvent({
1041
+ type: "compaction_end",
1042
+ sessionId,
1043
+ compactionId,
1044
+ reason: "threshold",
1045
+ status: "failed",
1046
+ willRetry: false,
1047
+ });
1048
+ }
1049
+ finally {
1050
+ controller.proactiveCompaction = false;
1051
+ }
1052
+ }
927
1053
  /**
928
1054
  * Resolve a skill in the current session inventory by server-issued resourceId.
929
1055
  * Returns undefined when the id is unknown (caller maps to PI_SKILL_NOT_FOUND).
@@ -1156,6 +1282,7 @@ export class ConsolePiRuntime {
1156
1282
  resourceLoader: services.resourceLoader,
1157
1283
  settingsManager: services.settingsManager,
1158
1284
  });
1285
+ this.applySessionCompactionOverride(sessionId, services, session);
1159
1286
  this.revisions.set(sessionId, 1);
1160
1287
  if (!this.thinkingSelections.has(sessionId)) {
1161
1288
  this.thinkingSelections.set(sessionId, "auto");
@@ -1470,6 +1597,9 @@ export class ConsolePiRuntime {
1470
1597
  // the latest resource-preferences.json written by PATCH.
1471
1598
  await this.refreshSkillPrefsCache().catch(() => undefined);
1472
1599
  await session.reload();
1600
+ const services = this.serviceScopes.get(sessionId);
1601
+ if (services)
1602
+ this.applySessionCompactionOverride(sessionId, services, session);
1473
1603
  }
1474
1604
  catch (error) {
1475
1605
  return {
@@ -2076,6 +2206,7 @@ export class ConsolePiRuntime {
2076
2206
  });
2077
2207
  await bindSessionExtensions(session);
2078
2208
  pinOperatorChatTools(session);
2209
+ this.applySessionCompactionOverride(init.sessionId, services, session);
2079
2210
  return {
2080
2211
  session,
2081
2212
  modelRuntime,
@@ -2117,6 +2248,12 @@ export class ConsolePiRuntime {
2117
2248
  session?.hasPendingBashMessages === true,
2118
2249
  };
2119
2250
  }
2251
+ /** Low-cardinality context-pressure counters; never includes payload text. */
2252
+ getContextPressureTelemetry(sessionId) {
2253
+ const counters = this.compactionControllers.get(sessionId)?.counters ??
2254
+ this.siftStates.get(sessionId)?.counters;
2255
+ return projectSafeTelemetry(counters ?? emptyContextPressureCounters());
2256
+ }
2120
2257
  /**
2121
2258
  * Send a prompt and stream events. Gate 3: each tool invocation is
2122
2259
  * re-authorized by authorizeOperatorChatTool before the dispatcher runs it
@@ -2212,6 +2349,7 @@ export class ConsolePiRuntime {
2212
2349
  }
2213
2350
  };
2214
2351
  emitContextUsage();
2352
+ await this.runPrePromptCompaction(sessionId, session, text, options?.images?.length ?? 0, onEvent);
2215
2353
  let stepStartedAt;
2216
2354
  let firstTokenAt;
2217
2355
  let pendingProviderError;
@@ -2461,6 +2599,7 @@ export class ConsolePiRuntime {
2461
2599
  throw new Error("CHAT_TURN_ACTIVE");
2462
2600
  if (typeof session.compact !== "function")
2463
2601
  throw new Error("COMPACTION_UNSUPPORTED");
2602
+ incrementPressureCounter(this.getCompactionController(sessionId).counters, "manual");
2464
2603
  const result = await session.compact(customInstructions);
2465
2604
  return projectCompactSnapshot(result ?? {});
2466
2605
  }
@@ -2468,6 +2607,8 @@ export class ConsolePiRuntime {
2468
2607
  // Mark disposed first so a late materialization never registers ready.
2469
2608
  this.disposedSessions.add(sessionId);
2470
2609
  this.sessionInit.delete(sessionId);
2610
+ this.compactionControllers.delete(sessionId);
2611
+ this.siftStates.delete(sessionId);
2471
2612
  const session = this.sessions.get(sessionId);
2472
2613
  if (session) {
2473
2614
  try {
@@ -1,3 +1,4 @@
1
+ import { isContextOverflow } from "../../../shared/pi-context-pressure/overflow.js";
1
2
  const MAX_RAW_CHARS = 8_000;
2
3
  const MAX_MESSAGE_CHARS = 1_000;
3
4
  function asRecord(value) {
@@ -52,7 +53,7 @@ function classify(input) {
52
53
  status === 403 ||
53
54
  /认证|鉴权|unauthori[sz]ed|forbidden|authentication|invalid api[-_ ]?key/.test(haystack))
54
55
  return "authentication";
55
- if (/请求上下文过大|上下文.{0,12}(?:过大|太大|过长|超限)|context_length_exceeded|maximum context|context.{0,24}(?:length|window|limit|large|long)|too many tokens/.test(haystack))
56
+ if (isContextOverflow(`${input.message}\n${input.raw}`))
56
57
  return "context-overflow";
57
58
  if (status === 429 ||
58
59
  /rate.?limit|too many requests|请求.{0,8}(?:频繁|过多)|限流/.test(haystack))
@@ -759,6 +759,9 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
759
759
  hasBash: true,
760
760
  sessionStats: telemetry.stats,
761
761
  usage: telemetry.usage,
762
+ contextPressure: typeof deps.runtime.getContextPressureTelemetry === "function"
763
+ ? deps.runtime.getContextPressureTelemetry(sessionId)
764
+ : undefined,
762
765
  compactions: snapshot
763
766
  .filter((event) => event.kind === "compact" ||
764
767
  event.kind === "compaction_start" ||
@@ -0,0 +1 @@
1
+ export { createSiftBridgeState, parseSiftBridgeConfig, compressProviderPayload, executeSiftRetrieve, housekeepSiftStash, createSiftRetrieveToolDefinition, SIFT_RETRIEVE_TOOL_NAME, SIFT_POLICY_VERSION, } from "../../../shared/pi-context-pressure/sift-bridge.js";