narraeon 0.5.1 → 0.5.3

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.
@@ -1,7 +1,10 @@
1
+ import { hasLegacyHistoryTools } from "../prompt/FileNativeToolRegistry.js";
2
+ import { PlayCallChainError } from "./PlayCallChainError.js";
3
+ import { preparePlayerRevisionTrace, prepareForkTrace, restorePlayDocuments, independentContextCopy, } from "./PlayTraceRetention.js";
1
4
  import { currentPlayPrompt, recordedPlayPrompt, playPromptRunsThroughEvents, } from "./PlayPromptRun.js";
2
5
  import { comparePromptPrefixes } from "../prompt/WorldPromptDiagnostics.js";
3
6
  import { createHash } from "node:crypto";
4
- import { completedPlayerRounds, continuationNotice, isPlayerRoundMarker, playerInputAppend, toolStepNotice, } from "./PlayContinuity.js";
7
+ import { completedPlayerRounds, continuationNotice, playerInputAppend, toolStepNotice, } from "./PlayContinuity.js";
5
8
  import { renderFreshContextCoverage } from "../prompt/WorldMaterialCoverage.js";
6
9
  import { renderWriteSizeAdvice, readDeclaredWorldClock, } from "../prompt/WorldMaintenanceReport.js";
7
10
  import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
