u-foo 3.0.25 → 3.0.26
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/tui/darwin-arm64/ufoo-tui +0 -0
- package/dist/tui/darwin-x64/ufoo-tui +0 -0
- package/dist/tui/linux-arm64/ufoo-tui +0 -0
- package/dist/tui/linux-x64/ufoo-tui +0 -0
- package/package.json +1 -1
- package/src/code/UcodeController.js +2 -10
- package/src/code/agent.js +105 -26
- package/src/code/context/assembler.js +0 -102
- package/src/code/context/transcript.js +5 -0
- package/src/code/conversation/sessionJournal.js +351 -0
- package/src/code/nativeRunner.js +26 -0
- package/src/code/sessionStore.js +9 -21
- package/src/code/taskDecomposer.js +7 -3
- package/src/runtime/contracts/uiProtocol.js +2 -0
- package/src/ui/format/index.js +100 -33
- package/src/ui/format/markdownRenderer.js +2 -2
- package/src/ui/rustChatHost.js +2 -0
- package/src/ui/rustUcodeHost.js +106 -30
- package/src/ui/toolMergeBridge.js +5 -7
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { randomUUID, createHash } = require("crypto");
|
|
6
|
+
const { stripVisionBase64, degradeVisionContent } = require("../providers/visionBlocks");
|
|
7
|
+
|
|
8
|
+
const JOURNAL_VERSION = 3;
|
|
9
|
+
|
|
10
|
+
function getJournalDir(workspaceRoot = process.cwd()) {
|
|
11
|
+
return path.join(path.resolve(workspaceRoot || process.cwd()), ".ufoo", "agent", "ucode", "journal");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getJournalPath(workspaceRoot = process.cwd(), sessionId = "") {
|
|
15
|
+
const id = String(sessionId || "").trim();
|
|
16
|
+
return id ? path.join(getJournalDir(workspaceRoot), `${id}.jsonl`) : "";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getLegacyTranscriptPath(workspaceRoot = process.cwd(), sessionId = "") {
|
|
20
|
+
const id = String(sessionId || "").trim();
|
|
21
|
+
return id
|
|
22
|
+
? path.join(path.resolve(workspaceRoot || process.cwd()), ".ufoo", "agent", "ucode", "transcripts", `${id}.jsonl`)
|
|
23
|
+
: "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function deleteSessionJournal(workspaceRoot = process.cwd(), sessionId = "") {
|
|
27
|
+
const filePath = getJournalPath(workspaceRoot, sessionId);
|
|
28
|
+
if (!filePath || !fs.existsSync(filePath)) return { ok: true, error: "" };
|
|
29
|
+
try {
|
|
30
|
+
fs.unlinkSync(filePath);
|
|
31
|
+
return { ok: true, error: "" };
|
|
32
|
+
} catch (err) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
error: err && err.message ? err.message : "failed to delete session journal",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readJsonl(filePath = "") {
|
|
41
|
+
if (!filePath || !fs.existsSync(filePath)) return [];
|
|
42
|
+
try {
|
|
43
|
+
return fs.readFileSync(filePath, "utf8")
|
|
44
|
+
.split(/\r?\n/)
|
|
45
|
+
.map((line) => line.trim())
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.flatMap((line) => {
|
|
48
|
+
try { return [JSON.parse(line)]; } catch { return []; }
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function stablePayloadFingerprint(value) {
|
|
56
|
+
try {
|
|
57
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
58
|
+
} catch {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function contentForStorage(content) {
|
|
64
|
+
if (typeof content === "string") return content;
|
|
65
|
+
if (Array.isArray(content)) {
|
|
66
|
+
const hasVision = content.some((block) => {
|
|
67
|
+
if (!block || typeof block !== "object") return false;
|
|
68
|
+
const type = String(block.type || "").trim().toLowerCase();
|
|
69
|
+
return type === "image" || type === "image_url"
|
|
70
|
+
|| (type === "tool_result" && Array.isArray(block.content));
|
|
71
|
+
});
|
|
72
|
+
return hasVision
|
|
73
|
+
? degradeVisionContent(stripVisionBase64(content))
|
|
74
|
+
: stripVisionBase64(content);
|
|
75
|
+
}
|
|
76
|
+
if (content && typeof content === "object") return stripVisionBase64(content);
|
|
77
|
+
return content;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseArtifactMessage(message = {}) {
|
|
81
|
+
if (typeof message.content !== "string" || !message.content.trim()) return null;
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(message.content);
|
|
84
|
+
const artifactId = String(parsed && parsed.artifactId || "").trim();
|
|
85
|
+
if (!artifactId) return null;
|
|
86
|
+
return { artifactId, preview: String(parsed.preview || "").trim() };
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function messageEventType(message = {}) {
|
|
93
|
+
const role = String(message.role || "").trim().toLowerCase();
|
|
94
|
+
if (
|
|
95
|
+
role === "user"
|
|
96
|
+
&& Array.isArray(message.content)
|
|
97
|
+
&& message.content.some((block) => String(block && block.type || "").toLowerCase() === "tool_result")
|
|
98
|
+
) {
|
|
99
|
+
return "tool.result";
|
|
100
|
+
}
|
|
101
|
+
if (role === "user") return "user.message";
|
|
102
|
+
if (role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
|
|
103
|
+
return "assistant.tool_calls";
|
|
104
|
+
}
|
|
105
|
+
if (role === "assistant") return "assistant.message";
|
|
106
|
+
if (role === "tool") return "tool.result";
|
|
107
|
+
return "conversation.item";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function messageToJournalEvent(message = {}, options = {}) {
|
|
111
|
+
const role = String(message && message.role || "").trim().toLowerCase();
|
|
112
|
+
if (!role || role === "system") return null;
|
|
113
|
+
const turnId = String(options.turnId || "").trim();
|
|
114
|
+
const index = Number.isFinite(options.index) ? Math.max(0, Math.floor(options.index)) : 0;
|
|
115
|
+
const toolCallId = String(message.tool_call_id || "").trim();
|
|
116
|
+
const parsedArtifact = role === "tool" ? parseArtifactMessage(message) : null;
|
|
117
|
+
const artifactId = String(message.artifactId || (parsedArtifact && parsedArtifact.artifactId) || "").trim();
|
|
118
|
+
const artifactPreview = String(message.preview || (parsedArtifact && parsedArtifact.preview) || "").trim();
|
|
119
|
+
const payload = {
|
|
120
|
+
role,
|
|
121
|
+
content: artifactId ? undefined : contentForStorage(message.content),
|
|
122
|
+
toolCalls: Array.isArray(message.tool_calls) ? message.tool_calls : undefined,
|
|
123
|
+
toolCallId: toolCallId || undefined,
|
|
124
|
+
artifactId: artifactId || undefined,
|
|
125
|
+
preview: artifactId ? artifactPreview : undefined,
|
|
126
|
+
};
|
|
127
|
+
const itemId = String(options.itemId || toolCallId || `${turnId || "turn"}:${index}`).trim();
|
|
128
|
+
return {
|
|
129
|
+
version: JOURNAL_VERSION,
|
|
130
|
+
eventId: String(options.eventId || `evt_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`),
|
|
131
|
+
idempotencyKey: String(options.idempotencyKey || `${turnId || "legacy"}:${index}:${messageEventType(message)}`),
|
|
132
|
+
sessionId: String(options.sessionId || "").trim(),
|
|
133
|
+
turnId,
|
|
134
|
+
itemId,
|
|
135
|
+
type: messageEventType(message),
|
|
136
|
+
createdAt: String(options.createdAt || new Date().toISOString()),
|
|
137
|
+
payload,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeJournalEvent(event = {}, sessionId = "", seq = 0) {
|
|
142
|
+
const source = event && typeof event === "object" ? event : {};
|
|
143
|
+
return {
|
|
144
|
+
version: JOURNAL_VERSION,
|
|
145
|
+
seq: Number.isFinite(source.seq) ? Math.max(1, Math.floor(source.seq)) : Math.max(1, seq),
|
|
146
|
+
eventId: String(source.eventId || `evt_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`),
|
|
147
|
+
idempotencyKey: String(source.idempotencyKey || ""),
|
|
148
|
+
sessionId: String(source.sessionId || sessionId || "").trim(),
|
|
149
|
+
turnId: String(source.turnId || "").trim(),
|
|
150
|
+
itemId: String(source.itemId || "").trim(),
|
|
151
|
+
type: String(source.type || "conversation.item").trim(),
|
|
152
|
+
createdAt: String(source.createdAt || new Date().toISOString()),
|
|
153
|
+
payload: source.payload && typeof source.payload === "object" ? source.payload : {},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function loadJournal(workspaceRoot = process.cwd(), sessionId = "") {
|
|
158
|
+
const filePath = getJournalPath(workspaceRoot, sessionId);
|
|
159
|
+
const events = readJsonl(filePath)
|
|
160
|
+
.filter((event) => Number(event && event.version) === JOURNAL_VERSION)
|
|
161
|
+
.map((event, index) => normalizeJournalEvent(event, sessionId, index + 1))
|
|
162
|
+
.sort((left, right) => left.seq - right.seq);
|
|
163
|
+
return { filePath, events };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function appendJournalEvents(workspaceRoot = process.cwd(), sessionId = "", events = []) {
|
|
167
|
+
const filePath = getJournalPath(workspaceRoot, sessionId);
|
|
168
|
+
if (!filePath) return { ok: false, error: "invalid session id", events: [] };
|
|
169
|
+
const existing = loadJournal(workspaceRoot, sessionId).events;
|
|
170
|
+
const eventIds = new Set(existing.map((event) => event.eventId).filter(Boolean));
|
|
171
|
+
const keys = new Set(existing.map((event) => event.idempotencyKey).filter(Boolean));
|
|
172
|
+
let seq = existing.reduce((max, event) => Math.max(max, Number(event.seq) || 0), 0);
|
|
173
|
+
const appended = [];
|
|
174
|
+
for (const candidate of Array.isArray(events) ? events : []) {
|
|
175
|
+
const normalized = normalizeJournalEvent(candidate, sessionId, seq + 1);
|
|
176
|
+
if (eventIds.has(normalized.eventId)) continue;
|
|
177
|
+
if (normalized.idempotencyKey && keys.has(normalized.idempotencyKey)) continue;
|
|
178
|
+
seq += 1;
|
|
179
|
+
normalized.seq = seq;
|
|
180
|
+
eventIds.add(normalized.eventId);
|
|
181
|
+
if (normalized.idempotencyKey) keys.add(normalized.idempotencyKey);
|
|
182
|
+
appended.push(normalized);
|
|
183
|
+
}
|
|
184
|
+
if (appended.length === 0) return { ok: true, error: "", events: [], filePath };
|
|
185
|
+
try {
|
|
186
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
187
|
+
fs.appendFileSync(filePath, `${appended.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
|
|
188
|
+
return { ok: true, error: "", events: appended, filePath };
|
|
189
|
+
} catch (err) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
error: err && err.message ? err.message : "failed to append session journal",
|
|
193
|
+
events: [],
|
|
194
|
+
filePath,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function sameLegacyAssistant(left = {}, right = {}) {
|
|
200
|
+
if (String(left.role || "") !== "assistant" || String(right.role || "") !== "assistant") return false;
|
|
201
|
+
return stablePayloadFingerprint({ content: left.content, toolCalls: left.toolCalls })
|
|
202
|
+
=== stablePayloadFingerprint({ content: right.content, toolCalls: right.toolCalls });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function legacyDuplicateIndexes(events = []) {
|
|
206
|
+
const duplicates = new Set();
|
|
207
|
+
for (let index = 1; index < events.length - 1; index += 1) {
|
|
208
|
+
const prior = events[index - 1] || {};
|
|
209
|
+
const candidate = events[index] || {};
|
|
210
|
+
const next = events[index + 1] || {};
|
|
211
|
+
if (!sameLegacyAssistant(prior, candidate) || String(next.role || "") !== "user") continue;
|
|
212
|
+
const candidateAt = Date.parse(String(candidate.createdAt || ""));
|
|
213
|
+
const priorAt = Date.parse(String(prior.createdAt || ""));
|
|
214
|
+
const nextAt = Date.parse(String(next.createdAt || ""));
|
|
215
|
+
if (
|
|
216
|
+
!Number.isFinite(priorAt)
|
|
217
|
+
|| !Number.isFinite(candidateAt)
|
|
218
|
+
|| !Number.isFinite(nextAt)
|
|
219
|
+
|| candidateAt - priorAt <= 2000
|
|
220
|
+
|| Math.abs(nextAt - candidateAt) > 2000
|
|
221
|
+
) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
duplicates.add(index);
|
|
225
|
+
}
|
|
226
|
+
return duplicates;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function readLegacyProjection(workspaceRoot = process.cwd(), sessionId = "") {
|
|
230
|
+
const filePath = getLegacyTranscriptPath(workspaceRoot, sessionId);
|
|
231
|
+
const events = readJsonl(filePath);
|
|
232
|
+
const duplicates = legacyDuplicateIndexes(events);
|
|
233
|
+
return {
|
|
234
|
+
filePath,
|
|
235
|
+
events: events.filter((_event, index) => !duplicates.has(index)),
|
|
236
|
+
suppressed: [...duplicates].map((index) => ({ duplicate: events[index], original: events[index - 1] })),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function journalEventToTranscriptEvent(event = {}) {
|
|
241
|
+
const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
|
|
242
|
+
if (!payload.role || event.type === "history.corrected") return null;
|
|
243
|
+
return {
|
|
244
|
+
id: event.eventId,
|
|
245
|
+
role: payload.role,
|
|
246
|
+
content: payload.content,
|
|
247
|
+
toolCalls: payload.toolCalls,
|
|
248
|
+
toolCallId: payload.toolCallId,
|
|
249
|
+
artifactId: payload.artifactId,
|
|
250
|
+
preview: payload.preview,
|
|
251
|
+
segmentId: payload.segmentId,
|
|
252
|
+
turnId: event.turnId,
|
|
253
|
+
itemId: event.itemId,
|
|
254
|
+
createdAt: event.createdAt,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function loadTranscriptProjection(workspaceRoot = process.cwd(), sessionId = "") {
|
|
259
|
+
const journal = loadJournal(workspaceRoot, sessionId);
|
|
260
|
+
if (journal.events.length > 0) {
|
|
261
|
+
return {
|
|
262
|
+
filePath: journal.filePath,
|
|
263
|
+
events: journal.events.map(journalEventToTranscriptEvent).filter(Boolean),
|
|
264
|
+
source: "journal-v3",
|
|
265
|
+
suppressed: [],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const legacy = readLegacyProjection(workspaceRoot, sessionId);
|
|
269
|
+
return { ...legacy, source: "transcript-v2" };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function migrateLegacyJournal(workspaceRoot = process.cwd(), sessionId = "") {
|
|
273
|
+
const current = loadJournal(workspaceRoot, sessionId);
|
|
274
|
+
if (current.events.length > 0) return { ok: true, migrated: false, ...current };
|
|
275
|
+
const legacy = readLegacyProjection(workspaceRoot, sessionId);
|
|
276
|
+
if (legacy.events.length === 0 && legacy.suppressed.length === 0) {
|
|
277
|
+
return { ok: true, migrated: false, filePath: current.filePath, events: [] };
|
|
278
|
+
}
|
|
279
|
+
const imported = legacy.events.flatMap((message, index) => {
|
|
280
|
+
const event = messageToJournalEvent({
|
|
281
|
+
role: message.role,
|
|
282
|
+
content: message.content,
|
|
283
|
+
tool_calls: message.toolCalls,
|
|
284
|
+
tool_call_id: message.toolCallId,
|
|
285
|
+
artifactId: message.artifactId,
|
|
286
|
+
preview: message.preview,
|
|
287
|
+
}, {
|
|
288
|
+
sessionId,
|
|
289
|
+
turnId: String(message.turnId || message.segmentId || "legacy"),
|
|
290
|
+
index,
|
|
291
|
+
eventId: `migrated_${String(message.id || index)}`,
|
|
292
|
+
idempotencyKey: `legacy:${String(message.id || index)}`,
|
|
293
|
+
createdAt: message.createdAt,
|
|
294
|
+
});
|
|
295
|
+
return event ? [event] : [];
|
|
296
|
+
});
|
|
297
|
+
const corrections = legacy.suppressed.map(({ duplicate, original }, index) => ({
|
|
298
|
+
version: JOURNAL_VERSION,
|
|
299
|
+
eventId: `correction_${String(duplicate && duplicate.id || index)}`,
|
|
300
|
+
idempotencyKey: `correction:${String(duplicate && duplicate.id || index)}`,
|
|
301
|
+
sessionId,
|
|
302
|
+
turnId: "migration",
|
|
303
|
+
itemId: "",
|
|
304
|
+
type: "history.corrected",
|
|
305
|
+
createdAt: new Date().toISOString(),
|
|
306
|
+
payload: {
|
|
307
|
+
suppressedEventId: String(duplicate && duplicate.id || ""),
|
|
308
|
+
duplicateOf: String(original && original.id || ""),
|
|
309
|
+
reason: "legacy_system_baseline_shift",
|
|
310
|
+
},
|
|
311
|
+
}));
|
|
312
|
+
const result = appendJournalEvents(workspaceRoot, sessionId, imported.concat(corrections));
|
|
313
|
+
return { ...result, migrated: result.ok, suppressed: legacy.suppressed.length };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function appendTurnMessages(
|
|
317
|
+
workspaceRoot = process.cwd(),
|
|
318
|
+
sessionId = "",
|
|
319
|
+
turnId = "",
|
|
320
|
+
messages = [],
|
|
321
|
+
options = {},
|
|
322
|
+
) {
|
|
323
|
+
const migration = migrateLegacyJournal(workspaceRoot, sessionId);
|
|
324
|
+
if (!migration.ok) return migration;
|
|
325
|
+
const scope = String(options.scope || "items").trim() || "items";
|
|
326
|
+
const events = (Array.isArray(messages) ? messages : []).flatMap((message, index) => {
|
|
327
|
+
const event = messageToJournalEvent(message, {
|
|
328
|
+
sessionId,
|
|
329
|
+
turnId,
|
|
330
|
+
index,
|
|
331
|
+
idempotencyKey: `${turnId || "turn"}:${scope}:${index}:${messageEventType(message)}`,
|
|
332
|
+
});
|
|
333
|
+
return event ? [event] : [];
|
|
334
|
+
});
|
|
335
|
+
return appendJournalEvents(workspaceRoot, sessionId, events);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
module.exports = {
|
|
339
|
+
JOURNAL_VERSION,
|
|
340
|
+
getJournalDir,
|
|
341
|
+
getJournalPath,
|
|
342
|
+
deleteSessionJournal,
|
|
343
|
+
loadJournal,
|
|
344
|
+
appendJournalEvents,
|
|
345
|
+
appendTurnMessages,
|
|
346
|
+
loadTranscriptProjection,
|
|
347
|
+
migrateLegacyJournal,
|
|
348
|
+
legacyDuplicateIndexes,
|
|
349
|
+
messageToJournalEvent,
|
|
350
|
+
journalEventToTranscriptEvent,
|
|
351
|
+
};
|
package/src/code/nativeRunner.js
CHANGED
|
@@ -1780,6 +1780,27 @@ async function runNativeLoop({
|
|
|
1780
1780
|
if (!resume) {
|
|
1781
1781
|
transport.prepareMessages({ messages, systemPrompt, prompt });
|
|
1782
1782
|
}
|
|
1783
|
+
// Request history is provider-local. Capture the boundary only after the
|
|
1784
|
+
// provider has prepared system/user input, then expose new model/tool items
|
|
1785
|
+
// as an explicit current-turn delta. Durable history never infers a delta
|
|
1786
|
+
// by comparing this mutable wire array with a persisted transcript.
|
|
1787
|
+
const turnOutputStart = messages.length;
|
|
1788
|
+
const isProviderToolResultUserMessage = (message) => (
|
|
1789
|
+
String(message && message.role || "").toLowerCase() === "user"
|
|
1790
|
+
&& Array.isArray(message.content)
|
|
1791
|
+
&& message.content.some((block) => {
|
|
1792
|
+
const type = String(block && block.type || "").toLowerCase();
|
|
1793
|
+
return type === "tool_result" || type === "image" || type === "image_url";
|
|
1794
|
+
})
|
|
1795
|
+
);
|
|
1796
|
+
const currentTurnItems = () => cloneMessageList(messages.slice(turnOutputStart))
|
|
1797
|
+
// External user input is committed by the turn coordinator before this
|
|
1798
|
+
// loop starts. Any later user-role messages are runtime nudges/mailbox
|
|
1799
|
+
// controls for the provider and must not appear as user chat history.
|
|
1800
|
+
.filter((message) => (
|
|
1801
|
+
String(message && message.role || "").toLowerCase() !== "user"
|
|
1802
|
+
|| isProviderToolResultUserMessage(message)
|
|
1803
|
+
));
|
|
1783
1804
|
|
|
1784
1805
|
let aggregated = "";
|
|
1785
1806
|
let streamed = false;
|
|
@@ -2002,6 +2023,7 @@ async function runNativeLoop({
|
|
|
2002
2023
|
streamed,
|
|
2003
2024
|
toolCallsExecuted,
|
|
2004
2025
|
messages,
|
|
2026
|
+
turnItems: currentTurnItems(),
|
|
2005
2027
|
usage,
|
|
2006
2028
|
executionState,
|
|
2007
2029
|
protocolLedger: lastProtocolLedger || snapshotLedger(activeLedger),
|
|
@@ -2023,6 +2045,7 @@ async function runNativeLoop({
|
|
|
2023
2045
|
streamed,
|
|
2024
2046
|
toolCallsExecuted,
|
|
2025
2047
|
messages,
|
|
2048
|
+
turnItems: currentTurnItems(),
|
|
2026
2049
|
usage,
|
|
2027
2050
|
executionState,
|
|
2028
2051
|
protocolLedger: lastProtocolLedger || snapshotLedger(activeLedger),
|
|
@@ -2202,6 +2225,7 @@ async function runNativeLoop({
|
|
|
2202
2225
|
streamed,
|
|
2203
2226
|
toolCallsExecuted,
|
|
2204
2227
|
messages,
|
|
2228
|
+
turnItems: currentTurnItems(),
|
|
2205
2229
|
usage,
|
|
2206
2230
|
executionState,
|
|
2207
2231
|
waitingUserInteraction: true,
|
|
@@ -2217,6 +2241,7 @@ async function runNativeLoop({
|
|
|
2217
2241
|
streamed,
|
|
2218
2242
|
toolCallsExecuted,
|
|
2219
2243
|
messages,
|
|
2244
|
+
turnItems: currentTurnItems(),
|
|
2220
2245
|
usage,
|
|
2221
2246
|
executionState,
|
|
2222
2247
|
waitingUserInteraction: true,
|
|
@@ -2376,6 +2401,7 @@ async function runNativeAgentTask({
|
|
|
2376
2401
|
error: "",
|
|
2377
2402
|
output: outputText,
|
|
2378
2403
|
messages: cloneMessageList(runResult.messages),
|
|
2404
|
+
turnItems: cloneMessageList(runResult.turnItems),
|
|
2379
2405
|
sessionId: nextSessionId,
|
|
2380
2406
|
usage,
|
|
2381
2407
|
contextMeter,
|
package/src/code/sessionStore.js
CHANGED
|
@@ -6,10 +6,9 @@ const {
|
|
|
6
6
|
getTranscriptFilePath,
|
|
7
7
|
loadTranscript,
|
|
8
8
|
migrateNlMessagesToTranscript,
|
|
9
|
-
appendTranscriptMessages,
|
|
10
|
-
transcriptEventsToMessages,
|
|
11
9
|
deleteTranscript,
|
|
12
10
|
} = require("./context/transcript");
|
|
11
|
+
const { getJournalPath, deleteSessionJournal } = require("./conversation/sessionJournal");
|
|
13
12
|
const { deleteSessionArtifacts } = require("./context/artifacts");
|
|
14
13
|
const { deleteSessionCommitLog, maybeGcSessionArtifacts } = require("./context/artifactGc");
|
|
15
14
|
const { defaultContextPolicy } = require("./context/assembler");
|
|
@@ -84,8 +83,11 @@ function buildSessionSnapshot(input = {}) {
|
|
|
84
83
|
};
|
|
85
84
|
|
|
86
85
|
return {
|
|
87
|
-
version:
|
|
86
|
+
version: 3,
|
|
88
87
|
...base,
|
|
88
|
+
journal: {
|
|
89
|
+
path: getJournalPath(base.workspaceRoot, sessionId),
|
|
90
|
+
},
|
|
89
91
|
transcript: {
|
|
90
92
|
path: getTranscriptFilePath(base.workspaceRoot, sessionId),
|
|
91
93
|
},
|
|
@@ -150,22 +152,6 @@ function hydrateSessionFromDisk(snapshot = {}, workspaceRoot = process.cwd()) {
|
|
|
150
152
|
return payload;
|
|
151
153
|
}
|
|
152
154
|
|
|
153
|
-
function syncTranscriptFromNlMessages(workspaceRoot = process.cwd(), sessionId = "", nlMessages = []) {
|
|
154
|
-
const messages = Array.isArray(nlMessages) ? nlMessages : [];
|
|
155
|
-
if (!sessionId || messages.length === 0) return;
|
|
156
|
-
const existing = loadTranscript(workspaceRoot, sessionId);
|
|
157
|
-
if (existing.events.length === 0) {
|
|
158
|
-
migrateNlMessagesToTranscript(workspaceRoot, sessionId, messages);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
const { matchTranscriptBaseline } = require("./context/assembler");
|
|
162
|
-
const priorMessages = transcriptEventsToMessages(existing.events);
|
|
163
|
-
const baseline = matchTranscriptBaseline(priorMessages, messages);
|
|
164
|
-
if (messages.length > baseline) {
|
|
165
|
-
appendTranscriptMessages(workspaceRoot, sessionId, messages.slice(baseline));
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
155
|
function listSessionSummaries(workspaceRoot = process.cwd(), { limit = 40 } = {}) {
|
|
170
156
|
const dir = getSessionsDir(workspaceRoot);
|
|
171
157
|
const cap = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 40;
|
|
@@ -250,8 +236,9 @@ function saveSessionSnapshot(workspaceRoot = process.cwd(), snapshot = {}) {
|
|
|
250
236
|
|
|
251
237
|
const toWrite = { ...payload };
|
|
252
238
|
if (toWrite.version >= 2) {
|
|
253
|
-
|
|
254
|
-
//
|
|
239
|
+
// Conversation content is committed explicitly by the turn coordinator.
|
|
240
|
+
// Session snapshot saves projections/metadata only and never tries to infer
|
|
241
|
+
// missing events from a mutable provider message array.
|
|
255
242
|
delete toWrite.nlMessages;
|
|
256
243
|
}
|
|
257
244
|
|
|
@@ -349,6 +336,7 @@ function deleteSessionData(workspaceRoot = process.cwd(), sessionId = "") {
|
|
|
349
336
|
const filePath = getSessionFilePath(workspaceRoot, normalizedId);
|
|
350
337
|
try {
|
|
351
338
|
if (filePath && fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
339
|
+
deleteSessionJournal(workspaceRoot, normalizedId);
|
|
352
340
|
deleteTranscript(workspaceRoot, normalizedId);
|
|
353
341
|
deleteSessionArtifacts(workspaceRoot, normalizedId);
|
|
354
342
|
deleteSessionCommitLog(workspaceRoot, normalizedId);
|
|
@@ -165,6 +165,7 @@ async function runDecomposedTask({
|
|
|
165
165
|
}) {
|
|
166
166
|
const steps = decomposeBugFixTask(task);
|
|
167
167
|
const results = [];
|
|
168
|
+
const turnItems = [];
|
|
168
169
|
let aborted = false;
|
|
169
170
|
|
|
170
171
|
// Check if already aborted
|
|
@@ -227,9 +228,8 @@ async function runDecomposedTask({
|
|
|
227
228
|
: null,
|
|
228
229
|
});
|
|
229
230
|
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
syncMessagesToTranscript(state, stepResult.messages, workspaceRoot);
|
|
231
|
+
if (stepResult && Array.isArray(stepResult.turnItems)) {
|
|
232
|
+
turnItems.push(...stepResult.turnItems);
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
results.push({
|
|
@@ -258,6 +258,7 @@ async function runDecomposedTask({
|
|
|
258
258
|
ok: false,
|
|
259
259
|
error: `Failed at ${step.name}: ${stepResult.error}`,
|
|
260
260
|
results,
|
|
261
|
+
turnItems,
|
|
261
262
|
};
|
|
262
263
|
}
|
|
263
264
|
|
|
@@ -274,6 +275,7 @@ async function runDecomposedTask({
|
|
|
274
275
|
ok: false,
|
|
275
276
|
error: `Error at ${step.name}: ${err.message}`,
|
|
276
277
|
results,
|
|
278
|
+
turnItems,
|
|
277
279
|
};
|
|
278
280
|
}
|
|
279
281
|
}
|
|
@@ -283,6 +285,7 @@ async function runDecomposedTask({
|
|
|
283
285
|
ok: false,
|
|
284
286
|
error: "Task aborted by user",
|
|
285
287
|
results,
|
|
288
|
+
turnItems,
|
|
286
289
|
};
|
|
287
290
|
}
|
|
288
291
|
|
|
@@ -293,6 +296,7 @@ async function runDecomposedTask({
|
|
|
293
296
|
ok: true,
|
|
294
297
|
summary,
|
|
295
298
|
results,
|
|
299
|
+
turnItems,
|
|
296
300
|
};
|
|
297
301
|
}
|
|
298
302
|
|