@averyyy/pi-coding-agent 0.82.1-piclient.6 → 0.82.1-piclient.7

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 (58) hide show
  1. package/CHANGELOG.md +0 -12
  2. package/dist/cli/args.d.ts.map +1 -1
  3. package/dist/cli/args.js +1 -2
  4. package/dist/cli/args.js.map +1 -1
  5. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  6. package/dist/core/agent-session-runtime.js +0 -5
  7. package/dist/core/agent-session-runtime.js.map +1 -1
  8. package/dist/core/agent-session.d.ts +1 -12
  9. package/dist/core/agent-session.d.ts.map +1 -1
  10. package/dist/core/agent-session.js +89 -317
  11. package/dist/core/agent-session.js.map +1 -1
  12. package/dist/core/pi-server-client.d.ts +2 -54
  13. package/dist/core/pi-server-client.d.ts.map +1 -1
  14. package/dist/core/pi-server-client.js +272 -2320
  15. package/dist/core/pi-server-client.js.map +1 -1
  16. package/dist/core/pi-server-protocol.d.ts +0 -2
  17. package/dist/core/pi-server-protocol.d.ts.map +1 -1
  18. package/dist/core/pi-server-protocol.js +1 -24
  19. package/dist/core/pi-server-protocol.js.map +1 -1
  20. package/dist/core/pi-server-request.d.ts +0 -28
  21. package/dist/core/pi-server-request.d.ts.map +1 -1
  22. package/dist/core/pi-server-request.js +75 -492
  23. package/dist/core/pi-server-request.js.map +1 -1
  24. package/dist/core/sdk.d.ts.map +1 -1
  25. package/dist/core/sdk.js +53 -69
  26. package/dist/core/sdk.js.map +1 -1
  27. package/dist/core/session-manager.d.ts +2 -86
  28. package/dist/core/session-manager.d.ts.map +1 -1
  29. package/dist/core/session-manager.js +144 -737
  30. package/dist/core/session-manager.js.map +1 -1
  31. package/dist/core/tools/output-accumulator.d.ts +1 -3
  32. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  33. package/dist/core/tools/output-accumulator.js +26 -46
  34. package/dist/core/tools/output-accumulator.js.map +1 -1
  35. package/dist/main.d.ts.map +1 -1
  36. package/dist/main.js +0 -13
  37. package/dist/main.js.map +1 -1
  38. package/dist/modes/interactive/components/session-selector.d.ts +0 -15
  39. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  40. package/dist/modes/interactive/components/session-selector.js +29 -98
  41. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  42. package/package.json +4 -4
  43. package/dist/cli/tool-effects.d.ts +0 -6
  44. package/dist/cli/tool-effects.d.ts.map +0 -1
  45. package/dist/cli/tool-effects.js +0 -164
  46. package/dist/cli/tool-effects.js.map +0 -1
  47. package/dist/core/pi-server-compact-state.d.ts +0 -81
  48. package/dist/core/pi-server-compact-state.d.ts.map +0 -1
  49. package/dist/core/pi-server-compact-state.js +0 -439
  50. package/dist/core/pi-server-compact-state.js.map +0 -1
  51. package/dist/core/pi-server-run-state.d.ts +0 -42
  52. package/dist/core/pi-server-run-state.d.ts.map +0 -1
  53. package/dist/core/pi-server-run-state.js +0 -320
  54. package/dist/core/pi-server-run-state.js.map +0 -1
  55. package/dist/core/tool-effect-journal.d.ts +0 -103
  56. package/dist/core/tool-effect-journal.d.ts.map +0 -1
  57. package/dist/core/tool-effect-journal.js +0 -1442
  58. package/dist/core/tool-effect-journal.js.map +0 -1
