chainlesschain 0.166.136 → 0.166.139
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/README.md +43 -1
- package/package.json +1 -1
- package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
- package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
- package/src/command-help-index.json +1 -1
- package/src/commands/agent.js +12 -0
- package/src/data/changelog.json +39 -0
- package/src/gateways/ws/ws-server.js +16 -0
- package/src/harness/prompt-compressor.js +57 -0
- package/src/harness/provider-backed-compaction.js +6 -0
- package/src/lib/coding-change-review-context.js +51 -0
- package/src/lib/coding-task-guidance.js +3 -1
- package/src/lib/context-memory-kernel/jsonl-session-context-port.js +19 -10
- package/src/lib/context-memory-kernel/live-compaction.js +114 -6
- package/src/lib/context-memory-kernel/message-adapter.js +17 -13
- package/src/lib/context-memory-kernel/task-binding.js +41 -0
- package/src/lib/context-memory-kernel/task-worklog-port.js +26 -3
- package/src/lib/evolution/evidence-json-text.js +324 -14
- package/src/lib/evolution/evolution-evidence-projector.js +67 -1
- package/src/lib/read-file-loop-guard.js +146 -13
- package/src/lib/read-file-page.js +4 -1
- package/src/lib/source-read-recovery.js +89 -0
- package/src/lib/tool-result-preview.js +18 -5
- package/src/repl/agent-repl.js +2 -0
- package/src/runtime/agent-core.js +103 -40
- package/src/runtime/headless-runner.js +1 -0
- package/src/runtime/headless-stream.js +82 -10
- package/src/runtime/message-roles.js +28 -0
- package/src/runtime/policies/agent-policy.js +1 -0
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import {
|
|
3
|
+
resolveTaskBinding,
|
|
4
|
+
taskBindingMatchCount,
|
|
5
|
+
advanceTaskBinding,
|
|
6
|
+
} from "./task-binding.js";
|
|
1
7
|
import { randomUUID } from "node:crypto";
|
|
2
8
|
import {
|
|
3
9
|
canonicalDigest,
|
|
@@ -84,6 +90,7 @@ class CallbackSessionContextPort extends InMemorySessionContextPort {
|
|
|
84
90
|
head,
|
|
85
91
|
physicalAuthority,
|
|
86
92
|
allowExternalization = false,
|
|
93
|
+
requiredUserMessages = [],
|
|
87
94
|
}) {
|
|
88
95
|
const initialHead =
|
|
89
96
|
head ||
|
|
@@ -99,6 +106,7 @@ class CallbackSessionContextPort extends InMemorySessionContextPort {
|
|
|
99
106
|
items: messagesToContextItems(messages, {
|
|
100
107
|
sessionId,
|
|
101
108
|
allowExternalization,
|
|
109
|
+
requiredUserMessages,
|
|
102
110
|
}),
|
|
103
111
|
},
|
|
104
112
|
]);
|
|
@@ -262,10 +270,22 @@ export async function compactLiveMessagesCanonical(
|
|
|
262
270
|
!isCompactionHostBase(message) &&
|
|
263
271
|
!isRuntimeOnlySessionMessage(message),
|
|
264
272
|
);
|
|
273
|
+
const requiredUserBindings = (options.requiredUserBindings || []).map(
|
|
274
|
+
(binding) => ({
|
|
275
|
+
...binding,
|
|
276
|
+
message: structuredClone(binding.message),
|
|
277
|
+
matchCount: taskBindingMatchCount(binding),
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
const summarizerOptions = { ...(options.compressOptions || {}) };
|
|
281
|
+
delete summarizerOptions.requiredUserBindings;
|
|
282
|
+
for (const binding of requiredUserBindings)
|
|
283
|
+
resolveTaskBinding(liveMessages, binding);
|
|
265
284
|
const ephemeralMessages = [];
|
|
285
|
+
const durableSourceIndexes = new Map();
|
|
266
286
|
{
|
|
267
287
|
let sequence = 0;
|
|
268
|
-
for (const message of liveMessages) {
|
|
288
|
+
for (const [liveIndex, message] of liveMessages.entries()) {
|
|
269
289
|
if (
|
|
270
290
|
isRuntimeOnlySessionMessage(message) ||
|
|
271
291
|
isCompactionHostBase(message) ||
|
|
@@ -274,9 +294,29 @@ export async function compactLiveMessagesCanonical(
|
|
|
274
294
|
!getDurableSystemMessageProvenance(message))
|
|
275
295
|
)
|
|
276
296
|
ephemeralMessages.push({ sequence, message });
|
|
277
|
-
else
|
|
297
|
+
else {
|
|
298
|
+
durableSourceIndexes.set(liveIndex, sequence);
|
|
299
|
+
sequence += 1;
|
|
300
|
+
}
|
|
278
301
|
}
|
|
279
302
|
}
|
|
303
|
+
// Carry host-resolved source indexes into the verified authority projection.
|
|
304
|
+
// Identical historical user text is not a substitute for this source identity.
|
|
305
|
+
const requiredUserMessages = requiredUserBindings.map((binding) => ({
|
|
306
|
+
index: durableSourceIndexes.get(
|
|
307
|
+
resolveTaskBinding(liveMessages, binding).index,
|
|
308
|
+
),
|
|
309
|
+
message: binding.message,
|
|
310
|
+
}));
|
|
311
|
+
const initialItems = messagesToContextItems(messages, {
|
|
312
|
+
sessionId,
|
|
313
|
+
allowExternalization: persistentSession,
|
|
314
|
+
requiredUserMessages,
|
|
315
|
+
});
|
|
316
|
+
const boundItems = requiredUserMessages.map(({ index }, bindingIndex) => ({
|
|
317
|
+
item: initialItems.find((item) => item.sourceRef.eventSequence === index),
|
|
318
|
+
binding: requiredUserBindings[bindingIndex],
|
|
319
|
+
}));
|
|
280
320
|
if (typeof options.commit === "function") {
|
|
281
321
|
const bootstrapRuntime = createCliContextMemoryRuntime({
|
|
282
322
|
sessionId,
|
|
@@ -288,6 +328,7 @@ export async function compactLiveMessagesCanonical(
|
|
|
288
328
|
if (persistentSession) {
|
|
289
329
|
verifiedAuthoritySnapshot = await new JsonlSessionContextPort({
|
|
290
330
|
sessionId,
|
|
331
|
+
requiredUserMessages,
|
|
291
332
|
}).readSnapshot(sessionId);
|
|
292
333
|
}
|
|
293
334
|
const sessionPort = new CallbackSessionContextPort({
|
|
@@ -298,6 +339,7 @@ export async function compactLiveMessagesCanonical(
|
|
|
298
339
|
trigger,
|
|
299
340
|
summaryStats: () => summaryStats,
|
|
300
341
|
allowExternalization: persistentSession,
|
|
342
|
+
requiredUserMessages,
|
|
301
343
|
...(verifiedAuthoritySnapshot?.head
|
|
302
344
|
? { head: verifiedAuthoritySnapshot.head, physicalAuthority: true }
|
|
303
345
|
: {}),
|
|
@@ -312,6 +354,10 @@ export async function compactLiveMessagesCanonical(
|
|
|
312
354
|
});
|
|
313
355
|
} else if (persistentSession) {
|
|
314
356
|
runtime = createCliContextMemoryRuntime({
|
|
357
|
+
sessionPort: new JsonlSessionContextPort({
|
|
358
|
+
sessionId,
|
|
359
|
+
requiredUserMessages,
|
|
360
|
+
}),
|
|
315
361
|
sessionId,
|
|
316
362
|
scopeKey: `cli:session:${sessionId}`,
|
|
317
363
|
env: options.env,
|
|
@@ -336,6 +382,7 @@ export async function compactLiveMessagesCanonical(
|
|
|
336
382
|
items: messagesToContextItems(messages, {
|
|
337
383
|
sessionId,
|
|
338
384
|
allowExternalization: false,
|
|
385
|
+
requiredUserMessages,
|
|
339
386
|
}),
|
|
340
387
|
},
|
|
341
388
|
]);
|
|
@@ -362,6 +409,7 @@ export async function compactLiveMessagesCanonical(
|
|
|
362
409
|
const inputItems = messagesToContextItems(messages, {
|
|
363
410
|
sessionId,
|
|
364
411
|
allowExternalization: true,
|
|
412
|
+
requiredUserMessages,
|
|
365
413
|
});
|
|
366
414
|
const inputDigest = canonicalDigest(
|
|
367
415
|
inputItems.map((item) => item.digest),
|
|
@@ -384,11 +432,59 @@ export async function compactLiveMessagesCanonical(
|
|
|
384
432
|
const model = options.model || "default";
|
|
385
433
|
const memoryRevision = await runtime.memoryPort.getRevision();
|
|
386
434
|
let settledOutputItems = null;
|
|
387
|
-
|
|
435
|
+
{
|
|
388
436
|
const appendCompaction = runtime.sessionPort.appendCompaction.bind(
|
|
389
437
|
runtime.sessionPort,
|
|
390
438
|
);
|
|
391
439
|
runtime.sessionPort.appendCompaction = async (event, expectedHead) => {
|
|
440
|
+
const outputMessages = contextItemsToMessages(event.outputItems, {
|
|
441
|
+
sessionId,
|
|
442
|
+
});
|
|
443
|
+
for (const { item, binding } of boundItems) {
|
|
444
|
+
resolveTaskBinding(messagesInput, binding);
|
|
445
|
+
const retained = event.outputItems.filter(
|
|
446
|
+
(output) => output.itemId === item.itemId,
|
|
447
|
+
);
|
|
448
|
+
const originalOutputs = contextItemsToMessages(
|
|
449
|
+
event.outputItems.filter((output) =>
|
|
450
|
+
initialItems.some(
|
|
451
|
+
(input) =>
|
|
452
|
+
input.itemId === output.itemId &&
|
|
453
|
+
input.digest === output.digest,
|
|
454
|
+
),
|
|
455
|
+
),
|
|
456
|
+
{ sessionId },
|
|
457
|
+
);
|
|
458
|
+
// A summary can faithfully retain an older identical user turn from
|
|
459
|
+
// its verified dropped parents. Such history is not a new task copy.
|
|
460
|
+
const derivedParents = new Set(
|
|
461
|
+
event.outputItems
|
|
462
|
+
.filter(
|
|
463
|
+
(output) =>
|
|
464
|
+
!initialItems.some((input) => input.itemId === output.itemId),
|
|
465
|
+
)
|
|
466
|
+
.flatMap((output) => output.provenance.parentDigests || []),
|
|
467
|
+
);
|
|
468
|
+
const historicalOutputs = contextItemsToMessages(
|
|
469
|
+
initialItems.filter((input) => derivedParents.has(input.digest)),
|
|
470
|
+
{ sessionId },
|
|
471
|
+
);
|
|
472
|
+
const matches = (list) =>
|
|
473
|
+
list.filter((message) => isDeepStrictEqual(message, binding.message))
|
|
474
|
+
.length;
|
|
475
|
+
if (
|
|
476
|
+
retained.length !== 1 ||
|
|
477
|
+
retained[0].digest !== item.digest ||
|
|
478
|
+
matches(outputMessages) >
|
|
479
|
+
matches(originalOutputs) + matches(historicalOutputs)
|
|
480
|
+
) {
|
|
481
|
+
const error = new Error(
|
|
482
|
+
"The current user task was lost, changed or duplicated in compaction output",
|
|
483
|
+
);
|
|
484
|
+
error.code = "CC_AGENT_TASK_CONTEXT_LOST";
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
392
488
|
const result = await appendCompaction(event, expectedHead);
|
|
393
489
|
if (result?.ok === true) settledOutputItems = event.outputItems;
|
|
394
490
|
return result;
|
|
@@ -427,7 +523,7 @@ export async function compactLiveMessagesCanonical(
|
|
|
427
523
|
});
|
|
428
524
|
const result = await compressor.compress(droppedMessages, {
|
|
429
525
|
preserveToolPairs: true,
|
|
430
|
-
...
|
|
526
|
+
...summarizerOptions,
|
|
431
527
|
});
|
|
432
528
|
summaryStats = result.stats || {};
|
|
433
529
|
summaryReceipt = usageReceipt(summaryStats, provider, model);
|
|
@@ -513,7 +609,9 @@ export async function compactLiveMessagesCanonical(
|
|
|
513
609
|
receipt,
|
|
514
610
|
};
|
|
515
611
|
}
|
|
516
|
-
const snapshot =
|
|
612
|
+
const snapshot = settledOutputItems
|
|
613
|
+
? null
|
|
614
|
+
: await runtime.sessionPort.readSnapshot(sessionId);
|
|
517
615
|
// Reattach host policy and private retry nudges at their durable sequence
|
|
518
616
|
// boundaries. They remain live, never enter the receipt, and do not move a
|
|
519
617
|
// retry instruction behind tool exchanges that happened after that retry.
|
|
@@ -531,11 +629,21 @@ export async function compactLiveMessagesCanonical(
|
|
|
531
629
|
ephemeralMessages[ephemeralIndex].sequence <= sequence
|
|
532
630
|
)
|
|
533
631
|
restoredMessages.push(ephemeralMessages[ephemeralIndex++].message);
|
|
534
|
-
|
|
632
|
+
const bound = boundItems.find((entry) => entry.item.itemId === item.itemId);
|
|
633
|
+
restoredMessages.push(
|
|
634
|
+
...(bound
|
|
635
|
+
? [bound.binding.original]
|
|
636
|
+
: contextItemsToMessages([item], { sessionId })),
|
|
637
|
+
);
|
|
535
638
|
}
|
|
536
639
|
restoredMessages.push(
|
|
537
640
|
...ephemeralMessages.slice(ephemeralIndex).map(({ message }) => message),
|
|
538
641
|
);
|
|
642
|
+
// Advance only the host binding's known multiplicity after a verified commit;
|
|
643
|
+
// older identical turns may have been legitimately summarized away.
|
|
644
|
+
requiredUserBindings.forEach((_, index) => {
|
|
645
|
+
advanceTaskBinding(options.requiredUserBindings[index], restoredMessages);
|
|
646
|
+
});
|
|
539
647
|
return {
|
|
540
648
|
messages: restoredMessages,
|
|
541
649
|
stats: {
|
|
@@ -206,17 +206,22 @@ export function messagesToContextItems(messagesInput, options = {}) {
|
|
|
206
206
|
? [...options.allowedSinks]
|
|
207
207
|
: DEFAULT_ALLOWED_SINKS;
|
|
208
208
|
const items = [];
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
requiredUser
|
|
215
|
-
requiredUser.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
209
|
+
const requiredUsers =
|
|
210
|
+
options.requiredUserMessages ||
|
|
211
|
+
(options.requiredUserMessage ? [options.requiredUserMessage] : []);
|
|
212
|
+
for (const requiredUser of requiredUsers) {
|
|
213
|
+
if (
|
|
214
|
+
requiredUser != null &&
|
|
215
|
+
(!Number.isSafeInteger(requiredUser.index) ||
|
|
216
|
+
requiredUser.index < 0 ||
|
|
217
|
+
requiredUser.index >= messages.length ||
|
|
218
|
+
requiredUser.message?.role !== "user" ||
|
|
219
|
+
!isDeepStrictEqual(messages[requiredUser.index], requiredUser.message))
|
|
220
|
+
) {
|
|
221
|
+
throw new TypeError("Invalid current-user task binding");
|
|
222
|
+
}
|
|
219
223
|
}
|
|
224
|
+
const requiredIndexes = new Set(requiredUsers.map(({ index }) => index));
|
|
220
225
|
const trustedSystemIndexes = new Set(
|
|
221
226
|
Array.isArray(options.trustedSystemIndexes)
|
|
222
227
|
? options.trustedSystemIndexes.filter(Number.isSafeInteger)
|
|
@@ -305,14 +310,13 @@ export function messagesToContextItems(messagesInput, options = {}) {
|
|
|
305
310
|
const item = items[index];
|
|
306
311
|
if (
|
|
307
312
|
index !== lastIndex &&
|
|
308
|
-
item.sourceRef.eventSequence
|
|
313
|
+
!requiredIndexes.has(item.sourceRef.eventSequence)
|
|
309
314
|
)
|
|
310
315
|
continue;
|
|
311
316
|
const payload = JSON.parse(loadBundleContent(item, sessionId));
|
|
312
317
|
if (
|
|
313
318
|
payload.messages.at(-1)?.role === "user" &&
|
|
314
|
-
(index === lastIndex ||
|
|
315
|
-
item.sourceRef.eventSequence === requiredUser?.index)
|
|
319
|
+
(index === lastIndex || requiredIndexes.has(item.sourceRef.eventSequence))
|
|
316
320
|
) {
|
|
317
321
|
const protectedItem = { ...item };
|
|
318
322
|
delete protectedItem.digest;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
|
|
3
|
+
const committedMatchCounts = new WeakMap();
|
|
4
|
+
export function taskBindingMatchCount(binding) {
|
|
5
|
+
return committedMatchCounts.get(binding) ?? binding.matchCount ?? 1;
|
|
6
|
+
}
|
|
7
|
+
export function advanceTaskBinding(binding, messages) {
|
|
8
|
+
committedMatchCounts.set(
|
|
9
|
+
binding,
|
|
10
|
+
messages.filter((message) => isDeepStrictEqual(message, binding.message))
|
|
11
|
+
.length,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// A compaction may recreate objects, but may never reconstruct the user's task
|
|
16
|
+
// from a generated summary. Only an exact, unambiguous snapshot can relocate.
|
|
17
|
+
export function resolveTaskBinding(messages, binding) {
|
|
18
|
+
if (!binding) return null;
|
|
19
|
+
const identities = messages.flatMap((message, index) =>
|
|
20
|
+
message === binding.original ? [index] : [],
|
|
21
|
+
);
|
|
22
|
+
const matches = messages.flatMap((message, index) =>
|
|
23
|
+
message?.role === "user" && isDeepStrictEqual(message, binding.message)
|
|
24
|
+
? [index]
|
|
25
|
+
: [],
|
|
26
|
+
);
|
|
27
|
+
if (
|
|
28
|
+
matches.length !== taskBindingMatchCount(binding) ||
|
|
29
|
+
identities.length > 1 ||
|
|
30
|
+
(identities.length === 1 && !matches.includes(identities[0])) ||
|
|
31
|
+
(!identities.length &&
|
|
32
|
+
(binding.allowContentRelocation !== true || matches.length !== 1))
|
|
33
|
+
) {
|
|
34
|
+
const error = new Error(
|
|
35
|
+
"The current user task was lost, changed or duplicated in compaction context",
|
|
36
|
+
);
|
|
37
|
+
error.code = "CC_AGENT_TASK_CONTEXT_LOST";
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
return { index: identities[0] ?? matches[0], message: binding.message };
|
|
41
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { renameWithRetry } from "../config-manager.js";
|
|
4
5
|
import { redactSecretsInText } from "../ide-context-redaction.js";
|
|
5
6
|
import {
|
|
6
7
|
diagnosticExcerpt,
|
|
@@ -437,6 +438,11 @@ export class TaskWorklog {
|
|
|
437
438
|
function writeProjection(cwd, sessionId, markdown) {
|
|
438
439
|
const file = worklogPath(cwd, sessionId, true);
|
|
439
440
|
const temporary = file + "." + randomUUID() + ".tmp";
|
|
441
|
+
let writeError;
|
|
442
|
+
const validatePath = () => {
|
|
443
|
+
if (worklogPath(cwd, sessionId) !== file)
|
|
444
|
+
throw new Error("Worklog workspace path changed while saving");
|
|
445
|
+
};
|
|
440
446
|
try {
|
|
441
447
|
const fd = fs.openSync(temporary, "wx", 0o600);
|
|
442
448
|
try {
|
|
@@ -445,9 +451,26 @@ function writeProjection(cwd, sessionId, markdown) {
|
|
|
445
451
|
} finally {
|
|
446
452
|
fs.closeSync(fd);
|
|
447
453
|
}
|
|
448
|
-
|
|
449
|
-
|
|
454
|
+
// Windows readers can briefly lock the existing projection. Retry only
|
|
455
|
+
// the atomic replacement, rechecking directory/link boundaries each time.
|
|
456
|
+
renameWithRetry(temporary, file, {
|
|
457
|
+
attempts: process.platform === "win32" ? 8 : 1,
|
|
458
|
+
_rename: (source, target) => {
|
|
459
|
+
validatePath();
|
|
460
|
+
fs.renameSync(source, target);
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
} catch (error) {
|
|
464
|
+
writeError = error;
|
|
465
|
+
throw error;
|
|
450
466
|
} finally {
|
|
451
|
-
|
|
467
|
+
try {
|
|
468
|
+
validatePath();
|
|
469
|
+
if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
// Never follow a redirected directory to clean up, or hide the original
|
|
472
|
+
// write failure behind a second transient lock on its temporary file.
|
|
473
|
+
if (!writeError) throw error;
|
|
474
|
+
}
|
|
452
475
|
}
|
|
453
476
|
}
|