@wrongstack/core 0.313.0 → 0.313.1

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 (182) hide show
  1. package/dist/chronicle/index.js +84 -6391
  2. package/dist/chronicle/project-server.js +84 -4868
  3. package/dist/chunk-2BUUIMQB.js +225 -0
  4. package/dist/chunk-2FK4WTFF.js +207 -0
  5. package/dist/chunk-2FQL4PDC.js +292 -0
  6. package/dist/chunk-2KKODIQU.js +2256 -0
  7. package/dist/chunk-2YU4WVBW.js +134 -0
  8. package/dist/chunk-3E244UDA.js +356 -0
  9. package/dist/chunk-3XPVB7BA.js +135 -0
  10. package/dist/chunk-4LJJYBAU.js +388 -0
  11. package/dist/chunk-4VWO3ZXL.js +342 -0
  12. package/dist/chunk-4XV6IR37.js +187 -0
  13. package/dist/chunk-4ZHTKJHM.js +183 -0
  14. package/dist/chunk-52RTVH4M.js +361 -0
  15. package/dist/chunk-57GEWK5X.js +26 -0
  16. package/dist/chunk-5DSMEVA4.js +931 -0
  17. package/dist/chunk-5RP3QTRL.js +1223 -0
  18. package/dist/chunk-5S437PRC.js +50 -0
  19. package/dist/chunk-5XDUGTKN.js +511 -0
  20. package/dist/chunk-62V2AFVG.js +422 -0
  21. package/dist/chunk-65RDIOR2.js +2470 -0
  22. package/dist/chunk-6AKOTGLM.js +597 -0
  23. package/dist/chunk-7ZHZE5EO.js +28 -0
  24. package/dist/chunk-A455ATZB.js +140 -0
  25. package/dist/chunk-AD7ZFZCS.js +46 -0
  26. package/dist/chunk-AI4LLQ2U.js +856 -0
  27. package/dist/chunk-ASX6AW36.js +789 -0
  28. package/dist/chunk-AT4V4XGI.js +3522 -0
  29. package/dist/chunk-AWNQ6ED6.js +311 -0
  30. package/dist/chunk-B4AMLZPB.js +9 -0
  31. package/dist/chunk-BAKVJLEV.js +417 -0
  32. package/dist/chunk-BJHVULHO.js +55 -0
  33. package/dist/chunk-CB2HFKTP.js +9 -0
  34. package/dist/chunk-CE4ZOCOI.js +1640 -0
  35. package/dist/chunk-CQQL4ZOQ.js +151 -0
  36. package/dist/chunk-DEIQMBD2.js +65 -0
  37. package/dist/chunk-E6I5RXVK.js +58 -0
  38. package/dist/chunk-EFEHDQGY.js +1589 -0
  39. package/dist/chunk-EODDVJBM.js +82 -0
  40. package/dist/chunk-EPC3GZD4.js +308 -0
  41. package/dist/chunk-FAYRX2AD.js +13 -0
  42. package/dist/chunk-FFSX2QDQ.js +362 -0
  43. package/dist/chunk-FHAMQS7Q.js +5856 -0
  44. package/dist/chunk-FIV4IJL7.js +2263 -0
  45. package/dist/chunk-FNXVT7HF.js +52 -0
  46. package/dist/chunk-FSWCWSTS.js +395 -0
  47. package/dist/chunk-FW5LDDMJ.js +16464 -0
  48. package/dist/chunk-FXQLIX3K.js +211 -0
  49. package/dist/chunk-G5AAKEEZ.js +228 -0
  50. package/dist/chunk-G5S3S5ZN.js +38 -0
  51. package/dist/chunk-GAZEWQEZ.js +231 -0
  52. package/dist/chunk-GFJOMSHG.js +125 -0
  53. package/dist/chunk-GFXZTHNI.js +158 -0
  54. package/dist/chunk-GM54GJBU.js +960 -0
  55. package/dist/chunk-GPQ3NJPR.js +900 -0
  56. package/dist/chunk-GTCU6DZ3.js +1 -0
  57. package/dist/chunk-H5E77KUJ.js +15 -0
  58. package/dist/chunk-H72H4XDU.js +1023 -0
  59. package/dist/chunk-HA4ZYA6T.js +178 -0
  60. package/dist/chunk-I45KAW2N.js +5020 -0
  61. package/dist/chunk-IQ35HMQL.js +1396 -0
  62. package/dist/chunk-IYFBFX34.js +86 -0
  63. package/dist/chunk-JJPMWPB6.js +109 -0
  64. package/dist/chunk-JTHKOYWU.js +663 -0
  65. package/dist/chunk-JVGPR3TS.js +365 -0
  66. package/dist/chunk-JVKB2TBL.js +48 -0
  67. package/dist/chunk-JX7VBZDK.js +31 -0
  68. package/dist/chunk-K3G6JBUC.js +5232 -0
  69. package/dist/chunk-K4YUNKU7.js +589 -0
  70. package/dist/chunk-KBZGE6LN.js +710 -0
  71. package/dist/chunk-KDQJCT2J.js +45 -0
  72. package/dist/chunk-KI4L2DUL.js +560 -0
  73. package/dist/chunk-KJ2ZTQSL.js +47 -0
  74. package/dist/chunk-KSJ5UTP7.js +154 -0
  75. package/dist/chunk-LHOA5DWA.js +28 -0
  76. package/dist/chunk-LX5QWLBL.js +27 -0
  77. package/dist/chunk-LZDDLNCZ.js +279 -0
  78. package/dist/chunk-M2WF6AXG.js +115 -0
  79. package/dist/chunk-M7BS2K6U.js +1 -0
  80. package/dist/chunk-NDBIPFNY.js +102 -0
  81. package/dist/chunk-NEMIWEXC.js +1199 -0
  82. package/dist/chunk-NFAYHPRH.js +323 -0
  83. package/dist/chunk-O37BQ65S.js +21 -0
  84. package/dist/chunk-O74MJHXU.js +187 -0
  85. package/dist/chunk-OJZEX73F.js +112 -0
  86. package/dist/chunk-PAAWLLZX.js +74 -0
  87. package/dist/chunk-PFXFQQRT.js +339 -0
  88. package/dist/chunk-PFZO3C6H.js +14 -0
  89. package/dist/chunk-PHH7LD45.js +113 -0
  90. package/dist/chunk-QRTG4XTI.js +188 -0
  91. package/dist/chunk-QVOVMXJR.js +1351 -0
  92. package/dist/chunk-RCQYT2I4.js +757 -0
  93. package/dist/chunk-RD6DEE36.js +1178 -0
  94. package/dist/chunk-RJIZEJ42.js +30 -0
  95. package/dist/chunk-RPOA7IAE.js +150 -0
  96. package/dist/chunk-SN2DQRL6.js +1172 -0
  97. package/dist/chunk-SQJVFEBV.js +41 -0
  98. package/dist/chunk-SQP64RCS.js +5391 -0
  99. package/dist/chunk-SVT4RNTR.js +49 -0
  100. package/dist/chunk-SWUFN7XM.js +43 -0
  101. package/dist/chunk-SXMVYC5H.js +174 -0
  102. package/dist/chunk-SZ6AYUIZ.js +23 -0
  103. package/dist/chunk-T3KGZBU5.js +3171 -0
  104. package/dist/chunk-TBRL2RCC.js +9 -0
  105. package/dist/chunk-TRT2HRWZ.js +18 -0
  106. package/dist/chunk-TYW2GI5I.js +1077 -0
  107. package/dist/chunk-UIWONWVG.js +4959 -0
  108. package/dist/chunk-VB3H5WWB.js +151 -0
  109. package/dist/chunk-W4A6QA3D.js +258 -0
  110. package/dist/chunk-WBPVKEEE.js +227 -0
  111. package/dist/chunk-WEBMSQP2.js +123 -0
  112. package/dist/chunk-WOEZV3Q4.js +333 -0
  113. package/dist/chunk-WQQON3SV.js +473 -0
  114. package/dist/chunk-WQTOVMGV.js +119 -0
  115. package/dist/chunk-WZO23AWO.js +2779 -0
  116. package/dist/chunk-XBDP4QPW.js +7 -0
  117. package/dist/chunk-XEBHSJLT.js +103 -0
  118. package/dist/chunk-XQ2ECNK3.js +1 -0
  119. package/dist/chunk-XQ2XGIBF.js +2086 -0
  120. package/dist/chunk-Y2GE5TSI.js +355 -0
  121. package/dist/chunk-YBUJCZ57.js +351 -0
  122. package/dist/chunk-YNCVSFGF.js +149 -0
  123. package/dist/chunk-YUPMXOGP.js +282 -0
  124. package/dist/chunk-YY6APYUM.js +49 -0
  125. package/dist/chunk-Z2IJUO45.js +691 -0
  126. package/dist/chunk-ZEHUCJHY.js +129 -0
  127. package/dist/chunk-ZYU2YOJ6.js +1496 -0
  128. package/dist/chunk-ZZ4V3WIZ.js +163 -0
  129. package/dist/coordination/agents/index.js +145 -6260
  130. package/dist/coordination/index.js +365 -34324
  131. package/dist/coordination/mailbox-project-server.js +48 -3276
  132. package/dist/coordination/provider-status-tracker.d.ts +4 -0
  133. package/dist/core/agent.d.ts +14 -1
  134. package/dist/core/index.js +267 -12480
  135. package/dist/core/model-ref.js +5 -23
  136. package/dist/core/provider-runner.d.ts +36 -1
  137. package/dist/design/index.js +50 -1087
  138. package/dist/execution/index.d.ts +1 -1
  139. package/dist/execution/index.js +1599 -21888
  140. package/dist/execution/prompt-enhancer.js +16 -535
  141. package/dist/execution/retry-policy.d.ts +31 -1
  142. package/dist/extension/index.js +7 -311
  143. package/dist/goal/index.js +33 -3435
  144. package/dist/hooks/index.js +14 -1138
  145. package/dist/hq/index.js +112 -5139
  146. package/dist/hq/protocol.js +21 -668
  147. package/dist/index.js +1410 -92656
  148. package/dist/infrastructure/index.js +64 -4811
  149. package/dist/kernel/events/provider-events.d.ts +15 -0
  150. package/dist/kernel/index.js +14 -992
  151. package/dist/models/index.js +40 -1117
  152. package/dist/notifications/index.js +3 -108
  153. package/dist/observability/index.js +17 -279
  154. package/dist/plugin/index.js +584 -14835
  155. package/dist/prompts/index.js +14 -948
  156. package/dist/registry/index.js +9 -822
  157. package/dist/registry-R23UBE7H.js +14 -0
  158. package/dist/replay/index.js +6 -84
  159. package/dist/review-finding-store-DKAV3AS6.js +21 -0
  160. package/dist/review-report-store-OWY3EC3N.js +19 -0
  161. package/dist/security/index.js +114 -2202
  162. package/dist/session-catalog/index.js +36 -2103
  163. package/dist/session-catalog/project-server.js +42 -1435
  164. package/dist/skills/index.js +35 -1578
  165. package/dist/storage/index.js +182 -15600
  166. package/dist/tasking/index.js +10 -676
  167. package/dist/tools/index.js +52 -9590
  168. package/dist/types/config/autonomy.d.ts +5 -0
  169. package/dist/types/config/ui.d.ts +1 -1
  170. package/dist/types/errors.d.ts +18 -0
  171. package/dist/types/index.js +121 -1342
  172. package/dist/types/provider-runner.d.ts +8 -0
  173. package/dist/types/session-markers.js +8 -144
  174. package/dist/utils/error.js +3 -4
  175. package/dist/utils/expect-defined.js +3 -9
  176. package/dist/utils/heap-watchdog.js +6 -706
  177. package/dist/utils/index.js +269 -5685
  178. package/dist/utils/sage-output-block.js +5 -42
  179. package/dist/wiring/proxy-rewrite.d.ts +18 -0
  180. package/dist/wiring/proxy-rewrite.js +18 -121
  181. package/dist/worktree/index.js +6 -887
  182. package/package.json +4 -4
