@tea-agent/loop-agent 0.39.0-beta.12 → 0.39.0-beta.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.
Files changed (109) hide show
  1. package/CHANGELOG.md +26 -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/dag-pi-executor.js +343 -40
  6. package/dist/executors/pi-executor.js +8 -4
  7. package/dist/executors/pi-sdk-executor.js +33 -5
  8. package/dist/executors/shell-executor.js +102 -32
  9. package/dist/governance/checks.js +1 -0
  10. package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
  11. package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
  12. package/dist/shared/pi-context-pressure/env.js +58 -0
  13. package/dist/shared/pi-context-pressure/extension.js +100 -0
  14. package/dist/shared/pi-context-pressure/index.js +7 -0
  15. package/dist/shared/pi-context-pressure/overflow.js +252 -0
  16. package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
  17. package/dist/shared/pi-context-pressure/telemetry.js +51 -0
  18. package/dist/task/frontend-project-capability.js +3 -1
  19. package/dist/task/source-prepare/fragment-inventory.js +4 -1
  20. package/dist/worker/console/chat/pi-runtime.js +146 -5
  21. package/dist/worker/console/chat/provider-error.js +2 -1
  22. package/dist/worker/console/chat/routes.js +3 -0
  23. package/dist/worker/console/chat/sift-bridge.js +1 -0
  24. package/dist/worker/console/dag-execution-receipt.js +20 -2
  25. package/dist/worker/console/operator-actions.js +4 -3
  26. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CXj_GnSb.js → abnfDiagram-N423BO3Z-DC863mud.js} +1 -1
  27. package/dist/worker/console/static/assets/{arc-BZp6JAp7.js → arc-CftC38G9.js} +1 -1
  28. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DfpcEuYU.js → architectureDiagram-T3A2C74G-B3PAPQCw.js} +1 -1
  29. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-_Gf0xadb.js → blockDiagram-VBNYF7ZC-C9UDJNRv.js} +1 -1
  30. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct2QPCmv.js → c4Diagram-5PPSVZJV--BJ76fv_.js} +1 -1
  31. package/dist/worker/console/static/assets/channel-CUz-Bg86.js +1 -0
  32. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BfklkzKl.js → chunk-2GRJ4B5K--hiIqoGp.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-Dy25vZJV.js → chunk-2Q5K7J3B-DgTzpIa3.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BFlCRZep.js → chunk-5RXB4S5H-DIwpJziP.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-op3oVxIE.js → chunk-5VM5RSS4-BJzbdUi0.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-C0R7rzF2.js → chunk-6Q2QTUOP-BbGouI1Z.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bt44TCGy.js → chunk-GF5L2VYU-B9247w69.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-_uEE_XFx.js → chunk-JWPE2WC7-CXob_wYy.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-C3TOYGZ9.js → chunk-KBJHAD2P-C6FUel1g.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Cz60oBPV.js → chunk-RYQCIY6F-DGKRYKHu.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-8Bik0qis.js → chunk-XXDRQBXY-kNBqNPfx.js} +1 -1
  42. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlUacmD.js +1 -0
  43. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlUacmD.js +1 -0
  44. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C2QIOA4a.js → cose-bilkent-JH36ORCC--xmkDwfD.js} +1 -1
  45. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CSH_yUUd.js → cynefin-VYW2F7L2-Cw5FMMuG.js} +1 -1
  46. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BDLw2XFM.js → cynefinDiagram-MW4NZA55-CuLslt2t.js} +1 -1
  47. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CcD9ZtF4.js → dagre-VZM6K2ZE-D9ngqrs2.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DqwtkBsI.js → diagram-7IWD3JNH-RDRSbHQp.js} +1 -1
  49. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BWVEcqsC.js → diagram-B4RE2ZJO-BgQ1P9aV.js} +1 -1
  50. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-M-WAbtQi.js → diagram-LBJQPF4R-D3WWyPtn.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DKW7SixP.js → diagram-Q27KOJAE-L2k28wTR.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-UB23O5K3-PHHPPtrj.js → diagram-UB23O5K3-DDNrA5Qw.js} +1 -1
  53. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BQD-B4RX.js → ebnfDiagram-BXEA7PRR-BaRFCOdM.js} +1 -1
  54. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BFvULb51.js → erDiagram-JOGREHBK-CM33DVVM.js} +1 -1
  55. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Bw-aXTCb.js → flowDiagram-UKHOOZJN-CVBSjOxe.js} +1 -1
  56. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BGKw04Qx.js → ganttDiagram-PKOTCBZU-BdscewE_.js} +1 -1
  57. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-RqKaPR-I.js → gitGraphDiagram-DS77QQ5N-HRuSZb7g.js} +1 -1
  58. package/dist/worker/console/static/assets/{index-B_D8rbWc.js → index-B9JJQsVK.js} +102 -72
  59. package/dist/worker/console/static/assets/index-rWaGv4jz.css +1 -0
  60. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-YO5dnzrJ.js → infoDiagram-6WML65LV-cYfHvfAR.js} +1 -1
  61. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bi1VZHMq.js → ishikawaDiagram-WSZJBQD7-CZmaoOuy.js} +1 -1
  62. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Bszls1DD.js → journeyDiagram-NVQOT4AX-ntYijh7g.js} +1 -1
  63. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-PhzeaZ09.js → kanban-definition-27J2QSJJ-Cz3b0DeD.js} +1 -1
  64. package/dist/worker/console/static/assets/{linear-BsjbDoXi.js → linear-DvGonpsP.js} +1 -1
  65. package/dist/worker/console/static/assets/{mermaid.core-0B7NnWKk.js → mermaid.core-B-3vjfyW.js} +5 -5
  66. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BJr4Fj-q.js → mindmap-definition-FAOFIHXS-5iKxlsX3.js} +1 -1
  67. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-moC4fpGB.js → pegDiagram-VL7TDLO6-C8BUwapW.js} +1 -1
  68. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BEw37-2c.js → pieDiagram-7S7Q4E2Y-B2rPoQQG.js} +1 -1
  69. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq6LyasU.js → quadrantDiagram-CIZ2JOQS-jDeVUAy4.js} +1 -1
  70. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DUCMcK0D.js → railroadDiagram-AXF67PYL-DV140Dwm.js} +1 -1
  71. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C3upTZm7.js → requirementDiagram-LRYGKXZP-CnYgHtYC.js} +1 -1
  72. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BI_gMsCW.js → sankeyDiagram-W5VNT64P-SIK3CdWw.js} +1 -1
  73. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-YFOIRzfN.js → sequenceDiagram-SI44F4Z6-DW_vZix7.js} +1 -1
  74. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-dOnB7UDD.js → sizeCapture-X5ZJPWSS-NBAtkg0C.js} +1 -1
  75. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BXUniaIh.js → stateDiagram-OKZ733FA-Bi1bQxpi.js} +1 -1
  76. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DjWKYjQ1.js +1 -0
  77. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-2tA4wTNu.js → swimlanes-SLNWSIFB-8PT_uP_i.js} +2 -2
  78. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B4cdm7bH.js +8 -0
  79. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DO0HkJXC.js → timeline-definition-Z64GVDOM-CD0ZZPk0.js} +1 -1
  80. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DvIOzixv.js → vennDiagram-T6HMQDX7-BbEgWdhK.js} +1 -1
  81. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DXDZ0cTj.js → wardleyDiagram-T6FBY63Y-DCwPKdLl.js} +1 -1
  82. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B32Ark0D.js → xychartDiagram-ELKLHX3M-sfC5PcNg.js} +1 -1
  83. package/dist/worker/console/static/index.html +2 -2
  84. package/dist/worker/observe/static/styles.css +9 -0
  85. package/dist/worker/observe/static/views/dag-inspector.js +40 -0
  86. package/dist/worker/observe/static/views/session-timeline.js +135 -0
  87. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +157 -7
  88. package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
  89. package/dist/workflows/dag/backend-test-result-contract.js +4 -0
  90. package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
  91. package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
  92. package/dist/workflows/dag/dag-retry-schema.js +138 -0
  93. package/dist/workflows/dag/frontend-implementation-contract.js +118 -22
  94. package/dist/workflows/dag/frontend-shadow-dual-write.js +1 -1
  95. package/dist/workflows/dag/frontend-writer-admission.js +13 -0
  96. package/dist/workflows/dag/init-hybrid.js +7 -3
  97. package/dist/workflows/dag/node-execution.js +277 -3
  98. package/dist/workflows/dag/rerun-feedback.js +135 -3
  99. package/dist/workflows/dag/retry-policy.js +13 -122
  100. package/dist/workflows/dag/types.js +6 -1
  101. package/docs/architecture/runtime-boundaries.md +2 -1
  102. package/docs/templates/backend-test-dag.json +4 -3
  103. package/package.json +4 -3
  104. package/dist/worker/console/static/assets/channel-3TxJgYaH.js +0 -1
  105. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BHIkXpp3.js +0 -1
  106. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BHIkXpp3.js +0 -1
  107. package/dist/worker/console/static/assets/index-BdNx6fj0.css +0 -1
  108. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cdi6UhLa.js +0 -1
  109. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D-RJBbb0.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
