@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.
- package/dist/chronicle/index.js +84 -6391
- package/dist/chronicle/project-server.js +84 -4868
- package/dist/chunk-2BUUIMQB.js +225 -0
- package/dist/chunk-2FK4WTFF.js +207 -0
- package/dist/chunk-2FQL4PDC.js +292 -0
- package/dist/chunk-2KKODIQU.js +2256 -0
- package/dist/chunk-2YU4WVBW.js +134 -0
- package/dist/chunk-3E244UDA.js +356 -0
- package/dist/chunk-3XPVB7BA.js +135 -0
- package/dist/chunk-4LJJYBAU.js +388 -0
- package/dist/chunk-4VWO3ZXL.js +342 -0
- package/dist/chunk-4XV6IR37.js +187 -0
- package/dist/chunk-4ZHTKJHM.js +183 -0
- package/dist/chunk-52RTVH4M.js +361 -0
- package/dist/chunk-57GEWK5X.js +26 -0
- package/dist/chunk-5DSMEVA4.js +931 -0
- package/dist/chunk-5RP3QTRL.js +1223 -0
- package/dist/chunk-5S437PRC.js +50 -0
- package/dist/chunk-5XDUGTKN.js +511 -0
- package/dist/chunk-62V2AFVG.js +422 -0
- package/dist/chunk-65RDIOR2.js +2470 -0
- package/dist/chunk-6AKOTGLM.js +597 -0
- package/dist/chunk-7ZHZE5EO.js +28 -0
- package/dist/chunk-A455ATZB.js +140 -0
- package/dist/chunk-AD7ZFZCS.js +46 -0
- package/dist/chunk-AI4LLQ2U.js +856 -0
- package/dist/chunk-ASX6AW36.js +789 -0
- package/dist/chunk-AT4V4XGI.js +3522 -0
- package/dist/chunk-AWNQ6ED6.js +311 -0
- package/dist/chunk-B4AMLZPB.js +9 -0
- package/dist/chunk-BAKVJLEV.js +417 -0
- package/dist/chunk-BJHVULHO.js +55 -0
- package/dist/chunk-CB2HFKTP.js +9 -0
- package/dist/chunk-CE4ZOCOI.js +1640 -0
- package/dist/chunk-CQQL4ZOQ.js +151 -0
- package/dist/chunk-DEIQMBD2.js +65 -0
- package/dist/chunk-E6I5RXVK.js +58 -0
- package/dist/chunk-EFEHDQGY.js +1589 -0
- package/dist/chunk-EODDVJBM.js +82 -0
- package/dist/chunk-EPC3GZD4.js +308 -0
- package/dist/chunk-FAYRX2AD.js +13 -0
- package/dist/chunk-FFSX2QDQ.js +362 -0
- package/dist/chunk-FHAMQS7Q.js +5856 -0
- package/dist/chunk-FIV4IJL7.js +2263 -0
- package/dist/chunk-FNXVT7HF.js +52 -0
- package/dist/chunk-FSWCWSTS.js +395 -0
- package/dist/chunk-FW5LDDMJ.js +16464 -0
- package/dist/chunk-FXQLIX3K.js +211 -0
- package/dist/chunk-G5AAKEEZ.js +228 -0
- package/dist/chunk-G5S3S5ZN.js +38 -0
- package/dist/chunk-GAZEWQEZ.js +231 -0
- package/dist/chunk-GFJOMSHG.js +125 -0
- package/dist/chunk-GFXZTHNI.js +158 -0
- package/dist/chunk-GM54GJBU.js +960 -0
- package/dist/chunk-GPQ3NJPR.js +900 -0
- package/dist/chunk-GTCU6DZ3.js +1 -0
- package/dist/chunk-H5E77KUJ.js +15 -0
- package/dist/chunk-H72H4XDU.js +1023 -0
- package/dist/chunk-HA4ZYA6T.js +178 -0
- package/dist/chunk-I45KAW2N.js +5020 -0
- package/dist/chunk-IQ35HMQL.js +1396 -0
- package/dist/chunk-IYFBFX34.js +86 -0
- package/dist/chunk-JJPMWPB6.js +109 -0
- package/dist/chunk-JTHKOYWU.js +663 -0
- package/dist/chunk-JVGPR3TS.js +365 -0
- package/dist/chunk-JVKB2TBL.js +48 -0
- package/dist/chunk-JX7VBZDK.js +31 -0
- package/dist/chunk-K3G6JBUC.js +5232 -0
- package/dist/chunk-K4YUNKU7.js +589 -0
- package/dist/chunk-KBZGE6LN.js +710 -0
- package/dist/chunk-KDQJCT2J.js +45 -0
- package/dist/chunk-KI4L2DUL.js +560 -0
- package/dist/chunk-KJ2ZTQSL.js +47 -0
- package/dist/chunk-KSJ5UTP7.js +154 -0
- package/dist/chunk-LHOA5DWA.js +28 -0
- package/dist/chunk-LX5QWLBL.js +27 -0
- package/dist/chunk-LZDDLNCZ.js +279 -0
- package/dist/chunk-M2WF6AXG.js +115 -0
- package/dist/chunk-M7BS2K6U.js +1 -0
- package/dist/chunk-NDBIPFNY.js +102 -0
- package/dist/chunk-NEMIWEXC.js +1199 -0
- package/dist/chunk-NFAYHPRH.js +323 -0
- package/dist/chunk-O37BQ65S.js +21 -0
- package/dist/chunk-O74MJHXU.js +187 -0
- package/dist/chunk-OJZEX73F.js +112 -0
- package/dist/chunk-PAAWLLZX.js +74 -0
- package/dist/chunk-PFXFQQRT.js +339 -0
- package/dist/chunk-PFZO3C6H.js +14 -0
- package/dist/chunk-PHH7LD45.js +113 -0
- package/dist/chunk-QRTG4XTI.js +188 -0
- package/dist/chunk-QVOVMXJR.js +1351 -0
- package/dist/chunk-RCQYT2I4.js +757 -0
- package/dist/chunk-RD6DEE36.js +1178 -0
- package/dist/chunk-RJIZEJ42.js +30 -0
- package/dist/chunk-RPOA7IAE.js +150 -0
- package/dist/chunk-SN2DQRL6.js +1172 -0
- package/dist/chunk-SQJVFEBV.js +41 -0
- package/dist/chunk-SQP64RCS.js +5391 -0
- package/dist/chunk-SVT4RNTR.js +49 -0
- package/dist/chunk-SWUFN7XM.js +43 -0
- package/dist/chunk-SXMVYC5H.js +174 -0
- package/dist/chunk-SZ6AYUIZ.js +23 -0
- package/dist/chunk-T3KGZBU5.js +3171 -0
- package/dist/chunk-TBRL2RCC.js +9 -0
- package/dist/chunk-TRT2HRWZ.js +18 -0
- package/dist/chunk-TYW2GI5I.js +1077 -0
- package/dist/chunk-UIWONWVG.js +4959 -0
- package/dist/chunk-VB3H5WWB.js +151 -0
- package/dist/chunk-W4A6QA3D.js +258 -0
- package/dist/chunk-WBPVKEEE.js +227 -0
- package/dist/chunk-WEBMSQP2.js +123 -0
- package/dist/chunk-WOEZV3Q4.js +333 -0
- package/dist/chunk-WQQON3SV.js +473 -0
- package/dist/chunk-WQTOVMGV.js +119 -0
- package/dist/chunk-WZO23AWO.js +2779 -0
- package/dist/chunk-XBDP4QPW.js +7 -0
- package/dist/chunk-XEBHSJLT.js +103 -0
- package/dist/chunk-XQ2ECNK3.js +1 -0
- package/dist/chunk-XQ2XGIBF.js +2086 -0
- package/dist/chunk-Y2GE5TSI.js +355 -0
- package/dist/chunk-YBUJCZ57.js +351 -0
- package/dist/chunk-YNCVSFGF.js +149 -0
- package/dist/chunk-YUPMXOGP.js +282 -0
- package/dist/chunk-YY6APYUM.js +49 -0
- package/dist/chunk-Z2IJUO45.js +691 -0
- package/dist/chunk-ZEHUCJHY.js +129 -0
- package/dist/chunk-ZYU2YOJ6.js +1496 -0
- package/dist/chunk-ZZ4V3WIZ.js +163 -0
- package/dist/coordination/agents/index.js +145 -6260
- package/dist/coordination/index.js +365 -34324
- package/dist/coordination/mailbox-project-server.js +48 -3276
- package/dist/coordination/provider-status-tracker.d.ts +4 -0
- package/dist/core/agent.d.ts +14 -1
- package/dist/core/index.js +267 -12480
- package/dist/core/model-ref.js +5 -23
- package/dist/core/provider-runner.d.ts +36 -1
- package/dist/design/index.js +50 -1087
- package/dist/execution/index.d.ts +1 -1
- package/dist/execution/index.js +1599 -21888
- package/dist/execution/prompt-enhancer.js +16 -535
- package/dist/execution/retry-policy.d.ts +31 -1
- package/dist/extension/index.js +7 -311
- package/dist/goal/index.js +33 -3435
- package/dist/hooks/index.js +14 -1138
- package/dist/hq/index.js +112 -5139
- package/dist/hq/protocol.js +21 -668
- package/dist/index.js +1410 -92656
- package/dist/infrastructure/index.js +64 -4811
- package/dist/kernel/events/provider-events.d.ts +15 -0
- package/dist/kernel/index.js +14 -992
- package/dist/models/index.js +40 -1117
- package/dist/notifications/index.js +3 -108
- package/dist/observability/index.js +17 -279
- package/dist/plugin/index.js +584 -14835
- package/dist/prompts/index.js +14 -948
- package/dist/registry/index.js +9 -822
- package/dist/registry-R23UBE7H.js +14 -0
- package/dist/replay/index.js +6 -84
- package/dist/review-finding-store-DKAV3AS6.js +21 -0
- package/dist/review-report-store-OWY3EC3N.js +19 -0
- package/dist/security/index.js +114 -2202
- package/dist/session-catalog/index.js +36 -2103
- package/dist/session-catalog/project-server.js +42 -1435
- package/dist/skills/index.js +35 -1578
- package/dist/storage/index.js +182 -15600
- package/dist/tasking/index.js +10 -676
- package/dist/tools/index.js +52 -9590
- package/dist/types/config/autonomy.d.ts +5 -0
- package/dist/types/config/ui.d.ts +1 -1
- package/dist/types/errors.d.ts +18 -0
- package/dist/types/index.js +121 -1342
- package/dist/types/provider-runner.d.ts +8 -0
- package/dist/types/session-markers.js +8 -144
- package/dist/utils/error.js +3 -4
- package/dist/utils/expect-defined.js +3 -9
- package/dist/utils/heap-watchdog.js +6 -706
- package/dist/utils/index.js +269 -5685
- package/dist/utils/sage-output-block.js +5 -42
- package/dist/wiring/proxy-rewrite.d.ts +18 -0
- package/dist/wiring/proxy-rewrite.js +18 -121
- package/dist/worktree/index.js +6 -887
- 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
|