u-foo 2.5.13 → 2.5.15

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 (39) hide show
  1. package/package.json +1 -1
  2. package/src/agents/prompts/native/environment.js +20 -8
  3. package/src/code/agent.js +339 -24
  4. package/src/code/commands.js +61 -0
  5. package/src/code/context/artifactGc.js +292 -0
  6. package/src/code/context/artifactIndex.js +161 -0
  7. package/src/code/context/artifacts.js +183 -0
  8. package/src/code/context/assembler.js +698 -0
  9. package/src/code/context/executionSegment.js +314 -0
  10. package/src/code/context/featureFlag.js +13 -0
  11. package/src/code/context/index.js +18 -0
  12. package/src/code/context/projectSnapshot.js +201 -0
  13. package/src/code/context/promptLayers.js +159 -0
  14. package/src/code/context/reducers.js +328 -0
  15. package/src/code/context/stableJson.js +29 -0
  16. package/src/code/context/stateCommit.js +412 -0
  17. package/src/code/context/transcript.js +182 -0
  18. package/src/code/context/transcriptSync.js +106 -0
  19. package/src/code/context/workingSet.js +323 -0
  20. package/src/code/dispatch.js +4 -1
  21. package/src/code/index.js +6 -0
  22. package/src/code/modelCommand.js +87 -0
  23. package/src/code/nativeRunner.js +187 -31
  24. package/src/code/repl.js +36 -32
  25. package/src/code/sessionStore.js +227 -15
  26. package/src/code/skills/index.js +10 -0
  27. package/src/code/skills/injection.js +65 -3
  28. package/src/code/skills/loader.js +21 -0
  29. package/src/code/skills/manifest.js +87 -0
  30. package/src/code/skills/render.js +15 -1
  31. package/src/code/taskDecomposer.js +32 -2
  32. package/src/code/tools/artifactRead.js +40 -0
  33. package/src/code/tui.js +2 -0
  34. package/src/code/usageStore.js +15 -0
  35. package/src/ui/format/index.js +260 -44
  36. package/src/ui/format/markdownRenderer.js +215 -72
  37. package/src/ui/ink/ChatApp.js +39 -8
  38. package/src/ui/ink/UcodeApp.js +408 -55
  39. package/src/ui/ink/chatLogModel.js +102 -21
