@ponythewhite/base-context 1.0.9 → 1.0.11
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/CHANGELOG.md +19 -0
- package/dist/base-context-runtime/pyproject.toml +1 -1
- package/dist/base-context-runtime/uv.lock +1 -1
- package/dist/build-info.json +1 -1
- package/dist/bundle/{bundled-modules-5JRM6P4T.js → bundled-modules-PEFT72QN.js} +2 -2
- package/dist/bundle/{chunk-VVI3UKGR.js → chunk-JZHGOOFM.js} +87 -57
- package/dist/bundle/{chunk-NEHLB7JV.js → chunk-TOOVREMP.js} +596 -102
- package/dist/bundle/{cli-main-YUDBEYE2.js → cli-main-BRXGYTGK.js} +2 -2
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/{main-ZLGGAAGU.js → main-POUXGEL6.js} +2 -2
- package/dist/core/agent-session.d.ts +0 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +85 -45
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/canonical-context.d.ts +19 -3
- package/dist/core/canonical-context.d.ts.map +1 -1
- package/dist/core/canonical-context.js +245 -39
- package/dist/core/canonical-context.js.map +1 -1
- package/dist/core/context-epoch-copy.d.ts +1 -1
- package/dist/core/context-epoch-copy.d.ts.map +1 -1
- package/dist/core/context-epoch-copy.js +18 -7
- package/dist/core/context-epoch-copy.js.map +1 -1
- package/dist/core/context-epoch.d.ts +18 -3
- package/dist/core/context-epoch.d.ts.map +1 -1
- package/dist/core/context-epoch.js +13 -2
- package/dist/core/context-epoch.js.map +1 -1
- package/dist/core/context-tree.d.ts +1 -1
- package/dist/core/context-tree.d.ts.map +1 -1
- package/dist/core/context-tree.js +98 -8
- package/dist/core/context-tree.js.map +1 -1
- package/dist/core/history-index-worker.d.ts.map +1 -1
- package/dist/core/history-index-worker.js +59 -6
- package/dist/core/history-index-worker.js.map +1 -1
- package/dist/core/history-index.d.ts +17 -0
- package/dist/core/history-index.d.ts.map +1 -1
- package/dist/core/history-index.js +22 -0
- package/dist/core/history-index.js.map +1 -1
- package/dist/core/inference-coordinator.d.ts.map +1 -1
- package/dist/core/inference-coordinator.js +5 -1
- package/dist/core/inference-coordinator.js.map +1 -1
- package/dist/core/public-context.d.ts.map +1 -1
- package/dist/core/public-context.js +20 -8
- package/dist/core/public-context.js.map +1 -1
- package/dist/core/request-view-selection.d.ts +2 -1
- package/dist/core/request-view-selection.d.ts.map +1 -1
- package/dist/core/request-view-selection.js +29 -2
- package/dist/core/request-view-selection.js.map +1 -1
- package/dist/core/session-fork-messages.d.ts +5 -0
- package/dist/core/session-fork-messages.d.ts.map +1 -1
- package/dist/core/session-fork-messages.js +44 -0
- package/dist/core/session-fork-messages.js.map +1 -1
- package/dist/core/session-history-index.d.ts +5 -0
- package/dist/core/session-history-index.d.ts.map +1 -1
- package/dist/core/session-history-index.js +7 -0
- package/dist/core/session-history-index.js.map +1 -1
- package/dist/core/session-manager.d.ts +2 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +155 -13
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-tree.d.ts +9 -0
- package/dist/core/session-tree.d.ts.map +1 -1
- package/dist/core/session-tree.js +71 -11
- package/dist/core/session-tree.js.map +1 -1
- package/dist/modes/agent-connection/in-process-agent-connection.d.ts.map +1 -1
- package/dist/modes/agent-connection/in-process-agent-connection.js +4 -4
- package/dist/modes/agent-connection/in-process-agent-connection.js.map +1 -1
- package/dist/modes/daemon/daemon-mode.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-mode.js +19 -34
- package/dist/modes/daemon/daemon-mode.js.map +1 -1
- package/dist/modes/daemon/daemon-socket.d.ts +5 -0
- package/dist/modes/daemon/daemon-socket.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-socket.js +19 -0
- package/dist/modes/daemon/daemon-socket.js.map +1 -1
- package/dist/modes/daemon/daemon-supervisor.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-supervisor.js +6 -7
- package/dist/modes/daemon/daemon-supervisor.js.map +1 -1
- package/dist/modes/daemon/daemon-worker-client.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-worker-client.js +5 -5
- package/dist/modes/daemon/daemon-worker-client.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stringifyBoundedJson } from "./bounded-json.js";
|
|
2
|
-
import { CONTEXT_EPOCH_DETAIL, CONTEXT_EPOCH_RENDERER, CONTEXT_POLICY_EPOCH_RENDERER, CONTEXT_SKILL_EPOCH_RENDERER, CONTEXT_TOOL_EPOCH_RENDERER, CONTEXT_TOOL_SUMMARY_RENDERER, contextEpochMode, readContextEpoch, retainedContextRequestContract, snapshotContextEpoch, } from "./context-epoch.js";
|
|
2
|
+
import { CONTEXT_EPOCH_DETAIL, CONTEXT_EPOCH_RENDERER, CONTEXT_INTERRUPTED_TOOL_RENDERER, CONTEXT_POLICY_EPOCH_RENDERER, CONTEXT_SKILL_EPOCH_RENDERER, CONTEXT_TOOL_EPOCH_RENDERER, CONTEXT_TOOL_SUMMARY_RENDERER, contextEpochMode, readContextEpoch, retainedContextRequestContract, snapshotContextEpoch, } from "./context-epoch.js";
|
|
3
3
|
import { createCompactionSummaryMessage, createCustomMessage } from "./messages.js";
|
|
4
4
|
import { PUBLIC_CONTEXT_RENDERER, PUBLIC_TOOL_CONTINUATION_RENDERER, renderPublicHistory, renderToolContinuation, } from "./public-context.js";
|
|
5
5
|
import { renderResourceView } from "./resource-view.js";
|
|
@@ -261,11 +261,13 @@ export function prepareCanonicalEpoch(messages, selectedUnitIds, representation,
|
|
|
261
261
|
return {
|
|
262
262
|
checkpoint: snapshotContextEpoch({
|
|
263
263
|
version: context.toolContinuations?.length ? 6 : 4,
|
|
264
|
-
renderer: context.
|
|
265
|
-
?
|
|
266
|
-
: context.
|
|
267
|
-
?
|
|
268
|
-
:
|
|
264
|
+
renderer: context.toolContinuations?.some((group) => group.calls.some((call) => call.admission === "absent"))
|
|
265
|
+
? CONTEXT_INTERRUPTED_TOOL_RENDERER
|
|
266
|
+
: context.selectedSkills?.length
|
|
267
|
+
? CONTEXT_SKILL_EPOCH_RENDERER
|
|
268
|
+
: context.toolContinuations?.length
|
|
269
|
+
? CONTEXT_TOOL_EPOCH_RENDERER
|
|
270
|
+
: CONTEXT_EPOCH_RENDERER,
|
|
269
271
|
...(context.selectedSkills?.length ? { selectedSkills: context.selectedSkills } : {}),
|
|
270
272
|
...(context.toolContinuations?.length ? { toolContinuations: context.toolContinuations } : {}),
|
|
271
273
|
source: context.source,
|
|
@@ -283,8 +285,27 @@ export function prepareCanonicalEpoch(messages, selectedUnitIds, representation,
|
|
|
283
285
|
messages: chosen,
|
|
284
286
|
};
|
|
285
287
|
}
|
|
286
|
-
/**
|
|
287
|
-
export
|
|
288
|
+
/** Missing prerequisite only; source, closure and projection failures remain ordinary errors. */
|
|
289
|
+
export class MissingRecoveryReplayContractError extends Error {
|
|
290
|
+
constructor() {
|
|
291
|
+
super("Recovery compaction requires an accepted replay contract for its selected results");
|
|
292
|
+
this.name = "MissingRecoveryReplayContractError";
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function sameRecoverySource(left, right, ownerStillCurrent = false) {
|
|
296
|
+
return (JSON.stringify(left) === JSON.stringify(right) ||
|
|
297
|
+
(ownerStillCurrent &&
|
|
298
|
+
left.sessionId === right.sessionId &&
|
|
299
|
+
left.sessionFile === right.sessionFile &&
|
|
300
|
+
left.persistent === right.persistent));
|
|
301
|
+
}
|
|
302
|
+
function sameRecoveryReference(left, right, ownerStillCurrent = false) {
|
|
303
|
+
return (sameRecoverySource(left.source, right.source, ownerStillCurrent) &&
|
|
304
|
+
JSON.stringify(left.ref) === JSON.stringify(right.ref) &&
|
|
305
|
+
left.sourceRevision === right.sourceRevision);
|
|
306
|
+
}
|
|
307
|
+
/** Use accepted coverage, or the exact actual-projection permit of a rejected oversized candidate. */
|
|
308
|
+
export function canonicalRecoveryBoundary(messages, authorization) {
|
|
288
309
|
const context = compiledEpochContexts.get(messages);
|
|
289
310
|
const units = getCanonicalViewUnits(messages);
|
|
290
311
|
if (!context || !units)
|
|
@@ -294,18 +315,36 @@ export function canonicalRecoveryBoundary(messages) {
|
|
|
294
315
|
const recoveries = units.flatMap((unit, index) => (unit.kind === "recovery" ? [context.references[index]] : []));
|
|
295
316
|
if (!recoveries.length)
|
|
296
317
|
return;
|
|
318
|
+
if (authorization) {
|
|
319
|
+
// The captured native owner may certify bookkeeping-only appends, never new context or a branch change.
|
|
320
|
+
const ownerStillCurrent = authorization.isSourceCurrent?.();
|
|
321
|
+
if (ownerStillCurrent === false ||
|
|
322
|
+
!sameRecoverySource(context.source, authorization.source, ownerStillCurrent === true) ||
|
|
323
|
+
context.resourceRevision !== authorization.resourceRevision ||
|
|
324
|
+
recoveries.length !== authorization.recoveries.length ||
|
|
325
|
+
recoveries.some((reference) => !reference ||
|
|
326
|
+
!authorization.recoveries.some((covered) => sameRecoveryReference(reference, covered, ownerStillCurrent === true))))
|
|
327
|
+
throw new Error("Recovery compaction authorization no longer matches its captured source");
|
|
328
|
+
return latestLiteralReference(context.references)?.ref.entryId;
|
|
329
|
+
}
|
|
297
330
|
const checkpoint = context.checkpoint;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
recoveries.some((reference) => !reference ||
|
|
301
|
-
|
|
331
|
+
const inherited = checkpoint?.includeSummary === true && checkpoint.replayContract === "message-groups";
|
|
332
|
+
if (checkpoint?.replayContract !== "message-groups" ||
|
|
333
|
+
recoveries.some((reference) => !reference ||
|
|
334
|
+
(checkpoint.representation
|
|
335
|
+
? reference.ref.sequence > checkpoint.source.sourceSequence
|
|
336
|
+
: !inherited ||
|
|
337
|
+
!context.inheritedRecoveryCoverage?.some((covered) => sameRecoveryReference(reference, covered)))))
|
|
338
|
+
throw new MissingRecoveryReplayContractError();
|
|
302
339
|
return checkpoint.literalTailId;
|
|
303
340
|
}
|
|
304
341
|
/** Retain exact recovery evidence. An accepted public-window transition replaces whole old protocol groups. */
|
|
305
|
-
export function prepareRecoveryCompaction(messages, firstKeptEntryId, maxBytes) {
|
|
306
|
-
const boundary = canonicalRecoveryBoundary(messages);
|
|
342
|
+
export function prepareRecoveryCompaction(messages, firstKeptEntryId, maxBytes, authorization) {
|
|
343
|
+
const boundary = canonicalRecoveryBoundary(messages, authorization);
|
|
307
344
|
const context = compiledEpochContexts.get(messages);
|
|
308
|
-
const publicWindow =
|
|
345
|
+
const publicWindow = authorization
|
|
346
|
+
? authorization.publicWindow
|
|
347
|
+
: context?.checkpoint?.publicWindow === true && context.checkpoint.replayContract === "message-groups";
|
|
309
348
|
if (!boundary && !publicWindow && !context?.selectedSkills?.length && !context?.toolContinuations?.length)
|
|
310
349
|
return;
|
|
311
350
|
if (!context)
|
|
@@ -349,11 +388,13 @@ export function prepareRecoveryCompaction(messages, firstKeptEntryId, maxBytes)
|
|
|
349
388
|
return;
|
|
350
389
|
return snapshotContextEpoch({
|
|
351
390
|
...(toolGroups.size ? { version: 8, mode: context.mode } : { version: 4 }),
|
|
352
|
-
renderer:
|
|
353
|
-
?
|
|
354
|
-
:
|
|
355
|
-
?
|
|
356
|
-
:
|
|
391
|
+
renderer: context.toolContinuations?.some((group) => group.calls.some((call) => call.admission === "absent"))
|
|
392
|
+
? CONTEXT_INTERRUPTED_TOOL_RENDERER
|
|
393
|
+
: toolGroups.size
|
|
394
|
+
? CONTEXT_TOOL_SUMMARY_RENDERER
|
|
395
|
+
: context.selectedSkills?.length
|
|
396
|
+
? CONTEXT_SKILL_EPOCH_RENDERER
|
|
397
|
+
: CONTEXT_EPOCH_RENDERER,
|
|
357
398
|
...(toolGroups.size ? { toolContinuations: context.toolContinuations } : {}),
|
|
358
399
|
...(context.selectedSkills?.length ? { selectedSkills: context.selectedSkills } : {}),
|
|
359
400
|
source: context.source,
|
|
@@ -947,6 +988,12 @@ export class CanonicalContextCompiler {
|
|
|
947
988
|
}
|
|
948
989
|
const replayUnits = bindMessageReplayUnits(messages, sourceUnits, unitLimits);
|
|
949
990
|
const frozenGroups = checkpoint?.toolContinuations ?? [];
|
|
991
|
+
const assistantSequences = messages
|
|
992
|
+
.flatMap((message) => {
|
|
993
|
+
const reference = epochReferences.get(message);
|
|
994
|
+
return message.role === "assistant" && reference ? [reference.ref.sequence] : [];
|
|
995
|
+
})
|
|
996
|
+
.sort((left, right) => left - right);
|
|
950
997
|
const candidates = messages.flatMap((message, index) => {
|
|
951
998
|
if (message.role !== "assistant")
|
|
952
999
|
return [];
|
|
@@ -962,6 +1009,8 @@ export class CanonicalContextCompiler {
|
|
|
962
1009
|
index,
|
|
963
1010
|
reference,
|
|
964
1011
|
prior,
|
|
1012
|
+
nextAssistantSequence: assistantSequences.find((sequence) => sequence > reference.ref.sequence) ?? Infinity,
|
|
1013
|
+
unqualifiedIntent: false,
|
|
965
1014
|
intents: new Map(),
|
|
966
1015
|
},
|
|
967
1016
|
];
|
|
@@ -977,21 +1026,32 @@ export class CanonicalContextCompiler {
|
|
|
977
1026
|
countBytes(metadata);
|
|
978
1027
|
return hydrateCapturedHistoryEntry(metadata, maxSourceBytes, view.readPayload);
|
|
979
1028
|
};
|
|
980
|
-
const acceptIntent = async (metadata) => {
|
|
981
|
-
if (metadata.kind !== "tool_intent"
|
|
982
|
-
metadata.qualification !== "native-tool-execution" ||
|
|
983
|
-
metadata.retention === "retained-import")
|
|
1029
|
+
const acceptIntent = async (metadata, selected = candidates) => {
|
|
1030
|
+
if (metadata.kind !== "tool_intent")
|
|
984
1031
|
return;
|
|
985
1032
|
const hydrated = await readToolEvidence(metadata);
|
|
986
1033
|
const entry = hydrated.entry;
|
|
987
|
-
if (entry.type !== "tool_intent"
|
|
988
|
-
entry.assistant?.sessionId !== view.source.sessionId ||
|
|
989
|
-
entry.assistant.sessionFile !== view.source.sessionFile)
|
|
1034
|
+
if (entry.type !== "tool_intent")
|
|
990
1035
|
return;
|
|
991
|
-
const
|
|
1036
|
+
const invocation = entry.invocation;
|
|
1037
|
+
const ownerMatch = selected.find((item) => item.reference.ref.entryId === entry.assistant?.entryId);
|
|
1038
|
+
const candidate = ownerMatch ??
|
|
1039
|
+
selected.find((item) => metadata.sequence > item.reference.ref.sequence &&
|
|
1040
|
+
metadata.sequence < item.nextAssistantSequence &&
|
|
1041
|
+
item.message.content.some((part) => part.type === "toolCall" &&
|
|
1042
|
+
part.id === invocation.toolCallId &&
|
|
1043
|
+
part.name === invocation.toolName));
|
|
992
1044
|
if (!candidate)
|
|
993
1045
|
return;
|
|
994
|
-
|
|
1046
|
+
// A present but unqualified/copied intent is not an absence of admission.
|
|
1047
|
+
if (metadata.qualification !== "native-tool-execution" ||
|
|
1048
|
+
metadata.retention === "retained-import" ||
|
|
1049
|
+
entry.assistant?.sessionId !== view.source.sessionId ||
|
|
1050
|
+
entry.assistant.sessionFile !== view.source.sessionFile ||
|
|
1051
|
+
entry.assistant.entryId !== candidate.reference.ref.entryId) {
|
|
1052
|
+
candidate.unqualifiedIntent = true;
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
995
1055
|
const call = candidate.message.content.filter((part) => part.type === "toolCall")[invocation.sourceOrder];
|
|
996
1056
|
if (!Number.isSafeInteger(invocation.sourceOrder) ||
|
|
997
1057
|
!call ||
|
|
@@ -1004,33 +1064,49 @@ export class CanonicalContextCompiler {
|
|
|
1004
1064
|
throw new Error("Tool continuation has ambiguous original intent");
|
|
1005
1065
|
candidate.intents.set(invocation.sourceOrder, hydrated);
|
|
1006
1066
|
};
|
|
1067
|
+
const absentPrefixes = new Map();
|
|
1007
1068
|
for (const candidate of candidates) {
|
|
1008
1069
|
if (!candidate.prior)
|
|
1009
1070
|
continue;
|
|
1010
1071
|
if (!Array.isArray(candidate.prior.calls))
|
|
1011
1072
|
throw new Error("Invalid committed tool continuation");
|
|
1012
1073
|
for (const call of candidate.prior.calls) {
|
|
1074
|
+
if (call.admission === "absent") {
|
|
1075
|
+
if (checkpoint?.renderer !== CONTEXT_INTERRUPTED_TOOL_RENDERER ||
|
|
1076
|
+
!call.source ||
|
|
1077
|
+
call.source.sourceSequence < candidate.reference.ref.sequence ||
|
|
1078
|
+
call.intent !== undefined ||
|
|
1079
|
+
call.executionId !== undefined ||
|
|
1080
|
+
call.outcome !== undefined ||
|
|
1081
|
+
call.result !== undefined)
|
|
1082
|
+
throw new Error("Invalid committed absent tool admission");
|
|
1083
|
+
const key = JSON.stringify(call.source);
|
|
1084
|
+
const prefix = absentPrefixes.get(key) ?? { source: call.source, candidates: [] };
|
|
1085
|
+
if (!prefix.candidates.includes(candidate))
|
|
1086
|
+
prefix.candidates.push(candidate);
|
|
1087
|
+
absentPrefixes.set(key, prefix);
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1013
1090
|
const actual = await view.get(call.intent.id);
|
|
1014
1091
|
if (!actual || actual.revision !== call.intent.revision || actual.sequence !== call.intent.sequence)
|
|
1015
1092
|
throw new Error("Committed tool intent is unavailable on this captured branch");
|
|
1016
|
-
await acceptIntent(actual);
|
|
1093
|
+
await acceptIntent(actual, [candidate]);
|
|
1017
1094
|
}
|
|
1018
1095
|
}
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1021
|
-
let after = Math.min(...newGroups.map((candidate) => candidate.reference.ref.sequence));
|
|
1096
|
+
const scanIntents = async (readView, selected) => {
|
|
1097
|
+
let after = Math.min(...selected.map((candidate) => candidate.reference.ref.sequence));
|
|
1022
1098
|
let scanned = 0;
|
|
1023
1099
|
for (;;) {
|
|
1024
|
-
const page = await
|
|
1100
|
+
const page = await readView.page(after, Math.min(128, maxMessages - scanned + 1));
|
|
1025
1101
|
scanned += page.events.length;
|
|
1026
1102
|
if (scanned > maxMessages)
|
|
1027
1103
|
throw new Error("Tool continuation source item budget exceeded");
|
|
1028
1104
|
if (page.coverage !== "complete")
|
|
1029
1105
|
throw new Error("Tool continuation source coverage is incomplete");
|
|
1030
1106
|
for (const metadata of page.events) {
|
|
1031
|
-
// Frozen
|
|
1032
|
-
if (!
|
|
1033
|
-
await acceptIntent(metadata);
|
|
1107
|
+
// Frozen qualified refs were already checked exactly, not another execution.
|
|
1108
|
+
if (!selected.some((candidate) => [...candidate.intents.values()].some((intent) => intent.source.id === metadata.id)))
|
|
1109
|
+
await acceptIntent(metadata, selected);
|
|
1034
1110
|
}
|
|
1035
1111
|
if (page.nextAfter === null)
|
|
1036
1112
|
break;
|
|
@@ -1038,23 +1114,69 @@ export class CanonicalContextCompiler {
|
|
|
1038
1114
|
throw new Error("Tool continuation source page did not advance");
|
|
1039
1115
|
after = page.nextAfter;
|
|
1040
1116
|
}
|
|
1117
|
+
};
|
|
1118
|
+
const newGroups = candidates.filter((candidate) => !candidate.prior);
|
|
1119
|
+
if (newGroups.length)
|
|
1120
|
+
await scanIntents(view, newGroups);
|
|
1121
|
+
for (const prefix of absentPrefixes.values()) {
|
|
1122
|
+
if (!view.atSnapshot)
|
|
1123
|
+
throw new Error("Absent tool admission requires its captured source");
|
|
1124
|
+
await scanIntents(await view.atSnapshot(prefix.source), prefix.candidates);
|
|
1041
1125
|
}
|
|
1126
|
+
const checkAbsentScope = async (candidate) => {
|
|
1127
|
+
const entry = next.get(candidate.reference.ref.entryId)?.entry;
|
|
1128
|
+
const output = entry?.type === "message" ? entry.requestOutput : undefined;
|
|
1129
|
+
// This correlation limits the public-history scope. It never qualifies a tool owner or outcome.
|
|
1130
|
+
if (!output ||
|
|
1131
|
+
typeof output.operationId !== "string" ||
|
|
1132
|
+
!output.operationId ||
|
|
1133
|
+
!Array.isArray(output.attemptIds) ||
|
|
1134
|
+
output.attemptIds.some((id) => typeof id !== "string") ||
|
|
1135
|
+
!output.source ||
|
|
1136
|
+
output.source.sessionId !== view.source.sessionId ||
|
|
1137
|
+
output.source.sessionFile !== view.source.sessionFile ||
|
|
1138
|
+
!output.source.persistent ||
|
|
1139
|
+
!Number.isSafeInteger(output.source.sourceSequence) ||
|
|
1140
|
+
output.source.sourceSequence >= candidate.reference.ref.sequence ||
|
|
1141
|
+
!view.atSnapshot)
|
|
1142
|
+
throw new Error("View-unit replay group is incomplete: original tool owner is unqualified");
|
|
1143
|
+
await view.atSnapshot(output.source);
|
|
1144
|
+
};
|
|
1042
1145
|
const toolContinuations = [];
|
|
1043
1146
|
const pendingPublicMessageGroups = [];
|
|
1044
1147
|
for (const candidate of candidates) {
|
|
1045
1148
|
const toolCalls = candidate.message.content.filter((part) => part.type === "toolCall");
|
|
1046
1149
|
if (!toolCalls.length ||
|
|
1047
|
-
candidate.
|
|
1150
|
+
candidate.unqualifiedIntent ||
|
|
1048
1151
|
(candidate.prior && candidate.prior.calls.length !== toolCalls.length))
|
|
1049
1152
|
throw new Error("View-unit replay group is incomplete: original tool owner is unqualified");
|
|
1153
|
+
if (candidate.intents.size !== toolCalls.length)
|
|
1154
|
+
await checkAbsentScope(candidate);
|
|
1050
1155
|
const members = [candidate.index];
|
|
1051
1156
|
const calls = [];
|
|
1052
1157
|
for (let order = 0; order < toolCalls.length; order++) {
|
|
1053
1158
|
const intent = candidate.intents.get(order);
|
|
1159
|
+
const original = candidate.prior?.calls[order];
|
|
1160
|
+
if (!intent) {
|
|
1161
|
+
if (original && original.admission !== "absent")
|
|
1162
|
+
throw new Error("Committed tool intent is unavailable on this captured branch");
|
|
1163
|
+
if (messages.some((message) => {
|
|
1164
|
+
const sequence = epochReferences.get(message)?.ref.sequence;
|
|
1165
|
+
return (message.role === "toolResult" &&
|
|
1166
|
+
message.toolCallId === toolCalls[order].id &&
|
|
1167
|
+
sequence !== undefined &&
|
|
1168
|
+
sequence > candidate.reference.ref.sequence &&
|
|
1169
|
+
sequence < candidate.nextAssistantSequence);
|
|
1170
|
+
}))
|
|
1171
|
+
throw new Error("Tool outcome lacks its original finalized owner");
|
|
1172
|
+
calls.push(original ?? { admission: "absent", source: { ...view.source } });
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
if (original?.admission === "absent")
|
|
1176
|
+
throw new Error("Committed absent tool admission changed");
|
|
1054
1177
|
if (intent.entry.type !== "tool_intent")
|
|
1055
1178
|
throw new Error("Invalid native tool intent");
|
|
1056
1179
|
const invocation = intent.entry.invocation;
|
|
1057
|
-
const original = candidate.prior?.calls[order];
|
|
1058
1180
|
if (original &&
|
|
1059
1181
|
(original.executionId !== invocation.executionId || original.intent.id !== intent.source.id))
|
|
1060
1182
|
throw new Error("Committed tool execution identity changed");
|
|
@@ -1137,10 +1259,94 @@ export class CanonicalContextCompiler {
|
|
|
1137
1259
|
units: selectionUnits,
|
|
1138
1260
|
limits: unitLimits,
|
|
1139
1261
|
...(mode === "on" && (taskFrameRebased || checkpoint) ? { requiresEpoch: true } : {}),
|
|
1262
|
+
...(mode === "on" && sourceUnits.some((unit) => unit.kind === "recovery")
|
|
1263
|
+
? { recoveryContractRequested: true }
|
|
1264
|
+
: {}),
|
|
1140
1265
|
...(pendingPublicMessageGroups.length ? { pendingPublicMessageGroups } : {}),
|
|
1141
1266
|
},
|
|
1142
1267
|
});
|
|
1268
|
+
const inheritedRecoveryCoverage = [];
|
|
1269
|
+
if (checkpoint?.includeSummary &&
|
|
1270
|
+
checkpoint.replayContract === "message-groups" &&
|
|
1271
|
+
closedUnits.some((unit) => unit.kind === "recovery")) {
|
|
1272
|
+
const prefix = await view.atSnapshot(checkpoint.source);
|
|
1273
|
+
const suffix = new Map();
|
|
1274
|
+
const frozenViews = new Map();
|
|
1275
|
+
let capturedMessages = 0;
|
|
1276
|
+
let retained = false;
|
|
1277
|
+
let cursor;
|
|
1278
|
+
for (;;) {
|
|
1279
|
+
const page = await prefix.contextManifest({ cursor, limit: 128 });
|
|
1280
|
+
if (page.selection !== "known")
|
|
1281
|
+
throw new Error("Summary recovery source is unavailable");
|
|
1282
|
+
if (!cursor && page.summaryRef) {
|
|
1283
|
+
// The frozen source may itself have pinned views before its literal manifest.
|
|
1284
|
+
const entry = await hydrate(page.summaryRef, undefined, prefix);
|
|
1285
|
+
const metadata = await prefix.get(page.summaryRef.entryId);
|
|
1286
|
+
const prior = entry.type === "compaction" &&
|
|
1287
|
+
metadata?.qualification === "native-context-epoch" &&
|
|
1288
|
+
metadata.retention !== "retained-import"
|
|
1289
|
+
? readContextEpoch(entry.details, maxSourceBytes)
|
|
1290
|
+
: undefined;
|
|
1291
|
+
if ((!prior || prior.includeSummary) && page.summaryRef.entryId === checkpoint.literalTailId)
|
|
1292
|
+
retained = true;
|
|
1293
|
+
capturedMessages += prior?.views.length ?? 0;
|
|
1294
|
+
for (const pinned of prior?.views ?? []) {
|
|
1295
|
+
if (pinned.ref.entryId === checkpoint.literalTailId)
|
|
1296
|
+
retained = true;
|
|
1297
|
+
if (retained) {
|
|
1298
|
+
suffix.set(pinned.ref.entryId, pinned.ref);
|
|
1299
|
+
frozenViews.set(pinned.ref.entryId, pinned);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
for (const ref of page.refs) {
|
|
1304
|
+
if (ref.entryId === checkpoint.literalTailId)
|
|
1305
|
+
retained = true;
|
|
1306
|
+
if (retained)
|
|
1307
|
+
suffix.set(ref.entryId, ref);
|
|
1308
|
+
}
|
|
1309
|
+
capturedMessages += page.refs.length;
|
|
1310
|
+
if (capturedMessages > maxMessages)
|
|
1311
|
+
throw new Error("Summary recovery source exceeds its message budget");
|
|
1312
|
+
if (!page.nextCursor)
|
|
1313
|
+
break;
|
|
1314
|
+
cursor = page.nextCursor;
|
|
1315
|
+
}
|
|
1316
|
+
for (const [index, message] of closedMessages.entries()) {
|
|
1317
|
+
if (closedUnits[index].kind !== "recovery")
|
|
1318
|
+
continue;
|
|
1319
|
+
const reference = epochReferences.get(message);
|
|
1320
|
+
if (checkpoint.views.some((covered) => sameRecoveryReference(reference, covered))) {
|
|
1321
|
+
inheritedRecoveryCoverage.push(reference);
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
const frozen = suffix.get(reference.ref.entryId);
|
|
1325
|
+
if (!frozen || JSON.stringify(frozen) !== JSON.stringify(reference.ref))
|
|
1326
|
+
continue;
|
|
1327
|
+
const pinned = frozenViews.get(frozen.entryId);
|
|
1328
|
+
if (pinned) {
|
|
1329
|
+
if (reference.sourceRevision === pinned.sourceRevision)
|
|
1330
|
+
inheritedRecoveryCoverage.push(reference);
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
const revisions = [frozen.revision];
|
|
1334
|
+
const entry = next.get(frozen.entryId)?.entry;
|
|
1335
|
+
if (entry?.type === "message" &&
|
|
1336
|
+
entry.message.role === "toolResult" &&
|
|
1337
|
+
entry.message.toolName === "ipython") {
|
|
1338
|
+
const updates = await prefix.contextUpdates({
|
|
1339
|
+
kind: "ipython-sent-message",
|
|
1340
|
+
toolCallId: entry.message.toolCallId,
|
|
1341
|
+
});
|
|
1342
|
+
revisions.push(...updates.refs.map((update) => update.revision));
|
|
1343
|
+
}
|
|
1344
|
+
if (reference.sourceRevision === JSON.stringify(revisions))
|
|
1345
|
+
inheritedRecoveryCoverage.push(reference);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1143
1348
|
compiledEpochContexts.set(closedMessages, {
|
|
1349
|
+
...(inheritedRecoveryCoverage.length ? { inheritedRecoveryCoverage } : {}),
|
|
1144
1350
|
...(purpose === "read" ? { readOnly: true } : {}),
|
|
1145
1351
|
mode,
|
|
1146
1352
|
source: { ...view.source },
|