@cjhyy/code-shell-core 0.9.6 → 0.9.7

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 (55) hide show
  1. package/dist/automation/scheduler.d.ts +3 -0
  2. package/dist/automation/scheduler.js +21 -0
  3. package/dist/context/manager.d.ts +8 -2
  4. package/dist/context/manager.js +20 -4
  5. package/dist/context/notes.d.ts +39 -0
  6. package/dist/context/notes.js +314 -0
  7. package/dist/engine/engine.d.ts +5 -4
  8. package/dist/engine/engine.js +79 -11
  9. package/dist/engine/run-tooling.d.ts +3 -0
  10. package/dist/engine/run-tooling.js +25 -23
  11. package/dist/engine/run-types.d.ts +4 -0
  12. package/dist/engine/subagent-spawner.d.ts +3 -0
  13. package/dist/engine/subagent-spawner.js +48 -17
  14. package/dist/engine/turn-loop.d.ts +10 -0
  15. package/dist/engine/turn-loop.js +91 -19
  16. package/dist/engine/types.d.ts +19 -0
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/prompt/section-loader.js +1 -0
  20. package/dist/prompt/sections/browser.md +4 -2
  21. package/dist/prompt/sections/context-notes.md +9 -0
  22. package/dist/protocol/server.d.ts +1 -0
  23. package/dist/protocol/server.js +207 -19
  24. package/dist/protocol/types.d.ts +2 -0
  25. package/dist/session/session-manager.js +8 -7
  26. package/dist/session/transcript.d.ts +17 -0
  27. package/dist/session/transcript.js +271 -16
  28. package/dist/settings/schema.d.ts +9 -0
  29. package/dist/settings/schema.js +4 -0
  30. package/dist/themes/paths.js +20 -1
  31. package/dist/tool-system/browser-bridge.d.ts +3 -1
  32. package/dist/tool-system/browser-discovery.d.ts +6 -0
  33. package/dist/tool-system/browser-discovery.js +17 -0
  34. package/dist/tool-system/builtin/browser-tools.js +12 -8
  35. package/dist/tool-system/builtin/context-notes.d.ts +12 -0
  36. package/dist/tool-system/builtin/context-notes.js +188 -0
  37. package/dist/tool-system/builtin/index.js +47 -0
  38. package/dist/tool-system/builtin/mcp-tools.d.ts +5 -3
  39. package/dist/tool-system/builtin/mcp-tools.js +10 -10
  40. package/dist/tool-system/builtin/tool-search.js +15 -3
  41. package/dist/tool-system/context.d.ts +9 -0
  42. package/dist/tool-system/executor.js +5 -3
  43. package/dist/tool-system/mcp-compat.d.ts +3 -0
  44. package/dist/tool-system/mcp-compat.js +51 -0
  45. package/dist/tool-system/mcp-manager.d.ts +27 -26
  46. package/dist/tool-system/mcp-manager.js +273 -111
  47. package/dist/tool-system/mcp-workspace.d.ts +18 -0
  48. package/dist/tool-system/mcp-workspace.js +56 -0
  49. package/dist/tool-system/permission.d.ts +6 -0
  50. package/dist/tool-system/permission.js +45 -9
  51. package/dist/tool-system/plan-mode-allowlist.js +5 -0
  52. package/dist/tool-system/sandbox/seatbelt.js +71 -2
  53. package/dist/tool-system/session-tool-host.js +9 -1
  54. package/dist/types.d.ts +4 -2
  55. package/package.json +1 -1