@@ -13,6 +16,8 @@ import { FileNativePlayDocuments, fingerprintControl, } from "./PlayDocumentTool
13
16
  const callChainToolNames = new Set([
14
17
  "state_list",
15
18
  "history_list",
19
+ "history_search",
20
+ "history_read",
16
21
  // Kept executable only for contexts whose frozen tool universe contains it.
17
22
  "context_list",
18
23
  "context_search",
@@ -23,12 +28,7 @@ const callChainToolNames = new Set([
23
28
  "world_checkpoint",
24
29
  ]);
25
30
  const projectedEventTail = 40;
26
- export class PlayCallChainError extends Error {
27
- constructor(message) {
28
- super(message);
29
- this.name = "PlayCallChainError";
30
- }
31
- }
31
+ export { PlayCallChainError } from "./PlayCallChainError.js";
32
32
  /**
33
33
  * The production play loop: one durable logical context, no Runtime-authored
34
34
  * adjudication/narration phases. Responses with tool calls are intermediate
@@ -200,10 +200,8 @@ export class PlayCallChain {
200
200
  return;
201
201
  const { hostBinding, playPreset } = await input.resolvePrompt();
202
202
  const binding = await this.#worlds.bindPlayCallChain(input.worldId);
203
- const baseline = await this.#worlds.bindPlayCallChainAt(input.worldId, session.baselineHead);
204
- const documents = new FileNativePlayDocuments(binding.files);
205
- const documentMaintenance = await this.#bindDocumentMaintenance(documents, input.worldId, binding.parentHead);
206
203
  const compilation = this.#compiler.compilePlayCallChain({
204
+ frozenWorldContext: currentPlayPrompt(session).bootstrap,
207
205
  extensionControls: await this.#worlds.extensionControls(input.worldId, playPreset, this.#compiler.locale),
208
206
  endpoint: {
209
207
  id: `${input.worldId}:${binding.parentHead}`,
@@ -212,21 +210,17 @@ export class PlayCallChain {
212
210
  hostBinding,
213
211
  world: {
214
212
  controlFingerprint: fingerprintControl(binding.files),
215
- documentSnapshot: documents.snapshot,
213
+ documentSnapshot: session.documents.snapshot,
216
214
  additionalMaterials: structuredClone(binding.additionalMaterials),
217
- // Only pre-context history belongs in the prefix. Native conversation stays appended.
218
215
  history: structuredClone(binding.history),
219
- historyAlreadyAppended: Object.keys(binding.history).filter((key) => !(key in baseline.history)),
220
- narrativeCheckpoint: baseline.narrativeCheckpoint,
221
- documentMaintenance,
222
- documentMaintenanceUnavailableReason: documents.maintenanceWarning,
216
+ narrativeCheckpoint: binding.narrativeCheckpoint,
223
217
  },
224
218
  playerInputPlacement: "append",
225
219
  playerInput: input.playerText,
226
220
  modelBinding: input.modelHost.binding(),
227
221
  }, playPreset);
228
- documents.bindBootstrap(compilation.bootstrap, session.documents.authorizationCheckpoint());
229
- session.documents = documents;
222
+ // The native conversation already contains successful writes and exact reads.
223
+ // Rebinding initial coverage here would incorrectly reauthorize stale scopes.
230
224
  session.history = historyEntries(binding.history);
231
225
  session.narrativeCheckpoint = binding.narrativeCheckpoint;
232
226
  session.nextMaterials = structuredClone(binding.additionalMaterials);
@@ -248,7 +242,7 @@ export class PlayCallChain {
248
242
  });
249
243
  session.documentAuthorizationCheckpoints.push({
250
244
  afterEventId: session.nextEventId,
251
- authorization: documents.authorizationCheckpoint(),
245
+ authorization: session.documents.authorizationCheckpoint(),
252
246
  });
253
247
  await this.#persist(session);
254
248
  crashAtPlayAdvanceEdge("after_prompt_run_prepared");
@@ -631,8 +625,7 @@ export class PlayCallChain {
631
625
  : null;
632
626
  const timelineGeneration = playerRevisionTimelineGeneration(input.source.timelineGeneration, request.operationId, request.worldId, request.continuation);
633
627
  const requestFingerprint = playerRevisionRequestFingerprint(request, input.selectedEvent.committedHead, restoresHead);
634
- const prefixEvents = structuredClone(sourceContext.events.slice(0, input.selectedEventIndex));
635
- const transcript = transcriptThroughEvents(sourceContext.transcript, prefixEvents);
628
+ const traceRetention = preparePlayerRevisionTrace(sourceContext, input.selectedEventIndex);
636
629
  const outcome = await this.#worlds.reviseTimeline({
637
630
  operationId: request.operationId,
638
631
  worldId: request.worldId,
@@ -642,33 +635,17 @@ export class PlayCallChain {
642
635
  replacementText: request.replacementText,
643
636
  requestFingerprint,
644
637
  });
645
- const events = [
646
- ...prefixEvents,
647
- {
648
- id: input.selectedEvent.id,
649
- kind: "player",
650
- exchangeId: request.replacementExchangeId,
651
- text: request.replacementText,
652
- context: input.selectedEvent.context,
653
- committedHead: outcome.head,
654
- },
655
- ];
656
- const completedKeys = completedToolKeys(prefixEvents);
657
638
  const [baseline, selected, binding] = await Promise.all([
658
639
  this.#worlds.recoverEndpoint(request.worldId, sourceContext.baselineHead),
659
640
  this.#worlds.recoverEndpoint(request.worldId, restoresHead),
660
641
  this.#worlds.bindPlayCallChain(request.worldId),
661
642
  ]);
662
- transcript.push(...playerInputAppend({
663
- history: Object.fromEntries(selected.history.map(({ messageId, exactText }) => [
664
- messageId,
665
- exactText,
666
- ])),
667
- checkpoint: selected.narrativeCheckpoint,
643
+ const continuedTrace = traceRetention.withReplacement(selected, {
644
+ head: outcome.head,
645
+ exchangeId: request.replacementExchangeId,
668
646
  text: request.replacementText,
669
647
  locale: this.#compiler.locale,
670
- checkpointAvailable: sourceContext.tools.some(({ name }) => name === "world_checkpoint"),
671
- }));
648
+ });
672
649
  if (binding.parentHead !== outcome.head)
673
650
  throw new PlayCallChainError("The timeline revision was committed, but current-world materialization has not reached the new endpoint.");
674
651
  if (request.continuation === "fresh_context") {
@@ -679,7 +656,7 @@ export class PlayCallChain {
679
656
  sourceContext,
680
657
  sourceContexts: input.sourceContexts,
681
658
  selectedContextIndex: input.selectedContextIndex,
682
- prefixEvents,
659
+ traceRetention,
683
660
  baseline,
684
661
  selected,
685
662
  binding,
@@ -700,9 +677,8 @@ export class PlayCallChain {
700
677
  playCallChain: projectView(revised),
701
678
  };
702
679
  }
703
- const documents = restorePlayDocuments(binding.files, sourceContext, prefixEvents);
704
680
  const revised = {
705
- continuityContextId: sourceContext.continuityContextId ?? sourceContext.chainId,
681
+ ...continuedTrace.atEndpoint({ baseline, selected, binding }),
706
682
  schemaVersion: 3,
707
683
  kind: "play_call_chain",
708
684
  chainId: playerRevisionChainId(sourceContext.chainId, request.operationId, request.worldId, request.continuation),
@@ -712,44 +688,7 @@ export class PlayCallChain {
712
688
  ? null
713
689
  : input.sourceContexts[input.selectedContextIndex - 1].chainId,
714
690
  timelineGeneration,
715
- baselineHead: sourceContext.baselineHead,
716
- baselineHistoryLength: sourceContext.baselineHistoryLength ?? baseline.history.length,
717
691
  parentHead: outcome.head,
718
- promptRuns: playPromptRunsThroughEvents(sourceContext, events),
719
- ...(sourceContext.presetFiles === undefined
720
- ? {}
721
- : { presetFiles: structuredClone(sourceContext.presetFiles) }),
722
- playPreset: structuredClone(sourceContext.playPreset),
723
- ...(sourceContext.followups === undefined
724
- ? {}
725
- : { followups: structuredClone(sourceContext.followups) }),
726
- ...(sourceContext.playPresetScriptsEnabled === undefined
727
- ? {}
728
- : {
729
- playPresetScriptsEnabled: sourceContext.playPresetScriptsEnabled,
730
- }),
731
- ...(sourceContext.modelBinding === undefined
732
- ? {}
733
- : { modelBinding: structuredClone(sourceContext.modelBinding) }),
734
- status: "ready",
735
- canRetry: false,
736
- bootstrap: structuredClone(sourceContext.bootstrap),
737
- tools: structuredClone(sourceContext.tools),
738
- transcript,
739
- events,
740
- completedTools: sourceContext.completedTools
741
- .filter(({ key }) => completedKeys.has(key))
742
- .map((item) => structuredClone(item)),
743
- documentAuthorizationCheckpoints: authorizationCheckpointsThroughEvents(sourceContext, events, documents.authorizationCheckpoint()),
744
- changedDocuments: changedDocumentsAtHead(sourceContext.changedDocuments, baseline.state, selected.state),
745
- nextMaterials: structuredClone(binding.additionalMaterials),
746
- nextEventId: Math.max(0, ...events.map(({ id }) => id)) + 1,
747
- exchange: Math.max(0, ...prefixEvents
748
- .filter((event) => event.kind === "assistant" && event.status === "completed")
749
- .map(({ exchange }) => exchange)),
750
- lastRequest: null,
751
- lastRequestAttempt: 0,
752
- lastFailure: null,
753
692
  updatedAt: Date.now(),
754
693
  };
755
694
  await this.#worlds.playTimeline.persist(structuredClone(revised));
@@ -803,12 +742,14 @@ export class PlayCallChain {
803
742
  let previousChainId = input.selectedContextIndex === 0
804
743
  ? null
805
744
  : input.sourceContexts[input.selectedContextIndex - 1].chainId;
806
- if (input.prefixEvents.length > 0) {
807
- const prefixTranscript = transcriptThroughEvents(sourceContext.transcript, input.prefixEvents);
808
- const completedKeys = completedToolKeys(input.prefixEvents);
809
- const documents = restorePlayDocuments(input.binding.files, sourceContext, input.prefixEvents);
745
+ const retainedPrefix = input.traceRetention.pagePrefixAtEndpoint({
746
+ baseline: input.baseline,
747
+ selected: input.selected,
748
+ binding: input.binding,
749
+ });
750
+ if (retainedPrefix !== null) {
810
751
  const prefix = {
811
- continuityContextId: sourceContext.continuityContextId ?? sourceContext.chainId,
752
+ ...retainedPrefix,
812
753
  schemaVersion: 3,
813
754
  kind: "play_call_chain",
814
755
  chainId: derivedChainId(sourceContext.chainId, "timeline-revision-prefix:" + request.operationId, request.worldId),
@@ -816,42 +757,7 @@ export class PlayCallChain {
816
757
  previousContexts: [],
817
758
  previousChainId,
818
759
  timelineGeneration: input.timelineGeneration,
819
- baselineHead: sourceContext.baselineHead,
820
- baselineHistoryLength: sourceContext.baselineHistoryLength ?? input.baseline.history.length,
821
760
  parentHead: input.restoresHead,
822
- promptRuns: playPromptRunsThroughEvents(sourceContext, input.prefixEvents),
823
- ...(sourceContext.presetFiles === undefined
824
- ? {}
825
- : { presetFiles: structuredClone(sourceContext.presetFiles) }),
826
- playPreset: structuredClone(sourceContext.playPreset),
827
- ...(sourceContext.followups === undefined
828
- ? {}
829
- : { followups: structuredClone(sourceContext.followups) }),
830
- ...(sourceContext.playPresetScriptsEnabled === undefined
831
- ? {}
832
- : {
833
- playPresetScriptsEnabled: sourceContext.playPresetScriptsEnabled,
834
- }),
835
- ...(sourceContext.modelBinding === undefined
836
- ? {}
837
- : { modelBinding: structuredClone(sourceContext.modelBinding) }),
838
- status: "ready",
839
- canRetry: false,
840
- bootstrap: structuredClone(sourceContext.bootstrap),
841
- tools: structuredClone(sourceContext.tools),
842
- transcript: prefixTranscript,
843
- events: structuredClone(input.prefixEvents),
844
- completedTools: sourceContext.completedTools
845
- .filter(({ key }) => completedKeys.has(key))
846
- .map((item) => structuredClone(item)),
847
- documentAuthorizationCheckpoints: authorizationCheckpointsThroughEvents(sourceContext, input.prefixEvents, documents.authorizationCheckpoint()),
848
- changedDocuments: changedDocumentsAtHead(sourceContext.changedDocuments, input.baseline.state, input.selected.state),
849
- nextMaterials: structuredClone(input.selected.additionalMaterials),
850
- nextEventId: Math.max(0, ...input.prefixEvents.map(({ id }) => id)) + 1,
851
- exchange: completedAssistantExchange(input.prefixEvents),
852
- lastRequest: null,
853
- lastRequestAttempt: 0,
854
- lastFailure: null,
855
761
  updatedAt: Date.now(),
856
762
  };
857
763
  await this.#worlds.playTimeline.persistDetached(structuredClone(prefix));
@@ -934,20 +840,14 @@ export class PlayCallChain {
934
840
  ? input.targetWorldId
935
841
  : input.sourceWorldId, input.sourceHead),
936
842
  ]);
937
- const selectsCompleteContext = input.sourceEvents.length === sourceContext.events.length;
938
- // A complete context already defines the selected trace closure. Preserve
939
- // its model transcript verbatim instead of reconstructing it from the
940
- // independently persisted page-event projection.
941
- const transcript = selectsCompleteContext
942
- ? structuredClone(sourceContext.transcript)
943
- : transcriptThroughEvents(sourceContext.transcript, input.sourceEvents);
944
- const completedKeys = selectsCompleteContext
945
- ? null
946
- : completedToolKeys(input.sourceEvents);
947
- const events = structuredClone(input.sourceEvents);
843
+ const traceRetention = prepareForkTrace(sourceContext, input.sourceEvents);
948
844
  const derivedBinding = input.targetBinding ??
949
845
  (await this.#worlds.bindPlayCallChain(input.targetWorldId));
950
- const derivedDocuments = restorePlayDocuments(derivedBinding.files, sourceContext, input.sourceEvents);
846
+ const retainedTrace = traceRetention.atEndpoint({
847
+ baseline,
848
+ selected,
849
+ binding: derivedBinding,
850
+ });
951
851
  const now = Date.now();
952
852
  const timelineGeneration = this.#worlds.playTimeline.newGeneration();
953
853
  let previousChainId = null;
@@ -977,7 +877,7 @@ export class PlayCallChain {
977
877
  previousChainId = chainId;
978
878
  }
979
879
  const derived = {
980
- continuityContextId: sourceContext.continuityContextId ?? sourceContext.chainId,
880
+ ...retainedTrace,
981
881
  schemaVersion: 3,
982
882
  kind: "play_call_chain",
983
883
  chainId: derivedChainId(sourceContext.chainId, input.branchIdentity, input.targetWorldId),
@@ -985,44 +885,7 @@ export class PlayCallChain {
985
885
  previousContexts: [],
986
886
  previousChainId,
987
887
  timelineGeneration,
988
- baselineHead: sourceContext.baselineHead,
989
- baselineHistoryLength: sourceContext.baselineHistoryLength ?? baseline.history.length,
990
888
  parentHead: input.sourceHead,
991
- promptRuns: playPromptRunsThroughEvents(sourceContext, events),
992
- ...(sourceContext.presetFiles === undefined
993
- ? {}
994
- : { presetFiles: structuredClone(sourceContext.presetFiles) }),
995
- playPreset: structuredClone(sourceContext.playPreset),
996
- ...(sourceContext.followups === undefined
997
- ? {}
998
- : { followups: structuredClone(sourceContext.followups) }),
999
- ...(sourceContext.playPresetScriptsEnabled === undefined
1000
- ? {}
1001
- : {
1002
- playPresetScriptsEnabled: sourceContext.playPresetScriptsEnabled,
1003
- }),
1004
- ...(sourceContext.modelBinding === undefined
1005
- ? {}
1006
- : { modelBinding: structuredClone(sourceContext.modelBinding) }),
1007
- status: "ready",
1008
- canRetry: false,
1009
- bootstrap: structuredClone(sourceContext.bootstrap),
1010
- tools: structuredClone(sourceContext.tools),
1011
- transcript,
1012
- events,
1013
- completedTools: sourceContext.completedTools
1014
- .filter(({ key }) => completedKeys === null || completedKeys.has(key))
1015
- .map((item) => structuredClone(item)),
1016
- documentAuthorizationCheckpoints: authorizationCheckpointsThroughEvents(sourceContext, events, derivedDocuments.authorizationCheckpoint()),
1017
- changedDocuments: changedDocumentsAtHead(sourceContext.changedDocuments, baseline.state, selected.state),
1018
- nextMaterials: structuredClone(derivedBinding.additionalMaterials),
1019
- nextEventId: Math.max(0, ...events.map(({ id }) => id)) + 1,
1020
- exchange: Math.max(0, ...events
1021
- .filter((event) => event.kind === "assistant" && event.status === "completed")
1022
- .map(({ exchange }) => exchange)),
1023
- lastRequest: null,
1024
- lastRequestAttempt: 0,
1025
- lastFailure: null,
1026
889
  updatedAt: now,
1027
890
  };
1028
891
  if (input.targetWorldRoot === undefined)
@@ -2123,7 +1986,11 @@ function prepareTool(session, working, exchange, call, locale) {
2123
1986
  failureKind: "protocol",
2124
1987
  markdown: "world_checkpoint requires an empty argument object.",
2125
1988
  }
2126
- : session.documents.execute(call, session.history);
1989
+ : session.documents.execute(call, session.history, {
1990
+ historyScope: session.worldId,
1991
+ locale,
1992
+ legacyHistoryTools: hasLegacyHistoryTools(currentPlayPrompt(session).tools),
1993
+ });
2127
1994
  }
2128
1995
  catch (error) {
2129
1996
  result = {
@@ -2184,53 +2051,6 @@ function mergeChangedDocuments(session, changes) {
2184
2051
  session.changedDocuments[index] = projected;
2185
2052
  }
2186
2053
  }
2187
- function restorePlayDocuments(files, context, events) {
2188
- const documents = new FileNativePlayDocuments(files);
2189
- try {
2190
- const checkpoint = documentAuthorizationThroughEvents(context, events);
2191
- const runs = playPromptRunsThroughEvents(context, events);
2192
- if (checkpoint === undefined || runs.length > 0)
2193
- documents.bindBootstrap(currentPlayPrompt({ ...context, promptRuns: runs }).bootstrap);
2194
- if (checkpoint !== undefined)
2195
- documents.restoreAuthorizationCheckpoint(checkpoint.authorization);
2196
- }
2197
- catch (error) {
2198
- throw new PlayCallChainError(`Call-chain document authorization could not be restored: ${error instanceof Error ? error.message : "invalid checkpoint"}。`);
2199
- }
2200
- return documents;
2201
- }
2202
- function documentAuthorizationThroughEvents(context, events) {
2203
- const checkpoints = context.documentAuthorizationCheckpoints;
2204
- if (checkpoints === undefined || checkpoints.length === 0)
2205
- return undefined;
2206
- const selectedEventIds = new Set(events.map(({ id }) => id));
2207
- const selected = checkpoints.findLast(({ afterEventId }) => afterEventId === 0 || selectedEventIds.has(afterEventId));
2208
- // Legacy V1 records had no durable dynamic authorization. If the selected
2209
- // prefix predates their first lazily-written checkpoint, retain the exact
2210
- // old recovery behavior and rebuild only bootstrap authorization.
2211
- return selected === undefined ? undefined : structuredClone(selected);
2212
- }
2213
- function independentContextCopy(source) {
2214
- const context = structuredClone(source);
2215
- context.continuityContextId ??= source.chainId;
2216
- delete context.derivedFrom;
2217
- delete context.branchedBeforePlayer;
2218
- return context;
2219
- }
2220
- function authorizationCheckpointsThroughEvents(context, events, fallback) {
2221
- const eventIds = new Set(events.map(({ id }) => id));
2222
- const selected = (context.documentAuthorizationCheckpoints ?? [])
2223
- .filter(({ afterEventId }) => afterEventId === 0 || eventIds.has(afterEventId))
2224
- .map((checkpoint) => structuredClone(checkpoint));
2225
- if (selected.length > 0)
2226
- return selected;
2227
- return [
2228
- {
2229
- afterEventId: Math.max(0, ...events.map(({ id }) => id)),
2230
- authorization: structuredClone(fallback),
2231
- },
2232
- ];
2233
- }
2234
2054
  function projectContext(context) {
2235
2055
  return {
2236
2056
  chainId: context.chainId,
@@ -2349,86 +2169,6 @@ function eventsThroughHead(events, head) {
2349
2169
  end += 1;
2350
2170
  return structuredClone(events.slice(0, end));
2351
2171
  }
2352
- function transcriptThroughEvents(transcript, events) {
2353
- const result = [];
2354
- let cursor = 0;
2355
- for (const [index, event] of events.entries()) {
2356
- if (event.kind === "player") {
2357
- const marker = transcript[cursor];
2358
- if (isPlayerRoundMarker(marker)) {
2359
- result.push(structuredClone(marker));
2360
- cursor += 1;
2361
- }
2362
- const item = transcript[cursor];
2363
- if (item?.kind !== "player" || item.text !== event.text)
2364
- throw new PlayCallChainError("Source call-chain events do not match the model transcript, so a fork cannot be created safely.");
2365
- result.push(structuredClone(item));
2366
- cursor += 1;
2367
- continue;
2368
- }
2369
- if (event.kind === "assistant" && event.status === "completed") {
2370
- const notice = transcript[cursor];
2371
- const hasContinuationNotice = notice?.kind === "runtime_notice" && notice.notice === "continuation";
2372
- const item = transcript[cursor + (hasContinuationNotice ? 1 : 0)];
2373
- const hasToolCall = events
2374
- .slice(index + 1)
2375
- .find((candidate) => candidate.kind === "player" ||
2376
- candidate.kind === "assistant" ||
2377
- candidate.kind === "tool_call");
2378
- const recorded = event.text.trim().length > 0 ||
2379
- hasToolCall?.kind === "tool_call" ||
2380
- item?.kind === "assistant";
2381
- if (!recorded)
2382
- continue;
2383
- if (item?.kind !== "assistant" || item.text !== event.text)
2384
- throw new PlayCallChainError("Source call-chain responses do not match the model transcript, so a fork cannot be created safely.");
2385
- // A continuation notice belongs to the generation it precedes, never
2386
- // to a fork ending at the previously completed narrative.
2387
- if (hasContinuationNotice) {
2388
- result.push(structuredClone(notice));
2389
- cursor += 1;
2390
- }
2391
- result.push(structuredClone(item));
2392
- cursor += 1;
2393
- continue;
2394
- }
2395
- if (event.kind === "tool_result") {
2396
- const item = transcript[cursor];
2397
- if (item?.kind !== "tool" || item.toolCallId !== event.callId)
2398
- throw new PlayCallChainError("Source call-chain tool results do not match the model transcript, so a fork cannot be created safely.");
2399
- result.push(structuredClone(item));
2400
- cursor += 1;
2401
- // A tool-step notice follows the entire settled batch and remains part
2402
- // of its closure even when the selected endpoint stops at that batch.
2403
- const notice = transcript[cursor];
2404
- if (notice?.kind === "runtime_notice" && notice.notice === "tool_step") {
2405
- result.push(structuredClone(notice));
2406
- cursor += 1;
2407
- }
2408
- }
2409
- }
2410
- return result;
2411
- }
2412
- function completedToolKeys(events) {
2413
- const keys = new Set();
2414
- let exchange = null;
2415
- for (const event of events) {
2416
- if (event.kind === "player")
2417
- exchange = null;
2418
- else if (event.kind === "assistant")
2419
- exchange = event.exchange;
2420
- else if (event.kind === "tool_call" && exchange !== null)
2421
- keys.add(`${exchange}:${event.callId}`);
2422
- }
2423
- return keys;
2424
- }
2425
- function changedDocumentsAtHead(changes, baseline, selected) {
2426
- const before = new Map(baseline.map((file) => [file.path, file.contents]));
2427
- const after = new Map(selected.map((file) => [file.path, file.contents]));
2428
- return changes
2429
- .filter(({ path }) => after.has(path) && before.get(path) !== after.get(path))
2430
- .map((change) => structuredClone(change));
2431
- }
2432
2172
  function derivedChainId(sourceChainId, branchIdentity, targetWorldId) {
2433
2173
  return `play-branch-${createHash("sha256")
2434
2174
  .update(`${sourceChainId}\0${branchIdentity}\0${targetWorldId}`)
@@ -2479,11 +2219,6 @@ function playerRevisionTimelineGeneration(sourceGeneration, operationId, worldId
2479
2219
  .digest("hex")
2480
2220
  .slice(0, 40)}`;
2481
2221
  }
2482
- function completedAssistantExchange(events) {
2483
- return Math.max(0, ...events
2484
- .filter((event) => event.kind === "assistant" && event.status === "completed")
2485
- .map(({ exchange }) => exchange));
2486
- }
2487
2222
  function historyEntries(history) {
2488
2223
  return Object.entries(history).map(([path, contents]) => ({
2489
2224
  path,
@@ -0,0 +1,6 @@
1
+ export class PlayCallChainError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "PlayCallChainError";
5
+ }
6
+ }
@@ -1,3 +1,5 @@
1
+ import { HistoryQuery, historyInputs } from "../history/HistoryQuery.js";
2
+ import { renderHistoryMessage, renderHistoryInjectionNotice, } from "../history/HistoryRendering.js";
1
3
  /** Narrative history contains only committed original inputs and final prose. */
2
4
  export function completedPlayerRounds(history) {
3
5
  let pending = false;
@@ -68,21 +70,24 @@ export function playerInputAppend(input) {
68
70
  { kind: "player", text: input.text },
69
71
  ];
70
72
  }
71
- export function checkpointReplayBlocks(history, checkpoint, locale) {
72
- const entries = checkpointHistory(history, checkpoint);
73
- if (entries.length === 0)
73
+ export function checkpointReplayBlocks(history, checkpoint, locale, excluded = new Set(), snapshot = new HistoryQuery("checkpoint-replay", historyInputs(Object.entries(history).map(([path, contents]) => ({ path, contents }))))) {
74
+ const eligible = checkpointHistory(history, checkpoint);
75
+ if (eligible.length === 0)
74
76
  return [];
77
+ const ids = new Set(eligible.filter(([id]) => !excluded.has(id)).map(([id]) => id));
78
+ const entries = snapshot.messages.filter(({ id }) => ids.has(id));
75
79
  const zh = locale === "zh-CN";
76
80
  return [
77
81
  {
78
82
  source: "runtime:checkpoint-history:notice",
79
- markdown: zh
83
+ markdown: (zh
80
84
  ? "# 检查点后的已提交原文\n\n以下是当前时间线上最近一次已生效检查点之后的玩家原文与最终主持叙事;尚无检查点时从世界起点选取,开场白除外。部分结果可能已写入当前文档;这些记录用于核对连续性,不代表需要再次执行其中的事件。当前明确的世界修订优先,不能用旧叙事推翻修订。"
81
- : "# Committed history after the checkpoint\n\nThese are the current timeline’s original player inputs and final narratives after its last effective checkpoint (or the world origin if none, excluding the opening). Some results may already be in current documents. These records support continuity, not repeated execution. Explicit current world corrections take precedence over old narrative.",
85
+ : "# Committed history after the checkpoint\n\nThese are the current timeline’s original player inputs and final narratives after its last effective checkpoint (or the world origin if none, excluding the opening). Some results may already be in current documents. These records support continuity, not repeated execution. Explicit current world corrections take precedence over old narrative.") +
86
+ `\n\n${renderHistoryInjectionNotice(snapshot.messages.length, entries, "checkpoint", locale)}`,
82
87
  },
83
- ...entries.map(([id, text]) => ({
84
- source: `runtime:checkpoint-history:${id}`,
85
- markdown: `## ${id.endsWith(".player") ? (zh ? "玩家原文" : "Player input") : zh ? "主持叙事" : "Host narrative"}\n\n${text}`,
88
+ ...entries.map((message) => ({
89
+ source: `runtime:checkpoint-history:${message.id}`,
90
+ markdown: renderHistoryMessage(message, locale),
86
91
  })),
87
92
  ];
88
93
  }