@@ -0,0 +1,698 @@
1
+ "use strict";
2
+
3
+ const { isContextV2Enabled } = require("./featureFlag");
4
+ const {
5
+ loadTranscript,
6
+ transcriptEventsToMessages,
7
+ migrateNlMessagesToTranscript,
8
+ } = require("./transcript");
9
+ const { appendTranscriptMessagesForStorage } = require("./transcriptSync");
10
+ const { reduceToolResult } = require("./reducers");
11
+ const { saveArtifact } = require("./artifacts");
12
+ const { buildLayeredSystemPrompt } = require("./promptLayers");
13
+ const {
14
+ buildProjectSnapshot,
15
+ renderProjectSnapshotContext,
16
+ isProjectSnapshotStale,
17
+ invalidateProjectSnapshotIfPathTouched,
18
+ } = require("./projectSnapshot");
19
+ const {
20
+ renderTaskContract,
21
+ renderStateEpoch,
22
+ applyStateCommit,
23
+ applyValidatedStateCommit,
24
+ buildDeterministicToolCommit,
25
+ resolveCommitInterval,
26
+ shouldCommitAfterToolCall,
27
+ ensureStateEpoch,
28
+ } = require("./stateCommit");
29
+ const {
30
+ applyWorkingSetPlan,
31
+ renderWorkingSetContext,
32
+ workingSetArtifactIds,
33
+ pruneWorkingSetByRetention,
34
+ } = require("./workingSet");
35
+ const { renderExecutionSegmentContext } = require("./executionSegment");
36
+
37
+ const DEFAULT_TRANSCRIPT_WINDOW = 12;
38
+ const DEFAULT_RECENT_TOOL_EVENTS = 4;
39
+
40
+ function resolveTranscriptWindow(env = process.env) {
41
+ const parsed = Number.parseInt(String(env.UFOO_UCODE_TRANSCRIPT_WINDOW || ""), 10);
42
+ if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
43
+ return DEFAULT_TRANSCRIPT_WINDOW;
44
+ }
45
+
46
+ function defaultContextPolicy(env = process.env) {
47
+ return {
48
+ transcriptWindow: resolveTranscriptWindow(env),
49
+ commitInterval: resolveCommitInterval(env),
50
+ v2: isContextV2Enabled(env),
51
+ };
52
+ }
53
+
54
+ function isToolTranscriptEvent(event = {}) {
55
+ if (!event || typeof event !== "object") return false;
56
+ if (event.artifactId) return true;
57
+ const role = String(event.role || "").trim().toLowerCase();
58
+ return role === "tool";
59
+ }
60
+
61
+ function eventToModelMessage(event = {}, options = {}) {
62
+ const preferArtifact = options.preferArtifact !== false;
63
+ if (!event || typeof event !== "object") return null;
64
+
65
+ if (preferArtifact && event.artifactId) {
66
+ return {
67
+ role: event.role || "tool",
68
+ content: JSON.stringify({
69
+ artifactId: event.artifactId,
70
+ preview: event.preview || "",
71
+ }),
72
+ tool_call_id: event.toolCallId,
73
+ };
74
+ }
75
+
76
+ if (!preferArtifact && event.rawMessage && typeof event.rawMessage === "object") {
77
+ return event.rawMessage;
78
+ }
79
+
80
+ const message = { role: event.role };
81
+ if (event.content !== undefined) message.content = event.content;
82
+ if (event.toolCalls) message.tool_calls = event.toolCalls;
83
+ if (event.toolCallId) message.tool_call_id = event.toolCallId;
84
+ return message;
85
+ }
86
+
87
+ function buildRollingSummary(transcriptEvents = [], existingSummary = "", session = null) {
88
+ const events = Array.isArray(transcriptEvents) ? transcriptEvents : [];
89
+ const window = resolveTranscriptWindow();
90
+ const parts = [];
91
+
92
+ const contract = session && session.taskContract && typeof session.taskContract === "object"
93
+ ? session.taskContract
94
+ : null;
95
+ if (contract && contract.objective) {
96
+ parts.push(`Objective: ${String(contract.objective).slice(0, 200)}`);
97
+ }
98
+
99
+ const epoch = session && session.stateEpoch && session.stateEpoch.snapshot
100
+ ? session.stateEpoch.snapshot
101
+ : null;
102
+ if (epoch) {
103
+ if (epoch.currentObjective) {
104
+ parts.push(`Current objective: ${String(epoch.currentObjective).slice(0, 160)}`);
105
+ }
106
+ if (Array.isArray(epoch.facts) && epoch.facts.length > 0) {
107
+ parts.push(`Facts: ${epoch.facts.slice(-4).map((f) => String(f).slice(0, 120)).join(" | ")}`);
108
+ }
109
+ if (Array.isArray(epoch.decisions) && epoch.decisions.length > 0) {
110
+ parts.push(`Decisions: ${epoch.decisions.slice(-3).map((d) => String(d).slice(0, 120)).join(" | ")}`);
111
+ }
112
+ }
113
+
114
+ const modified = session
115
+ && session.executionState
116
+ && Array.isArray(session.executionState.modifiedFiles)
117
+ ? session.executionState.modifiedFiles.slice(-8)
118
+ : [];
119
+ if (modified.length > 0) {
120
+ parts.push(`Modified files: ${modified.join(", ")}`);
121
+ }
122
+
123
+ if (events.length > window) {
124
+ const omitted = events.length - window;
125
+ parts.push(`Earlier transcript (${omitted} events omitted from model input).`);
126
+ const userGoals = events
127
+ .filter((e) => e.role === "user")
128
+ .map((e) => {
129
+ const text = typeof e.content === "string" ? e.content : "";
130
+ return text.split(/\r?\n/)[0].trim();
131
+ })
132
+ .filter(Boolean)
133
+ .slice(-3);
134
+ const toolErrors = events
135
+ .filter((e) => e.preview && /error|failed/i.test(e.preview))
136
+ .slice(-2)
137
+ .map((e) => String(e.preview).slice(0, 160));
138
+ if (userGoals.length > 0) parts.push(`Recent goals: ${userGoals.join(" | ")}`);
139
+ if (toolErrors.length > 0) parts.push(`Recent errors: ${toolErrors.join(" | ")}`);
140
+ }
141
+
142
+ const prior = String(existingSummary || "").trim();
143
+ // Keep a short prior digest only when it adds distinct content.
144
+ if (prior && !parts.some((part) => prior.includes(part.slice(0, 40)))) {
145
+ parts.unshift(prior.split(/\n/).slice(0, 3).join("\n").slice(0, 400));
146
+ }
147
+
148
+ return parts.join("\n").slice(0, 1800);
149
+ }
150
+
151
+ function selectRecentToolArtifactIds(events = [], limit = DEFAULT_RECENT_TOOL_EVENTS) {
152
+ const list = Array.isArray(events) ? events : [];
153
+ const ids = [];
154
+ for (let i = list.length - 1; i >= 0; i -= 1) {
155
+ const event = list[i];
156
+ if (!isToolTranscriptEvent(event)) continue;
157
+ const id = String(event.artifactId || "").trim();
158
+ if (!id || ids.includes(id)) continue;
159
+ ids.push(id);
160
+ if (ids.length >= limit) break;
161
+ }
162
+ return new Set(ids);
163
+ }
164
+
165
+ function toolCallIdsFromAssistantEvent(event = {}) {
166
+ const calls = Array.isArray(event.toolCalls) ? event.toolCalls : [];
167
+ return calls
168
+ .map((call) => String((call && (call.id || call.tool_call_id)) || "").trim())
169
+ .filter(Boolean);
170
+ }
171
+
172
+ function isAssistantToolCallEvent(event = {}) {
173
+ if (!event || typeof event !== "object") return false;
174
+ if (String(event.role || "").trim().toLowerCase() !== "assistant") return false;
175
+ return toolCallIdsFromAssistantEvent(event).length > 0;
176
+ }
177
+
178
+ /**
179
+ * OpenAI-compatible APIs reject history that contains assistant.tool_calls
180
+ * without a matching tool message for every call id. Working-set filtering
181
+ * must never break that pairing.
182
+ */
183
+ function ensureToolCallPairs(selectedEvents = [], allEvents = []) {
184
+ const selected = Array.isArray(selectedEvents) ? selectedEvents.slice() : [];
185
+ const all = Array.isArray(allEvents) ? allEvents : [];
186
+ if (selected.length === 0) return selected;
187
+
188
+ const indexInAll = new Map();
189
+ all.forEach((event, index) => {
190
+ if (event) indexInAll.set(event, index);
191
+ });
192
+
193
+ const toolsByCallId = new Map();
194
+ for (const event of all) {
195
+ if (!isToolTranscriptEvent(event)) continue;
196
+ const callId = String(event.toolCallId || "").trim();
197
+ if (callId) toolsByCallId.set(callId, event);
198
+ }
199
+
200
+ const selectedSet = new Set(selected);
201
+ for (const event of selected) {
202
+ if (!isAssistantToolCallEvent(event)) continue;
203
+ for (const callId of toolCallIdsFromAssistantEvent(event)) {
204
+ const toolEvent = toolsByCallId.get(callId);
205
+ if (toolEvent && !selectedSet.has(toolEvent)) {
206
+ selected.push(toolEvent);
207
+ selectedSet.add(toolEvent);
208
+ }
209
+ }
210
+ }
211
+
212
+ selected.sort((a, b) => {
213
+ const ai = indexInAll.has(a) ? indexInAll.get(a) : Number.MAX_SAFE_INTEGER;
214
+ const bi = indexInAll.has(b) ? indexInAll.get(b) : Number.MAX_SAFE_INTEGER;
215
+ return ai - bi;
216
+ });
217
+
218
+ const selectedToolIds = new Set(
219
+ selected
220
+ .filter((event) => isToolTranscriptEvent(event))
221
+ .map((event) => String(event.toolCallId || "").trim())
222
+ .filter(Boolean),
223
+ );
224
+
225
+ const keepAssistant = new Set();
226
+ const keepToolIds = new Set();
227
+ for (const event of selected) {
228
+ if (!isAssistantToolCallEvent(event)) continue;
229
+ const callIds = toolCallIdsFromAssistantEvent(event);
230
+ if (callIds.length === 0) continue;
231
+ if (!callIds.every((id) => selectedToolIds.has(id))) continue;
232
+ keepAssistant.add(event);
233
+ for (const id of callIds) keepToolIds.add(id);
234
+ }
235
+
236
+ return selected.filter((event) => {
237
+ if (isAssistantToolCallEvent(event)) return keepAssistant.has(event);
238
+ if (isToolTranscriptEvent(event)) {
239
+ const callId = String(event.toolCallId || "").trim();
240
+ return Boolean(callId) && keepToolIds.has(callId);
241
+ }
242
+ return true;
243
+ });
244
+ }
245
+
246
+ function sanitizeModelMessages(messages = []) {
247
+ const list = Array.isArray(messages) ? messages : [];
248
+ const out = [];
249
+ for (let i = 0; i < list.length; i += 1) {
250
+ const message = list[i];
251
+ if (!message || typeof message !== "object") continue;
252
+ const role = String(message.role || "").trim().toLowerCase();
253
+
254
+ if (role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
255
+ const callIds = message.tool_calls
256
+ .map((call) => String((call && call.id) || "").trim())
257
+ .filter(Boolean);
258
+ const following = [];
259
+ let j = i + 1;
260
+ while (j < list.length && String(list[j].role || "").trim().toLowerCase() === "tool") {
261
+ following.push(list[j]);
262
+ j += 1;
263
+ }
264
+ const followingIds = new Set(
265
+ following.map((entry) => String(entry.tool_call_id || "").trim()).filter(Boolean),
266
+ );
267
+ if (callIds.length === 0 || !callIds.every((id) => followingIds.has(id))) {
268
+ i = j - 1;
269
+ continue;
270
+ }
271
+ out.push({
272
+ role: "assistant",
273
+ content: message.content == null ? null : message.content,
274
+ tool_calls: message.tool_calls,
275
+ });
276
+ for (const toolMessage of following) {
277
+ const toolCallId = String(toolMessage.tool_call_id || "").trim();
278
+ if (!toolCallId || !callIds.includes(toolCallId)) continue;
279
+ out.push({
280
+ role: "tool",
281
+ tool_call_id: toolCallId,
282
+ content: toolMessage.content == null ? "" : toolMessage.content,
283
+ });
284
+ }
285
+ i = j - 1;
286
+ continue;
287
+ }
288
+
289
+ if (role === "tool") {
290
+ // Orphan tool rows are skipped; valid ones are consumed with their assistant.
291
+ continue;
292
+ }
293
+
294
+ out.push(message);
295
+ }
296
+ return out;
297
+ }
298
+
299
+ function buildModelMessagesFromTranscript(transcriptEvents = [], session = {}, windowSize = DEFAULT_TRANSCRIPT_WINDOW) {
300
+ const events = Array.isArray(transcriptEvents) ? transcriptEvents : [];
301
+ const wsIds = workingSetArtifactIds(session.workingSet);
302
+ const recentToolIds = selectRecentToolArtifactIds(events, DEFAULT_RECENT_TOOL_EVENTS);
303
+ const allowedToolIds = new Set([...wsIds, ...recentToolIds]);
304
+
305
+ const toolsByCallId = new Map();
306
+ for (const event of events) {
307
+ if (!isToolTranscriptEvent(event)) continue;
308
+ const callId = String(event.toolCallId || "").trim();
309
+ if (callId) toolsByCallId.set(callId, event);
310
+ }
311
+
312
+ function assistantGroupAllowed(event) {
313
+ const callIds = toolCallIdsFromAssistantEvent(event);
314
+ if (callIds.length === 0) return true;
315
+ return callIds.every((callId) => {
316
+ const toolEvent = toolsByCallId.get(callId);
317
+ if (!toolEvent) return false;
318
+ const artifactId = String(toolEvent.artifactId || "").trim();
319
+ if (!artifactId) return true;
320
+ return allowedToolIds.has(artifactId);
321
+ });
322
+ }
323
+
324
+ const selected = [];
325
+ for (let i = events.length - 1; i >= 0 && selected.length < windowSize; i -= 1) {
326
+ const event = events[i];
327
+ if (isAssistantToolCallEvent(event) && !assistantGroupAllowed(event)) {
328
+ continue;
329
+ }
330
+ if (isToolTranscriptEvent(event)) {
331
+ const artifactId = String(event.artifactId || "").trim();
332
+ if (artifactId && !allowedToolIds.has(artifactId)) continue;
333
+ if (!artifactId && !event.preview && !event.toolCallId) continue;
334
+ }
335
+ selected.unshift(event);
336
+ }
337
+
338
+ const paired = ensureToolCallPairs(selected, events);
339
+ return sanitizeModelMessages(
340
+ paired.map((event) => eventToModelMessage(event, { preferArtifact: true })).filter(Boolean),
341
+ );
342
+ }
343
+
344
+ function buildRecentMessages(transcriptEvents = [], windowSize = DEFAULT_TRANSCRIPT_WINDOW, session = null) {
345
+ if (session && isContextV2Enabled()) {
346
+ return buildModelMessagesFromTranscript(transcriptEvents, session, windowSize);
347
+ }
348
+ const recent = eventsSliceWindow(transcriptEvents, windowSize);
349
+ return recent.map((event) => eventToModelMessage(event, { preferArtifact: isContextV2Enabled() })).filter(Boolean);
350
+ }
351
+
352
+ function eventsSliceWindow(events = [], windowSize = DEFAULT_TRANSCRIPT_WINDOW) {
353
+ const list = Array.isArray(events) ? events : [];
354
+ if (list.length <= windowSize) return list.slice();
355
+ return list.slice(-windowSize);
356
+ }
357
+
358
+ function ensureTranscript(session = {}, workspaceRoot = process.cwd()) {
359
+ const sessionId = String(session.sessionId || "").trim();
360
+ if (!sessionId) return [];
361
+ if (Array.isArray(session.transcriptEvents) && session.transcriptEvents.length > 0) {
362
+ return session.transcriptEvents;
363
+ }
364
+ const loaded = loadTranscript(workspaceRoot, sessionId);
365
+ if (loaded.events.length > 0) {
366
+ session.transcriptEvents = loaded.events;
367
+ return loaded.events;
368
+ }
369
+ if (Array.isArray(session.nlMessages) && session.nlMessages.length > 0) {
370
+ session.transcriptEvents = migrateNlMessagesToTranscript(workspaceRoot, sessionId, session.nlMessages);
371
+ return session.transcriptEvents;
372
+ }
373
+ session.transcriptEvents = [];
374
+ return session.transcriptEvents;
375
+ }
376
+
377
+ function assembleModelContext(session = {}, request = {}, env = process.env) {
378
+ const workspaceRoot = String(session.workspaceRoot || request.workspaceRoot || process.cwd());
379
+ const policy = {
380
+ ...defaultContextPolicy(env),
381
+ ...(session.contextPolicy && typeof session.contextPolicy === "object" ? session.contextPolicy : {}),
382
+ };
383
+ session.workingSet = pruneWorkingSetByRetention(session.workingSet, session);
384
+ const transcriptEvents = ensureTranscript(session, workspaceRoot);
385
+ const windowSize = policy.transcriptWindow || DEFAULT_TRANSCRIPT_WINDOW;
386
+ const summary = buildRollingSummary(transcriptEvents, session.summary, session);
387
+
388
+ const layered = buildLayeredSystemPrompt({
389
+ workspaceRoot,
390
+ model: session.model || request.model || "",
391
+ provider: session.provider || request.provider || "",
392
+ appendSystemPrompt: request.appendSystemPrompt || "",
393
+ overrideSystemPrompt: request.overrideSystemPrompt || "",
394
+ epochDynamic: [
395
+ renderTaskContract(session.taskContract),
396
+ renderStateEpoch(session.stateEpoch),
397
+ ].filter(Boolean).join("\n\n"),
398
+ turnDynamic: [
399
+ request.turnDynamic || "",
400
+ renderProjectSnapshotContext(session.projectSnapshot),
401
+ renderWorkingSetContext(session.workingSet, session),
402
+ renderExecutionSegmentContext(session.executionState),
403
+ summary ? `Session summary:\n${summary}` : "",
404
+ ].filter(Boolean).join("\n\n"),
405
+ sessionStableExtras: request.sessionStableExtras || "",
406
+ });
407
+
408
+ const recentMessages = buildRecentMessages(transcriptEvents, windowSize, session);
409
+
410
+ return {
411
+ systemPrompt: layered.flatText,
412
+ systemBlocks: layered.blocks,
413
+ messages: recentMessages,
414
+ summary,
415
+ transcriptEvents,
416
+ policy,
417
+ };
418
+ }
419
+
420
+ function persistToolResultToContext({
421
+ workspaceRoot = process.cwd(),
422
+ sessionId = "",
423
+ tool = "",
424
+ args = {},
425
+ rawResult = {},
426
+ segmentId = "",
427
+ } = {}) {
428
+ const saved = saveArtifact(workspaceRoot, sessionId, {
429
+ type: "tool_result",
430
+ tool,
431
+ args,
432
+ raw: rawResult,
433
+ createdBy: tool,
434
+ });
435
+ const artifactId = saved.artifact && saved.artifact.artifactId
436
+ ? saved.artifact.artifactId
437
+ : "";
438
+ const reduced = reduceToolResult(tool, rawResult, artifactId, args);
439
+ return {
440
+ artifactId,
441
+ preview: reduced.preview,
442
+ summary: reduced.summary,
443
+ modelPayload: reduced.modelPayload,
444
+ artifact: saved.artifact,
445
+ segmentId,
446
+ tool,
447
+ args,
448
+ };
449
+ }
450
+
451
+ function recordToolCallInSession(session = {}, persisted = {}, workspaceRoot = process.cwd()) {
452
+ if (!session || typeof session !== "object") return;
453
+ ensureStateEpoch(session);
454
+ session.toolCallsSinceCommit = (Number(session.toolCallsSinceCommit) || 0) + 1;
455
+ if (!Array.isArray(session.workingSet)) session.workingSet = [];
456
+ if (!session.executionState || typeof session.executionState !== "object") {
457
+ session.executionState = require("./executionSegment").emptyExecutionState();
458
+ }
459
+ const plan = require("./workingSet").defaultContextPlanFromToolEvent(
460
+ persisted.tool,
461
+ persisted.artifactId,
462
+ persisted.args,
463
+ );
464
+ if (plan) {
465
+ session.workingSet = applyWorkingSetPlan(session.workingSet, plan, session);
466
+ }
467
+
468
+ const payload = persisted.modelPayload && typeof persisted.modelPayload === "object"
469
+ ? persisted.modelPayload
470
+ : {};
471
+ const exitCode = Number.isFinite(payload.exitCode)
472
+ ? payload.exitCode
473
+ : (Number.isFinite(persisted.exitCode) ? persisted.exitCode : null);
474
+ if (exitCode !== null) {
475
+ const codes = Array.isArray(session.executionState.lastExitCodes)
476
+ ? session.executionState.lastExitCodes.slice()
477
+ : [];
478
+ codes.push({ tool: persisted.tool || "", exitCode, at: new Date().toISOString() });
479
+ session.executionState.lastExitCodes = codes.slice(-20);
480
+ }
481
+
482
+ if (persisted.tool === "write" || persisted.tool === "edit") {
483
+ const filePath = persisted.args && persisted.args.path ? String(persisted.args.path) : "";
484
+ if (filePath) {
485
+ const files = Array.isArray(session.executionState.modifiedFiles)
486
+ ? session.executionState.modifiedFiles.slice()
487
+ : [];
488
+ if (!files.includes(filePath)) files.push(filePath);
489
+ session.executionState.modifiedFiles = files;
490
+ invalidateProjectSnapshotIfPathTouched(session, filePath);
491
+ }
492
+ }
493
+
494
+ if (payload.kind === "git_diff" && Array.isArray(payload.files)) {
495
+ const files = Array.isArray(session.executionState.modifiedFiles)
496
+ ? session.executionState.modifiedFiles.slice()
497
+ : [];
498
+ for (const filePath of payload.files) {
499
+ const text = String(filePath || "").trim();
500
+ if (text && !files.includes(text)) files.push(text);
501
+ }
502
+ session.executionState.modifiedFiles = files.slice(0, 200);
503
+ }
504
+
505
+ const failed = payload.ok === false
506
+ || (exitCode !== null && exitCode !== 0)
507
+ || (Number.isFinite(payload.failed) && payload.failed > 0);
508
+ if (failed) {
509
+ const toolKey = String(persisted.tool || "tool");
510
+ const retries = session.executionState.retries && typeof session.executionState.retries === "object"
511
+ ? { ...session.executionState.retries }
512
+ : {};
513
+ retries[toolKey] = (Number(retries[toolKey]) || 0) + 1;
514
+ session.executionState.retries = retries;
515
+ }
516
+
517
+ const interval = resolveCommitInterval();
518
+ if (shouldCommitAfterToolCall(session, interval)) {
519
+ const commit = buildDeterministicToolCommit(persisted);
520
+ if (commit) {
521
+ applyValidatedStateCommit(session, commit, workspaceRoot);
522
+ } else {
523
+ session.toolCallsSinceCommit = 0;
524
+ }
525
+ }
526
+ }
527
+
528
+ function syncMessagesToTranscript(session = {}, messages = [], workspaceRoot = process.cwd(), options = {}) {
529
+ const sessionId = String(session.sessionId || "").trim();
530
+ if (!sessionId) return [];
531
+ const prior = ensureTranscript(session, workspaceRoot);
532
+ const full = Array.isArray(messages) ? messages : [];
533
+ if (full.length === 0) return prior;
534
+
535
+ // `messages` is often a WINDOWED model view (plus this turn's delta), not the
536
+ // full transcript. Comparing lengths to prior transcript events dropped every
537
+ // new user turn once the transcript grew past the window — resume then lost
538
+ // green › user rows. Prefer an explicit baseline, else suffix/prefix match.
539
+ let baseline = Number.isFinite(options.baselineCount)
540
+ ? Math.max(0, Math.floor(options.baselineCount))
541
+ : null;
542
+ if (baseline == null) {
543
+ const existingMessages = transcriptEventsToMessages(prior, {
544
+ preferArtifact: isContextV2Enabled(),
545
+ });
546
+ baseline = matchTranscriptBaseline(existingMessages, full);
547
+ }
548
+ baseline = Math.max(0, Math.min(full.length, baseline));
549
+
550
+ if (full.length <= baseline) {
551
+ session.nlMessages = transcriptEventsToMessages(session.transcriptEvents, {
552
+ preferArtifact: isContextV2Enabled(),
553
+ });
554
+ return session.transcriptEvents || prior;
555
+ }
556
+
557
+ const delta = full.slice(baseline);
558
+ const extra = {
559
+ segmentId: session.executionState && session.executionState.currentSegmentId
560
+ ? session.executionState.currentSegmentId
561
+ : "",
562
+ };
563
+ if (isContextV2Enabled()) {
564
+ appendTranscriptMessagesForStorage(workspaceRoot, sessionId, delta, extra);
565
+ } else {
566
+ const { appendTranscriptMessages } = require("./transcript");
567
+ appendTranscriptMessages(workspaceRoot, sessionId, delta, extra);
568
+ }
569
+ session.transcriptEvents = loadTranscript(workspaceRoot, sessionId).events;
570
+ session.nlMessages = transcriptEventsToMessages(session.transcriptEvents, {
571
+ preferArtifact: isContextV2Enabled(),
572
+ });
573
+ session.summary = buildRollingSummary(session.transcriptEvents, session.summary, session);
574
+ return session.transcriptEvents;
575
+ }
576
+
577
+ /**
578
+ * Fingerprint a chat message for transcript alignment. Tool payloads are
579
+ * compared by call id (content is often artifact-compressed on disk).
580
+ */
581
+ function messageSyncFingerprint(message = {}) {
582
+ if (!message || typeof message !== "object") return "";
583
+ const role = String(message.role || "").trim().toLowerCase();
584
+ if (role === "tool") {
585
+ return `tool|${String(message.tool_call_id || "").trim()}`;
586
+ }
587
+ if (role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
588
+ const ids = message.tool_calls
589
+ .map((call) => String((call && call.id) || "").trim())
590
+ .filter(Boolean)
591
+ .join(",");
592
+ return `assistant_tools|${ids}`;
593
+ }
594
+ let content = "";
595
+ if (typeof message.content === "string") content = message.content;
596
+ else if (message.content != null) {
597
+ try {
598
+ content = JSON.stringify(message.content);
599
+ } catch {
600
+ content = String(message.content);
601
+ }
602
+ }
603
+ const compact = content.replace(/\s+/g, " ").trim();
604
+ return `${role}|${compact.length}|${compact.slice(0, 240)}`;
605
+ }
606
+
607
+ /**
608
+ * How much of `next` is already covered by the end of `existing`.
609
+ * Returns the length of the matched prefix of `next` (baseline for slicing).
610
+ */
611
+ function matchTranscriptBaseline(existing = [], next = []) {
612
+ const prior = Array.isArray(existing) ? existing : [];
613
+ const incoming = Array.isArray(next) ? next : [];
614
+ if (incoming.length === 0) return 0;
615
+ const priorFingerprints = prior.map(messageSyncFingerprint);
616
+ const nextFingerprints = incoming.map(messageSyncFingerprint);
617
+ const max = Math.min(priorFingerprints.length, nextFingerprints.length);
618
+ for (let k = max; k >= 0; k -= 1) {
619
+ let matched = true;
620
+ for (let i = 0; i < k; i += 1) {
621
+ if (priorFingerprints[priorFingerprints.length - k + i] !== nextFingerprints[i]) {
622
+ matched = false;
623
+ break;
624
+ }
625
+ }
626
+ if (matched) return k;
627
+ }
628
+ return 0;
629
+ }
630
+
631
+ function applyContextSideEffects(session = {}, sideEffects = {}, workspaceRoot = process.cwd()) {
632
+ const effects = sideEffects && typeof sideEffects === "object" ? sideEffects : {};
633
+ if (effects.stateCommit) {
634
+ applyValidatedStateCommit(session, effects.stateCommit, workspaceRoot);
635
+ }
636
+ if (effects.contextPlan) {
637
+ session.workingSet = applyWorkingSetPlan(session.workingSet, effects.contextPlan, session);
638
+ const summarize = Array.isArray(effects.contextPlan.summarize) ? effects.contextPlan.summarize : [];
639
+ if (summarize.length > 0) {
640
+ session.workingSet = applyWorkingSetPlan(session.workingSet, { summarize }, session);
641
+ }
642
+ }
643
+ if (effects.projectSnapshot) {
644
+ session.projectSnapshot = effects.projectSnapshot;
645
+ }
646
+ return session;
647
+ }
648
+
649
+ function commitAfterSegmentEnd(session = {}, segmentResult = {}, workspaceRoot = process.cwd()) {
650
+ if (!session || typeof session !== "object") return null;
651
+ const result = segmentResult && typeof segmentResult === "object" ? segmentResult : {};
652
+ const commit = {
653
+ factsAdd: [
654
+ `segment:${result.segmentId || "unknown"} status=${result.ok === false ? "failed" : (result.stoppedAt || "success")}`,
655
+ ],
656
+ nextObjective: result.objective || "",
657
+ };
658
+ return applyValidatedStateCommit(session, commit, workspaceRoot);
659
+ }
660
+
661
+ function ensureProjectSnapshot(session = {}, workspaceRoot = process.cwd()) {
662
+ const root = workspaceRoot || process.cwd();
663
+ if (
664
+ session.projectSnapshot
665
+ && session.projectSnapshot.projectSnapshotId
666
+ && !isProjectSnapshotStale(session.projectSnapshot, root)
667
+ ) {
668
+ return session.projectSnapshot;
669
+ }
670
+ session.projectSnapshot = buildProjectSnapshot({
671
+ workspaceRoot: root,
672
+ sessionId: session.sessionId,
673
+ existing: null,
674
+ });
675
+ return session.projectSnapshot;
676
+ }
677
+
678
+ module.exports = {
679
+ DEFAULT_TRANSCRIPT_WINDOW,
680
+ DEFAULT_RECENT_TOOL_EVENTS,
681
+ defaultContextPolicy,
682
+ buildRollingSummary,
683
+ buildRecentMessages,
684
+ buildModelMessagesFromTranscript,
685
+ selectRecentToolArtifactIds,
686
+ ensureToolCallPairs,
687
+ sanitizeModelMessages,
688
+ ensureTranscript,
689
+ assembleModelContext,
690
+ persistToolResultToContext,
691
+ recordToolCallInSession,
692
+ syncMessagesToTranscript,
693
+ applyContextSideEffects,
694
+ commitAfterSegmentEnd,
695
+ ensureProjectSnapshot,
696
+ messageSyncFingerprint,
697
+ matchTranscriptBaseline,
698
+ };