+ }
@@ -205,6 +205,7 @@ export async function listOpenspecDiscovery(repoRoot, options = {}) {
205
205
  const collected = [];
206
206
  let excludedGovernanceCount = 0;
207
207
  let scannedFiles = 0;
208
+ let maxFilesHit = false;
208
209
  let scannedBytes = 0;
209
210
  const warnings = [];
210
211
  const maxDepth = options.maxDepth ?? 4;
@@ -250,7 +251,7 @@ export async function listOpenspecDiscovery(repoRoot, options = {}) {
250
251
  .filter((candidate) => !defaultPaths.includes(candidate))
251
252
  .sort();
252
253
  const deduped = [...defaultPaths, ...customPaths];
253
- const truncated = deduped.length > MAX_OPENSPEC_CANDIDATE_PATHS;
254
+ const truncated = maxFilesHit || deduped.length > MAX_OPENSPEC_CANDIDATE_PATHS;
254
255
  const paths = truncated
255
256
  ? deduped.slice(0, MAX_OPENSPEC_CANDIDATE_PATHS)
256
257
  : deduped;
@@ -289,6 +290,7 @@ export async function listOpenspecDiscovery(repoRoot, options = {}) {
289
290
  if (scannedFiles >= maxFiles) {
290
291
  if (!warnings.includes("max-files-exceeded"))
291
292
  warnings.push("max-files-exceeded");
293
+ maxFilesHit = true;
292
294
  return;
293
295
  }
294
296
  const full = path.join(dir, name);
@@ -175,8 +175,11 @@ export function splitDocumentIntoFragments(doc) {
175
175
  const explicitSectionAc = matchExplicitAcHeading(lastHeadingSegment);
176
176
  const sectionKind = classifySectionHeading(section.headingPath);
177
177
  const effectiveKind = explicitSectionAc ? "acceptance" : sectionKind;
178
+ // V4 显式 AC 标题行已在下方入口 emit(explicitSectionAc 分支);正文循环
179
+ // 必须从标题行的下一行开始,否则同一标题行会被正文循环的 heading 分支
180
+ // 再次 emit,产生结构性 duplicate(每个 AC 标题式片段出现两次)。
178
181
  const bodyStart = section.headingPath
179
- ? section.startLine + (explicitSectionAc ? 0 : 1)
182
+ ? section.startLine + 1
180
183
  : section.startLine;
181
184
  const bodyEnd = section.endLine;
182
185
  const bodyLines = lines.slice(Math.max(0, bodyStart - 1), Math.min(bodyEnd, lines.length));