@@ -1,1442 +0,0 @@
1
- import { Buffer } from "node:buffer";
2
- import { createHash, randomUUID } from "node:crypto";
3
- import { chmodSync, closeSync, existsSync, fsyncSync, openSync, readSync, renameSync, statSync, truncateSync, unlinkSync, writeSync, } from "node:fs";
4
- import { dirname } from "node:path";
5
- import { StringDecoder } from "node:string_decoder";
6
- const TOOL_EFFECT_JOURNAL_VERSION = 1;
7
- const TOOL_EFFECT_CHECKPOINT_VERSION = 1;
8
- const JOURNAL_READ_BUFFER_BYTES = 1024 * 1024;
9
- const SHA256_PATTERN = /^[a-f0-9]{64}$/;
10
- const TOOL_EFFECT_JOURNAL_SUFFIX = ".tool-effects.jsonl";
11
- const DEFAULT_MAX_UNRESOLVED_EFFECTS = 1024;
12
- const DEFAULT_MAX_UNRESOLVED_BYTES = 64 * 1024 * 1024;
13
- const DEFAULT_CHECKPOINT_RECORDS = 4096;
14
- const DEFAULT_CHECKPOINT_BYTES = 16 * 1024 * 1024;
15
- const DEFAULT_WINDOWS_RENAME_RETRIES = 5;
16
- const MAX_WINDOWS_RENAME_DELAY_MS = 100;
17
- const TOOL_EFFECT_MAX_UNRESOLVED_EFFECTS_ENV = "PI_TOOL_EFFECT_MAX_UNRESOLVED_EFFECTS";
18
- const TOOL_EFFECT_MAX_UNRESOLVED_BYTES_ENV = "PI_TOOL_EFFECT_MAX_UNRESOLVED_BYTES";
19
- const TOOL_EFFECT_CHECKPOINT_RECORDS_ENV = "PI_TOOL_EFFECT_CHECKPOINT_RECORDS";
20
- const TOOL_EFFECT_CHECKPOINT_BYTES_ENV = "PI_TOOL_EFFECT_CHECKPOINT_BYTES";
21
- const TOOL_EFFECT_WINDOWS_RENAME_RETRIES_ENV = "PI_TOOL_EFFECT_WINDOWS_RENAME_RETRIES";
22
- let toolEffectJournalTestHooks;
23
- export function setToolEffectJournalTestHooks(hooks) {
24
- toolEffectJournalTestHooks = hooks;
25
- }
26
- export class ToolEffectUnknownOutcomeError extends Error {
27
- code = "unknown_outcome";
28
- toolCallIds;
29
- effects;
30
- constructor(effects, message) {
31
- const details = effects
32
- .map((effect) => `sessionPath=${effect.sessionPath}\njournalPath=${effect.journalPath}\nsessionId=${effect.sessionId}\neffectId=${effect.effectId}\ntoolCallId=${effect.toolCallId}\ntoolName=${effect.toolName}\nphase=${effect.phase}`)
33
- .join("\n\n");
34
- super([
35
- message ?? "Cannot safely continue: tool effects may have occurred but their final outcomes are unknown.",
36
- details,
37
- "Resolve explicitly with `tool-effects mark-failed` or `tool-effects accept-result`; the tool will not be re-executed automatically.",
38
- ].join("\n"));
39
- this.name = "ToolEffectUnknownOutcomeError";
40
- this.toolCallIds = effects.map((effect) => effect.toolCallId);
41
- this.effects = effects.map((effect) => ({ ...effect }));
42
- }
43
- }
44
- export class ToolEffectRecoveryRequiredError extends Error {
45
- code = "recovery_required";
46
- toolCallId;
47
- constructor(toolCallId) {
48
- super(`Cannot execute tool call ${toolCallId}: a durable result is waiting to be recovered into session history`);
49
- this.name = "ToolEffectRecoveryRequiredError";
50
- this.toolCallId = toolCallId;
51
- }
52
- }
53
- export class ToolEffectJournalCapacityError extends Error {
54
- code = "tool_effect_journal_capacity";
55
- constructor(message) {
56
- super(message);
57
- this.name = "ToolEffectJournalCapacityError";
58
- }
59
- }
60
- function sha256(value) {
61
- return createHash("sha256").update(value).digest("hex");
62
- }
63
- function readPositiveIntegerEnvironment(name, defaultValue) {
64
- const raw = process.env[name];
65
- if (raw === undefined)
66
- return defaultValue;
67
- if (!/^[1-9][0-9]*$/.test(raw)) {
68
- throw new Error(`${name} must be a positive integer`);
69
- }
70
- const value = Number(raw);
71
- if (!Number.isSafeInteger(value)) {
72
- throw new Error(`${name} must be a safe integer`);
73
- }
74
- return value;
75
- }
76
- function readNonNegativeIntegerEnvironment(name, defaultValue) {
77
- const raw = process.env[name];
78
- if (raw === undefined)
79
- return defaultValue;
80
- if (!/^(?:0|[1-9][0-9]*)$/.test(raw)) {
81
- throw new Error(`${name} must be a non-negative integer`);
82
- }
83
- const value = Number(raw);
84
- if (!Number.isSafeInteger(value)) {
85
- throw new Error(`${name} must be a safe integer`);
86
- }
87
- return value;
88
- }
89
- function getToolEffectJournalConfig() {
90
- return {
91
- maxUnresolvedEffects: readPositiveIntegerEnvironment(TOOL_EFFECT_MAX_UNRESOLVED_EFFECTS_ENV, DEFAULT_MAX_UNRESOLVED_EFFECTS),
92
- maxUnresolvedBytes: readPositiveIntegerEnvironment(TOOL_EFFECT_MAX_UNRESOLVED_BYTES_ENV, DEFAULT_MAX_UNRESOLVED_BYTES),
93
- checkpointRecords: readPositiveIntegerEnvironment(TOOL_EFFECT_CHECKPOINT_RECORDS_ENV, DEFAULT_CHECKPOINT_RECORDS),
94
- checkpointBytes: readPositiveIntegerEnvironment(TOOL_EFFECT_CHECKPOINT_BYTES_ENV, DEFAULT_CHECKPOINT_BYTES),
95
- windowsRenameRetries: readNonNegativeIntegerEnvironment(TOOL_EFFECT_WINDOWS_RENAME_RETRIES_ENV, DEFAULT_WINDOWS_RENAME_RETRIES),
96
- };
97
- }
98
- function canonicalizeJson(value, ancestors) {
99
- if (value === null || typeof value === "string" || typeof value === "boolean") {
100
- return value;
101
- }
102
- if (typeof value === "number") {
103
- if (!Number.isFinite(value)) {
104
- throw new Error("Tool effect identity cannot contain non-finite numbers");
105
- }
106
- return value;
107
- }
108
- if (Array.isArray(value)) {
109
- if (ancestors.has(value)) {
110
- throw new Error("Tool effect identity cannot contain circular values");
111
- }
112
- ancestors.add(value);
113
- try {
114
- return value.map((item) => (item === undefined ? null : canonicalizeJson(item, ancestors)));
115
- }
116
- finally {
117
- ancestors.delete(value);
118
- }
119
- }
120
- if (typeof value === "object") {
121
- if (ancestors.has(value)) {
122
- throw new Error("Tool effect identity cannot contain circular values");
123
- }
124
- ancestors.add(value);
125
- try {
126
- const canonical = {};
127
- for (const key of Object.keys(value).sort()) {
128
- const item = value[key];
129
- if (item !== undefined) {
130
- canonical[key] = canonicalizeJson(item, ancestors);
131
- }
132
- }
133
- return canonical;
134
- }
135
- finally {
136
- ancestors.delete(value);
137
- }
138
- }
139
- throw new Error(`Tool effect identity cannot contain ${typeof value} values`);
140
- }
141
- function canonicalJson(value) {
142
- return JSON.stringify(canonicalizeJson(value, new Set()));
143
- }
144
- function canonicalPersistedJson(value) {
145
- const serialized = JSON.stringify(value);
146
- if (serialized === undefined) {
147
- throw new Error("Tool effect result is not JSON serializable");
148
- }
149
- return canonicalJson(JSON.parse(serialized));
150
- }
151
- function assertObject(value, path) {
152
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
153
- throw new Error(`${path} must be an object`);
154
- }
155
- }
156
- function assertNonEmptyString(value, path) {
157
- if (typeof value !== "string" || value.length === 0) {
158
- throw new Error(`${path} must be a non-empty string`);
159
- }
160
- }
161
- function assertHash(value, path) {
162
- if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
163
- throw new Error(`${path} must be a 64-character lowercase hex digest`);
164
- }
165
- }
166
- function assertNonNegativeSafeInteger(value, path) {
167
- if (!Number.isSafeInteger(value) || value < 0) {
168
- throw new Error(`${path} must be a non-negative safe integer`);
169
- }
170
- }
171
- function assertToolResultMessage(value, path) {
172
- assertObject(value, path);
173
- if (value.role !== "toolResult") {
174
- throw new Error(`${path}.role must be toolResult`);
175
- }
176
- assertNonEmptyString(value.toolCallId, `${path}.toolCallId`);
177
- assertNonEmptyString(value.toolName, `${path}.toolName`);
178
- if (!Array.isArray(value.content)) {
179
- throw new Error(`${path}.content must be an array`);
180
- }
181
- for (const [index, content] of value.content.entries()) {
182
- assertObject(content, `${path}.content[${index}]`);
183
- if (content.type === "text") {
184
- if (typeof content.text !== "string") {
185
- throw new Error(`${path}.content[${index}].text must be a string`);
186
- }
187
- if (content.textSignature !== undefined && typeof content.textSignature !== "string") {
188
- throw new Error(`${path}.content[${index}].textSignature must be a string when present`);
189
- }
190
- continue;
191
- }
192
- if (content.type === "image") {
193
- if (typeof content.data !== "string") {
194
- throw new Error(`${path}.content[${index}].data must be a string`);
195
- }
196
- if (typeof content.mimeType !== "string") {
197
- throw new Error(`${path}.content[${index}].mimeType must be a string`);
198
- }
199
- continue;
200
- }
201
- throw new Error(`${path}.content[${index}].type must be text or image`);
202
- }
203
- if (typeof value.isError !== "boolean") {
204
- throw new Error(`${path}.isError must be a boolean`);
205
- }
206
- assertNonNegativeSafeInteger(value.timestamp, `${path}.timestamp`);
207
- if (value.addedToolNames !== undefined) {
208
- if (!Array.isArray(value.addedToolNames)) {
209
- throw new Error(`${path}.addedToolNames must be an array when present`);
210
- }
211
- for (const [index, toolName] of value.addedToolNames.entries()) {
212
- assertNonEmptyString(toolName, `${path}.addedToolNames[${index}]`);
213
- }
214
- }
215
- if (value.usage !== undefined) {
216
- assertObject(value.usage, `${path}.usage`);
217
- for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens"]) {
218
- assertNonNegativeSafeInteger(value.usage[key], `${path}.usage.${key}`);
219
- }
220
- for (const key of ["cacheWrite1h", "reasoning"]) {
221
- if (value.usage[key] !== undefined) {
222
- assertNonNegativeSafeInteger(value.usage[key], `${path}.usage.${key}`);
223
- }
224
- }
225
- assertObject(value.usage.cost, `${path}.usage.cost`);
226
- for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"]) {
227
- const cost = value.usage.cost[key];
228
- if (typeof cost !== "number" || !Number.isFinite(cost) || cost < 0) {
229
- throw new Error(`${path}.usage.cost.${key} must be a non-negative finite number`);
230
- }
231
- }
232
- }
233
- }
234
- function parseRecord(payload, lineNumber) {
235
- let value;
236
- try {
237
- value = JSON.parse(payload);
238
- }
239
- catch (error) {
240
- throw new Error(`Tool effect journal payload is invalid JSON at line ${lineNumber}`, { cause: error });
241
- }
242
- assertObject(value, `Tool effect journal line ${lineNumber}`);
243
- if (value.version !== TOOL_EFFECT_JOURNAL_VERSION) {
244
- throw new Error(`Unsupported tool effect journal version at line ${lineNumber}`);
245
- }
246
- assertNonNegativeSafeInteger(value.sequence, `Tool effect journal line ${lineNumber}.sequence`);
247
- assertNonNegativeSafeInteger(value.timestamp, `Tool effect journal line ${lineNumber}.timestamp`);
248
- if (value.kind === "checkpoint") {
249
- if (value.checkpointVersion !== TOOL_EFFECT_CHECKPOINT_VERSION) {
250
- throw new Error(`Unsupported tool effect checkpoint version at line ${lineNumber}`);
251
- }
252
- assertNonNegativeSafeInteger(value.generation, `Tool effect journal line ${lineNumber}.generation`);
253
- if (value.generation === 0) {
254
- throw new Error(`Tool effect journal line ${lineNumber}.generation must be positive`);
255
- }
256
- assertNonNegativeSafeInteger(value.previousRecordCount, `Tool effect journal line ${lineNumber}.previousRecordCount`);
257
- assertHash(value.previousSha256, `Tool effect journal line ${lineNumber}.previousSha256`);
258
- assertNonNegativeSafeInteger(value.snapshotRecordCount, `Tool effect journal line ${lineNumber}.snapshotRecordCount`);
259
- assertHash(value.snapshotSha256, `Tool effect journal line ${lineNumber}.snapshotSha256`);
260
- return {
261
- version: TOOL_EFFECT_JOURNAL_VERSION,
262
- checkpointVersion: TOOL_EFFECT_CHECKPOINT_VERSION,
263
- kind: "checkpoint",
264
- sequence: value.sequence,
265
- timestamp: value.timestamp,
266
- generation: value.generation,
267
- previousRecordCount: value.previousRecordCount,
268
- previousSha256: value.previousSha256,
269
- snapshotRecordCount: value.snapshotRecordCount,
270
- snapshotSha256: value.snapshotSha256,
271
- };
272
- }
273
- assertHash(value.effectId, `Tool effect journal line ${lineNumber}.effectId`);
274
- const base = {
275
- version: TOOL_EFFECT_JOURNAL_VERSION,
276
- sequence: value.sequence,
277
- timestamp: value.timestamp,
278
- effectId: value.effectId,
279
- };
280
- if (value.kind === "intent") {
281
- assertNonEmptyString(value.sessionId, `Tool effect journal line ${lineNumber}.sessionId`);
282
- assertNonEmptyString(value.assistantEntryId, `Tool effect journal line ${lineNumber}.assistantEntryId`);
283
- assertNonEmptyString(value.toolCallId, `Tool effect journal line ${lineNumber}.toolCallId`);
284
- assertNonEmptyString(value.toolName, `Tool effect journal line ${lineNumber}.toolName`);
285
- assertNonNegativeSafeInteger(value.toolCallIndex, `Tool effect journal line ${lineNumber}.toolCallIndex`);
286
- assertHash(value.argumentsHash, `Tool effect journal line ${lineNumber}.argumentsHash`);
287
- return {
288
- ...base,
289
- kind: "intent",
290
- sessionId: value.sessionId,
291
- assistantEntryId: value.assistantEntryId,
292
- toolCallId: value.toolCallId,
293
- toolName: value.toolName,
294
- toolCallIndex: value.toolCallIndex,
295
- argumentsHash: value.argumentsHash,
296
- };
297
- }
298
- if (value.kind === "result") {
299
- assertToolResultMessage(value.message, `Tool effect journal line ${lineNumber}.message`);
300
- if (value.finalizationPending !== undefined && value.finalizationPending !== true) {
301
- throw new Error(`Tool effect journal line ${lineNumber}.finalizationPending must be true when present`);
302
- }
303
- return {
304
- ...base,
305
- kind: "result",
306
- message: value.message,
307
- finalizationPending: value.finalizationPending,
308
- };
309
- }
310
- if (value.kind === "finalization") {
311
- return { ...base, kind: "finalization" };
312
- }
313
- if (value.kind === "final") {
314
- assertToolResultMessage(value.message, `Tool effect journal line ${lineNumber}.message`);
315
- return { ...base, kind: "final", message: value.message };
316
- }
317
- if (value.kind === "commit") {
318
- assertNonEmptyString(value.sessionEntryId, `Tool effect journal line ${lineNumber}.sessionEntryId`);
319
- return { ...base, kind: "commit", sessionEntryId: value.sessionEntryId };
320
- }
321
- throw new Error(`Unsupported tool effect journal record kind at line ${lineNumber}`);
322
- }
323
- function applyRecord(effects, record, lineNumber) {
324
- if (record.kind === "intent") {
325
- if (effects.has(record.effectId)) {
326
- throw new Error(`Duplicate tool effect intent at line ${lineNumber}`);
327
- }
328
- for (const effect of effects.values()) {
329
- if (effect.intent.assistantEntryId === record.assistantEntryId &&
330
- effect.intent.toolCallIndex === record.toolCallIndex) {
331
- throw new Error(`Conflicting tool effect intent slot at line ${lineNumber}`);
332
- }
333
- }
334
- effects.set(record.effectId, {
335
- intent: record,
336
- resultRecordSeen: false,
337
- resultFinalizationPending: false,
338
- finalizationStarted: false,
339
- finalResultWritten: false,
340
- });
341
- return;
342
- }
343
- const effect = effects.get(record.effectId);
344
- if (!effect) {
345
- throw new Error(`Tool effect ${record.kind} has no matching intent at line ${lineNumber}`);
346
- }
347
- if (effect.committedSessionEntryId) {
348
- throw new Error(`Tool effect record follows commit at line ${lineNumber}`);
349
- }
350
- if (record.kind === "result") {
351
- if (record.message.toolCallId !== effect.intent.toolCallId ||
352
- record.message.toolName !== effect.intent.toolName) {
353
- throw new Error(`Tool effect result identity mismatch at line ${lineNumber}`);
354
- }
355
- if (effect.resultRecordSeen) {
356
- throw new Error(`Duplicate tool effect result at line ${lineNumber}`);
357
- }
358
- effect.resultRecordSeen = true;
359
- effect.executionResult = record.message;
360
- effect.resultFinalizationPending = record.finalizationPending === true;
361
- effect.finalizationStarted = record.finalizationPending === true;
362
- effect.result = record.message;
363
- effect.resultTimestamp = record.timestamp;
364
- if (record.finalizationPending === true) {
365
- effect.finalizationTimestamp = record.timestamp;
366
- }
367
- return;
368
- }
369
- if (record.kind === "finalization") {
370
- if (!effect.resultRecordSeen || !effect.result) {
371
- throw new Error(`Tool effect finalization precedes its durable result at line ${lineNumber}`);
372
- }
373
- if (effect.finalizationStarted) {
374
- throw new Error(`Duplicate tool effect finalization at line ${lineNumber}`);
375
- }
376
- effect.finalizationStarted = true;
377
- effect.finalizationTimestamp = record.timestamp;
378
- return;
379
- }
380
- if (record.kind === "final") {
381
- if (!effect.finalizationStarted) {
382
- throw new Error(`Tool effect final result has no matching finalization at line ${lineNumber}`);
383
- }
384
- if (effect.finalResultWritten) {
385
- throw new Error(`Duplicate tool effect final result at line ${lineNumber}`);
386
- }
387
- if (record.message.toolCallId !== effect.intent.toolCallId ||
388
- record.message.toolName !== effect.intent.toolName) {
389
- throw new Error(`Tool effect final result identity mismatch at line ${lineNumber}`);
390
- }
391
- effect.finalResultWritten = true;
392
- effect.result = record.message;
393
- effect.finalResultTimestamp = record.timestamp;
394
- return;
395
- }
396
- if (!effect.result) {
397
- throw new Error(`Tool effect commit precedes its durable result at line ${lineNumber}`);
398
- }
399
- if (effect.finalizationStarted && !effect.finalResultWritten) {
400
- throw new Error(`Tool effect commit precedes its final result at line ${lineNumber}`);
401
- }
402
- effect.committedSessionEntryId = record.sessionEntryId;
403
- }
404
- function findLastCompleteRecordBytes(path, fileBytes) {
405
- if (fileBytes === 0)
406
- return 0;
407
- const handle = openSync(path, "r");
408
- try {
409
- const buffer = Buffer.allocUnsafe(Math.min(JOURNAL_READ_BUFFER_BYTES, fileBytes));
410
- let end = fileBytes;
411
- while (end > 0) {
412
- const start = Math.max(0, end - buffer.byteLength);
413
- const length = end - start;
414
- const bytesRead = readSync(handle, buffer, 0, length, start);
415
- if (bytesRead !== length) {
416
- throw new Error(`Could not read tool effect journal tail: ${path}`);
417
- }
418
- const newlineIndex = buffer.lastIndexOf(0x0a, length - 1);
419
- if (newlineIndex !== -1) {
420
- return start + newlineIndex + 1;
421
- }
422
- end = start;
423
- }
424
- return 0;
425
- }
426
- finally {
427
- closeSync(handle);
428
- }
429
- }
430
- function createEmptyParsedJournal(fileBytes = 0) {
431
- return {
432
- nextSequence: 0,
433
- effects: new Map(),
434
- validBytes: 0,
435
- fileBytes,
436
- recordCount: 0,
437
- validSha256: sha256(""),
438
- checkpointGeneration: 0,
439
- };
440
- }
441
- function parseJournal(path) {
442
- if (!existsSync(path)) {
443
- return createEmptyParsedJournal();
444
- }
445
- const fileBytes = statSync(path).size;
446
- const validBytes = findLastCompleteRecordBytes(path, fileBytes);
447
- if (validBytes === 0) {
448
- return createEmptyParsedJournal(fileBytes);
449
- }
450
- const effects = new Map();
451
- const handle = openSync(path, "r");
452
- try {
453
- const decoder = new StringDecoder("utf8");
454
- const validFileHash = createHash("sha256");
455
- const snapshotHash = createHash("sha256");
456
- const buffer = Buffer.allocUnsafe(Math.min(JOURNAL_READ_BUFFER_BYTES, validBytes));
457
- let pending = "";
458
- let position = 0;
459
- let lineNumber = 0;
460
- let expectedSequence = 0;
461
- let checkpoint;
462
- let snapshotRecordsSeen = 0;
463
- const consumeLine = (line) => {
464
- lineNumber++;
465
- if (line.length === 0) {
466
- throw new Error(`Tool effect journal contains an empty record at line ${lineNumber}`);
467
- }
468
- let envelopeValue;
469
- try {
470
- envelopeValue = JSON.parse(line);
471
- }
472
- catch (error) {
473
- throw new Error(`Tool effect journal envelope is invalid JSON at line ${lineNumber}`, { cause: error });
474
- }
475
- assertObject(envelopeValue, `Tool effect journal envelope line ${lineNumber}`);
476
- if (typeof envelopeValue.payload !== "string") {
477
- throw new Error(`Tool effect journal envelope payload must be a string at line ${lineNumber}`);
478
- }
479
- assertHash(envelopeValue.sha256, `Tool effect journal envelope line ${lineNumber}.sha256`);
480
- if (sha256(envelopeValue.payload) !== envelopeValue.sha256) {
481
- throw new Error(`Tool effect journal checksum mismatch at line ${lineNumber}`);
482
- }
483
- const record = parseRecord(envelopeValue.payload, lineNumber);
484
- if (record.sequence !== expectedSequence) {
485
- throw new Error(`Tool effect journal sequence mismatch at line ${lineNumber}: expected ${expectedSequence}, received ${record.sequence}`);
486
- }
487
- expectedSequence++;
488
- if (record.kind === "checkpoint") {
489
- if (lineNumber !== 1 || checkpoint) {
490
- throw new Error(`Tool effect checkpoint must be the first and only checkpoint record`);
491
- }
492
- checkpoint = record;
493
- return;
494
- }
495
- if (checkpoint && snapshotRecordsSeen < checkpoint.snapshotRecordCount) {
496
- if (record.kind === "commit") {
497
- throw new Error(`Tool effect checkpoint snapshot contains a committed effect at line ${lineNumber}`);
498
- }
499
- snapshotHash.update(envelopeValue.payload);
500
- snapshotHash.update("\n");
501
- snapshotRecordsSeen++;
502
- }
503
- applyRecord(effects, record, lineNumber);
504
- };
505
- while (position < validBytes) {
506
- const length = Math.min(buffer.byteLength, validBytes - position);
507
- const bytesRead = readSync(handle, buffer, 0, length, position);
508
- if (bytesRead === 0) {
509
- throw new Error(`Tool effect journal ended before its last complete record: ${path}`);
510
- }
511
- position += bytesRead;
512
- validFileHash.update(buffer.subarray(0, bytesRead));
513
- pending += decoder.write(buffer.subarray(0, bytesRead));
514
- let lineStart = 0;
515
- let newlineIndex = pending.indexOf("\n", lineStart);
516
- while (newlineIndex !== -1) {
517
- consumeLine(pending.slice(lineStart, newlineIndex));
518
- lineStart = newlineIndex + 1;
519
- newlineIndex = pending.indexOf("\n", lineStart);
520
- }
521
- pending = pending.slice(lineStart);
522
- }
523
- pending += decoder.end();
524
- if (pending.length !== 0) {
525
- throw new Error(`Tool effect journal complete prefix does not end on a record boundary: ${path}`);
526
- }
527
- if (checkpoint && snapshotRecordsSeen !== checkpoint.snapshotRecordCount) {
528
- throw new Error(`Tool effect checkpoint snapshot is incomplete: expected ${checkpoint.snapshotRecordCount} records, received ${snapshotRecordsSeen}`);
529
- }
530
- if (checkpoint && snapshotHash.digest("hex") !== checkpoint.snapshotSha256) {
531
- throw new Error(`Tool effect checkpoint snapshot checksum mismatch`);
532
- }
533
- return {
534
- nextSequence: expectedSequence,
535
- effects,
536
- validBytes,
537
- fileBytes,
538
- recordCount: lineNumber,
539
- validSha256: validFileHash.digest("hex"),
540
- checkpointGeneration: checkpoint?.generation ?? 0,
541
- };
542
- }
543
- finally {
544
- closeSync(handle);
545
- }
546
- }
547
- function encodeRecord(record) {
548
- const payload = JSON.stringify(record);
549
- const envelope = { payload, sha256: sha256(payload) };
550
- return { payload, encoded: Buffer.from(`${JSON.stringify(envelope)}\n`, "utf8") };
551
- }
552
- function appendRecord(path, state, record) {
553
- if (state.validBytes !== state.fileBytes) {
554
- truncateSync(path, state.validBytes);
555
- }
556
- const { encoded } = encodeRecord(record);
557
- const fileExisted = existsSync(path);
558
- const handle = openSync(path, "a", 0o600);
559
- try {
560
- let written = 0;
561
- while (written < encoded.byteLength) {
562
- const bytesWritten = writeSync(handle, encoded, written, encoded.byteLength - written);
563
- if (bytesWritten === 0) {
564
- throw new Error(`Failed to make progress writing tool effect journal: ${path}`);
565
- }
566
- written += bytesWritten;
567
- }
568
- fsyncSync(handle);
569
- }
570
- finally {
571
- closeSync(handle);
572
- }
573
- if (!fileExisted && process.platform !== "win32") {
574
- const directoryHandle = openSync(dirname(path), "r");
575
- try {
576
- fsyncSync(directoryHandle);
577
- }
578
- finally {
579
- closeSync(directoryHandle);
580
- }
581
- }
582
- }
583
- function requireRecordTimestamp(value, label) {
584
- if (value === undefined) {
585
- throw new Error(`Tool effect journal lost ${label} timestamp`);
586
- }
587
- return value;
588
- }
589
- function buildUnresolvedSnapshotRecords(effects, startSequence) {
590
- const records = [];
591
- let sequence = startSequence;
592
- for (const effect of effects) {
593
- if (effect.committedSessionEntryId !== undefined)
594
- continue;
595
- records.push({ ...effect.intent, sequence: sequence++ });
596
- if (!effect.resultRecordSeen)
597
- continue;
598
- if (!effect.executionResult) {
599
- throw new Error(`Tool effect ${effect.intent.effectId} has a result marker without an execution result`);
600
- }
601
- records.push({
602
- version: TOOL_EFFECT_JOURNAL_VERSION,
603
- kind: "result",
604
- sequence: sequence++,
605
- timestamp: requireRecordTimestamp(effect.resultTimestamp, "result"),
606
- effectId: effect.intent.effectId,
607
- message: effect.executionResult,
608
- ...(effect.resultFinalizationPending ? { finalizationPending: true } : {}),
609
- });
610
- if (effect.finalizationStarted && !effect.resultFinalizationPending) {
611
- records.push({
612
- version: TOOL_EFFECT_JOURNAL_VERSION,
613
- kind: "finalization",
614
- sequence: sequence++,
615
- timestamp: requireRecordTimestamp(effect.finalizationTimestamp, "finalization"),
616
- effectId: effect.intent.effectId,
617
- });
618
- }
619
- if (effect.finalResultWritten) {
620
- if (!effect.result) {
621
- throw new Error(`Tool effect ${effect.intent.effectId} has a final marker without a final result`);
622
- }
623
- records.push({
624
- version: TOOL_EFFECT_JOURNAL_VERSION,
625
- kind: "final",
626
- sequence: sequence++,
627
- timestamp: requireRecordTimestamp(effect.finalResultTimestamp, "final result"),
628
- effectId: effect.intent.effectId,
629
- message: effect.result,
630
- });
631
- }
632
- }
633
- return records;
634
- }
635
- function hashSnapshotPayloads(payloads) {
636
- const hash = createHash("sha256");
637
- for (const payload of payloads) {
638
- hash.update(payload);
639
- hash.update("\n");
640
- }
641
- return hash.digest("hex");
642
- }
643
- function checkpointStateProjection(effects) {
644
- return canonicalPersistedJson([...effects]
645
- .filter((effect) => effect.committedSessionEntryId === undefined)
646
- .map((effect) => ({
647
- intent: { ...effect.intent, sequence: 0 },
648
- resultRecordSeen: effect.resultRecordSeen,
649
- executionResult: effect.executionResult,
650
- resultFinalizationPending: effect.resultFinalizationPending,
651
- finalizationStarted: effect.finalizationStarted,
652
- finalResultWritten: effect.finalResultWritten,
653
- result: effect.result,
654
- resultTimestamp: effect.resultTimestamp,
655
- finalizationTimestamp: effect.finalizationTimestamp,
656
- finalResultTimestamp: effect.finalResultTimestamp,
657
- })));
658
- }
659
- function getErrorCode(error) {
660
- if (typeof error !== "object" || error === null || !("code" in error))
661
- return undefined;
662
- return typeof error.code === "string" ? error.code : undefined;
663
- }
664
- function waitForWindowsRenameRetry(attempt) {
665
- const delay = Math.min(2 ** attempt * 5, MAX_WINDOWS_RENAME_DELAY_MS);
666
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay);
667
- }
668
- function replaceCheckpointFile(tempPath, path, windowsRenameRetries) {
669
- for (let attempt = 0;; attempt++) {
670
- try {
671
- toolEffectJournalTestHooks?.beforeRenameAttempt?.(attempt);
672
- renameSync(tempPath, path);
673
- return;
674
- }
675
- catch (error) {
676
- if (process.platform !== "win32" || getErrorCode(error) !== "EPERM" || attempt >= windowsRenameRetries) {
677
- throw error;
678
- }
679
- waitForWindowsRenameRetry(attempt);
680
- }
681
- }
682
- }
683
- function fsyncCheckpointDirectory(path) {
684
- if (process.platform === "win32") {
685
- // libuv cannot fsync a Windows directory handle. Re-open and fsync the
686
- // replaced file so MoveFileEx/rename metadata and file contents are flushed.
687
- const handle = openSync(path, "r+");
688
- try {
689
- fsyncSync(handle);
690
- }
691
- finally {
692
- closeSync(handle);
693
- }
694
- return;
695
- }
696
- const handle = openSync(dirname(path), "r");
697
- try {
698
- fsyncSync(handle);
699
- }
700
- finally {
701
- closeSync(handle);
702
- }
703
- }
704
- function writeCheckpointAtomically(path, encoded, expectedProjection, config) {
705
- const tempPath = `${path}.checkpoint-${process.pid}-${randomUUID()}.tmp`;
706
- let tempHandle;
707
- let replaced = false;
708
- let primaryError;
709
- let result;
710
- const cleanupErrors = [];
711
- try {
712
- tempHandle = openSync(tempPath, "wx", 0o600);
713
- chmodSync(tempPath, 0o600);
714
- toolEffectJournalTestHooks?.onCheckpointStage?.("after_temp_open");
715
- let written = 0;
716
- while (written < encoded.byteLength) {
717
- const bytesWritten = writeSync(tempHandle, encoded, written, encoded.byteLength - written);
718
- if (bytesWritten === 0) {
719
- throw new Error(`Failed to make progress writing tool effect checkpoint: ${tempPath}`);
720
- }
721
- written += bytesWritten;
722
- }
723
- toolEffectJournalTestHooks?.onCheckpointStage?.("after_temp_write");
724
- fsyncSync(tempHandle);
725
- toolEffectJournalTestHooks?.onCheckpointStage?.("after_temp_fsync");
726
- closeSync(tempHandle);
727
- tempHandle = undefined;
728
- const checkpointState = parseJournal(tempPath);
729
- if (checkpointStateProjection(checkpointState.effects.values()) !== expectedProjection) {
730
- throw new Error(`Tool effect checkpoint verification did not preserve unresolved state`);
731
- }
732
- toolEffectJournalTestHooks?.onCheckpointStage?.("before_replace");
733
- replaceCheckpointFile(tempPath, path, config.windowsRenameRetries);
734
- replaced = true;
735
- toolEffectJournalTestHooks?.onCheckpointStage?.("after_replace");
736
- toolEffectJournalTestHooks?.onCheckpointStage?.("before_directory_fsync");
737
- fsyncCheckpointDirectory(path);
738
- toolEffectJournalTestHooks?.onCheckpointStage?.("after_directory_fsync");
739
- result = checkpointState;
740
- }
741
- catch (error) {
742
- primaryError = error;
743
- }
744
- finally {
745
- if (tempHandle !== undefined) {
746
- try {
747
- closeSync(tempHandle);
748
- }
749
- catch (closeError) {
750
- cleanupErrors.push(closeError);
751
- }
752
- }
753
- if (!replaced && existsSync(tempPath)) {
754
- try {
755
- unlinkSync(tempPath);
756
- }
757
- catch (cleanupError) {
758
- cleanupErrors.push(cleanupError);
759
- }
760
- }
761
- }
762
- if (primaryError !== undefined && cleanupErrors.length === 0) {
763
- throw primaryError;
764
- }
765
- if (primaryError !== undefined || cleanupErrors.length > 0) {
766
- throw new AggregateError(primaryError === undefined ? cleanupErrors : [primaryError, ...cleanupErrors], `Failed to write tool effect checkpoint`);
767
- }
768
- if (!result) {
769
- throw new Error(`Tool effect checkpoint did not produce a verified state`);
770
- }
771
- return result;
772
- }
773
- function checkpointJournal(path, state, config) {
774
- if (state.recordCount === 0)
775
- return state;
776
- const snapshotRecords = buildUnresolvedSnapshotRecords(state.effects.values(), 1);
777
- const snapshotEncodings = snapshotRecords.map((record) => encodeRecord(record));
778
- const checkpoint = {
779
- version: TOOL_EFFECT_JOURNAL_VERSION,
780
- checkpointVersion: TOOL_EFFECT_CHECKPOINT_VERSION,
781
- kind: "checkpoint",
782
- sequence: 0,
783
- timestamp: Date.now(),
784
- generation: state.checkpointGeneration + 1,
785
- previousRecordCount: state.recordCount,
786
- previousSha256: state.validSha256,
787
- snapshotRecordCount: snapshotRecords.length,
788
- snapshotSha256: hashSnapshotPayloads(snapshotEncodings.map(({ payload }) => payload)),
789
- };
790
- const encoded = Buffer.concat([
791
- encodeRecord(checkpoint).encoded,
792
- ...snapshotEncodings.map(({ encoded }) => encoded),
793
- ]);
794
- return writeCheckpointAtomically(path, encoded, checkpointStateProjection(state.effects.values()), config);
795
- }
796
- function checkpointIfNeeded(path, state, config) {
797
- if (state.recordCount < config.checkpointRecords && state.validBytes < config.checkpointBytes) {
798
- return state;
799
- }
800
- return checkpointJournal(path, state, config);
801
- }
802
- export function checkpointToolEffectJournal(path) {
803
- const state = parseJournal(path);
804
- if (state.recordCount === 0)
805
- return false;
806
- checkpointJournal(path, state, getToolEffectJournalConfig());
807
- return true;
808
- }
809
- function buildEffectIdentity(input) {
810
- assertNonEmptyString(input.sessionId, "Tool effect sessionId");
811
- assertNonEmptyString(input.assistantEntryId, "Tool effect assistantEntryId");
812
- assertNonEmptyString(input.toolCallId, "Tool effect toolCallId");
813
- assertNonEmptyString(input.toolName, "Tool effect toolName");
814
- assertNonNegativeSafeInteger(input.toolCallIndex, "Tool effect toolCallIndex");
815
- const argumentsHash = sha256(canonicalJson(input.arguments));
816
- const effectId = sha256(canonicalJson({
817
- sessionId: input.sessionId,
818
- assistantEntryId: input.assistantEntryId,
819
- toolCallId: input.toolCallId,
820
- toolName: input.toolName,
821
- toolCallIndex: input.toolCallIndex,
822
- argumentsHash,
823
- }));
824
- return { effectId, argumentsHash };
825
- }
826
- function getUncommittedEffects(state) {
827
- return [...state.effects.values()].filter((effect) => effect.committedSessionEntryId === undefined);
828
- }
829
- function getSnapshotEncodedBytes(effects) {
830
- return buildUnresolvedSnapshotRecords(effects, 1).reduce((total, record) => total + encodeRecord(record).encoded.byteLength, 0);
831
- }
832
- export function getToolEffectJournalPath(sessionFile) {
833
- return `${sessionFile}${TOOL_EFFECT_JOURNAL_SUFFIX}`;
834
- }
835
- function getToolEffectResolutionPhase(effect) {
836
- if (effect.committedSessionEntryId)
837
- return "committed";
838
- if (!effect.resultRecordSeen)
839
- return "execution_unknown";
840
- if (effect.finalizationStarted && !effect.finalResultWritten)
841
- return "finalization_unknown";
842
- return "result_durable";
843
- }
844
- function getToolEffectResolutionTarget(effect, journalPath) {
845
- if (!journalPath.endsWith(TOOL_EFFECT_JOURNAL_SUFFIX)) {
846
- throw new Error(`Tool effect journal path must end with ${TOOL_EFFECT_JOURNAL_SUFFIX}`);
847
- }
848
- return {
849
- sessionId: effect.intent.sessionId,
850
- sessionPath: journalPath.slice(0, -TOOL_EFFECT_JOURNAL_SUFFIX.length),
851
- journalPath,
852
- effectId: effect.intent.effectId,
853
- toolCallId: effect.intent.toolCallId,
854
- toolName: effect.intent.toolName,
855
- phase: getToolEffectResolutionPhase(effect),
856
- };
857
- }
858
- function getUnknownToolEffectResolutionTarget(effect, journalPath) {
859
- const target = getToolEffectResolutionTarget(effect, journalPath);
860
- if (target.phase !== "execution_unknown" && target.phase !== "finalization_unknown") {
861
- throw new Error(`Tool effect ${effect.intent.effectId} does not have an unknown outcome`);
862
- }
863
- return { ...target, phase: target.phase };
864
- }
865
- export function beginToolEffect(path, input) {
866
- const config = getToolEffectJournalConfig();
867
- let state = parseJournal(path);
868
- const { effectId, argumentsHash } = buildEffectIdentity(input);
869
- const existing = state.effects.get(effectId);
870
- if (existing) {
871
- if (existing.result) {
872
- throw new ToolEffectRecoveryRequiredError(existing.intent.toolCallId);
873
- }
874
- throw new ToolEffectUnknownOutcomeError([getUnknownToolEffectResolutionTarget(existing, path)]);
875
- }
876
- for (const effect of state.effects.values()) {
877
- if (effect.intent.assistantEntryId === input.assistantEntryId &&
878
- effect.intent.toolCallIndex === input.toolCallIndex) {
879
- throw new Error(`Tool effect slot ${input.assistantEntryId}:${input.toolCallIndex} already belongs to ${effect.intent.toolCallId}`);
880
- }
881
- }
882
- state = checkpointIfNeeded(path, state, config);
883
- const timestamp = input.timestamp ?? Date.now();
884
- assertNonNegativeSafeInteger(timestamp, "Tool effect timestamp");
885
- const record = {
886
- version: TOOL_EFFECT_JOURNAL_VERSION,
887
- kind: "intent",
888
- sequence: state.nextSequence,
889
- timestamp,
890
- effectId,
891
- sessionId: input.sessionId,
892
- assistantEntryId: input.assistantEntryId,
893
- toolCallId: input.toolCallId,
894
- toolName: input.toolName,
895
- toolCallIndex: input.toolCallIndex,
896
- argumentsHash,
897
- };
898
- const unresolvedEffects = getUncommittedEffects(state);
899
- if (unresolvedEffects.length + 1 > config.maxUnresolvedEffects) {
900
- throw new ToolEffectJournalCapacityError(`Cannot execute tool ${input.toolCallId}: unresolved tool effects would exceed ${config.maxUnresolvedEffects}`);
901
- }
902
- const projectedEffect = {
903
- intent: record,
904
- resultRecordSeen: false,
905
- resultFinalizationPending: false,
906
- finalizationStarted: false,
907
- finalResultWritten: false,
908
- };
909
- const projectedBytes = getSnapshotEncodedBytes([...unresolvedEffects, projectedEffect]);
910
- if (projectedBytes > config.maxUnresolvedBytes) {
911
- throw new ToolEffectJournalCapacityError(`Cannot execute tool ${input.toolCallId}: unresolved tool effect state would exceed ${config.maxUnresolvedBytes} bytes`);
912
- }
913
- appendRecord(path, state, record);
914
- return record;
915
- }
916
- export function writeToolEffectResult(path, effectId, message, options) {
917
- assertHash(effectId, "Tool effect id");
918
- assertToolResultMessage(message, "Tool effect result");
919
- const state = parseJournal(path);
920
- const effect = state.effects.get(effectId);
921
- if (!effect) {
922
- throw new Error(`Cannot write result for unknown tool effect ${effectId}`);
923
- }
924
- if (effect.committedSessionEntryId) {
925
- throw new Error(`Cannot replace committed tool effect ${effectId}`);
926
- }
927
- if (message.toolCallId !== effect.intent.toolCallId || message.toolName !== effect.intent.toolName) {
928
- throw new Error(`Tool effect result identity does not match intent ${effectId}`);
929
- }
930
- if (effect.resultRecordSeen) {
931
- if (effect.executionResult &&
932
- canonicalPersistedJson(effect.executionResult) === canonicalPersistedJson(message) &&
933
- effect.resultFinalizationPending === (options?.finalizationPending === true)) {
934
- return;
935
- }
936
- throw new Error(`Divergent duplicate result for tool effect ${effectId}`);
937
- }
938
- appendRecord(path, state, {
939
- version: TOOL_EFFECT_JOURNAL_VERSION,
940
- kind: "result",
941
- sequence: state.nextSequence,
942
- timestamp: Date.now(),
943
- effectId,
944
- message,
945
- ...(options?.finalizationPending === true ? { finalizationPending: true } : {}),
946
- });
947
- }
948
- export function beginToolEffectFinalization(path, effectId, timestamp = Date.now()) {
949
- assertHash(effectId, "Tool effect id");
950
- assertNonNegativeSafeInteger(timestamp, "Tool effect finalization timestamp");
951
- const state = parseJournal(path);
952
- const effect = state.effects.get(effectId);
953
- if (!effect) {
954
- throw new Error(`Cannot finalize unknown tool effect ${effectId}`);
955
- }
956
- if (effect.committedSessionEntryId) {
957
- throw new Error(`Cannot finalize committed tool effect ${effectId}`);
958
- }
959
- if (!effect.resultRecordSeen || !effect.result) {
960
- throw new Error(`Cannot finalize tool effect ${effectId} before its durable result`);
961
- }
962
- if (effect.finalizationStarted) {
963
- if (effect.finalResultWritten) {
964
- throw new ToolEffectRecoveryRequiredError(effect.intent.toolCallId);
965
- }
966
- throw new ToolEffectUnknownOutcomeError([getUnknownToolEffectResolutionTarget(effect, path)], `Cannot safely continue: final tool result transformation is incomplete (${effect.intent.toolCallId})`);
967
- }
968
- appendRecord(path, state, {
969
- version: TOOL_EFFECT_JOURNAL_VERSION,
970
- kind: "finalization",
971
- sequence: state.nextSequence,
972
- timestamp,
973
- effectId,
974
- });
975
- }
976
- export function writeToolEffectFinalResult(path, effectId, message, timestamp = Date.now()) {
977
- assertHash(effectId, "Tool effect id");
978
- assertToolResultMessage(message, "Tool effect final result");
979
- assertNonNegativeSafeInteger(timestamp, "Tool effect final result timestamp");
980
- const state = parseJournal(path);
981
- const effect = state.effects.get(effectId);
982
- if (!effect) {
983
- throw new Error(`Cannot write final result for unknown tool effect ${effectId}`);
984
- }
985
- if (effect.committedSessionEntryId) {
986
- throw new Error(`Cannot replace committed tool effect ${effectId}`);
987
- }
988
- if (!effect.finalizationStarted) {
989
- throw new Error(`Cannot write final result for tool effect ${effectId} before finalization`);
990
- }
991
- if (message.toolCallId !== effect.intent.toolCallId || message.toolName !== effect.intent.toolName) {
992
- throw new Error(`Tool effect final result identity does not match intent ${effectId}`);
993
- }
994
- if (effect.finalResultWritten) {
995
- if (effect.result && canonicalPersistedJson(effect.result) === canonicalPersistedJson(message)) {
996
- return;
997
- }
998
- throw new Error(`Divergent duplicate final result for tool effect ${effectId}`);
999
- }
1000
- appendRecord(path, state, {
1001
- version: TOOL_EFFECT_JOURNAL_VERSION,
1002
- kind: "final",
1003
- sequence: state.nextSequence,
1004
- timestamp,
1005
- effectId,
1006
- message,
1007
- });
1008
- }
1009
- export function commitToolEffect(path, effectId, sessionEntryId, timestamp = Date.now()) {
1010
- assertHash(effectId, "Tool effect id");
1011
- assertNonEmptyString(sessionEntryId, "Tool effect sessionEntryId");
1012
- assertNonNegativeSafeInteger(timestamp, "Tool effect commit timestamp");
1013
- const state = parseJournal(path);
1014
- const effect = state.effects.get(effectId);
1015
- if (!effect) {
1016
- throw new Error(`Cannot commit unknown tool effect ${effectId}`);
1017
- }
1018
- if (effect.committedSessionEntryId) {
1019
- if (effect.committedSessionEntryId !== sessionEntryId) {
1020
- throw new Error(`Tool effect ${effectId} is already committed to ${effect.committedSessionEntryId}`);
1021
- }
1022
- return;
1023
- }
1024
- if (!effect.result) {
1025
- throw new Error(`Cannot commit tool effect ${effectId} before its durable result`);
1026
- }
1027
- if (effect.finalizationStarted && !effect.finalResultWritten) {
1028
- throw new Error(`Cannot commit tool effect ${effectId} before its final result`);
1029
- }
1030
- appendRecord(path, state, {
1031
- version: TOOL_EFFECT_JOURNAL_VERSION,
1032
- kind: "commit",
1033
- sequence: state.nextSequence,
1034
- timestamp,
1035
- effectId,
1036
- sessionEntryId,
1037
- });
1038
- }
1039
- function checkpointAfterSessionCommit(path) {
1040
- const config = getToolEffectJournalConfig();
1041
- checkpointIfNeeded(path, parseJournal(path), config);
1042
- }
1043
- export function readDurableToolEffects(path) {
1044
- return [...parseJournal(path).effects.values()].map((effect) => ({
1045
- intent: { ...effect.intent },
1046
- result: effect.result ? structuredClone(effect.result) : undefined,
1047
- committedSessionEntryId: effect.committedSessionEntryId,
1048
- }));
1049
- }
1050
- function getJournalPathForSession(sessionManager) {
1051
- const sessionFile = sessionManager.getSessionFile();
1052
- return sessionFile ? getToolEffectJournalPath(sessionFile) : undefined;
1053
- }
1054
- function findUncommittedEffectForToolCall(state, sessionId, toolCallId, toolName) {
1055
- let matchingEffect;
1056
- for (const effect of getUncommittedEffects(state)) {
1057
- if (effect.intent.sessionId !== sessionId ||
1058
- effect.intent.toolCallId !== toolCallId ||
1059
- effect.intent.toolName !== toolName) {
1060
- continue;
1061
- }
1062
- if (matchingEffect) {
1063
- throw new Error(`Multiple pending tool effects match tool call ${toolCallId}`);
1064
- }
1065
- matchingEffect = effect;
1066
- }
1067
- return matchingEffect;
1068
- }
1069
- export function beginSessionToolEffect(sessionManager, event) {
1070
- if (!sessionManager.isPersisted())
1071
- return undefined;
1072
- const path = getJournalPathForSession(sessionManager);
1073
- if (!path) {
1074
- throw new Error("Persistent session has no file for durable tool effect intent");
1075
- }
1076
- const branch = sessionManager.getBranch();
1077
- for (let entryIndex = branch.length - 1; entryIndex >= 0; entryIndex--) {
1078
- const entry = branch[entryIndex];
1079
- if (entry.type !== "message" || entry.message.role !== "assistant")
1080
- continue;
1081
- const toolCalls = entry.message.content.filter((content) => content.type === "toolCall");
1082
- const toolCallIndex = toolCalls.findIndex((toolCall) => toolCall.id === event.toolCallId && toolCall.name === event.toolName);
1083
- if (toolCallIndex === -1)
1084
- continue;
1085
- const toolCall = toolCalls[toolCallIndex];
1086
- if (canonicalJson(toolCall.arguments) !== canonicalJson(event.args)) {
1087
- throw new Error(`Tool execution start arguments do not match assistant tool call ${event.toolCallId}`);
1088
- }
1089
- sessionManager.flushSessionFile();
1090
- return beginToolEffect(path, {
1091
- sessionId: sessionManager.getSessionId(),
1092
- assistantEntryId: entry.id,
1093
- toolCallId: event.toolCallId,
1094
- toolName: event.toolName,
1095
- toolCallIndex,
1096
- arguments: event.args,
1097
- });
1098
- }
1099
- throw new Error(`Cannot find assistant entry for tool execution ${event.toolCallId}`);
1100
- }
1101
- export function writeSessionToolEffectResult(sessionManager, message, options) {
1102
- if (!sessionManager.isPersisted())
1103
- return;
1104
- const path = getJournalPathForSession(sessionManager);
1105
- if (!path || !existsSync(path)) {
1106
- throw new Error(`Missing durable tool effect intent for result ${message.toolCallId}`);
1107
- }
1108
- const effect = findUncommittedEffectForToolCall(parseJournal(path), sessionManager.getSessionId(), message.toolCallId, message.toolName);
1109
- if (!effect) {
1110
- throw new Error(`Missing pending tool effect intent for result ${message.toolCallId}`);
1111
- }
1112
- writeToolEffectResult(path, effect.intent.effectId, message, options);
1113
- }
1114
- export function beginSessionToolEffectFinalization(sessionManager, message) {
1115
- if (!sessionManager.isPersisted())
1116
- return;
1117
- const path = getJournalPathForSession(sessionManager);
1118
- if (!path || !existsSync(path)) {
1119
- throw new Error(`Missing durable tool effect result for finalization ${message.toolCallId}`);
1120
- }
1121
- const effect = findUncommittedEffectForToolCall(parseJournal(path), sessionManager.getSessionId(), message.toolCallId, message.toolName);
1122
- if (!effect) {
1123
- throw new Error(`Missing pending tool effect result for finalization ${message.toolCallId}`);
1124
- }
1125
- beginToolEffectFinalization(path, effect.intent.effectId);
1126
- }
1127
- export function writeSessionToolEffectFinalResult(sessionManager, message) {
1128
- if (!sessionManager.isPersisted())
1129
- return;
1130
- const path = getJournalPathForSession(sessionManager);
1131
- if (!path || !existsSync(path)) {
1132
- throw new Error(`Missing durable tool effect finalization for result ${message.toolCallId}`);
1133
- }
1134
- const effect = findUncommittedEffectForToolCall(parseJournal(path), sessionManager.getSessionId(), message.toolCallId, message.toolName);
1135
- if (!effect) {
1136
- throw new Error(`Missing pending tool effect finalization for result ${message.toolCallId}`);
1137
- }
1138
- writeToolEffectFinalResult(path, effect.intent.effectId, message);
1139
- }
1140
- export function commitSessionToolEffect(sessionManager, message, sessionEntryId) {
1141
- if (!sessionManager.isPersisted())
1142
- return;
1143
- const path = getJournalPathForSession(sessionManager);
1144
- if (!path || !existsSync(path)) {
1145
- throw new Error(`Missing durable tool effect result for session entry ${sessionEntryId}`);
1146
- }
1147
- const state = parseJournal(path);
1148
- const effect = findUncommittedEffectForToolCall(state, sessionManager.getSessionId(), message.toolCallId, message.toolName);
1149
- if (effect) {
1150
- if (!effect.result || canonicalPersistedJson(effect.result) !== canonicalPersistedJson(message)) {
1151
- throw new Error(`Session tool result does not match durable effect ${effect.intent.effectId}`);
1152
- }
1153
- commitToolEffect(path, effect.intent.effectId, sessionEntryId);
1154
- checkpointAfterSessionCommit(path);
1155
- return;
1156
- }
1157
- const committed = [...state.effects.values()].find((candidate) => candidate.intent.sessionId === sessionManager.getSessionId() &&
1158
- candidate.intent.toolCallId === message.toolCallId &&
1159
- candidate.intent.toolName === message.toolName &&
1160
- candidate.committedSessionEntryId === sessionEntryId);
1161
- if (!committed) {
1162
- throw new Error(`Missing pending tool effect result for session entry ${sessionEntryId}`);
1163
- }
1164
- if (!committed.result || canonicalPersistedJson(committed.result) !== canonicalPersistedJson(message)) {
1165
- throw new Error(`Session tool result does not match committed durable effect ${committed.intent.effectId}`);
1166
- }
1167
- checkpointIfNeeded(path, state, getToolEffectJournalConfig());
1168
- }
1169
- function getAssistantToolCalls(entry) {
1170
- if (entry.message.role !== "assistant") {
1171
- throw new Error(`Tool effect assistant entry ${entry.id} is not an assistant message`);
1172
- }
1173
- return entry.message.content;
1174
- }
1175
- function validateEffectAgainstAssistant(effect, entry) {
1176
- const toolCalls = getAssistantToolCalls(entry).filter((content) => content.type === "toolCall");
1177
- const toolCall = toolCalls[effect.intent.toolCallIndex];
1178
- if (!toolCall ||
1179
- toolCall.id !== effect.intent.toolCallId ||
1180
- toolCall.name !== effect.intent.toolName ||
1181
- sha256(canonicalJson(toolCall.arguments)) !== effect.intent.argumentsHash) {
1182
- throw new Error(`Tool effect ${effect.intent.effectId} does not match assistant entry ${effect.intent.assistantEntryId}`);
1183
- }
1184
- }
1185
- function findPersistedToolResult(branch, assistantIndex, effect) {
1186
- for (let index = assistantIndex + 1; index < branch.length; index++) {
1187
- const entry = branch[index];
1188
- if (entry.type === "message" &&
1189
- entry.message.role === "toolResult" &&
1190
- entry.message.toolCallId === effect.intent.toolCallId) {
1191
- if (entry.message.toolName !== effect.intent.toolName) {
1192
- throw new Error(`Persisted tool result name does not match effect ${effect.intent.effectId}`);
1193
- }
1194
- return entry;
1195
- }
1196
- }
1197
- return undefined;
1198
- }
1199
- function assertSafeRecoveryTail(branch, effects) {
1200
- if (effects.length === 0)
1201
- return;
1202
- const firstAssistantIndex = Math.min(...effects.map((candidate) => candidate.assistantIndex));
1203
- const firstAssistantEntry = branch[firstAssistantIndex];
1204
- if (firstAssistantEntry.type !== "message" || firstAssistantEntry.message.role !== "assistant") {
1205
- throw new Error(`Tool effect recovery entry ${firstAssistantEntry.id} is not an assistant message`);
1206
- }
1207
- const allowedToolCallIds = new Set(firstAssistantEntry.message.content
1208
- .filter((content) => content.type === "toolCall")
1209
- .map((toolCall) => toolCall.id));
1210
- for (let index = firstAssistantIndex + 1; index < branch.length; index++) {
1211
- const entry = branch[index];
1212
- if (entry.type === "message") {
1213
- if (entry.message.role === "toolResult" && allowedToolCallIds.has(entry.message.toolCallId)) {
1214
- continue;
1215
- }
1216
- if (entry.message.role === "bashExecution") {
1217
- continue;
1218
- }
1219
- throw new Error(`Cannot recover tool effects after later model-visible history at session entry ${entry.id}`);
1220
- }
1221
- if (entry.type === "custom_message" || entry.type === "compaction" || entry.type === "branch_summary") {
1222
- throw new Error(`Cannot recover tool effects after later model-visible history at session entry ${entry.id}`);
1223
- }
1224
- }
1225
- }
1226
- function loadToolEffectResolutionTarget(sessionManager, effectId) {
1227
- assertHash(effectId, "Tool effect id");
1228
- if (!sessionManager.isPersisted()) {
1229
- throw new Error("Tool effect resolution requires a persisted session");
1230
- }
1231
- const sessionFile = sessionManager.getSessionFile();
1232
- if (!sessionFile) {
1233
- throw new Error("Persistent session has no session file");
1234
- }
1235
- const path = getToolEffectJournalPath(sessionFile);
1236
- if (!existsSync(path)) {
1237
- throw new Error(`Tool effect journal does not exist: ${path}`);
1238
- }
1239
- const state = parseJournal(path);
1240
- const effect = state.effects.get(effectId);
1241
- if (!effect) {
1242
- throw new Error(`Tool effect ${effectId} does not exist in ${path}`);
1243
- }
1244
- if (effect.intent.sessionId !== sessionManager.getSessionId()) {
1245
- throw new Error(`Tool effect ${effectId} does not belong to session ${sessionManager.getSessionId()}`);
1246
- }
1247
- const assistantEntry = sessionManager.getEntry(effect.intent.assistantEntryId);
1248
- if (!assistantEntry || assistantEntry.type !== "message") {
1249
- throw new Error(`Tool effect ${effectId} references a missing assistant entry`);
1250
- }
1251
- validateEffectAgainstAssistant(effect, assistantEntry);
1252
- return {
1253
- path,
1254
- state,
1255
- effect,
1256
- target: getToolEffectResolutionTarget(effect, path),
1257
- };
1258
- }
1259
- export function inspectToolEffect(sessionManager, effectId) {
1260
- return loadToolEffectResolutionTarget(sessionManager, effectId).target;
1261
- }
1262
- function resolveToolEffectWithResult(sessionManager, effectId, message) {
1263
- assertToolResultMessage(message, "Accepted tool result");
1264
- const serializedMessage = JSON.stringify(message);
1265
- if (serializedMessage === undefined) {
1266
- throw new Error("Accepted tool result is not JSON serializable");
1267
- }
1268
- const durableMessage = JSON.parse(serializedMessage);
1269
- assertToolResultMessage(durableMessage, "Accepted tool result");
1270
- const { path, state, effect, target } = loadToolEffectResolutionTarget(sessionManager, effectId);
1271
- if (durableMessage.toolCallId !== effect.intent.toolCallId || durableMessage.toolName !== effect.intent.toolName) {
1272
- throw new Error(`Accepted tool result identity does not match tool effect ${effectId}`);
1273
- }
1274
- if (effect.committedSessionEntryId) {
1275
- const committedEntry = sessionManager.getEntry(effect.committedSessionEntryId);
1276
- if (!committedEntry ||
1277
- committedEntry.type !== "message" ||
1278
- committedEntry.message.role !== "toolResult" ||
1279
- canonicalPersistedJson(committedEntry.message) !== canonicalPersistedJson(durableMessage) ||
1280
- !effect.result ||
1281
- canonicalPersistedJson(effect.result) !== canonicalPersistedJson(durableMessage)) {
1282
- throw new Error(`Committed tool effect ${effectId} does not match the accepted result`);
1283
- }
1284
- checkpointIfNeeded(path, state, getToolEffectJournalConfig());
1285
- return {
1286
- target,
1287
- sessionEntryId: effect.committedSessionEntryId,
1288
- alreadyResolved: true,
1289
- };
1290
- }
1291
- const branch = sessionManager.getBranch();
1292
- const assistantIndex = branch.findIndex((entry) => entry.id === effect.intent.assistantEntryId);
1293
- if (assistantIndex === -1) {
1294
- throw new Error(`Tool effect ${effectId} is not on the active session branch`);
1295
- }
1296
- const persisted = findPersistedToolResult(branch, assistantIndex, effect);
1297
- if (persisted && canonicalPersistedJson(persisted.message) !== canonicalPersistedJson(durableMessage)) {
1298
- throw new Error(`Persisted tool result ${persisted.id} does not match the accepted result`);
1299
- }
1300
- if (target.phase === "result_durable") {
1301
- if (!effect.result || canonicalPersistedJson(effect.result) !== canonicalPersistedJson(durableMessage)) {
1302
- throw new Error(`Durable tool effect ${effectId} does not match the accepted result`);
1303
- }
1304
- }
1305
- else if (target.phase !== "execution_unknown" && target.phase !== "finalization_unknown") {
1306
- throw new Error(`Tool effect ${effectId} cannot be resolved from phase ${target.phase}`);
1307
- }
1308
- let sessionEntryId = persisted?.id;
1309
- if (!sessionEntryId) {
1310
- for (const candidate of getUncommittedEffects(state)) {
1311
- if (candidate.intent.assistantEntryId !== effect.intent.assistantEntryId ||
1312
- candidate.intent.toolCallIndex >= effect.intent.toolCallIndex) {
1313
- continue;
1314
- }
1315
- if (!findPersistedToolResult(branch, assistantIndex, candidate)) {
1316
- throw new Error(`Resolve earlier tool effect ${candidate.intent.effectId} before ${effect.intent.effectId}`);
1317
- }
1318
- }
1319
- assertSafeRecoveryTail(branch, [{ effect, assistantIndex }]);
1320
- sessionEntryId = sessionManager.appendMessage(durableMessage);
1321
- sessionManager.flushSessionFile();
1322
- }
1323
- else {
1324
- sessionManager.flushSessionFile();
1325
- }
1326
- if (!effect.resultRecordSeen) {
1327
- writeToolEffectResult(path, effectId, durableMessage, { finalizationPending: true });
1328
- }
1329
- const refreshed = parseJournal(path).effects.get(effectId);
1330
- if (!refreshed) {
1331
- throw new Error(`Tool effect ${effectId} disappeared during resolution`);
1332
- }
1333
- if (refreshed.finalizationStarted && !refreshed.finalResultWritten) {
1334
- writeToolEffectFinalResult(path, effectId, durableMessage);
1335
- }
1336
- commitToolEffect(path, effectId, sessionEntryId);
1337
- checkpointAfterSessionCommit(path);
1338
- return { target, sessionEntryId, alreadyResolved: persisted !== undefined };
1339
- }
1340
- export function markToolEffectFailed(sessionManager, effectId) {
1341
- const { effect } = loadToolEffectResolutionTarget(sessionManager, effectId);
1342
- return resolveToolEffectWithResult(sessionManager, effectId, {
1343
- role: "toolResult",
1344
- toolCallId: effect.intent.toolCallId,
1345
- toolName: effect.intent.toolName,
1346
- content: [
1347
- {
1348
- type: "text",
1349
- text: "Tool execution outcome was explicitly marked failed during recovery. The interrupted tool was not re-executed; external side effects may already have occurred.",
1350
- },
1351
- ],
1352
- isError: true,
1353
- timestamp: effect.intent.timestamp,
1354
- });
1355
- }
1356
- export function acceptToolEffectResult(sessionManager, effectId, message) {
1357
- assertToolResultMessage(message, "Accepted tool result");
1358
- return resolveToolEffectWithResult(sessionManager, effectId, message);
1359
- }
1360
- export function recoverToolEffects(sessionManager, journalPath) {
1361
- const sessionFile = sessionManager.getSessionFile();
1362
- const path = journalPath ?? (sessionFile ? getToolEffectJournalPath(sessionFile) : undefined);
1363
- if (!path || !existsSync(path)) {
1364
- return { recoveredToolCallIds: [], acknowledgedToolCallIds: [] };
1365
- }
1366
- const state = parseJournal(path);
1367
- const branch = sessionManager.getBranch();
1368
- const branchIndexes = new Map(branch.map((entry, index) => [entry.id, index]));
1369
- const activeEffects = [];
1370
- for (const effect of state.effects.values()) {
1371
- if (!effect.committedSessionEntryId)
1372
- continue;
1373
- const result = effect.result;
1374
- if (!result) {
1375
- throw new Error(`Committed tool effect ${effect.intent.effectId} has no durable result`);
1376
- }
1377
- const sessionEntry = sessionManager.getEntry(effect.committedSessionEntryId);
1378
- if (!sessionEntry ||
1379
- sessionEntry.type !== "message" ||
1380
- sessionEntry.message.role !== "toolResult" ||
1381
- canonicalPersistedJson(sessionEntry.message) !== canonicalPersistedJson(result)) {
1382
- throw new Error(`Committed tool effect ${effect.intent.effectId} does not match session entry ${effect.committedSessionEntryId}`);
1383
- }
1384
- }
1385
- for (const effect of getUncommittedEffects(state)) {
1386
- if (effect.intent.sessionId !== sessionManager.getSessionId()) {
1387
- throw new Error(`Tool effect journal session id does not match ${sessionManager.getSessionId()}`);
1388
- }
1389
- const assistantIndex = branchIndexes.get(effect.intent.assistantEntryId);
1390
- if (assistantIndex === undefined) {
1391
- continue;
1392
- }
1393
- const assistantEntry = branch[assistantIndex];
1394
- if (assistantEntry.type !== "message") {
1395
- throw new Error(`Tool effect assistant entry ${assistantEntry.id} is not a message`);
1396
- }
1397
- validateEffectAgainstAssistant(effect, assistantEntry);
1398
- const persisted = findPersistedToolResult(branch, assistantIndex, effect);
1399
- if (persisted) {
1400
- const result = effect.result;
1401
- if (result &&
1402
- (!effect.finalizationStarted || effect.finalResultWritten) &&
1403
- canonicalPersistedJson(persisted.message) !== canonicalPersistedJson(result)) {
1404
- throw new Error(`Persisted tool result ${persisted.id} does not match durable effect ${effect.intent.effectId}`);
1405
- }
1406
- }
1407
- activeEffects.push({ effect, assistantIndex, persisted });
1408
- }
1409
- const unknownEffects = activeEffects
1410
- .filter((candidate) => candidate.effect.result === undefined ||
1411
- (candidate.effect.finalizationStarted && !candidate.effect.finalResultWritten))
1412
- .map((candidate) => getUnknownToolEffectResolutionTarget(candidate.effect, path));
1413
- if (unknownEffects.length > 0) {
1414
- throw new ToolEffectUnknownOutcomeError(unknownEffects, "Cannot safely continue: tool effects may have occurred but their final outcomes are unknown.");
1415
- }
1416
- const acknowledgedToolCallIds = [];
1417
- for (const candidate of activeEffects) {
1418
- if (!candidate.persisted)
1419
- continue;
1420
- sessionManager.flushSessionFile();
1421
- commitToolEffect(path, candidate.effect.intent.effectId, candidate.persisted.id);
1422
- acknowledgedToolCallIds.push(candidate.effect.intent.toolCallId);
1423
- }
1424
- const recoverable = activeEffects.filter((candidate) => candidate.persisted === undefined);
1425
- assertSafeRecoveryTail(branch, recoverable);
1426
- recoverable.sort((left, right) => left.assistantIndex - right.assistantIndex ||
1427
- left.effect.intent.toolCallIndex - right.effect.intent.toolCallIndex);
1428
- const recoveredToolCallIds = [];
1429
- for (const candidate of recoverable) {
1430
- const result = candidate.effect.result;
1431
- if (!result) {
1432
- throw new Error(`Tool effect ${candidate.effect.intent.effectId} lost its durable result during recovery`);
1433
- }
1434
- const sessionEntryId = sessionManager.appendMessage(structuredClone(result));
1435
- sessionManager.flushSessionFile();
1436
- commitToolEffect(path, candidate.effect.intent.effectId, sessionEntryId);
1437
- recoveredToolCallIds.push(candidate.effect.intent.toolCallId);
1438
- }
1439
- checkpointAfterSessionCommit(path);
1440
- return { recoveredToolCallIds, acknowledgedToolCallIds };
1441
- }
1442
- //# sourceMappingURL=tool-effect-journal.js.map