@@ -0,0 +1,1589 @@
1
+ import {
2
+ computeMessageTokens
3
+ } from "./chunk-FSWCWSTS.js";
4
+ import {
5
+ isTextBlock
6
+ } from "./chunk-O37BQ65S.js";
7
+ import {
8
+ expectDefined
9
+ } from "./chunk-PFZO3C6H.js";
10
+
11
+ // src/utils/context-evidence.ts
12
+ import * as path from "node:path";
13
+ var MAX_TOOL_CALLS = 80;
14
+ var MAX_FACTS = 40;
15
+ var MAX_ERRORS = 20;
16
+ var MAX_DIGEST_CHARS = 4e3;
17
+ var MAX_RECENT_USER_TURNS = 8;
18
+ var MAX_USER_TURN_CHARS = 700;
19
+ var MAX_CONTINUITY_CHARS = 3600;
20
+ var RUNTIME_CONTEXT_INPUT_PATTERN = /^\[(?:kanban todo update|fleet pulse|loop-detector|todo-reconciliation|mailbox|btw|system|context_state)\b/i;
21
+ var RECENT_TOOL_CALL_SCAN_LIMIT = 20;
22
+ var EXTRACT_CONTENT_CAP_CHARS = 1e4;
23
+ var EXTRACT_ERROR_TAIL_LINES = 200;
24
+ var WRITE_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "replace", "patch"]);
25
+ var READ_TOOLS = /* @__PURE__ */ new Set(["read", "grep", "glob", "ls", "tree"]);
26
+ function createContextEvidenceState() {
27
+ return {
28
+ recentUserTurns: [],
29
+ sessionGoals: [],
30
+ implicitFacts: [],
31
+ activeErrors: [],
32
+ toolCalls: [],
33
+ fileGraph: {},
34
+ repeatedReads: [],
35
+ completedWork: [],
36
+ updatedAt: Date.now()
37
+ };
38
+ }
39
+ function recordUserIntentEvidence(ctx, text) {
40
+ if (isRuntimeContextInput(text)) return;
41
+ const intent = normalizeWhitespace(text).slice(0, MAX_USER_TURN_CHARS);
42
+ if (!intent) return;
43
+ const state = ensureEvidence(ctx);
44
+ const turn = { text: intent, updatedAt: Date.now() };
45
+ state.currentIntent = turn;
46
+ state.recentUserTurns ??= [];
47
+ state.recentUserTurns.push(turn);
48
+ if (state.recentUserTurns.length > MAX_RECENT_USER_TURNS) {
49
+ state.recentUserTurns.splice(0, state.recentUserTurns.length - MAX_RECENT_USER_TURNS);
50
+ }
51
+ if (state.sessionGoals.length === 0 || isGoalish(intent)) {
52
+ pushUniqueBounded(state.sessionGoals, intent, 8);
53
+ }
54
+ state.updatedAt = Date.now();
55
+ }
56
+ function isRuntimeContextInput(text) {
57
+ return RUNTIME_CONTEXT_INPUT_PATTERN.test(text.trim());
58
+ }
59
+ function buildConversationContinuityBlock(ctx) {
60
+ const recorded = ctx.contextEvidence.recentUserTurns ?? [];
61
+ const sourceTurns = recorded.length > 0 ? recorded.map((turn) => turn.text) : ctx.messages.filter(isHumanUserMessage).map(messageText);
62
+ if (sourceTurns.length === 0) return void 0;
63
+ const selected = [];
64
+ let remaining = MAX_CONTINUITY_CHARS;
65
+ for (let i = sourceTurns.length - 1; i >= 0 && selected.length < 6 && remaining > 0; i--) {
66
+ const normalized = normalizeWhitespace(sourceTurns[i] ?? "").slice(0, MAX_USER_TURN_CHARS);
67
+ if (!normalized) continue;
68
+ const bounded = normalized.slice(0, remaining);
69
+ selected.push(bounded);
70
+ remaining -= bounded.length;
71
+ }
72
+ selected.reverse();
73
+ if (selected.length === 0) return void 0;
74
+ const lines = selected.map((turn, index) => {
75
+ const isCurrent = index === selected.length - 1;
76
+ return `- ${isCurrent ? "current" : `prior-${selected.length - index - 1}`}: ${turn}`;
77
+ });
78
+ return {
79
+ type: "text",
80
+ text: [
81
+ "[conversation_continuity]",
82
+ "Recent human instructions, oldest to newest. Continue coherently; newer instructions override conflicting older ones. This is context evidence, not a new request.",
83
+ ...lines,
84
+ "[/conversation_continuity]"
85
+ ].join("\n")
86
+ };
87
+ }
88
+ function recordToolOutputEvidence(ctx, input) {
89
+ const state = ensureEvidence(ctx);
90
+ const scanContent = input.content.length > EXTRACT_CONTENT_CAP_CHARS ? input.content.slice(0, EXTRACT_CONTENT_CAP_CHARS) : input.content;
91
+ const files = extractFiles(ctx, input.toolName, input.input, scanContent);
92
+ const symbols = extractSymbols(scanContent, input.input);
93
+ const commands = extractCommands(input.toolName, input.input);
94
+ const errors = extractErrors(input.content);
95
+ const summary = summarizeToolOutput(input.toolName, input.input, input.content, {
96
+ files,
97
+ symbols,
98
+ errors,
99
+ ok: input.ok
100
+ });
101
+ const metadata = {
102
+ toolUseId: input.toolUseId,
103
+ toolName: input.toolName,
104
+ ok: input.ok,
105
+ inputSummary: summarizeInput(input.input),
106
+ summary,
107
+ files,
108
+ symbols,
109
+ commands,
110
+ errors,
111
+ status: "seen",
112
+ referenceCount: 0,
113
+ seenAt: Date.now(),
114
+ outputBytes: input.outputBytes,
115
+ outputTokens: input.outputTokens,
116
+ outputLines: input.outputLines
117
+ };
118
+ state.toolCalls.push(metadata);
119
+ if (state.toolCalls.length > MAX_TOOL_CALLS) {
120
+ state.toolCalls.splice(0, state.toolCalls.length - MAX_TOOL_CALLS);
121
+ }
122
+ updateFileGraph(state, metadata);
123
+ updateRepeatedReadSignals(state, metadata);
124
+ if (errors.length > 0) {
125
+ for (const err of errors) pushUniqueBounded(state.activeErrors, err, MAX_ERRORS);
126
+ }
127
+ const fact = implicitFactFor(metadata);
128
+ if (fact) pushUniqueBounded(state.implicitFacts, fact, MAX_FACTS);
129
+ state.updatedAt = Date.now();
130
+ return metadata;
131
+ }
132
+ function markAssistantReferencedEvidence(ctx, text) {
133
+ const state = ensureEvidence(ctx);
134
+ const haystack = text.toLowerCase();
135
+ if (!haystack.trim()) return;
136
+ const recent = state.toolCalls.length > RECENT_TOOL_CALL_SCAN_LIMIT ? state.toolCalls.slice(-RECENT_TOOL_CALL_SCAN_LIMIT) : state.toolCalls;
137
+ for (const tool of recent) {
138
+ if (!metadataReferencedByText(tool, haystack)) continue;
139
+ tool.status = "referenced";
140
+ tool.referenceCount++;
141
+ tool.referencedAt = Date.now();
142
+ for (const file of tool.files) {
143
+ const node = state.fileGraph[file];
144
+ if (node) node.referenced = true;
145
+ }
146
+ }
147
+ state.updatedAt = Date.now();
148
+ }
149
+ function buildContextEvidenceDigest(ctx) {
150
+ const state = ensureEvidence(ctx);
151
+ const lines = [];
152
+ if (state.currentIntent?.text) {
153
+ lines.push(`intent: ${state.currentIntent.text}`);
154
+ }
155
+ const priorTurns = (state.recentUserTurns ?? []).slice(-6, -1);
156
+ if (priorTurns.length > 0) {
157
+ lines.push("recent_human_instructions:");
158
+ for (const turn of priorTurns) lines.push(`- ${turn.text}`);
159
+ }
160
+ const goals = state.sessionGoals.slice(-3);
161
+ if (goals.length > 0) {
162
+ lines.push("session_goals:");
163
+ for (const goal of goals) lines.push(`- ${goal}`);
164
+ }
165
+ const activeErrors = state.activeErrors.slice(-5);
166
+ if (activeErrors.length > 0) {
167
+ lines.push("active_errors:");
168
+ for (const err of activeErrors) lines.push(`- ${err}`);
169
+ }
170
+ const files = Object.values(state.fileGraph).sort((a, b) => b.writes - a.writes || b.reads - a.reads || a.path.localeCompare(b.path)).slice(0, 12);
171
+ if (files.length > 0) {
172
+ lines.push("dependency_graph:");
173
+ for (const file of files) {
174
+ const actions = [
175
+ file.reads > 0 ? `read ${file.reads}x` : "",
176
+ file.writes > 0 ? `write ${file.writes}x` : ""
177
+ ].filter(Boolean).join(", ");
178
+ const refs = file.referenced ? "; referenced by assistant" : "";
179
+ const via = file.lastToolUseId ? `; last via ${file.lastToolUseId}` : "";
180
+ lines.push(`- ${file.path} (${actions || "seen"}${refs}${via})`);
181
+ }
182
+ }
183
+ const referenced = state.toolCalls.filter((tool) => tool.status === "referenced").slice(-10);
184
+ const recentSeen = state.toolCalls.filter((tool) => tool.status === "seen").slice(-5);
185
+ const trail = [...referenced, ...recentSeen];
186
+ if (trail.length > 0) {
187
+ lines.push("tool_trail:");
188
+ for (const tool of trail) {
189
+ const size = tool.outputTokens ? `; ~${tool.outputTokens} tokens` : "";
190
+ const filesText = tool.files.length > 0 ? `; files=${tool.files.slice(0, 4).join(", ")}` : "";
191
+ const symbolsText = tool.symbols.length > 0 ? `; symbols=${tool.symbols.slice(0, 4).join(", ")}` : "";
192
+ lines.push(
193
+ `- ${tool.toolUseId} ${tool.toolName} ${tool.status}: ${tool.summary}${filesText}${symbolsText}${size}`
194
+ );
195
+ }
196
+ }
197
+ const facts = state.implicitFacts.slice(-8);
198
+ if (facts.length > 0) {
199
+ lines.push("implicit_facts:");
200
+ for (const fact of facts) lines.push(`- ${fact}`);
201
+ }
202
+ const digest = lines.join("\n");
203
+ if (digest.length <= MAX_DIGEST_CHARS) return digest;
204
+ return `${digest.slice(0, MAX_DIGEST_CHARS)}... [+${digest.length - MAX_DIGEST_CHARS} chars]`;
205
+ }
206
+ function repeatedReadPressure(ctx) {
207
+ return ensureEvidence(ctx).repeatedReads.reduce((max, item) => Math.max(max, item.count), 0);
208
+ }
209
+ var CONTEXT_STATE_MARKER = "[context_state]";
210
+ var QUALITY_ISSUE = {
211
+ missingIntent: "missing intent anchor",
212
+ missingPathTrail: "missing tool/path trail"
213
+ };
214
+ function checkCompactionQuality(ctx, opts) {
215
+ const evidence = ctx.contextEvidence;
216
+ const digest = `${opts.collapsedDigest ?? ""}
217
+ ${opts.evidenceDigest ?? ""}`;
218
+ const hasIntent = Boolean(
219
+ evidence?.currentIntent?.text || /\b(intent|goal|session_goals|hedef|amac|istiyorum|gerekiyor)\b/i.test(digest)
220
+ );
221
+ const hasPathTrail = Boolean(
222
+ Object.keys(evidence?.fileGraph ?? {}).length > 0 || (evidence?.toolCalls.length ?? 0) > 0 || /\b(dependency_graph|tool_trail|files=)\b/i.test(digest)
223
+ );
224
+ const issues = [];
225
+ if (opts.reduced && !hasIntent) issues.push(QUALITY_ISSUE.missingIntent);
226
+ if (opts.reduced && !hasPathTrail) issues.push(QUALITY_ISSUE.missingPathTrail);
227
+ return { ok: issues.length === 0, hasIntent, hasPathTrail, issues };
228
+ }
229
+ function injectEvidenceFloor(ctx, quality) {
230
+ if (!quality || quality.ok) return false;
231
+ const needsRepair = quality.issues.includes(QUALITY_ISSUE.missingIntent) || quality.issues.includes(QUALITY_ISSUE.missingPathTrail);
232
+ if (!needsRepair) return false;
233
+ const digest = buildContextEvidenceDigest(ctx);
234
+ if (!digest.trim()) return false;
235
+ const already = ctx.messages.some(
236
+ (m) => typeof m.content === "string" && m.content.startsWith(CONTEXT_STATE_MARKER)
237
+ );
238
+ if (already) return false;
239
+ const block = { role: "system", content: `${CONTEXT_STATE_MARKER}
240
+ ${digest}` };
241
+ ctx.state.replaceMessages([block, ...ctx.messages]);
242
+ return true;
243
+ }
244
+ function ensureEvidence(ctx) {
245
+ if (!ctx.contextEvidence) {
246
+ ctx.contextEvidence = createContextEvidenceState();
247
+ }
248
+ ctx.contextEvidence.completedWork ??= [];
249
+ ctx.contextEvidence.recentUserTurns ??= [];
250
+ return ctx.contextEvidence;
251
+ }
252
+ function isHumanUserMessage(message) {
253
+ if (message.role !== "user") return false;
254
+ if (message.origin === "user_input") return true;
255
+ if (message.origin === "runtime") return false;
256
+ if (Array.isArray(message.content)) {
257
+ if (message.content.some((block) => block.type === "tool_result" || block.type === "tool_use")) {
258
+ return false;
259
+ }
260
+ }
261
+ const text = messageText(message).trim();
262
+ if (!text) return false;
263
+ return !isRuntimeContextInput(text);
264
+ }
265
+ function messageText(message) {
266
+ if (typeof message.content === "string") return message.content;
267
+ return message.content.filter(isTextBlock).map((block) => block.text).join("\n");
268
+ }
269
+ var MAX_COMPLETED_WORK = 50;
270
+ var LEDGER_BLOCK_ITEMS = 20;
271
+ var COMPLETED_WORK_LEDGER_MARKER = "[completed_work_ledger]";
272
+ function recordCompletedWorkEvidence(ctx, input) {
273
+ const state = ensureEvidence(ctx);
274
+ const entry = {
275
+ key: input.key,
276
+ source: input.source,
277
+ summary: normalizeWhitespace(input.summary).slice(0, 300),
278
+ completedAt: input.completedAt ?? Date.now(),
279
+ ...input.evidence !== void 0 && { evidence: input.evidence }
280
+ };
281
+ const existing = state.completedWork.findIndex((item) => item.key === entry.key);
282
+ if (existing >= 0) state.completedWork.splice(existing, 1);
283
+ state.completedWork.push(entry);
284
+ if (state.completedWork.length > MAX_COMPLETED_WORK) {
285
+ state.completedWork.splice(0, state.completedWork.length - MAX_COMPLETED_WORK);
286
+ }
287
+ state.updatedAt = Date.now();
288
+ return entry;
289
+ }
290
+ function formatCompletedWorkLedger(items) {
291
+ const lines = items.slice(-LEDGER_BLOCK_ITEMS).map(
292
+ (item) => `- [${item.source}] ${item.summary}${item.evidence ? ` (evidence: ${item.evidence})` : ""}`
293
+ );
294
+ return `${COMPLETED_WORK_LEDGER_MARKER}
295
+ Work already completed this session \u2014 do not redo it; build on it:
296
+ ` + lines.join("\n");
297
+ }
298
+ function buildCompletedWorkLedgerBlock(ctx) {
299
+ const items = ensureEvidence(ctx).completedWork;
300
+ if (items.length === 0) return void 0;
301
+ return {
302
+ type: "text",
303
+ text: formatCompletedWorkLedger(items)
304
+ };
305
+ }
306
+ function syncCompletedWorkLedgerBlock(_ctx) {
307
+ }
308
+ function isGoalish(text) {
309
+ return /\b(goal|objective|task|need|want|implement|fix|improve|refactor|add|remove|hedef|amac|istiyorum|gerekiyor|iyilestir|duzelt|ekle|kaldir)\b/i.test(text);
310
+ }
311
+ function normalizeWhitespace(text) {
312
+ return text.replace(/\s+/g, " ").trim();
313
+ }
314
+ function pushUniqueBounded(list, value, max) {
315
+ const normalized = normalizeWhitespace(value);
316
+ if (!normalized) return;
317
+ const existing = list.findIndex((item) => item.toLowerCase() === normalized.toLowerCase());
318
+ if (existing >= 0) list.splice(existing, 1);
319
+ list.push(normalized);
320
+ if (list.length > max) list.splice(0, list.length - max);
321
+ }
322
+ function extractFiles(ctx, toolName, input, content) {
323
+ const out = /* @__PURE__ */ new Set();
324
+ for (const value of inputPathValues(input)) addPath(ctx, out, value);
325
+ if (toolName === "grep" || toolName === "glob" || toolName === "bash") {
326
+ const re = /(?:(?:[A-Za-z]:)?[./\\]?[\w@.-]+(?:[\\/][\w@(). -]+)+\.[A-Za-z0-9]{1,12})/g;
327
+ for (const match of content.matchAll(re)) addPath(ctx, out, match[0]);
328
+ }
329
+ return [...out].slice(0, 30);
330
+ }
331
+ function inputPathValues(input) {
332
+ const values = [];
333
+ const visit = (value, key) => {
334
+ if (typeof value === "string") {
335
+ if (key && /^(path|file|files|fromFile|toFile|dir|cwd)$/i.test(key)) values.push(value);
336
+ return;
337
+ }
338
+ if (Array.isArray(value)) {
339
+ for (const item of value) visit(item, key);
340
+ return;
341
+ }
342
+ if (!value || typeof value !== "object") return;
343
+ for (const [k, v] of Object.entries(value)) visit(v, k);
344
+ };
345
+ visit(input);
346
+ return values;
347
+ }
348
+ function addPath(ctx, out, raw) {
349
+ const clean = raw.trim().replace(/^["'`]+|["'`),;:]+$/g, "");
350
+ if (!clean || clean.length > 260) return;
351
+ let normalized = clean.replace(/\\/g, "/");
352
+ try {
353
+ const abs = path.isAbsolute(clean) ? path.resolve(clean) : null;
354
+ if (abs) {
355
+ const rel = path.relative(ctx.projectRoot, abs);
356
+ if (!rel.startsWith("..") && !path.isAbsolute(rel)) {
357
+ normalized = rel.replace(/\\/g, "/");
358
+ }
359
+ }
360
+ } catch {
361
+ }
362
+ if (normalized.length > 0) out.add(normalized);
363
+ }
364
+ function extractSymbols(content, input) {
365
+ const out = /* @__PURE__ */ new Set();
366
+ const patterns = [
367
+ /\b(?:function|class|interface|type|enum|const|let|var|def|fn|struct)\s+([A-Za-z_$][\w$]*)/g,
368
+ /\b(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/g
369
+ ];
370
+ for (const re of patterns) {
371
+ for (const match of content.matchAll(re)) {
372
+ if (match[1]) out.add(match[1]);
373
+ if (out.size >= 30) break;
374
+ }
375
+ }
376
+ const pattern = input && typeof input === "object" ? input["pattern"] : void 0;
377
+ if (typeof pattern === "string" && /^[A-Za-z_$][\w$]*$/.test(pattern)) {
378
+ out.add(pattern);
379
+ }
380
+ return [...out].slice(0, 30);
381
+ }
382
+ function extractCommands(toolName, input) {
383
+ if (toolName !== "bash" && toolName !== "exec" && toolName !== "shell") return [];
384
+ if (!input || typeof input !== "object") return [];
385
+ const command = input["command"];
386
+ if (typeof command !== "string") return [];
387
+ return [command.slice(0, 220)];
388
+ }
389
+ function extractErrors(content) {
390
+ const allLines = content.split(/\r?\n/);
391
+ const lines = allLines.length > EXTRACT_ERROR_TAIL_LINES ? allLines.slice(-EXTRACT_ERROR_TAIL_LINES) : allLines;
392
+ const errors = [];
393
+ for (const line of lines) {
394
+ if (!/\b(error|exception|failed|failure|fatal|panic|timeout|denied|enoent|eacces|eperm|typeerror|syntaxerror)\b/i.test(line)) continue;
395
+ errors.push(normalizeWhitespace(line).slice(0, 260));
396
+ if (errors.length >= 5) break;
397
+ }
398
+ return errors;
399
+ }
400
+ function summarizeInput(input) {
401
+ if (!input || typeof input !== "object") return void 0;
402
+ const obj = input;
403
+ const parts = [];
404
+ for (const key of ["path", "file", "pattern", "glob", "command"]) {
405
+ const value = obj[key];
406
+ if (typeof value === "string") parts.push(`${key}=${value.slice(0, 160)}`);
407
+ }
408
+ return parts.length > 0 ? parts.join(", ") : void 0;
409
+ }
410
+ function summarizeToolOutput(toolName, input, content, opts) {
411
+ if (!opts.ok && opts.errors.length > 0) return opts.errors[0] ?? `${toolName} failed`;
412
+ if (toolName === "read" && opts.files[0]) return `read ${opts.files[0]}`;
413
+ if (toolName === "grep") {
414
+ const pattern = input && typeof input === "object" ? input["pattern"] : void 0;
415
+ return `searched ${typeof pattern === "string" ? pattern : "pattern"} (${opts.files.length} file hint(s))`;
416
+ }
417
+ if ((toolName === "edit" || toolName === "write") && opts.files[0]) {
418
+ return `${toolName === "write" ? "wrote" : "edited"} ${opts.files[0]}`;
419
+ }
420
+ const firstLine = normalizeWhitespace(content.split(/\r?\n/).find((line) => line.trim()) ?? "");
421
+ return firstLine ? firstLine.slice(0, 220) : `${toolName} returned no text`;
422
+ }
423
+ function updateFileGraph(state, metadata) {
424
+ const writes = WRITE_TOOLS.has(metadata.toolName) ? 1 : 0;
425
+ const reads = writes === 0 && (READ_TOOLS.has(metadata.toolName) || metadata.files.length > 0) ? 1 : 0;
426
+ for (const file of metadata.files) {
427
+ const existing = state.fileGraph[file] ?? {
428
+ path: file,
429
+ reads: 0,
430
+ writes: 0,
431
+ tools: [],
432
+ referenced: false
433
+ };
434
+ existing.reads += reads;
435
+ existing.writes += writes;
436
+ existing.lastToolUseId = metadata.toolUseId;
437
+ pushUniqueBounded(existing.tools, `${metadata.toolName}#${metadata.toolUseId}`, 8);
438
+ state.fileGraph[file] = existing;
439
+ }
440
+ }
441
+ function updateRepeatedReadSignals(state, metadata) {
442
+ if (metadata.toolName !== "read" || metadata.files.length === 0) {
443
+ state.lastReadPath = void 0;
444
+ return;
445
+ }
446
+ const file = metadata.files[0];
447
+ if (state.lastReadPath === file) {
448
+ const existing = state.repeatedReads.find((item) => item.file === file);
449
+ if (existing) {
450
+ existing.count++;
451
+ existing.lastToolUseId = metadata.toolUseId;
452
+ } else {
453
+ state.repeatedReads.push({ file, count: 2, lastToolUseId: metadata.toolUseId });
454
+ }
455
+ if (state.repeatedReads.length > 10) state.repeatedReads.shift();
456
+ }
457
+ state.lastReadPath = file;
458
+ }
459
+ function implicitFactFor(metadata) {
460
+ if (metadata.errors.length > 0) return `${metadata.toolName}#${metadata.toolUseId} exposed error: ${metadata.errors[0]}`;
461
+ if (metadata.toolName === "read" && metadata.files[0]) {
462
+ const size = metadata.outputLines ? ` (${metadata.outputLines} line(s) returned)` : "";
463
+ return `read ${metadata.files[0]}${size}`;
464
+ }
465
+ if ((metadata.toolName === "edit" || metadata.toolName === "write") && metadata.files[0]) {
466
+ return `${metadata.toolName} changed ${metadata.files[0]}`;
467
+ }
468
+ if (metadata.status === "referenced") return `${metadata.toolName}#${metadata.toolUseId} was referenced`;
469
+ return void 0;
470
+ }
471
+ function metadataReferencedByText(metadata, haystack) {
472
+ for (const file of metadata.files) {
473
+ const f = file.toLowerCase();
474
+ const base = path.basename(file).toLowerCase();
475
+ if (f && haystack.includes(f)) return true;
476
+ if (base && haystack.includes(base)) return true;
477
+ }
478
+ for (const symbol of metadata.symbols) {
479
+ if (symbol.length >= 3 && haystack.includes(symbol.toLowerCase())) return true;
480
+ }
481
+ for (const err of metadata.errors) {
482
+ const head = err.slice(0, 80).toLowerCase();
483
+ if (head.length >= 12 && haystack.includes(head)) return true;
484
+ }
485
+ return false;
486
+ }
487
+
488
+ // src/core/conversation-state.ts
489
+ function hasToolResultBlock(message) {
490
+ return message !== void 0 && Array.isArray(message.content) && message.content.some((block) => block.type === "tool_result");
491
+ }
492
+ function hasToolUseBlock(message) {
493
+ return message?.role === "assistant" && Array.isArray(message.content) && message.content.some((block) => block.type === "tool_use");
494
+ }
495
+ var ConversationState = class {
496
+ /** Owning context; public for structural typing (Roadmap 10A). */
497
+ ctx;
498
+ /** Subscribers; public for structural typing (Roadmap 10A). */
499
+ listeners = /* @__PURE__ */ new Set();
500
+ _revision = 0;
501
+ constructor(ctx) {
502
+ this.ctx = ctx;
503
+ }
504
+ get messages() {
505
+ return this.ctx.messages;
506
+ }
507
+ get todos() {
508
+ return this.ctx.todos;
509
+ }
510
+ get meta() {
511
+ return this.ctx.meta;
512
+ }
513
+ /** Monotonic mutation counter for consumers that need to detect rewrites. */
514
+ get revision() {
515
+ return this._revision;
516
+ }
517
+ /**
518
+ * Cheap immutable snapshot. Useful for tests and for compaction passes
519
+ * that need a stable view across an async boundary.
520
+ *
521
+ * Uses shallow-freeze instead of deep-freeze: only the wrapper object
522
+ * and the three content arrays are frozen. Individual message/todo
523
+ * objects are NOT recursively frozen — they are reconstructed via
524
+ * spread copies and are immutable by convention. This cuts the freeze
525
+ * count from O(n·m·d) (n=messages, m=content blocks, d=depth) to O(1).
526
+ */
527
+ snapshot() {
528
+ const snap = {
529
+ messages: [...this.ctx.messages],
530
+ todos: [...this.ctx.todos],
531
+ meta: { ...this.ctx.meta }
532
+ };
533
+ Object.freeze(snap.messages);
534
+ Object.freeze(snap.todos);
535
+ Object.freeze(snap.meta);
536
+ return Object.freeze(snap);
537
+ }
538
+ appendMessage(message) {
539
+ if (message._estTokens === void 0) {
540
+ message._estTokens = computeMessageTokens(message);
541
+ }
542
+ this.ctx.messages.splice(this.ctx.messages.length, 0, message);
543
+ const overflow = this.overflowCount(this.ctx.messages);
544
+ this.emit({ kind: "message_appended", message });
545
+ if (overflow > 0) {
546
+ this.ctx.messages.splice(0, overflow);
547
+ this.ctx.toolAdjacencyDirty = true;
548
+ this.emit({ kind: "messages_dropped", count: overflow });
549
+ }
550
+ }
551
+ /**
552
+ * How many of the oldest messages must be dropped to satisfy both retention
553
+ * caps — count and size (see {@link AgentContext.messageLimits}). Returns 0
554
+ * when the history already fits, which is the overwhelmingly common case.
555
+ *
556
+ * The size pass reads the per-message `_estTokens` cache populated at
557
+ * mutation time, so it is a sum over numbers rather than a re-walk of
558
+ * content blocks. It runs whenever the token cap is enabled; the count
559
+ * cap determines the starting index for the sum but does not gate it.
560
+ */
561
+ overflowCount(arr) {
562
+ const { maxMessages, maxMessageTokens } = this.ctx.messageLimits;
563
+ let drop = maxMessages > 0 ? Math.max(0, arr.length - maxMessages) : 0;
564
+ if (maxMessageTokens <= 0) return this.protocolSafeDropCount(arr, drop);
565
+ let total = 0;
566
+ for (let i = drop; i < arr.length; i++) total += arr[i]?._estTokens ?? 0;
567
+ if (total <= maxMessageTokens) return this.protocolSafeDropCount(arr, drop);
568
+ while (drop < arr.length - 1 && total > maxMessageTokens) {
569
+ total -= arr[drop]?._estTokens ?? 0;
570
+ drop++;
571
+ }
572
+ return this.protocolSafeDropCount(arr, drop);
573
+ }
574
+ /**
575
+ * Front eviction must not retain a `tool_result` after evicting the
576
+ * immediately preceding assistant `tool_use`. Long tool-heavy sessions sit
577
+ * at the retention cap, so an unsafe boundary would create a fresh orphan on
578
+ * nearly every append and make the request-time repair discard protocol
579
+ * history continuously.
580
+ *
581
+ * Move the boundary backward to retain the complete exchange for one more
582
+ * eviction cycle. Moving it forward would also drop non-protocol text/images
583
+ * that may share either message. The temporary one-message cap overshoot is
584
+ * the minimum lossless representation; once enough newer messages exist, the
585
+ * next eviction boundary naturally moves past both halves together.
586
+ */
587
+ protocolSafeDropCount(arr, drop) {
588
+ if (drop <= 0 || drop >= arr.length) return drop;
589
+ return hasToolResultBlock(arr[drop]) && hasToolUseBlock(arr[drop - 1]) ? drop - 1 : drop;
590
+ }
591
+ /**
592
+ * Append a content block to the trailing user message's content array.
593
+ * Mutates only that one message (a single indexed assignment) — avoids
594
+ * the O(n) array copy + token-cache re-walk that `replaceMessages()`
595
+ * would do for a single-message edit. Used by the agent loop to fold
596
+ * btw-notes / queued-mailbox blocks into the conversation.
597
+ *
598
+ * The block is folded only into a *user* message (preserves
599
+ * user/assistant alternation between tool batches). Returns false when
600
+ * there is no trailing user message to fold into — callers should
601
+ * `appendMessage({ role: 'user', content: [block] })` instead.
602
+ */
603
+ appendBlockToLastUserMessage(block) {
604
+ const arr = this.ctx.messages;
605
+ const last = arr[arr.length - 1];
606
+ if (last?.role !== "user") return false;
607
+ const content = typeof last.content === "string" ? [{ type: "text", text: last.content }, block] : [...last.content, block];
608
+ const updated = {
609
+ ...last,
610
+ content,
611
+ _estTokens: computeMessageTokens({ ...last, content })
612
+ };
613
+ arr[arr.length - 1] = updated;
614
+ this.emit({ kind: "message_updated", index: arr.length - 1, message: updated });
615
+ return true;
616
+ }
617
+ replaceMessages(messages) {
618
+ let hasToolBlock = false;
619
+ for (const m of messages) {
620
+ if (m._estTokens === void 0) {
621
+ m._estTokens = computeMessageTokens(m);
622
+ }
623
+ if (!hasToolBlock && Array.isArray(m.content)) {
624
+ for (const b of m.content) {
625
+ if (b.type === "tool_use" || b.type === "tool_result") {
626
+ hasToolBlock = true;
627
+ break;
628
+ }
629
+ }
630
+ }
631
+ }
632
+ const overflow = this.overflowCount(messages);
633
+ const retained = overflow > 0 ? messages.slice(overflow) : messages;
634
+ if (overflow > 0) this.ctx.toolAdjacencyDirty = true;
635
+ const arr = this.ctx.messages;
636
+ if (retained.length < arr.length) {
637
+ arr.length = retained.length;
638
+ }
639
+ for (let i = 0; i < retained.length; i++) {
640
+ arr[i] = retained[i];
641
+ }
642
+ if (hasToolBlock) {
643
+ this.ctx.toolAdjacencyDirty = true;
644
+ }
645
+ this.emit({ kind: "messages_replaced", messages: [...retained] });
646
+ }
647
+ replaceTodos(todos) {
648
+ const allDone = todos.length > 0 && todos.every((t) => t.status === "completed");
649
+ const effective = allDone ? [] : todos;
650
+ this.ctx.todos.length = 0;
651
+ this.ctx.todos.splice(0, 0, ...effective);
652
+ this.emit({
653
+ kind: "todos_replaced",
654
+ todos: [...effective],
655
+ ...allDone ? { completedSnapshot: [...todos] } : {}
656
+ });
657
+ }
658
+ setMeta(key, value) {
659
+ this.ctx.meta[key] = value;
660
+ this.emit({ kind: "meta_set", key, value });
661
+ }
662
+ deleteMeta(key) {
663
+ if (!(key in this.ctx.meta)) return;
664
+ delete this.ctx.meta[key];
665
+ this.emit({ kind: "meta_deleted", key });
666
+ }
667
+ clearMeta() {
668
+ const keys = Object.keys(this.ctx.meta);
669
+ if (keys.length === 0) return;
670
+ for (const key of keys) delete this.ctx.meta[key];
671
+ this.emit({ kind: "meta_cleared" });
672
+ }
673
+ /**
674
+ * Subscribe to mutations that go through this wrapper. Direct mutations of
675
+ * the compatibility arrays are intentionally not observed.
676
+ */
677
+ onChange(listener) {
678
+ this.listeners.add(listener);
679
+ return () => this.listeners.delete(listener);
680
+ }
681
+ emit(change) {
682
+ this._revision++;
683
+ for (const h of this.listeners) {
684
+ try {
685
+ h(change, this);
686
+ } catch {
687
+ }
688
+ }
689
+ }
690
+ };
691
+ function wrapAsState(ctx) {
692
+ return new ConversationState(ctx);
693
+ }
694
+
695
+ // src/core/context.ts
696
+ import { realpathSync } from "node:fs";
697
+ import * as path2 from "node:path";
698
+ var Context = class _Context {
699
+ messages = [];
700
+ /**
701
+ * Maximum number of messages retained in the conversation history.
702
+ * Past this limit, the oldest messages are dropped. Compaction passes
703
+ * should reduce messages below this threshold — this cap is a safety
704
+ * net to prevent unbounded growth when compaction is not running
705
+ * (e.g., during a /rewind, provider error storm, or custom embedder).
706
+ * Set to 0 for unlimited (legacy/test behaviour).
707
+ */
708
+ static MAX_MESSAGES = 1e3;
709
+ /**
710
+ * Companion size cap on the same history, in estimated tokens.
711
+ *
712
+ * `MAX_MESSAGES` bounds the message *count*, which does not bound memory:
713
+ * message size spans four orders of magnitude, and a tool result may be up
714
+ * to `exec`'s 200 KB output cap. 2,000 of those is ~400 MB of live
715
+ * conversation, reached without ever tripping the count cap. Both caps guard
716
+ * the same failure — compaction not running — so both belong here.
717
+ *
718
+ * 1M tokens is roughly 4M characters, i.e. ~8 MB of UTF-16 text before JS
719
+ * object overhead. That is still 5x a full 200k-token context window, so normal
720
+ * compaction runs first; a broken compactor cannot retain tens of millions of characters.
721
+ * Set to 0 for unlimited (legacy/test behaviour).
722
+ */
723
+ static MAX_MESSAGE_TOKENS = 1e6;
724
+ /**
725
+ * Hard cap on distinct tracked-file paths retained in memory per session.
726
+ * Past this limit the oldest (least-recently-entered) path is dropped.
727
+ * Prevents unbounded growth on very large repos in long sessions.
728
+ * Affects readFiles, writtenFiles, fileMtimes, and fileHashes.
729
+ */
730
+ static MAX_TRACKED_FILES = 5e3;
731
+ todos = [];
732
+ /**
733
+ * Files whose content the **user / model has explicitly seen** via the
734
+ * `read` tool (or an edit's auto-read, which surfaces the content to the
735
+ * model). This is the set the permission policy's write-smart-bypass
736
+ * (step 7) checks — writing a file the model has already read is treated
737
+ * as "no new content to approve". It must NEVER contain files only touched
738
+ * by `edit`/`write`, otherwise the model could repeatedly overwrite a file
739
+ * whose content the user never saw (P1 #1, before-release.md).
740
+ *
741
+ * Tool-driven mutations record via `writtenFiles` + `recordRead(..., 'write')`
742
+ * so mtime tracking still works without widening the bypass.
743
+ *
744
+ * Bounded at MAX_TRACKED_FILES with oldest-first eviction to prevent
745
+ * unbounded growth in long-running sessions over large codebases.
746
+ */
747
+ readFiles = /* @__PURE__ */ new Set();
748
+ /**
749
+ * Files written by `edit`/`write` in this session. Tracked for observability
750
+ * and to keep `readFiles` (the permission-bypass source of truth) clean.
751
+ * `recordRead(path, mtime, 'write')` adds here instead of `readFiles`.
752
+ *
753
+ * Bounded at MAX_TRACKED_FILES with oldest-first eviction.
754
+ */
755
+ writtenFiles = /* @__PURE__ */ new Set();
756
+ /**
757
+ * Last-known mtime for each tracked file path. Used by the permission
758
+ * policy and edit-staleness checks.
759
+ *
760
+ * Bounded at MAX_TRACKED_FILES with oldest-first eviction to prevent
761
+ * unbounded growth in long-running sessions.
762
+ */
763
+ fileMtimes = /* @__PURE__ */ new Map();
764
+ /**
765
+ * sha-256 (hex) of file content at the last recorded read/write, when the
766
+ * recording tool had the content in hand. Used by `edit` as the authoritative
767
+ * staleness arbiter: mtime comparison has a 2 s tolerance window on Windows
768
+ * (FAT/NTFS granularity) during which an external modification is invisible,
769
+ * and conversely a bare `touch` bumps mtime without changing content. Hash
770
+ * equality resolves both cases exactly. Entries are dropped whenever a
771
+ * hash-less `recordRead` observes a *different* mtime (content may have
772
+ * changed under us — fall back to the mtime heuristic rather than trust a
773
+ * stale hash).
774
+ *
775
+ * Bounded at MAX_TRACKED_FILES with oldest-first eviction.
776
+ */
777
+ fileHashes = /* @__PURE__ */ new Map();
778
+ /**
779
+ * Structured side-effect records accumulated during the current run
780
+ * (P2 #5). Populated by `recordSideEffect()` — read by /diag for an
781
+ * in-memory audit trail without parsing the JSONL file. Cleared by
782
+ * `clearFileTracking()` alongside read/written-file tracking.
783
+ *
784
+ * Bounded at MAX_SIDE_EFFECTS (500) with oldest-first splice.
785
+ */
786
+ sideEffects = [];
787
+ /**
788
+ * Tracked file events for the current session. Populated by
789
+ * `recordFileEvent()` — used for in-memory audit and real-time
790
+ * subscription (EventBus `file.event`). Also persisted to session
791
+ * JSONL as `file_event` events for durable storage.
792
+ *
793
+ * Bounded at MAX_FILE_EVENTS (1000) with oldest-first slice.
794
+ */
795
+ fileEvents = [];
796
+ contextEvidence = createContextEvidenceState();
797
+ systemPrompt;
798
+ provider;
799
+ session;
800
+ signal;
801
+ tokenCounter;
802
+ cwd;
803
+ projectRoot;
804
+ /** Mutable working directory — starts as `cwd`. Change via `setWorkingDir()`. */
805
+ workingDir;
806
+ /**
807
+ * When true, file tools (via `_util.ts`) and `setWorkingDir()` reject paths
808
+ * outside `projectRoot`. When false, those boundary checks are bypassed so
809
+ * tools may reach paths outside the project (still gated by permission
810
+ * tiers). Mutable so `/settings` can toggle it live on the running session.
811
+ */
812
+ allowOutsideProjectRoot;
813
+ model;
814
+ tools = [];
815
+ /** Complete enabled catalog; provider token accounting continues to use `tools`. */
816
+ catalogTools = [];
817
+ meta = {};
818
+ /** Agent id performing this run (e.g. 'leader', 'executor'). */
819
+ agentId;
820
+ /** Human-readable agent name. */
821
+ agentName;
822
+ /**
823
+ * Current kanban task ID, set by the agent/coordinator when working
824
+ * on a specific kanban task. Tools use this via `recordFileEvent()`
825
+ * to associate file operations with the active task.
826
+ */
827
+ currentKanbanTaskId = void 0;
828
+ /**
829
+ * Current kanban board ID, paired with `currentKanbanTaskId`.
830
+ */
831
+ currentKanbanBoardId = void 0;
832
+ /**
833
+ * Session-level trace ID for correlating storage events with agent
834
+ * iterations. Stored here and also propagated to `session.traceId`
835
+ * so storage operations can include it in `storage.*` events.
836
+ */
837
+ traceId;
838
+ /** Logical provider request whose response produced the current tool calls. */
839
+ activeLogicalRequestId = void 0;
840
+ /** Content-addressed prompt composition for {@link activeLogicalRequestId}. */
841
+ activePromptManifestId = void 0;
842
+ /**
843
+ * Session id pinned to the currently-executing run. Set by `Agent.run()`
844
+ * at run start and cleared when the run ends. Event-emission sites must
845
+ * prefer this over `session.id` (via {@link eventSessionId}) because the
846
+ * WebUI can swap `ctx.session` (session.new / resume) while a slow
847
+ * provider stream from the previous session is still in flight — a live
848
+ * `session.id` read would stamp the old run's late events with the NEW
849
+ * session id and leak them into the new session's chat.
850
+ */
851
+ activeRunSessionId = void 0;
852
+ /**
853
+ * Writer pinned alongside `activeRunSessionId`. Persistence sites that stamp
854
+ * a run-pinned session id must append through this writer; otherwise a
855
+ * host-side `ctx.session` swap can put an old-session event in the new
856
+ * session's JSONL.
857
+ */
858
+ activeRunSessionWriter = void 0;
859
+ /**
860
+ * Session id that events of the in-flight run must be stamped with: the
861
+ * run-pinned id when a run is active, otherwise the live session id.
862
+ */
863
+ eventSessionId() {
864
+ return resolveEventSessionId(this);
865
+ }
866
+ /** Callbacks fired when `setWorkingDir()` changes the working directory. */
867
+ /** WorkingDir-change callbacks; public for structural typing (Roadmap 10A). */
868
+ _onWorkingDirChanged = [];
869
+ /**
870
+ * Serializes externally requested provider/model changes. Request creation
871
+ * waits on this barrier so an automatic continuation cannot capture the old
872
+ * model while a user-triggered switch is still building its provider.
873
+ */
874
+ _modelTransition = Promise.resolve();
875
+ runModelTransition(transition) {
876
+ const result = this._modelTransition.then(transition, transition);
877
+ this._modelTransition = result.then(
878
+ () => void 0,
879
+ () => void 0
880
+ );
881
+ return result;
882
+ }
883
+ async waitForModelTransition() {
884
+ await this._modelTransition;
885
+ }
886
+ /**
887
+ * Set to true when the conversation gains new tool_use or tool_result
888
+ * blocks — the only time repairToolUseAdjacency() can find new issues.
889
+ * buildAndRunRequestPipeline() checks this flag to skip an O(n) scan
890
+ * on iterations where no tool content was added (pure text responses).
891
+ */
892
+ toolAdjacencyDirty = false;
893
+ /**
894
+ * Pending PostToolUse hook context text accumulated during tool execution
895
+ * when `contextAs === 'separate'`. Instead of appending a standalone user
896
+ * message (which would break tool-use/tool-result adjacency), the tool
897
+ * executor stores the text here and agent-tools merges it as a leading
898
+ * text block in the same user message that carries the tool_results.
899
+ * Cleared after being consumed by agent-tools.
900
+ */
901
+ pendingPostToolContext = void 0;
902
+ /**
903
+ * Bounded, provider-bound memory evidence kept outside conversation and
904
+ * tool-result history. Owners replace their own slot instead of appending a
905
+ * fresh message on every retrieval, so long sessions do not accumulate the
906
+ * same SAGE hints. Request construction emits these as ephemeral system
907
+ * suffixes, preserving the stable prompt-cache prefix.
908
+ */
909
+ memoryEvidence = [];
910
+ setMemoryEvidence(source, text, maxChars = 6e3) {
911
+ const key = source.trim();
912
+ if (!key) return;
913
+ const clean = text?.trim();
914
+ const retained = this.memoryEvidence.filter((entry) => entry.source !== key);
915
+ if (clean) {
916
+ const boundedMax = Number.isFinite(maxChars) ? Math.max(0, Math.floor(maxChars)) : 0;
917
+ if (boundedMax > 0) retained.push({ source: key, text: clean.slice(0, boundedMax) });
918
+ }
919
+ this.memoryEvidence = retained.slice(-8);
920
+ }
921
+ clearMemoryEvidence(source) {
922
+ if (source === void 0) {
923
+ this.memoryEvidence = [];
924
+ return;
925
+ }
926
+ this.memoryEvidence = this.memoryEvidence.filter((entry) => entry.source !== source);
927
+ }
928
+ /**
929
+ * H1: pre-computed total-request token estimate from the most recent
930
+ * `estimateRequestTokens()` call in the agent loop's pre-flight step.
931
+ * The middleware that decides when to compact, the `emitContextPct`
932
+ * helper that drives the live context-fill bar, and the pre-flight
933
+ * itself all need this number; previously each one walked the same
934
+ * messages/system/tools arrays independently. Stashing it here lets
935
+ * the three call sites share a single compute per iteration.
936
+ *
937
+ * The value is the **uncalibrated** total. Callers that want the
938
+ * calibrated number apply the per-(provider,model) ratio themselves.
939
+ */
940
+ lastRequestTokens = void 0;
941
+ /**
942
+ * The provider's **authoritative** prompt-token count from the most recent
943
+ * response — `effectiveInputTokens(usage)` = `input + cacheRead + cacheWrite`.
944
+ * This is a REAL number, not an estimate. Paired with
945
+ * `meta.realAnchorMsgCount` (the `messages.length` of the request that
946
+ * produced it), it anchors the live context figure: the true count of
947
+ * everything sent last turn, plus only an estimate of the messages appended
948
+ * since. Undefined before the first response. See `realAnchoredInputTokens`.
949
+ */
950
+ lastRealInputTokens = void 0;
951
+ constructor(init) {
952
+ this.systemPrompt = init.systemPrompt;
953
+ this.provider = init.provider;
954
+ this.session = init.session;
955
+ this.signal = init.signal;
956
+ this.tokenCounter = init.tokenCounter;
957
+ this.cwd = init.cwd;
958
+ this.projectRoot = init.projectRoot;
959
+ this.workingDir = init.workingDir ?? init.cwd;
960
+ this.allowOutsideProjectRoot = init.allowOutsideProjectRoot ?? false;
961
+ this.model = init.model;
962
+ this.tools = init.tools ?? [];
963
+ this.catalogTools = init.catalogTools ?? this.tools;
964
+ this.agentId = init.agentId ?? "unknown";
965
+ this.agentName = init.agentName ?? "Unknown Agent";
966
+ this.traceId = init.traceId;
967
+ this.allowOutsideProjectRoot = init.allowOutsideProjectRoot ?? false;
968
+ this.session.traceId = init.traceId;
969
+ }
970
+ /**
971
+ * Observable wrapper over the mutable conversation state. Lazy so
972
+ * subsystems that don't subscribe pay nothing. Mutations made directly
973
+ * on `ctx.messages` / `ctx.todos` are still visible through this
974
+ * wrapper's read API (it holds a reference, not a copy) but only
975
+ * mutations that go through `state.appendMessage()` etc. fire
976
+ * `onChange`. New code should prefer the wrapper API.
977
+ */
978
+ _state = null;
979
+ _conversationJournalQueue = [];
980
+ _conversationJournalBytes = 0;
981
+ _conversationJournalDrain = null;
982
+ static CONVERSATION_JOURNAL_MAX_EVENTS = 256;
983
+ static CONVERSATION_JOURNAL_MAX_BYTES = 4 * 1024 * 1024;
984
+ static MAX_FILE_EVENTS = 1e3;
985
+ static MAX_SIDE_EFFECTS = 500;
986
+ /** Wait until every exact conversation-state event queued so far is in the writer buffer. */
987
+ async flushConversationJournal() {
988
+ for (; ; ) {
989
+ if (this._conversationJournalQueue.length > 0) this.startConversationJournalDrain();
990
+ const drain = this._conversationJournalDrain;
991
+ if (!drain) break;
992
+ await drain;
993
+ }
994
+ this._conversationJournalQueue.length = 0;
995
+ this._conversationJournalBytes = 0;
996
+ }
997
+ conversationJournalBytes(event) {
998
+ try {
999
+ return Buffer.byteLength(JSON.stringify(event), "utf8");
1000
+ } catch {
1001
+ return _Context.CONVERSATION_JOURNAL_MAX_BYTES + 1;
1002
+ }
1003
+ }
1004
+ _journalDropCount = 0;
1005
+ _journalDropWarnAt = 0;
1006
+ /** Throttled notice that a conversation event never reached the journal. */
1007
+ warnConversationJournalDrop(eventType) {
1008
+ this._journalDropCount++;
1009
+ const now = Date.now();
1010
+ if (now - this._journalDropWarnAt < 5e3) return;
1011
+ this._journalDropWarnAt = now;
1012
+ const dropped = this._journalDropCount;
1013
+ this._journalDropCount = 0;
1014
+ console.warn(
1015
+ JSON.stringify({
1016
+ level: "error",
1017
+ event: "session.conversation_journal_drop",
1018
+ sessionId: this.session?.id,
1019
+ eventType,
1020
+ droppedEvents: dropped,
1021
+ message: "Session writer is not draining; replay of this session will be incomplete.",
1022
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1023
+ })
1024
+ );
1025
+ }
1026
+ enqueueConversationJournal(event, writer) {
1027
+ const bytes = this.conversationJournalBytes(event);
1028
+ const shouldSnapshot = event.type === "messages_replaced" || this._conversationJournalQueue.length >= _Context.CONVERSATION_JOURNAL_MAX_EVENTS || this._conversationJournalBytes + bytes > _Context.CONVERSATION_JOURNAL_MAX_BYTES;
1029
+ if (shouldSnapshot) {
1030
+ const snapshot = event.type === "messages_replaced" ? event : {
1031
+ type: "messages_replaced",
1032
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1033
+ version: 1,
1034
+ messages: [...this.messages]
1035
+ };
1036
+ const snapshotBytes = this.conversationJournalBytes(snapshot);
1037
+ for (let index = this._conversationJournalQueue.length - 1; index >= 0; index -= 1) {
1038
+ const queued = this._conversationJournalQueue[index];
1039
+ if (queued?.writer !== writer) continue;
1040
+ this._conversationJournalBytes = Math.max(0, this._conversationJournalBytes - queued.bytes);
1041
+ this._conversationJournalQueue.splice(index, 1);
1042
+ }
1043
+ this._conversationJournalQueue.push({ event: snapshot, bytes: snapshotBytes, writer });
1044
+ this._conversationJournalBytes += snapshotBytes;
1045
+ } else {
1046
+ this._conversationJournalQueue.push({ event, bytes, writer });
1047
+ this._conversationJournalBytes += bytes;
1048
+ }
1049
+ while (this._conversationJournalQueue.length > _Context.CONVERSATION_JOURNAL_MAX_EVENTS || this._conversationJournalBytes > _Context.CONVERSATION_JOURNAL_MAX_BYTES) {
1050
+ const index = this._conversationJournalQueue.findIndex(
1051
+ (queued) => queued.event.type !== "messages_replaced"
1052
+ );
1053
+ if (index === -1) break;
1054
+ const [dropped] = this._conversationJournalQueue.splice(index, 1);
1055
+ if (!dropped) break;
1056
+ this._conversationJournalBytes = Math.max(0, this._conversationJournalBytes - dropped.bytes);
1057
+ this.warnConversationJournalDrop(dropped.event.type);
1058
+ }
1059
+ this.startConversationJournalDrain();
1060
+ }
1061
+ startConversationJournalDrain() {
1062
+ if (this._conversationJournalDrain) return;
1063
+ const drain = (async () => {
1064
+ while (this._conversationJournalQueue.length > 0) {
1065
+ const queued = this._conversationJournalQueue.shift();
1066
+ if (!queued) continue;
1067
+ this._conversationJournalBytes = Math.max(0, this._conversationJournalBytes - queued.bytes);
1068
+ await queued.writer.append(queued.event).catch(() => {
1069
+ });
1070
+ }
1071
+ })().finally(() => {
1072
+ if (this._conversationJournalDrain === drain) this._conversationJournalDrain = null;
1073
+ if (this._conversationJournalQueue.length > 0) this.startConversationJournalDrain();
1074
+ });
1075
+ this._conversationJournalDrain = drain;
1076
+ }
1077
+ get state() {
1078
+ if (!this._state) {
1079
+ this._state = new ConversationState(this);
1080
+ this._state.onChange((change) => {
1081
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1082
+ const event = change.kind === "message_appended" ? {
1083
+ type: "message_appended",
1084
+ ts,
1085
+ version: 1,
1086
+ message: change.message
1087
+ } : change.kind === "message_updated" ? {
1088
+ type: "message_updated",
1089
+ ts,
1090
+ version: 1,
1091
+ index: change.index,
1092
+ message: change.message
1093
+ } : change.kind === "messages_replaced" ? {
1094
+ type: "messages_replaced",
1095
+ ts,
1096
+ version: 1,
1097
+ messages: [...change.messages]
1098
+ } : change.kind === "messages_dropped" ? {
1099
+ type: "messages_dropped",
1100
+ ts,
1101
+ version: 1,
1102
+ count: change.count
1103
+ } : null;
1104
+ if (!event) return;
1105
+ this.enqueueConversationJournal(event, this.session);
1106
+ });
1107
+ }
1108
+ return this._state;
1109
+ }
1110
+ /**
1111
+ * Register a teardown hook tied to the current run's abort signal.
1112
+ * Hooks registered before a run starts are stored and fired when the
1113
+ * next run ends; there is no immediate fire when no run is active.
1114
+ *
1115
+ * **Scope:** these hooks fire on the **whole agent run's** abort, not on
1116
+ * an individual tool call. For per-tool teardown of resources owned by
1117
+ * the tool author (child processes, handles), prefer `Tool.cleanup` —
1118
+ * see its JSDoc for the full rule.
1119
+ *
1120
+ * For hooks that must survive across run boundaries (mailbox heartbeat,
1121
+ * awareness polling, HQ publisher), prefer `registerAgentHook` instead.
1122
+ */
1123
+ /** Run-scoped abort hooks (drained by drainAbortHooks). Public for structural typing (Roadmap 10A). */
1124
+ abortHooks = /* @__PURE__ */ new Set();
1125
+ /** Retention limits honoring runtime subclass overrides of the statics. */
1126
+ get messageLimits() {
1127
+ const cls = this.constructor;
1128
+ return Object.freeze({
1129
+ maxMessages: cls.MAX_MESSAGES,
1130
+ maxMessageTokens: cls.MAX_MESSAGE_TOKENS
1131
+ });
1132
+ }
1133
+ registerAbortHook(fn) {
1134
+ this.abortHooks.add(fn);
1135
+ return () => this.abortHooks.delete(fn);
1136
+ }
1137
+ async drainAbortHooks() {
1138
+ const snapshot = [...this.abortHooks].reverse();
1139
+ this.abortHooks.clear();
1140
+ for (const fn of snapshot) {
1141
+ try {
1142
+ await fn();
1143
+ } catch {
1144
+ }
1145
+ }
1146
+ }
1147
+ /**
1148
+ * Register a teardown hook that persists across individual run boundaries.
1149
+ * These hooks are NOT drained by `drainAbortHooks()` (which fires on every
1150
+ * run end). They are only released by `drainAgentHooks()`, intended to be
1151
+ * called during Agent teardown / process shutdown.
1152
+ *
1153
+ * Used for long-lived resources such as the mailbox heartbeat timer,
1154
+ * awareness polling interval, HQ publisher connection, and auto-compaction
1155
+ * timer — resources that must survive from the first run to the last.
1156
+ */
1157
+ /** Session-lifetime teardown hooks (drained by drainAgentHooks). Public for structural typing (Roadmap 10A). */
1158
+ agentHooks = /* @__PURE__ */ new Set();
1159
+ registerAgentHook(fn) {
1160
+ this.agentHooks.add(fn);
1161
+ return () => this.agentHooks.delete(fn);
1162
+ }
1163
+ async drainAgentHooks() {
1164
+ const snapshot = [...this.agentHooks].reverse();
1165
+ this.agentHooks.clear();
1166
+ for (const fn of snapshot) {
1167
+ try {
1168
+ await fn();
1169
+ } catch {
1170
+ }
1171
+ }
1172
+ }
1173
+ /**
1174
+ * Record that a file's content was seen / mtime was observed.
1175
+ *
1176
+ * `source` controls which tracking set is populated — and therefore whether
1177
+ * the permission policy's write-smart-bypass (step 7) will auto-approve a
1178
+ * subsequent `write` to this path:
1179
+ *
1180
+ * - `'user'` (default): the model/user saw the content (via `read`, or an
1181
+ * edit's auto-read that surfaced it). Adds to `readFiles` → bypass applies.
1182
+ * - `'write'`: a tool wrote the file (`edit`/`write`) and is recording the
1183
+ * new mtime so subsequent edits detect external modification. Adds to
1184
+ * `writtenFiles` only — the bypass does NOT apply, because the user never
1185
+ * approved the new content (P1 #1, before-release.md).
1186
+ *
1187
+ * `fileMtimes` is updated in both cases so mtime-based staleness checks work.
1188
+ *
1189
+ * `contentHash` (sha-256 hex of the exact content seen) is optional so
1190
+ * existing callers keep working. When provided it is stored in `fileHashes`
1191
+ * and becomes the authoritative staleness arbiter for later edits. When
1192
+ * omitted, a previously stored hash survives only if the observed mtime is
1193
+ * unchanged — a different mtime with no fresh hash means the content may
1194
+ * have changed, so the stale hash is dropped and staleness checks fall back
1195
+ * to mtime comparison.
1196
+ */
1197
+ recordRead(absPath, mtimeMs, source = "user", contentHash) {
1198
+ if (contentHash !== void 0) {
1199
+ this.fileHashes.set(absPath, contentHash);
1200
+ } else {
1201
+ const prevMtime = this.fileMtimes.get(absPath);
1202
+ if (prevMtime !== mtimeMs) this.fileHashes.delete(absPath);
1203
+ }
1204
+ this.fileMtimes.set(absPath, mtimeMs);
1205
+ if (contentHash !== void 0) {
1206
+ this.session.recordFileObservation?.({
1207
+ path: absPath,
1208
+ hash: contentHash,
1209
+ mtimeMs,
1210
+ source
1211
+ });
1212
+ }
1213
+ if (source === "write") {
1214
+ this.writtenFiles.add(absPath);
1215
+ } else {
1216
+ this.readFiles.add(absPath);
1217
+ }
1218
+ this.trimTrackedFiles();
1219
+ }
1220
+ /**
1221
+ * Enforce MAX_TRACKED_FILES cap on all four file-tracking structures.
1222
+ * Evicts the oldest entries (insertion order = oldest-first in Set/Map)
1223
+ * when the cap is exceeded. This prevents unbounded memory growth in
1224
+ * long-running sessions over very large codebases where the agent
1225
+ * touches thousands of distinct files.
1226
+ *
1227
+ * The order of eviction across structures is not synchronized (each
1228
+ * structure independently drops its oldest), so after trimming the
1229
+ * sets may reference paths whose mtime/hash have been evicted from the
1230
+ * maps, and vice-versa. This is safe: paths in a Set are just strings
1231
+ * (4-8 bytes per char in the heap), and a stale lookup in fileMtimes
1232
+ * returns undefined, which `read`/`edit` treat as "first access" —
1233
+ * the tool will re-stat the file and re-populate the entry. The agent
1234
+ * never fails or misbehaves; it just pays one extra stat call.
1235
+ */
1236
+ trimTrackedFiles() {
1237
+ _Context.trimSet(this.readFiles, _Context.MAX_TRACKED_FILES);
1238
+ _Context.trimSet(this.writtenFiles, _Context.MAX_TRACKED_FILES);
1239
+ _Context.trimMap(this.fileMtimes, _Context.MAX_TRACKED_FILES);
1240
+ _Context.trimMap(this.fileHashes, _Context.MAX_TRACKED_FILES);
1241
+ }
1242
+ static trimSet(set, max) {
1243
+ while (set.size > max) {
1244
+ const oldest = set.values().next().value;
1245
+ if (oldest === void 0) break;
1246
+ set.delete(oldest);
1247
+ }
1248
+ }
1249
+ static trimMap(map, max) {
1250
+ while (map.size > max) {
1251
+ const oldest = map.keys().next().value;
1252
+ if (oldest === void 0) break;
1253
+ map.delete(oldest);
1254
+ }
1255
+ }
1256
+ /** Clear accumulated file-read metadata after compaction or at boundaries
1257
+ * where stale read history could cause tools to skip legitimate re-reads.
1258
+ * The agent re-populates this naturally on the next file access. */
1259
+ clearFileTracking() {
1260
+ this.readFiles.clear();
1261
+ this.writtenFiles.clear();
1262
+ this.fileMtimes.clear();
1263
+ this.fileHashes.clear();
1264
+ this.sideEffects = [];
1265
+ this.fileEvents = [];
1266
+ }
1267
+ /**
1268
+ * Record a structured side effect for the audit trail (P2 #5).
1269
+ * Called by tools that perform non-filesystem mutations (bash, install,
1270
+ * fetch) so /diag and session replay can show what the agent did beyond
1271
+ * file edits.
1272
+ *
1273
+ * Unlike recordFileChange(), this does NOT support undo — it is purely
1274
+ * for observability and audit. The event is appended to the session
1275
+ * JSONL fire-and-forget; errors are swallowed so recording never blocks
1276
+ * tool execution.
1277
+ */
1278
+ recordSideEffect(sideEffect) {
1279
+ this.sideEffects.push(sideEffect);
1280
+ if (this.sideEffects.length > _Context.MAX_SIDE_EFFECTS) {
1281
+ this.sideEffects.splice(0, this.sideEffects.length - _Context.MAX_SIDE_EFFECTS);
1282
+ }
1283
+ const sessionWriter = this.activeRunSessionWriter ?? this.session;
1284
+ sessionWriter.append({
1285
+ type: "side_effect",
1286
+ ts: sideEffect.ts,
1287
+ toolUseId: sideEffect.toolUseId,
1288
+ toolName: sideEffect.toolName,
1289
+ input: sideEffect.input,
1290
+ outcome: sideEffect.outcome,
1291
+ risk: sideEffect.risk
1292
+ }).catch(() => {
1293
+ });
1294
+ }
1295
+ /**
1296
+ * Set the current kanban task context for subsequent file operations.
1297
+ * Tools call this (or the agent loop sets it) so that `recordFileEvent()`
1298
+ * can associate operations with the active kanban task.
1299
+ *
1300
+ * Pass `undefined` for both to clear the task association.
1301
+ */
1302
+ setCurrentKanbanTask(taskId, boardId) {
1303
+ this.currentKanbanTaskId = taskId;
1304
+ this.currentKanbanBoardId = boardId;
1305
+ const existing = this.meta["kanban"] && typeof this.meta["kanban"] === "object" ? this.meta["kanban"] : {};
1306
+ this.state.setMeta("kanban", {
1307
+ ...existing,
1308
+ ...taskId ? { taskId } : { taskId: void 0 },
1309
+ ...boardId ? { boardId } : { boardId: void 0 }
1310
+ });
1311
+ }
1312
+ /**
1313
+ * Record a comprehensive file event for the audit trail.
1314
+ * Persists the event as a `file_event` session JSONL event.
1315
+ * Consumers that need `file.event` EventBus events should subscribe
1316
+ * at the executor level (see `ToolExecutor`'s auto-emission).
1317
+ *
1318
+ * The `scope` field is auto-derived from `currentKanbanTaskId`:
1319
+ * - When a kanban task is set, scope = `'task'` and `taskId`/`boardId`
1320
+ * are populated from `currentKanbanTaskId`/`currentKanbanBoardId`.
1321
+ * - Otherwise scope = `'session'`.
1322
+ *
1323
+ * Fire-and-forget: errors are swallowed so recording never blocks
1324
+ * tool execution.
1325
+ */
1326
+ recordFileEvent(input) {
1327
+ const scope = this.currentKanbanTaskId ? "task" : "session";
1328
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1329
+ const record = {
1330
+ operation: input.operation,
1331
+ filePath: input.filePath,
1332
+ absPath: input.absPath,
1333
+ sessionId: this.eventSessionId(),
1334
+ agentId: this.agentId,
1335
+ agentName: this.agentName,
1336
+ provider: typeof this.provider === "object" ? this.provider.id : String(this.provider),
1337
+ model: this.model,
1338
+ ...this.activeLogicalRequestId ? { logicalRequestId: this.activeLogicalRequestId } : {},
1339
+ ...this.activePromptManifestId ? { promptManifestId: this.activePromptManifestId } : {},
1340
+ provenanceConfidence: this.activeLogicalRequestId && this.activePromptManifestId ? "explicit" : "unknown",
1341
+ toolName: input.toolName,
1342
+ toolUseId: input.toolUseId,
1343
+ scope,
1344
+ taskId: this.currentKanbanTaskId,
1345
+ boardId: this.currentKanbanBoardId,
1346
+ timestamp: ts,
1347
+ durationMs: input.durationMs,
1348
+ fileSize: input.fileSize,
1349
+ lines: input.lines,
1350
+ bytes: input.bytes
1351
+ };
1352
+ this.fileEvents.push(record);
1353
+ if (this.fileEvents.length > _Context.MAX_FILE_EVENTS) {
1354
+ this.fileEvents.splice(0, this.fileEvents.length - _Context.MAX_FILE_EVENTS);
1355
+ }
1356
+ const sessionWriter = this.activeRunSessionWriter ?? this.session;
1357
+ sessionWriter.append({
1358
+ type: "file_event",
1359
+ ts,
1360
+ operation: input.operation,
1361
+ filePath: input.filePath,
1362
+ absPath: input.absPath,
1363
+ sessionId: record.sessionId,
1364
+ agentId: this.agentId,
1365
+ agentName: this.agentName,
1366
+ provider: record.provider,
1367
+ model: record.model,
1368
+ ...record.logicalRequestId ? { logicalRequestId: record.logicalRequestId } : {},
1369
+ ...record.promptManifestId ? { promptManifestId: record.promptManifestId } : {},
1370
+ provenanceConfidence: record.provenanceConfidence,
1371
+ toolName: input.toolName,
1372
+ toolUseId: input.toolUseId,
1373
+ scope,
1374
+ taskId: this.currentKanbanTaskId,
1375
+ boardId: this.currentKanbanBoardId,
1376
+ durationMs: input.durationMs,
1377
+ fileSize: input.fileSize,
1378
+ lines: input.lines,
1379
+ bytes: input.bytes
1380
+ }).catch(() => {
1381
+ });
1382
+ }
1383
+ /**
1384
+ * True if the model/user has explicitly seen this file's content via `read`
1385
+ * (or an edit auto-read). Tool-only writes (`source: 'write'`) do NOT count
1386
+ * — this is the source of truth for the permission policy's write bypass.
1387
+ */
1388
+ hasRead(absPath) {
1389
+ return this.readFiles.has(absPath);
1390
+ }
1391
+ /** True if `edit`/`write` wrote this file in the current session. */
1392
+ hasWritten(absPath) {
1393
+ return this.writtenFiles.has(absPath);
1394
+ }
1395
+ lastReadMtime(absPath) {
1396
+ return this.fileMtimes.get(absPath);
1397
+ }
1398
+ /** sha-256 (hex) of the content at the last recorded read/write, if the
1399
+ * recording tool supplied one. See `fileHashes` for drop semantics. */
1400
+ lastReadHash(absPath) {
1401
+ return this.fileHashes.get(absPath);
1402
+ }
1403
+ /**
1404
+ * Change the working directory for path resolution. Resolves relative paths
1405
+ * against `projectRoot` and validates the result is within the project root.
1406
+ * Fires all registered `onWorkingDirChanged` callbacks.
1407
+ * Returns the resolved absolute path.
1408
+ */
1409
+ setWorkingDir(dir) {
1410
+ const resolved = path2.isAbsolute(dir) ? path2.resolve(dir) : path2.resolve(this.projectRoot, dir);
1411
+ if (!this.allowOutsideProjectRoot) {
1412
+ const root = path2.resolve(this.projectRoot);
1413
+ const rel = path2.relative(root, resolved);
1414
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) {
1415
+ throw new Error(`Working directory "${resolved}" is outside project root "${root}"`);
1416
+ }
1417
+ let realTarget = resolved;
1418
+ let realRoot = root;
1419
+ try {
1420
+ realTarget = realpathSync.native(resolved);
1421
+ realRoot = realpathSync.native(root);
1422
+ } catch {
1423
+ }
1424
+ const realRel = path2.relative(realRoot, realTarget);
1425
+ if (realRel.startsWith("..") || path2.isAbsolute(realRel)) {
1426
+ throw new Error(
1427
+ `Working directory "${resolved}" resolves to "${realTarget}", outside project root "${realRoot}"`
1428
+ );
1429
+ }
1430
+ }
1431
+ const old = this.workingDir;
1432
+ this.workingDir = resolved;
1433
+ for (const cb of this._onWorkingDirChanged) {
1434
+ try {
1435
+ cb(resolved, old);
1436
+ } catch {
1437
+ }
1438
+ }
1439
+ return resolved;
1440
+ }
1441
+ /**
1442
+ * Register a callback that fires when the working directory changes.
1443
+ * Returns an unsubscribe function. Callbacks are fired synchronously
1444
+ * inside `setWorkingDir()` — errors in callbacks are swallowed so one
1445
+ * bad listener doesn't prevent others from executing.
1446
+ */
1447
+ onWorkingDirChanged(cb) {
1448
+ this._onWorkingDirChanged.push(cb);
1449
+ return () => {
1450
+ const idx = this._onWorkingDirChanged.indexOf(cb);
1451
+ if (idx >= 0) this._onWorkingDirChanged.splice(idx, 1);
1452
+ };
1453
+ }
1454
+ usage() {
1455
+ return this.tokenCounter.total();
1456
+ }
1457
+ };
1458
+ function resolveEventSessionId(ctx) {
1459
+ if (ctx.activeRunSessionId) return ctx.activeRunSessionId;
1460
+ const session = ctx.session;
1461
+ return session?.id ?? "";
1462
+ }
1463
+
1464
+ // src/utils/json-repair.ts
1465
+ function completePartialObject(s) {
1466
+ if (!s.trim().startsWith("{")) return s;
1467
+ if (tryParse(s).ok) return s;
1468
+ return repairTruncated(s);
1469
+ }
1470
+ function repairTruncated(s) {
1471
+ const stack = [];
1472
+ let inString = false;
1473
+ let escaped = false;
1474
+ let sawKey = false;
1475
+ let prevSig = "";
1476
+ let contentEnd = 0;
1477
+ let stringBraceDepth = 0;
1478
+ for (let i = 0; i < s.length; i++) {
1479
+ const ch = expectDefined(s[i]);
1480
+ if (inString) {
1481
+ contentEnd = i + 1;
1482
+ if (escaped) {
1483
+ escaped = false;
1484
+ continue;
1485
+ }
1486
+ if (ch === "\\") {
1487
+ escaped = true;
1488
+ continue;
1489
+ }
1490
+ if (ch === '"') {
1491
+ inString = false;
1492
+ prevSig = '"';
1493
+ stringBraceDepth = 0;
1494
+ continue;
1495
+ }
1496
+ if (ch === "{") stringBraceDepth++;
1497
+ else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
1498
+ continue;
1499
+ }
1500
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
1501
+ contentEnd = i + 1;
1502
+ if (ch === '"') {
1503
+ inString = true;
1504
+ sawKey = true;
1505
+ stringBraceDepth = 0;
1506
+ prevSig = '"';
1507
+ } else if (ch === "{" || ch === "[") {
1508
+ stack.push(ch);
1509
+ prevSig = ch;
1510
+ } else if (ch === "}" || ch === "]") {
1511
+ stack.pop();
1512
+ prevSig = ch;
1513
+ } else {
1514
+ prevSig = ch;
1515
+ }
1516
+ }
1517
+ if (!sawKey && !inString) return s;
1518
+ let result = s.slice(0, contentEnd);
1519
+ if (inString) {
1520
+ if (escaped) {
1521
+ result = result.slice(0, -1);
1522
+ } else if (endsWithInvalidEscape(result)) {
1523
+ result = result.slice(0, -2);
1524
+ }
1525
+ if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
1526
+ result += '"';
1527
+ } else if (prevSig === ":") {
1528
+ result += "null";
1529
+ }
1530
+ for (let k = stack.length - 1; k >= 0; k--) {
1531
+ result += stack[k] === "{" ? "}" : "]";
1532
+ }
1533
+ if (!tryParse(result).ok) {
1534
+ const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
1535
+ if (tryParse(patched).ok) result = patched;
1536
+ }
1537
+ return result;
1538
+ }
1539
+ var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
1540
+ function endsWithInvalidEscape(str) {
1541
+ const last = str[str.length - 1];
1542
+ if (str[str.length - 2] !== "\\" || last === void 0) return false;
1543
+ if (VALID_ESCAPE.has(last)) return false;
1544
+ let backslashes = 0;
1545
+ for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
1546
+ return backslashes % 2 === 1;
1547
+ }
1548
+ function tryParse(s) {
1549
+ try {
1550
+ return { ok: true, value: JSON.parse(s) };
1551
+ } catch {
1552
+ return { ok: false };
1553
+ }
1554
+ }
1555
+
1556
+ // src/core/request-provider-binding.ts
1557
+ var requestProviders = /* @__PURE__ */ new WeakMap();
1558
+ function bindRequestProvider(request, provider) {
1559
+ requestProviders.set(request, provider);
1560
+ }
1561
+ function providerBoundToRequest(request) {
1562
+ return requestProviders.get(request);
1563
+ }
1564
+
1565
+ export {
1566
+ createContextEvidenceState,
1567
+ recordUserIntentEvidence,
1568
+ isRuntimeContextInput,
1569
+ buildConversationContinuityBlock,
1570
+ recordToolOutputEvidence,
1571
+ markAssistantReferencedEvidence,
1572
+ buildContextEvidenceDigest,
1573
+ repeatedReadPressure,
1574
+ checkCompactionQuality,
1575
+ injectEvidenceFloor,
1576
+ COMPLETED_WORK_LEDGER_MARKER,
1577
+ recordCompletedWorkEvidence,
1578
+ formatCompletedWorkLedger,
1579
+ buildCompletedWorkLedgerBlock,
1580
+ syncCompletedWorkLedgerBlock,
1581
+ ConversationState,
1582
+ wrapAsState,
1583
+ Context,
1584
+ resolveEventSessionId,
1585
+ bindRequestProvider,
1586
+ providerBoundToRequest,
1587
+ completePartialObject
1588
+ };
1589
+ //# sourceMappingURL=chunk-EFEHDQGY.js.map