@@ -21,8 +21,9 @@ import { sessionMessageOutcome, sessionMessageFailure, sessionMessageResultNotif
21
21
  import { resolveSessionMessageWorkspace } from "./session-message-workspace.js";
22
22
  import { validateWorkspaceContext, workspacePrimaryRoot } from "../workspace/workspace-context.js";
23
23
  import { canonicalKey } from "../workspace/canonical-key.js";
24
- import { getApprovalRouter, getInteractiveApprovalBackend, } from "../tool-system/permission.js";
24
+ import { getApprovalRouter, getInteractiveApprovalBackend, InteractiveApprovalBackend, } from "../tool-system/permission.js";
25
25
  import { agentNotificationBus, notificationQueue, notificationEnvelopeToLegacyStreamEvent, } from "../tool-system/builtin/agent-notifications.js";
26
+ import { clearSessionPathApprovals, openSessionPathApprovals } from "../tool-system/path-policy.js";
26
27
  import { backgroundShellManager } from "../runtime/background-shell.js";
27
28
  import { backgroundJobRegistry } from "../tool-system/builtin/background-jobs.js";
28
29
  import { listBackgroundWorkForUI } from "../tool-system/builtin/background-work.js";
@@ -172,6 +173,7 @@ const COMPACT_STREAM_STRATEGIES = new Set([
172
173
  // Range archival (archive_range query) emits its boundary through this map;
173
174
  // without "range" here it would be silently downgraded to "compacted".
174
175
  "range",
176
+ "notes",
175
177
  ]);
176
178
  function toCompactStreamStrategy(strategy) {
177
179
  return COMPACT_STREAM_STRATEGIES.has(strategy)
@@ -322,6 +324,7 @@ export class AgentServer {
322
324
  panelBridgeEnabled;
323
325
  connectionId;
324
326
  strictApprovalRouting;
327
+ childHostDisposers = new Map();
325
328
  approvalRouter;
326
329
  approvalConnectionUnregister = null;
327
330
  disconnected = false;
@@ -674,6 +677,151 @@ export class AgentServer {
674
677
  session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
675
678
  session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
676
679
  session.engine.setInjectCredential((credentialId, credentialScope) => this.requestCredentialInjectForSession(session, sid, credentialId, credentialScope));
680
+ session.engine.setChildHostBindings?.((input) => {
681
+ if (input.parentSessionId !== sid)
682
+ throw new Error("child host parent identity mismatch");
683
+ assertSafeSessionId(input.sessionId);
684
+ const route = this.approvalRouter.current(sid);
685
+ if (!route || route.connectionId !== this.connectionId) {
686
+ throw new Error("child host unavailable: parent approval connection no longer owns session");
687
+ }
688
+ const backend = new InteractiveApprovalBackend();
689
+ backend.openSession(input.sessionId);
690
+ let active = false;
691
+ let closed = false;
692
+ let closedFailure = {
693
+ failure: "cancelled",
694
+ reason: "child run ended or was cancelled",
695
+ };
696
+ const scope = {
697
+ sourceSessionId: input.sessionId,
698
+ bindingId: nanoid(),
699
+ route,
700
+ pending: new Set(),
701
+ failure: () => {
702
+ if (closed || input.signal?.aborted)
703
+ return closedFailure;
704
+ if (this.chatManager?.get(sid) !== session)
705
+ return { failure: "session_closed", reason: "parent host session closed" };
706
+ if (this.disconnected || !this.approvalRouter.matches(route))
707
+ return { failure: "owner_lost", reason: "parent host approval ownership changed" };
708
+ return undefined;
709
+ },
710
+ };
711
+ backend.setRequestGuard((request) => {
712
+ const failure = scope.failure();
713
+ if (failure)
714
+ return { approved: false, ...failure };
715
+ if (request.sessionId !== input.sessionId)
716
+ return {
717
+ approved: false,
718
+ failure: "owner_lost",
719
+ reason: "child approval identity mismatch",
720
+ };
721
+ return undefined;
722
+ });
723
+ backend.setPromptFn((request) => {
724
+ const failure = scope.failure();
725
+ if (failure)
726
+ return Promise.resolve({ approved: false, ...failure });
727
+ if (request.sessionId !== input.sessionId) {
728
+ return Promise.resolve({
729
+ approved: false,
730
+ failure: "owner_lost",
731
+ reason: "child approval identity mismatch",
732
+ });
733
+ }
734
+ return this.requestApprovalFromClient(request, route, scope);
735
+ });
736
+ const dispose = () => {
737
+ if (closed)
738
+ return;
739
+ closed = true;
740
+ input.signal?.removeEventListener("abort", dispose);
741
+ this.childHostDisposers.delete(scope.bindingId);
742
+ backend.clearSession(input.sessionId);
743
+ if (active)
744
+ clearSessionPathApprovals(input.sessionId);
745
+ for (const id of scope.pending) {
746
+ const entry = this.takeSessionApproval(session, id, "cancelled");
747
+ this.clearApprovalTimer(id);
748
+ this.pendingApprovalTargets.delete(id);
749
+ entry?.resolve(entry.metadata.kind === "internal"
750
+ ? internalCancellation(closedFailure.failure, closedFailure.reason)
751
+ : {
752
+ approved: false,
753
+ ...closedFailure,
754
+ });
755
+ try {
756
+ this.notify(Methods.ApprovalResolved, { sessionId: sid, requestId: id });
757
+ }
758
+ catch {
759
+ /* transport may already be closed */
760
+ }
761
+ }
762
+ scope.pending.clear();
763
+ // One-way release uses the existing private host channel; it is never
764
+ // a model tool and never inherits a parent tab handoff grant.
765
+ try {
766
+ this.notify(Methods.ApprovalRequest, {
767
+ sessionId: sid,
768
+ requestId: nanoid(12),
769
+ childHost: {
770
+ sourceSessionId: input.sessionId,
771
+ bindingId: scope.bindingId,
772
+ release: true,
773
+ },
774
+ request: {
775
+ toolName: "__browser_action__",
776
+ args: { action: "release" },
777
+ description: "release child browser",
778
+ riskLevel: "low",
779
+ },
780
+ });
781
+ }
782
+ catch {
783
+ /* resource owner also closes targets on host teardown */
784
+ }
785
+ };
786
+ this.childHostDisposers.set(scope.bindingId, {
787
+ route,
788
+ dispose: (reason) => {
789
+ closedFailure = { failure: "owner_lost", reason };
790
+ dispose();
791
+ },
792
+ });
793
+ input.signal?.addEventListener("abort", dispose, { once: true });
794
+ if (input.signal?.aborted)
795
+ dispose();
796
+ return {
797
+ activate: () => {
798
+ if (!closed && !active) {
799
+ this.notify(Methods.ApprovalRequest, {
800
+ sessionId: sid,
801
+ requestId: nanoid(12),
802
+ childHost: {
803
+ sourceSessionId: input.sessionId,
804
+ bindingId: scope.bindingId,
805
+ activate: true,
806
+ },
807
+ request: {
808
+ toolName: "__browser_action__",
809
+ args: { action: "activate" },
810
+ description: "bind child browser",
811
+ riskLevel: "low",
812
+ },
813
+ });
814
+ active = true;
815
+ openSessionPathApprovals(input.sessionId);
816
+ }
817
+ },
818
+ approvalBackend: backend,
819
+ browserBridge: this.makeBrowserBridge(session, sid, scope),
820
+ askUser: (question, opts) => this.requestAskUserForSession(session, sid, question, opts, scope),
821
+ injectCredentialToBrowser: (id, credentialScope) => this.requestCredentialInjectForSession(session, sid, id, credentialScope, scope),
822
+ dispose,
823
+ };
824
+ });
677
825
  session.engine.setSessionMessageRouter((input) => this.routeSessionMessage(input));
678
826
  if (this.workspaceBridgeEnabled && typeof session.engine.setWorkspaceBridge === "function") {
679
827
  session.engine.setWorkspaceBridge(this.makeWorkspaceBridge(session, sid));
@@ -1591,7 +1739,7 @@ export class AgentServer {
1591
1739
  }
1592
1740
  this.abortController.abort();
1593
1741
  for (const [, resolve] of this.pendingApprovals) {
1594
- resolve({ approved: false, reason: "cancelled" });
1742
+ resolve({ approved: false, failure: "cancelled", reason: "cancelled" });
1595
1743
  }
1596
1744
  this.pendingApprovals.clear();
1597
1745
  this.clearAllApprovalTimers();
@@ -2795,7 +2943,10 @@ export class AgentServer {
2795
2943
  /**
2796
2944
  * Ask the client to approve a tool operation (legacy single-engine path).
2797
2945
  */
2798
- requestApprovalFromClient(request, route) {
2946
+ requestApprovalFromClient(request, route, scope) {
2947
+ const failure = scope?.failure();
2948
+ if (failure)
2949
+ return Promise.resolve({ approved: false, ...failure });
2799
2950
  return new Promise((resolve) => {
2800
2951
  const requestId = nanoid(12);
2801
2952
  const effectiveRoute = route ?? {
@@ -2806,7 +2957,11 @@ export class AgentServer {
2806
2957
  const sessionId = effectiveRoute.sessionId;
2807
2958
  const session = this.chatManager && sessionId ? this.chatManager.get(sessionId) : undefined;
2808
2959
  if (this.chatManager && sessionId && !session) {
2809
- resolve({ approved: false, reason: `session closed: ${sessionId}` });
2960
+ resolve({
2961
+ approved: false,
2962
+ failure: "session_closed",
2963
+ reason: `session closed: ${sessionId}`,
2964
+ });
2810
2965
  return;
2811
2966
  }
2812
2967
  if (session) {
@@ -2824,7 +2979,7 @@ export class AgentServer {
2824
2979
  createdAt: Date.now(),
2825
2980
  expiresAt: Date.now() + AgentServer.APPROVAL_TIMEOUT_MS,
2826
2981
  surfaceable: !internal,
2827
- }, (decision) => resolve(decision));
2982
+ }, (decision) => resolve(decision), scope);
2828
2983
  }
2829
2984
  else {
2830
2985
  this.pendingApprovals.set(requestId, resolve);
@@ -2841,7 +2996,8 @@ export class AgentServer {
2841
2996
  }
2842
2997
  this.pendingApprovalTargets.delete(requestId);
2843
2998
  this.approvalTimers.delete(requestId);
2844
- resolve({ approved: false, reason: "approval timed out" });
2999
+ scope?.pending.delete(requestId);
3000
+ resolve({ approved: false, failure: "timed_out", reason: "approval timed out" });
2845
3001
  }
2846
3002
  }, AgentServer.APPROVAL_TIMEOUT_MS);
2847
3003
  this.approvalTimers.set(requestId, timer);
@@ -2869,7 +3025,10 @@ export class AgentServer {
2869
3025
  * renderer routes the question to the right chat tab. This intentionally has
2870
3026
  * no wall-clock timeout; Stop/cancel drains the pending ask.
2871
3027
  */
2872
- requestAskUserForSession(session, sessionId, question, opts) {
3028
+ requestAskUserForSession(session, sessionId, question, opts, scope) {
3029
+ const failure = scope?.failure();
3030
+ if (failure)
3031
+ return Promise.resolve(`(question ${failure.failure}: ${failure.reason})`);
2873
3032
  return new Promise((resolve) => {
2874
3033
  const requestId = nanoid(12);
2875
3034
  const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
@@ -2894,7 +3053,7 @@ export class AgentServer {
2894
3053
  // chatManager approve handler resolves with the raw decision value.
2895
3054
  resolve(typeof decision === "string" ? decision : "");
2896
3055
  }
2897
- });
3056
+ }, scope);
2898
3057
  const args = { question };
2899
3058
  if (opts?.header !== undefined)
2900
3059
  args.header = opts.header;
@@ -2906,6 +3065,7 @@ export class AgentServer {
2906
3065
  args.optionsOnly = opts.optionsOnly;
2907
3066
  this.notify(Methods.ApprovalRequest, {
2908
3067
  ...routeEnvelope,
3068
+ ...(scope ? { sourceSessionId: scope.sourceSessionId } : {}),
2909
3069
  request: {
2910
3070
  toolName: "__ask_user__",
2911
3071
  args,
@@ -2953,8 +3113,8 @@ export class AgentServer {
2953
3113
  * result string we parse back into the typed result. On timeout / malformed
2954
3114
  * reply we degrade to a safe error result rather than throwing.
2955
3115
  */
2956
- makeBrowserBridge(session, sessionId) {
2957
- const call = (action, payload) => this.requestBrowserActionForSession(session, sessionId, action, payload);
3116
+ makeBrowserBridge(session, sessionId, scope) {
3117
+ const call = (action, payload) => this.requestBrowserActionForSession(session, sessionId, action, payload, scope);
2958
3118
  return {
2959
3119
  requestHumanTakeover: () => call("requestTakeover", {}),
2960
3120
  snapshot: () => call("snapshot", {}),
@@ -2974,7 +3134,10 @@ export class AgentServer {
2974
3134
  switchTab: (tabId) => call("switchTab", { tabId }),
2975
3135
  };
2976
3136
  }
2977
- requestBrowserActionForSession(session, sessionId, action, payload) {
3137
+ requestBrowserActionForSession(session, sessionId, action, payload, scope) {
3138
+ const failure = scope?.failure();
3139
+ if (failure)
3140
+ return Promise.resolve({ ok: false, failure: failure.failure, detail: failure.reason });
2978
3141
  return new Promise((resolve) => {
2979
3142
  const requestId = nanoid(12);
2980
3143
  const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
@@ -2986,12 +3149,13 @@ export class AgentServer {
2986
3149
  resolve(parsed.ok
2987
3150
  ? parsed.value
2988
3151
  : { ok: false, failure: parsed.failure, detail: parsed.detail });
2989
- });
3152
+ }, scope);
2990
3153
  const timer = setTimeout(() => {
2991
3154
  if (session.pendingApprovals.has(requestId)) {
2992
3155
  this.takeSessionApproval(session, requestId, "expired");
2993
3156
  this.pendingApprovalTargets.delete(requestId);
2994
3157
  this.approvalTimers.delete(requestId);
3158
+ scope?.pending.delete(requestId);
2995
3159
  resolve({
2996
3160
  ok: false,
2997
3161
  failure: "timed_out",
@@ -3002,6 +3166,9 @@ export class AgentServer {
3002
3166
  this.approvalTimers.set(requestId, timer);
3003
3167
  this.notify(Methods.ApprovalRequest, {
3004
3168
  ...routeEnvelope,
3169
+ ...(scope
3170
+ ? { childHost: { sourceSessionId: scope.sourceSessionId, bindingId: scope.bindingId } }
3171
+ : {}),
3005
3172
  request: {
3006
3173
  toolName: "__browser_action__",
3007
3174
  args: { action, ...payload },
@@ -3017,7 +3184,10 @@ export class AgentServer {
3017
3184
  * calls restoreCookiesToBrowser and replies with a JSON result string we
3018
3185
  * parse into { ok, count?, error? }. Degrades to ok:false on timeout/malformed.
3019
3186
  */
3020
- requestCredentialInjectForSession(session, sessionId, credentialId, credentialScope = "full") {
3187
+ requestCredentialInjectForSession(session, sessionId, credentialId, credentialScope = "full", scope) {
3188
+ const failure = scope?.failure();
3189
+ if (failure)
3190
+ return Promise.resolve({ ok: false, error: `${failure.failure}: ${failure.reason}` });
3021
3191
  return new Promise((resolve) => {
3022
3192
  const requestId = nanoid(12);
3023
3193
  const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
@@ -3027,12 +3197,13 @@ export class AgentServer {
3027
3197
  resolve(parsed.ok
3028
3198
  ? parsed.value
3029
3199
  : { ok: false, error: parsed.detail });
3030
- });
3200
+ }, scope);
3031
3201
  const timer = setTimeout(() => {
3032
3202
  if (session.pendingApprovals.has(requestId)) {
3033
3203
  this.takeSessionApproval(session, requestId, "expired");
3034
3204
  this.pendingApprovalTargets.delete(requestId);
3035
3205
  this.approvalTimers.delete(requestId);
3206
+ scope?.pending.delete(requestId);
3036
3207
  resolve({
3037
3208
  ok: false,
3038
3209
  error: `credential inject ${HOST_LOOPBACK_FAILURE_DETAIL.timed_out}`,
@@ -3042,6 +3213,7 @@ export class AgentServer {
3042
3213
  this.approvalTimers.set(requestId, timer);
3043
3214
  this.notify(Methods.ApprovalRequest, {
3044
3215
  ...routeEnvelope,
3216
+ ...(scope ? { sourceSessionId: scope.sourceSessionId } : {}),
3045
3217
  request: {
3046
3218
  toolName: "__credential_action__",
3047
3219
  args: { action: "injectCookie", credentialId, credentialScope },
@@ -3331,7 +3503,7 @@ export class AgentServer {
3331
3503
  }
3332
3504
  }
3333
3505
  for (const [, resolve] of this.pendingApprovals) {
3334
- resolve({ approved: false, reason: "server closing" });
3506
+ resolve({ approved: false, failure: "session_closed", reason: "server closing" });
3335
3507
  }
3336
3508
  this.pendingApprovals.clear();
3337
3509
  this.clearAllApprovalTimers();
@@ -3364,9 +3536,19 @@ export class AgentServer {
3364
3536
  workerGeneration() {
3365
3537
  return this.chatManager?.getLiveSessionSnapshot().generation ?? 0;
3366
3538
  }
3367
- registerSessionApproval(session, metadata, resolve) {
3539
+ registerSessionApproval(session, metadata, resolve, scope) {
3540
+ if (scope) {
3541
+ metadata = { ...metadata, sourceSessionId: scope.sourceSessionId };
3542
+ scope.pending.add(metadata.requestId);
3543
+ }
3368
3544
  metadata = this.observeApprovalCreated(metadata);
3369
- session.pendingApprovals.set(metadata.requestId, { resolve, metadata });
3545
+ session.pendingApprovals.set(metadata.requestId, {
3546
+ resolve: (decision) => {
3547
+ scope?.pending.delete(metadata.requestId);
3548
+ resolve(decision);
3549
+ },
3550
+ metadata,
3551
+ });
3370
3552
  }
3371
3553
  takeSessionApproval(session, requestId, status) {
3372
3554
  const entry = session.pendingApprovals.get(requestId);
@@ -3446,7 +3628,7 @@ export class AgentServer {
3446
3628
  try {
3447
3629
  entry.resolve(entry.metadata.kind === "internal"
3448
3630
  ? internalCancellation(failure, reason)
3449
- : { approved: false, reason });
3631
+ : { approved: false, failure, reason });
3450
3632
  }
3451
3633
  catch {
3452
3634
  /* a resolver must never break cancel cleanup */
@@ -3457,6 +3639,12 @@ export class AgentServer {
3457
3639
  }
3458
3640
  failClosedApprovalTargets(targets, reason) {
3459
3641
  for (const target of targets) {
3642
+ for (const child of this.childHostDisposers.values()) {
3643
+ if (child.route.connectionId === target.connectionId &&
3644
+ child.route.sessionId === target.sessionId &&
3645
+ child.route.generation === target.generation)
3646
+ child.dispose(reason);
3647
+ }
3460
3648
  const session = this.chatManager?.get(target.sessionId);
3461
3649
  if (session) {
3462
3650
  this.cancelSessionApprovals(session, reason, "owner-lost", "owner_lost");
@@ -3472,7 +3660,7 @@ export class AgentServer {
3472
3660
  this.pendingApprovals.delete(requestId);
3473
3661
  this.pendingApprovalTargets.delete(requestId);
3474
3662
  this.clearApprovalTimer(requestId);
3475
- resolve?.({ approved: false, reason });
3663
+ resolve?.({ approved: false, failure: "owner_lost", reason });
3476
3664
  }
3477
3665
  }
3478
3666
  }
@@ -13,6 +13,8 @@ import type { SessionMessageTarget } from "../session/session-message.js";
13
13
  export type PendingApprovalKind = "tool_approval" | "ask_user" | "internal";
14
14
  /** Resolver-free metadata retained beside a pending approval callback. */
15
15
  export interface PendingApprovalMetadata {
16
+ /** Child task that originated this request; sessionId remains its parent UI route. */
17
+ sourceSessionId?: string;
16
18
  sessionId: string;
17
19
  requestId: string;
18
20
  routeGeneration?: number;
@@ -160,6 +160,8 @@ const FORK_EVENT_POLICY = {
160
160
  tool_result: "copy",
161
161
  summary: "copy",
162
162
  context_transfer: "copy",
163
+ context_note: "copy",
164
+ context_checkpoint: "copy",
163
165
  range_archive: "copy",
164
166
  content_replace: "copy",
165
167
  file_history: "skip",
@@ -1922,13 +1924,12 @@ export function buildForkTranscript(sourceEvents, state) {
1922
1924
  forkedFrom: structuredClone(state.forkedFrom),
1923
1925
  },
1924
1926
  };
1925
- return [
1926
- meta,
1927
- ...sourceEvents.map((source) => ({
1928
- ...structuredClone(source),
1929
- id: nanoid(12),
1930
- })),
1931
- ];
1927
+ const copied = sourceEvents.map((source) => ({
1928
+ ...structuredClone(source),
1929
+ id: nanoid(12),
1930
+ }));
1931
+ Transcript.remapContextForkReferences(sourceEvents, copied);
1932
+ return [meta, ...copied];
1932
1933
  }
1933
1934
  function validateForkToolPairs(events) {
1934
1935
  const projectedUses = [];
@@ -27,6 +27,16 @@ export interface SelectedContextRange {
27
27
  messages: Message[];
28
28
  sourceEventCount: number;
29
29
  }
30
+ /** Host-generated replay state. Models supply the note text, never this snapshot. */
31
+ export interface ContextCheckpointSnapshot {
32
+ version: 1;
33
+ noteId: string;
34
+ coveredThroughEventId: string;
35
+ messages: Message[];
36
+ clientMessageIds: Array<[string, number]>;
37
+ }
38
+ /** Complete, ordered provider pairs are required before a checkpoint can commit. */
39
+ export declare function hasCompleteContextToolPairs(messages: readonly Message[]): boolean;
30
40
  export type SummaryAppendMetadata = {
31
41
  fromTurn: number;
32
42
  toTurn: number;
@@ -59,8 +69,14 @@ export declare class Transcript {
59
69
  static inMemory(label: string): Transcript;
60
70
  /** Rehydrate a process-local fork without serializing its copied history. */
61
71
  static fromMemoryEvents(label: string, events: readonly TranscriptEvent[]): Transcript;
72
+ /** Preserve checkpoint provenance when a fork assigns fresh event ids. */
73
+ static remapContextForkReferences(sourceEvents: readonly TranscriptEvent[], copiedEvents: TranscriptEvent[]): void;
62
74
  isPersistent(): boolean;
63
75
  append(type: TranscriptEventType, data: Record<string, unknown>): TranscriptEvent;
76
+ /** Failed note/checkpoint writes must never change the active replay. */
77
+ appendContextNote(text: string, coveredThroughEventId: string): TranscriptEvent | undefined;
78
+ appendContextCheckpoint(snapshot: ContextCheckpointSnapshot): TranscriptEvent | undefined;
79
+ private appendDurableContextEvent;
64
80
  /**
65
81
  * Append a chat message to the transcript.
66
82
  *
@@ -148,6 +164,7 @@ export declare class Transcript {
148
164
  messages: Message[];
149
165
  liveIndexByClientMessageId: Map<string, number>;
150
166
  };
167
+ private contextReplayEvents;
151
168
  getEvents(type?: TranscriptEventType): TranscriptEvent[];
152
169
  get turnNumber(): number;
153
170
  get eventCount(): number;