@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.
Files changed (80) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/base-context-runtime/pyproject.toml +1 -1
  3. package/dist/base-context-runtime/uv.lock +1 -1
  4. package/dist/build-info.json +1 -1
  5. package/dist/bundle/{bundled-modules-5JRM6P4T.js → bundled-modules-PEFT72QN.js} +2 -2
  6. package/dist/bundle/{chunk-VVI3UKGR.js → chunk-JZHGOOFM.js} +87 -57
  7. package/dist/bundle/{chunk-NEHLB7JV.js → chunk-TOOVREMP.js} +596 -102
  8. package/dist/bundle/{cli-main-YUDBEYE2.js → cli-main-BRXGYTGK.js} +2 -2
  9. package/dist/bundle/cli.js +1 -1
  10. package/dist/bundle/{main-ZLGGAAGU.js → main-POUXGEL6.js} +2 -2
  11. package/dist/core/agent-session.d.ts +0 -1
  12. package/dist/core/agent-session.d.ts.map +1 -1
  13. package/dist/core/agent-session.js +85 -45
  14. package/dist/core/agent-session.js.map +1 -1
  15. package/dist/core/canonical-context.d.ts +19 -3
  16. package/dist/core/canonical-context.d.ts.map +1 -1
  17. package/dist/core/canonical-context.js +245 -39
  18. package/dist/core/canonical-context.js.map +1 -1
  19. package/dist/core/context-epoch-copy.d.ts +1 -1
  20. package/dist/core/context-epoch-copy.d.ts.map +1 -1
  21. package/dist/core/context-epoch-copy.js +18 -7
  22. package/dist/core/context-epoch-copy.js.map +1 -1
  23. package/dist/core/context-epoch.d.ts +18 -3
  24. package/dist/core/context-epoch.d.ts.map +1 -1
  25. package/dist/core/context-epoch.js +13 -2
  26. package/dist/core/context-epoch.js.map +1 -1
  27. package/dist/core/context-tree.d.ts +1 -1
  28. package/dist/core/context-tree.d.ts.map +1 -1
  29. package/dist/core/context-tree.js +98 -8
  30. package/dist/core/context-tree.js.map +1 -1
  31. package/dist/core/history-index-worker.d.ts.map +1 -1
  32. package/dist/core/history-index-worker.js +59 -6
  33. package/dist/core/history-index-worker.js.map +1 -1
  34. package/dist/core/history-index.d.ts +17 -0
  35. package/dist/core/history-index.d.ts.map +1 -1
  36. package/dist/core/history-index.js +22 -0
  37. package/dist/core/history-index.js.map +1 -1
  38. package/dist/core/inference-coordinator.d.ts.map +1 -1
  39. package/dist/core/inference-coordinator.js +5 -1
  40. package/dist/core/inference-coordinator.js.map +1 -1
  41. package/dist/core/public-context.d.ts.map +1 -1
  42. package/dist/core/public-context.js +20 -8
  43. package/dist/core/public-context.js.map +1 -1
  44. package/dist/core/request-view-selection.d.ts +2 -1
  45. package/dist/core/request-view-selection.d.ts.map +1 -1
  46. package/dist/core/request-view-selection.js +29 -2
  47. package/dist/core/request-view-selection.js.map +1 -1
  48. package/dist/core/session-fork-messages.d.ts +5 -0
  49. package/dist/core/session-fork-messages.d.ts.map +1 -1
  50. package/dist/core/session-fork-messages.js +44 -0
  51. package/dist/core/session-fork-messages.js.map +1 -1
  52. package/dist/core/session-history-index.d.ts +5 -0
  53. package/dist/core/session-history-index.d.ts.map +1 -1
  54. package/dist/core/session-history-index.js +7 -0
  55. package/dist/core/session-history-index.js.map +1 -1
  56. package/dist/core/session-manager.d.ts +2 -0
  57. package/dist/core/session-manager.d.ts.map +1 -1
  58. package/dist/core/session-manager.js +155 -13
  59. package/dist/core/session-manager.js.map +1 -1
  60. package/dist/core/session-tree.d.ts +9 -0
  61. package/dist/core/session-tree.d.ts.map +1 -1
  62. package/dist/core/session-tree.js +71 -11
  63. package/dist/core/session-tree.js.map +1 -1
  64. package/dist/modes/agent-connection/in-process-agent-connection.d.ts.map +1 -1
  65. package/dist/modes/agent-connection/in-process-agent-connection.js +4 -4
  66. package/dist/modes/agent-connection/in-process-agent-connection.js.map +1 -1
  67. package/dist/modes/daemon/daemon-mode.d.ts.map +1 -1
  68. package/dist/modes/daemon/daemon-mode.js +19 -34
  69. package/dist/modes/daemon/daemon-mode.js.map +1 -1
  70. package/dist/modes/daemon/daemon-socket.d.ts +5 -0
  71. package/dist/modes/daemon/daemon-socket.d.ts.map +1 -1
  72. package/dist/modes/daemon/daemon-socket.js +19 -0
  73. package/dist/modes/daemon/daemon-socket.js.map +1 -1
  74. package/dist/modes/daemon/daemon-supervisor.d.ts.map +1 -1
  75. package/dist/modes/daemon/daemon-supervisor.js +6 -7
  76. package/dist/modes/daemon/daemon-supervisor.js.map +1 -1
  77. package/dist/modes/daemon/daemon-worker-client.d.ts.map +1 -1
  78. package/dist/modes/daemon/daemon-worker-client.js +5 -5
  79. package/dist/modes/daemon/daemon-worker-client.js.map +1 -1
  80. 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.selectedSkills?.length
265
- ? CONTEXT_SKILL_EPOCH_RENDERER
266
- : context.toolContinuations?.length
267
- ? CONTEXT_TOOL_EPOCH_RENDERER
268
- : CONTEXT_EPOCH_RENDERER,
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
- /** Use only a replay contract granted to these recovery results by an actual accepted request. */
287
- export function canonicalRecoveryBoundary(messages) {
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
- if (!checkpoint?.representation ||
299
- checkpoint.replayContract !== "message-groups" ||
300
- recoveries.some((reference) => !reference || reference.ref.sequence > checkpoint.source.sourceSequence))
301
- throw new Error("Recovery compaction requires an accepted replay contract for its selected results");
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 = context?.checkpoint?.publicWindow === true && context.checkpoint.replayContract === "message-groups";
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: toolGroups.size
353
- ? CONTEXT_TOOL_SUMMARY_RENDERER
354
- : context.selectedSkills?.length
355
- ? CONTEXT_SKILL_EPOCH_RENDERER
356
- : CONTEXT_EPOCH_RENDERER,
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 candidate = candidates.find((item) => item.reference.ref.entryId === entry.assistant?.entryId);
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
- const invocation = entry.invocation;
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 newGroups = candidates.filter((candidate) => !candidate.prior);
1020
- if (newGroups.length) {
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 view.page(after, Math.min(128, maxMessages - scanned + 1));
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 intent refs were already read exactly; do not count them twice as another execution.
1032
- if (!candidates.some((candidate) => candidate.prior?.calls.some((call) => call.intent.id === metadata.id)))
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.intents.size !== toolCalls.length ||
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 },