chainlesschain 0.166.135 → 0.166.138
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 +32 -2
- 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 +19 -0
- package/src/data/changelog.json +39 -0
- package/src/harness/prompt-compressor.js +57 -0
- package/src/harness/provider-backed-compaction.js +6 -0
- package/src/lib/background-launch-profile.js +143 -28
- package/src/lib/coding-task-guidance.js +2 -0
- 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 +134 -4
- package/src/lib/safe-mode.js +10 -0
- package/src/lib/session-history-origins.js +36 -0
- package/src/lib/tool-result-preview.js +11 -2
- package/src/repl/agent-repl.js +2 -0
- package/src/runtime/agent-core.js +40 -6
- 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
|
@@ -48,6 +48,29 @@ function positive(value, integer = false) {
|
|
|
48
48
|
return integer ? Math.floor(number) : number;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function invalidProfileField(field) {
|
|
52
|
+
const error = new Error(`Invalid background launch profile field: ${field}`);
|
|
53
|
+
error.code = "BACKGROUND_LAUNCH_PROFILE_INVALID";
|
|
54
|
+
return error;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function policyBudget(value, field) {
|
|
58
|
+
const text = String(value).trim();
|
|
59
|
+
const cost = field.toLowerCase().includes("cost");
|
|
60
|
+
const number = Number(value);
|
|
61
|
+
if (
|
|
62
|
+
value == null ||
|
|
63
|
+
!(cost ? /^\+?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i : /^\+?\d+$/).test(
|
|
64
|
+
text,
|
|
65
|
+
) ||
|
|
66
|
+
!Number.isFinite(number) ||
|
|
67
|
+
number <= 0 ||
|
|
68
|
+
(!cost && !Number.isSafeInteger(number))
|
|
69
|
+
)
|
|
70
|
+
throw invalidProfileField(`budget.${field}`);
|
|
71
|
+
return number;
|
|
72
|
+
}
|
|
73
|
+
|
|
51
74
|
function sortedStrings(values, { split = false, pathsFrom = null } = {}) {
|
|
52
75
|
const raw = Array.isArray(values) ? values : values ? [values] : [];
|
|
53
76
|
const expanded = split
|
|
@@ -356,6 +379,17 @@ export function captureBackgroundLaunchProfile({
|
|
|
356
379
|
index++;
|
|
357
380
|
return next;
|
|
358
381
|
};
|
|
382
|
+
const takePolicy = (allowEmpty = false) => {
|
|
383
|
+
const next = inline !== null ? inline : args[index + 1];
|
|
384
|
+
if (
|
|
385
|
+
next == null ||
|
|
386
|
+
(inline === null && next.startsWith("--")) ||
|
|
387
|
+
(!allowEmpty && next.trim() === "")
|
|
388
|
+
)
|
|
389
|
+
throw invalidProfileField(`argv.${name}`);
|
|
390
|
+
if (inline === null) index++;
|
|
391
|
+
return next;
|
|
392
|
+
};
|
|
359
393
|
switch (name) {
|
|
360
394
|
case "--provider":
|
|
361
395
|
profile.llm.provider = scalar(take());
|
|
@@ -376,14 +410,17 @@ export function captureBackgroundLaunchProfile({
|
|
|
376
410
|
fallbackModels.push(...orderedStrings(take(), { split: true }));
|
|
377
411
|
break;
|
|
378
412
|
case "--allowed-tools":
|
|
379
|
-
profile.tools.allowed = sortedStrings(
|
|
413
|
+
profile.tools.allowed = sortedStrings(takePolicy(true), {
|
|
414
|
+
split: true,
|
|
415
|
+
});
|
|
380
416
|
break;
|
|
381
417
|
case "--disallowed-tools":
|
|
382
|
-
profile.tools.disallowed = sortedStrings(
|
|
418
|
+
profile.tools.disallowed = sortedStrings(takePolicy(true), {
|
|
419
|
+
split: true,
|
|
420
|
+
});
|
|
383
421
|
break;
|
|
384
422
|
case "--permission-mode": {
|
|
385
|
-
|
|
386
|
-
profile.permission.mode = PERMISSION_MODES.has(mode) ? mode : "default";
|
|
423
|
+
profile.permission.mode = scalar(takePolicy());
|
|
387
424
|
break;
|
|
388
425
|
}
|
|
389
426
|
case "--allow-dangerous-bypass":
|
|
@@ -395,7 +432,7 @@ export function captureBackgroundLaunchProfile({
|
|
|
395
432
|
profile.permission.unattended = true;
|
|
396
433
|
break;
|
|
397
434
|
case "--unattended-allow":
|
|
398
|
-
profile.permission.unattendedAllow = sortedStrings(
|
|
435
|
+
profile.permission.unattendedAllow = sortedStrings(takePolicy(true), {
|
|
399
436
|
split: true,
|
|
400
437
|
});
|
|
401
438
|
break;
|
|
@@ -413,8 +450,7 @@ export function captureBackgroundLaunchProfile({
|
|
|
413
450
|
profile.sandbox.image = scalar(take(true));
|
|
414
451
|
break;
|
|
415
452
|
case "--sandbox-mode": {
|
|
416
|
-
|
|
417
|
-
profile.sandbox.mode = SANDBOX_MODES.has(mode) ? mode : null;
|
|
453
|
+
profile.sandbox.mode = scalar(takePolicy());
|
|
418
454
|
break;
|
|
419
455
|
}
|
|
420
456
|
case "--sandbox-network":
|
|
@@ -427,6 +463,12 @@ export function captureBackgroundLaunchProfile({
|
|
|
427
463
|
profile.mcp.disabled = true;
|
|
428
464
|
profile.runtime.noMcpAutoConnect = true;
|
|
429
465
|
break;
|
|
466
|
+
case "--no-mcp-auto-connect":
|
|
467
|
+
profile.runtime.noMcpAutoConnect = true;
|
|
468
|
+
break;
|
|
469
|
+
case "--no-plugins":
|
|
470
|
+
profile.plugins.disabled = true;
|
|
471
|
+
break;
|
|
430
472
|
case "--strict-mcp-config":
|
|
431
473
|
profile.mcp.strict = true;
|
|
432
474
|
break;
|
|
@@ -461,28 +503,43 @@ export function captureBackgroundLaunchProfile({
|
|
|
461
503
|
addDirs.push(take());
|
|
462
504
|
break;
|
|
463
505
|
case "--max-turns":
|
|
464
|
-
profile.budget.maxTurns =
|
|
506
|
+
profile.budget.maxTurns = policyBudget(takePolicy(), "maxTurns");
|
|
465
507
|
break;
|
|
466
508
|
case "--max-budget-usd":
|
|
467
|
-
profile.budget.maxCostUsd =
|
|
509
|
+
profile.budget.maxCostUsd = policyBudget(takePolicy(), "maxCostUsd");
|
|
468
510
|
break;
|
|
469
511
|
case "--thinking-budget":
|
|
470
|
-
profile.budget.thinkingTokens =
|
|
512
|
+
profile.budget.thinkingTokens = policyBudget(
|
|
513
|
+
takePolicy(),
|
|
514
|
+
"thinkingTokens",
|
|
515
|
+
);
|
|
471
516
|
break;
|
|
472
517
|
case "--max-rewakes":
|
|
473
|
-
profile.budget.maxRewakes =
|
|
518
|
+
profile.budget.maxRewakes = policyBudget(takePolicy(), "maxRewakes");
|
|
474
519
|
break;
|
|
475
520
|
case "--max-outer-turns":
|
|
476
|
-
profile.budget.maxOuterTurns =
|
|
521
|
+
profile.budget.maxOuterTurns = policyBudget(
|
|
522
|
+
takePolicy(),
|
|
523
|
+
"maxOuterTurns",
|
|
524
|
+
);
|
|
477
525
|
break;
|
|
478
526
|
case "--goal-max-tokens":
|
|
479
|
-
profile.budget.goalMaxTokens =
|
|
527
|
+
profile.budget.goalMaxTokens = policyBudget(
|
|
528
|
+
takePolicy(),
|
|
529
|
+
"goalMaxTokens",
|
|
530
|
+
);
|
|
480
531
|
break;
|
|
481
532
|
case "--goal-max-cost":
|
|
482
|
-
profile.budget.goalMaxCostUsd =
|
|
533
|
+
profile.budget.goalMaxCostUsd = policyBudget(
|
|
534
|
+
takePolicy(),
|
|
535
|
+
"goalMaxCostUsd",
|
|
536
|
+
);
|
|
483
537
|
break;
|
|
484
538
|
case "--goal-max-time":
|
|
485
|
-
profile.budget.goalMaxTimeMs =
|
|
539
|
+
profile.budget.goalMaxTimeMs = policyBudget(
|
|
540
|
+
takePolicy(),
|
|
541
|
+
"goalMaxTimeMs",
|
|
542
|
+
);
|
|
486
543
|
break;
|
|
487
544
|
case "--safe-mode":
|
|
488
545
|
profile.runtime.safeMode = true;
|
|
@@ -667,12 +724,70 @@ export function normalizeBackgroundLaunchProfile(input) {
|
|
|
667
724
|
);
|
|
668
725
|
}
|
|
669
726
|
const rejectField = (field) => {
|
|
670
|
-
|
|
671
|
-
`Invalid background launch profile field: ${field}`,
|
|
672
|
-
);
|
|
673
|
-
error.code = "BACKGROUND_LAUNCH_PROFILE_INVALID";
|
|
674
|
-
throw error;
|
|
727
|
+
throw invalidProfileField(field);
|
|
675
728
|
};
|
|
729
|
+
for (const group of [
|
|
730
|
+
"llm",
|
|
731
|
+
"tools",
|
|
732
|
+
"permission",
|
|
733
|
+
"sandbox",
|
|
734
|
+
"mcp",
|
|
735
|
+
"plugins",
|
|
736
|
+
"runtime",
|
|
737
|
+
"budget",
|
|
738
|
+
])
|
|
739
|
+
if (
|
|
740
|
+
input[group] != null &&
|
|
741
|
+
(typeof input[group] !== "object" || Array.isArray(input[group]))
|
|
742
|
+
)
|
|
743
|
+
rejectField(group);
|
|
744
|
+
for (const [group, fields] of Object.entries({
|
|
745
|
+
permission: [
|
|
746
|
+
"dangerousBypass",
|
|
747
|
+
"unattended",
|
|
748
|
+
"interactiveApprovals",
|
|
749
|
+
"remoteControl",
|
|
750
|
+
],
|
|
751
|
+
sandbox: ["enabled", "network"],
|
|
752
|
+
mcp: ["disabled", "strict", "project"],
|
|
753
|
+
plugins: ["disabled"],
|
|
754
|
+
runtime: [
|
|
755
|
+
"safeMode",
|
|
756
|
+
"bare",
|
|
757
|
+
"managedCheckpoint",
|
|
758
|
+
"noProjectMemory",
|
|
759
|
+
"noFileRefs",
|
|
760
|
+
"noSlashMacros",
|
|
761
|
+
"noRecallMemory",
|
|
762
|
+
"noMcpAutoConnect",
|
|
763
|
+
"autoRewake",
|
|
764
|
+
"autoPin",
|
|
765
|
+
"noStream",
|
|
766
|
+
"noParkOnExit",
|
|
767
|
+
],
|
|
768
|
+
}))
|
|
769
|
+
for (const field of fields)
|
|
770
|
+
if (
|
|
771
|
+
input[group]?.[field] !== undefined &&
|
|
772
|
+
typeof input[group][field] !== "boolean"
|
|
773
|
+
)
|
|
774
|
+
rejectField(`${group}.${field}`);
|
|
775
|
+
for (const field of ["ide", "pdh", "jetbrains"])
|
|
776
|
+
if (input.mcp?.[field] != null && !TRI_STATES.has(input.mcp[field]))
|
|
777
|
+
rejectField(`mcp.${field}`);
|
|
778
|
+
for (const [group, fields] of Object.entries({
|
|
779
|
+
tools: ["allowed", "disallowed"],
|
|
780
|
+
permission: ["unattendedAllow"],
|
|
781
|
+
}))
|
|
782
|
+
for (const field of fields) {
|
|
783
|
+
const value = input[group]?.[field];
|
|
784
|
+
if (
|
|
785
|
+
value != null &&
|
|
786
|
+
(!Array.isArray(value) ||
|
|
787
|
+
value.some((item) => typeof item !== "string" || !item.trim()))
|
|
788
|
+
)
|
|
789
|
+
rejectField(`${group}.${field}`);
|
|
790
|
+
}
|
|
676
791
|
// A persisted policy must not silently fall back to a weaker default.
|
|
677
792
|
if (
|
|
678
793
|
input.permission?.mode != null &&
|
|
@@ -692,14 +807,8 @@ export function normalizeBackgroundLaunchProfile(input) {
|
|
|
692
807
|
"goalMaxTimeMs",
|
|
693
808
|
]) {
|
|
694
809
|
const value = input.budget?.[field];
|
|
695
|
-
if (value == null
|
|
696
|
-
|
|
697
|
-
if (
|
|
698
|
-
!Number.isFinite(number) ||
|
|
699
|
-
number <= 0 ||
|
|
700
|
-
(!field.toLowerCase().includes("cost") && !Number.isSafeInteger(number))
|
|
701
|
-
)
|
|
702
|
-
rejectField(`budget.${field}`);
|
|
810
|
+
if (value == null) continue;
|
|
811
|
+
policyBudget(value, field);
|
|
703
812
|
}
|
|
704
813
|
const cwd = canonicalPath(input.workspace?.cwd || process.cwd());
|
|
705
814
|
const safeBase = sanitizeBackgroundBaseUrl(input.llm?.baseUrl);
|
|
@@ -1020,6 +1129,11 @@ export function buildArgvFromBackgroundLaunchProfile(profile) {
|
|
|
1020
1129
|
pushFlag(argv, "--sandbox-network", p.sandbox.network);
|
|
1021
1130
|
pushValue(argv, "--mcp-config", p.mcp.configFile);
|
|
1022
1131
|
pushFlag(argv, "--no-mcp", p.mcp.disabled);
|
|
1132
|
+
pushFlag(
|
|
1133
|
+
argv,
|
|
1134
|
+
"--no-mcp-auto-connect",
|
|
1135
|
+
p.runtime.noMcpAutoConnect && !p.mcp.disabled,
|
|
1136
|
+
);
|
|
1023
1137
|
pushFlag(argv, "--strict-mcp-config", p.mcp.strict);
|
|
1024
1138
|
pushFlag(argv, "--project-mcp", p.mcp.project);
|
|
1025
1139
|
for (const [key, value] of [
|
|
@@ -1032,6 +1146,7 @@ export function buildArgvFromBackgroundLaunchProfile(profile) {
|
|
|
1032
1146
|
}
|
|
1033
1147
|
pushValue(argv, "--settings", p.settings.file);
|
|
1034
1148
|
pushValue(argv, "--bundle", p.plugins.bundle);
|
|
1149
|
+
pushFlag(argv, "--no-plugins", p.plugins.disabled && !p.runtime.bare);
|
|
1035
1150
|
for (const dir of p.workspace.addDirs) pushValue(argv, "--add-dir", dir);
|
|
1036
1151
|
pushValue(argv, "--max-turns", p.budget.maxTurns);
|
|
1037
1152
|
pushValue(argv, "--max-budget-usd", p.budget.maxCostUsd);
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
export const CODING_TASK_QUALITY_GUIDANCE = `Coding task quality:
|
|
3
3
|
- Read the actual implementation and its exported API before writing a caller or test. Verify parameter order, return shapes, fixture requirements and the delegated owner of each lifecycle transition.
|
|
4
4
|
- Keep progress explanations to one or two sentences. Resolve uncertainty with targeted reads, reuse unchanged source already inspected, and put code directly in write/edit arguments.
|
|
5
|
+
- Omitted or undelivered source has not been inspected. For an incomplete source excerpt, select a complete enclosing statement or container; a larger range or whole file within the tool limits may be necessary. Do not keep halving the same omitted page. For oversized output, select bounded fields or sections. If safe source remains unavailable, report that limitation without claiming completion.
|
|
5
6
|
- Resolve each relative import from the destination file's directory. Read back completed files and fix newDiagnostics. A successful write proves persistence, not test execution or acceptance.
|
|
6
7
|
- Map every acceptance requirement to a fixture, real production call path and observable assertion. Positive and negative controls must reach the same boundary with unrelated prerequisites valid. Avoid mocks that bypass it; identify which assertion would fail without that boundary.
|
|
7
8
|
- Derive case allocation from the user's requirements. A test-count cap does not waive coverage: merge redundant cases to cover gaps within the cap, authorized scope and remaining budget. Report unresolved gaps as incomplete.
|
|
9
|
+
- Keep test fixtures and assertions within the runner's memory and time limits. For large binary payloads, assert lengths and use Buffer.equals or a digest for content checks; avoid deep matchers that enumerate every byte or print the entire payload on failure. Check boundary rejection before allocating a large body when the contract permits it.
|
|
8
10
|
- For races, control the intended transition and complete the current worker/request when testing cancellation or replacement. An earlier unrelated rejection does not prove the target guard. Derive expectations independently from the contract or authoritative state, not by calling the function under test.
|
|
9
11
|
- Trace rejection before asserting its aftermath: it may precede registration or persistence. Inspect whether state stays unchanged, a failed record exists or an event is emitted. Compare independently recorded identities and provenance; equal text does not prove ownership.
|
|
10
12
|
- Inspect persistence projections before comparing histories. Host notices, lineage and imports may accompany user/assistant messages; verify their provenance separately. Report defects that cannot be repaired within the permitted scope instead of inventing passing behavior.
|
|
@@ -54,12 +54,17 @@ function compactStats(event, messages) {
|
|
|
54
54
|
|
|
55
55
|
/** SessionContextPort backed by the CLI's verified, anchored JSONL transcript. */
|
|
56
56
|
export class JsonlSessionContextPort {
|
|
57
|
-
constructor({
|
|
57
|
+
constructor({
|
|
58
|
+
sessionId,
|
|
59
|
+
allowedSinks = ["*"],
|
|
60
|
+
requiredUserMessages = [],
|
|
61
|
+
} = {}) {
|
|
58
62
|
if (typeof sessionId !== "string" || !sessionId.trim()) {
|
|
59
63
|
throw new TypeError("JsonlSessionContextPort requires sessionId");
|
|
60
64
|
}
|
|
61
65
|
this.sessionId = sessionId.trim();
|
|
62
66
|
this.allowedSinks = [...allowedSinks];
|
|
67
|
+
this.requiredUserMessages = requiredUserMessages;
|
|
63
68
|
this.ownedHeadAdvances = new Map();
|
|
64
69
|
}
|
|
65
70
|
|
|
@@ -113,16 +118,20 @@ export class JsonlSessionContextPort {
|
|
|
113
118
|
memoryRevision = revision;
|
|
114
119
|
}
|
|
115
120
|
},
|
|
116
|
-
finish: (authority) =>
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
memoryRevision,
|
|
120
|
-
items: messagesToContextItems(authority.readMessages(), {
|
|
121
|
+
finish: (authority) => {
|
|
122
|
+
const messages = authority.readMessages();
|
|
123
|
+
return {
|
|
121
124
|
sessionId,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
head: authority.headHash,
|
|
126
|
+
memoryRevision,
|
|
127
|
+
items: messagesToContextItems(messages, {
|
|
128
|
+
sessionId,
|
|
129
|
+
allowedSinks: this.allowedSinks,
|
|
130
|
+
allowExternalization: true,
|
|
131
|
+
requiredUserMessages: this.requiredUserMessages,
|
|
132
|
+
}),
|
|
133
|
+
};
|
|
134
|
+
},
|
|
126
135
|
};
|
|
127
136
|
});
|
|
128
137
|
}
|
|
@@ -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
|
+
}
|