@gajae-code/coding-agent 0.12.21 → 0.13.0

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 (222) hide show
  1. package/CHANGELOG.md +67 -2
  2. package/dist/types/cli/args.d.ts +5 -2
  3. package/dist/types/cli/root-flags.d.ts +246 -0
  4. package/dist/types/cli.d.ts +11 -19
  5. package/dist/types/commands/daemon.d.ts +0 -30
  6. package/dist/types/commands/launch.d.ts +11 -19
  7. package/dist/types/commands/quick-lane.d.ts +32 -0
  8. package/dist/types/commands/sdk.d.ts +21 -15
  9. package/dist/types/config/file-lock-gc.d.ts +2 -0
  10. package/dist/types/config/model-equivalence.d.ts +20 -0
  11. package/dist/types/config/model-profile-activation.d.ts +62 -17
  12. package/dist/types/config/model-profiles.d.ts +10 -0
  13. package/dist/types/config/model-registry.d.ts +68 -1
  14. package/dist/types/config/model-resolver.d.ts +15 -4
  15. package/dist/types/config/models-config-schema.d.ts +7 -2
  16. package/dist/types/config/provider-selection-policy.d.ts +62 -0
  17. package/dist/types/config/settings-schema.d.ts +36 -78
  18. package/dist/types/config/settings.d.ts +2 -0
  19. package/dist/types/edit/renderer.d.ts +5 -2
  20. package/dist/types/edit/streaming.d.ts +13 -0
  21. package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
  22. package/dist/types/extensibility/extensions/compact-handler.d.ts +2 -1
  23. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  24. package/dist/types/gjc-runtime/gc-runtime.d.ts +19 -5
  25. package/dist/types/internal-urls/local-protocol.d.ts +1 -0
  26. package/dist/types/modes/components/model-selector.d.ts +0 -2
  27. package/dist/types/modes/components/provider-order-context.d.ts +45 -0
  28. package/dist/types/modes/components/provider-order-editor.d.ts +24 -0
  29. package/dist/types/modes/components/settings-defs.d.ts +5 -1
  30. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  31. package/dist/types/quick-lane/classify.d.ts +40 -0
  32. package/dist/types/runtime/stt-settings-setup.d.ts +18 -0
  33. package/dist/types/sdk/broker/broker.d.ts +56 -2
  34. package/dist/types/sdk/broker/operation-receipt-ledger.d.ts +25 -0
  35. package/dist/types/sdk/broker/session-index.d.ts +83 -2
  36. package/dist/types/sdk/broker/state-version.d.ts +1 -1
  37. package/dist/types/sdk/bus/notification-service.d.ts +6 -0
  38. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +8 -1
  39. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +26 -0
  41. package/dist/types/sdk/cli/rows.d.ts +98 -0
  42. package/dist/types/sdk/cli/session-cli.d.ts +35 -3
  43. package/dist/types/sdk/elevation/allowlist.d.ts +20 -0
  44. package/dist/types/sdk/elevation/audit.d.ts +93 -0
  45. package/dist/types/sdk/elevation/capability.d.ts +3 -0
  46. package/dist/types/sdk/elevation/digest.d.ts +72 -0
  47. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +85 -0
  48. package/dist/types/sdk/elevation/grant-ledger.d.ts +163 -0
  49. package/dist/types/sdk/elevation/owner.d.ts +48 -0
  50. package/dist/types/sdk/guides/cache.d.ts +98 -0
  51. package/dist/types/sdk/guides/catalog.d.ts +156 -0
  52. package/dist/types/sdk/guides/cli.d.ts +39 -0
  53. package/dist/types/sdk/guides/manifest.d.ts +66 -0
  54. package/dist/types/sdk/guides/verify.d.ts +66 -0
  55. package/dist/types/sdk/host/control/dispatch.d.ts +8 -0
  56. package/dist/types/sdk/host/control/index.d.ts +1 -1
  57. package/dist/types/sdk/host/control/operations.d.ts +3 -2
  58. package/dist/types/sdk/host/query/cursor.d.ts +10 -0
  59. package/dist/types/sdk/host/query/handlers.d.ts +21 -0
  60. package/dist/types/sdk/host/session-runtime.d.ts +9 -0
  61. package/dist/types/sdk/mcp/server.d.ts +2 -2
  62. package/dist/types/sdk/prompt-status.d.ts +11 -0
  63. package/dist/types/sdk/session.d.ts +4 -0
  64. package/dist/types/sdk/transport/index.d.ts +1 -0
  65. package/dist/types/sdk/transport/relay.d.ts +1 -0
  66. package/dist/types/sdk/transport/serve-cli.d.ts +20 -1
  67. package/dist/types/session/agent-session.d.ts +32 -18
  68. package/dist/types/session/blob-store.d.ts +13 -7
  69. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  70. package/dist/types/session/internal/managed-session-storage.d.ts +25 -1
  71. package/dist/types/session/internal/session-memory-sidecar.d.ts +5 -5
  72. package/dist/types/session/session-manager.d.ts +139 -10
  73. package/dist/types/session/session-storage.d.ts +48 -0
  74. package/dist/types/setup/provider-onboarding.d.ts +7 -1
  75. package/dist/types/slash-commands/builtin-registry.d.ts +14 -0
  76. package/dist/types/stt/setup.d.ts +2 -0
  77. package/dist/types/task/executor.d.ts +10 -2
  78. package/dist/types/tools/browser/runtime-diagnostics.d.ts +65 -0
  79. package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
  80. package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
  81. package/dist/types/tools/browser/tab-worker.d.ts +2 -0
  82. package/dist/types/tools/browser.d.ts +2 -0
  83. package/dist/types/tools/image-gen.d.ts +14 -18
  84. package/dist/types/tools/index.d.ts +2 -0
  85. package/dist/types/tools/resource-gc.d.ts +2 -0
  86. package/dist/types/utils/pasted-image-path.d.ts +6 -0
  87. package/package.json +9 -9
  88. package/scripts/generate-sdk-operation-inventory.ts +8 -0
  89. package/scripts/run-test-manifest.ts +61 -21
  90. package/scripts/verify-gjc-sdk-canonicalization.ts +9 -4
  91. package/src/cli/args.ts +62 -9
  92. package/src/cli/completion-cli.ts +1 -0
  93. package/src/cli/fast-help.ts +1 -0
  94. package/src/cli/root-flags.ts +100 -0
  95. package/src/cli/setup-cli.ts +6 -2
  96. package/src/cli.ts +42 -65
  97. package/src/commands/acp.ts +1 -1
  98. package/src/commands/daemon.ts +2 -31
  99. package/src/commands/gc.ts +1 -1
  100. package/src/commands/launch.ts +8 -129
  101. package/src/commands/quick-lane.ts +51 -0
  102. package/src/commands/sdk.ts +200 -26
  103. package/src/commands/setup.ts +4 -2
  104. package/src/config/file-lock-gc.ts +4 -1
  105. package/src/config/model-equivalence.ts +76 -7
  106. package/src/config/model-profile-activation.ts +1115 -265
  107. package/src/config/model-profiles.ts +134 -2
  108. package/src/config/model-registry.ts +352 -50
  109. package/src/config/model-resolver.ts +289 -38
  110. package/src/config/models-config-schema.ts +11 -4
  111. package/src/config/provider-selection-policy.ts +146 -0
  112. package/src/config/settings-schema.ts +50 -77
  113. package/src/config/settings.ts +40 -0
  114. package/src/coordinator-mcp/server.ts +32 -2
  115. package/src/edit/renderer.ts +175 -223
  116. package/src/edit/streaming.ts +108 -1
  117. package/src/export/html/index.ts +57 -7
  118. package/src/extensibility/custom-tools/types.ts +2 -0
  119. package/src/extensibility/extensions/compact-handler.ts +3 -2
  120. package/src/extensibility/extensions/runner.ts +6 -0
  121. package/src/extensibility/extensions/types.ts +3 -0
  122. package/src/gjc-runtime/gc-runtime.ts +379 -20
  123. package/src/gjc-runtime/launch-worktree.ts +5 -1
  124. package/src/gjc-runtime/team-runtime.ts +59 -2
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +19 -0
  126. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +16 -1
  127. package/src/internal-urls/docs-index.generated.ts +17 -13
  128. package/src/internal-urls/local-protocol.ts +1 -0
  129. package/src/main.ts +40 -3
  130. package/src/memories/index.ts +4 -2
  131. package/src/modes/acp/acp-agent.ts +235 -91
  132. package/src/modes/components/model-selector.ts +206 -42
  133. package/src/modes/components/provider-order-context.ts +197 -0
  134. package/src/modes/components/provider-order-editor.ts +441 -0
  135. package/src/modes/components/settings-defs.ts +21 -1
  136. package/src/modes/components/settings-selector.ts +45 -2
  137. package/src/modes/components/tool-execution.ts +47 -15
  138. package/src/modes/controllers/command-controller.ts +6 -3
  139. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  140. package/src/modes/controllers/input-controller.ts +73 -2
  141. package/src/modes/controllers/selector-controller.ts +260 -153
  142. package/src/modes/prompt-suggestion-controller.ts +1 -1
  143. package/src/modes/runtime-init.ts +1 -0
  144. package/src/modes/utils/ui-helpers.ts +18 -0
  145. package/src/prompts/tools/browser.md +1 -0
  146. package/src/quick-lane/classify.ts +257 -0
  147. package/src/rlm/index.ts +1 -1
  148. package/src/runtime/stt-settings-setup.ts +57 -0
  149. package/src/sdk/acp/adapter.ts +2 -1
  150. package/src/sdk/broker/broker.ts +780 -12
  151. package/src/sdk/broker/lifecycle.ts +70 -29
  152. package/src/sdk/broker/operation-receipt-ledger.ts +144 -0
  153. package/src/sdk/broker/session-index.ts +463 -48
  154. package/src/sdk/broker/state-version.ts +13 -9
  155. package/src/sdk/broker/transport.ts +14 -1
  156. package/src/sdk/bus/index.ts +34 -13
  157. package/src/sdk/bus/kind-aware-reconciliation.ts +21 -2
  158. package/src/sdk/bus/notification-service.ts +47 -1
  159. package/src/sdk/bus/prompt-reconciliation.ts +22 -3
  160. package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
  161. package/src/sdk/bus/telegram-daemon.ts +54 -3
  162. package/src/sdk/cli/rows.ts +247 -0
  163. package/src/sdk/cli/session-cli.ts +930 -79
  164. package/src/sdk/elevation/allowlist.ts +54 -0
  165. package/src/sdk/elevation/audit.ts +208 -0
  166. package/src/sdk/elevation/capability.ts +41 -0
  167. package/src/sdk/elevation/digest.ts +129 -0
  168. package/src/sdk/elevation/dispatch-receipt.ts +184 -0
  169. package/src/sdk/elevation/grant-ledger.ts +1142 -0
  170. package/src/sdk/elevation/owner.ts +109 -0
  171. package/src/sdk/guides/cache.ts +363 -0
  172. package/src/sdk/guides/catalog.ts +554 -0
  173. package/src/sdk/guides/cli.ts +235 -0
  174. package/src/sdk/guides/manifest.ts +152 -0
  175. package/src/sdk/guides/verify.ts +178 -0
  176. package/src/sdk/host/control/dispatch.ts +49 -1
  177. package/src/sdk/host/control/index.ts +1 -0
  178. package/src/sdk/host/control/operations.ts +8 -1
  179. package/src/sdk/host/query/cursor.ts +47 -3
  180. package/src/sdk/host/query/handlers.ts +155 -2
  181. package/src/sdk/host/sdk-surface-parity.test.ts +1 -0
  182. package/src/sdk/host/session-runtime.test.ts +391 -1
  183. package/src/sdk/host/session-runtime.ts +308 -73
  184. package/src/sdk/host/surface-policy.ts +3 -0
  185. package/src/sdk/host/websocket-transport.ts +7 -1
  186. package/src/sdk/mcp/server.ts +88 -15
  187. package/src/sdk/prompt-status.ts +8 -0
  188. package/src/sdk/protocol/operation-inventory.generated.json +62 -0
  189. package/src/sdk/protocol/operation-registry.ts +3 -2
  190. package/src/sdk/session.ts +87 -39
  191. package/src/sdk/transport/index.ts +1 -0
  192. package/src/sdk/transport/relay.ts +3 -0
  193. package/src/sdk/transport/serve-cli.ts +111 -24
  194. package/src/secrets/obfuscator.ts +92 -12
  195. package/src/session/agent-session.ts +322 -93
  196. package/src/session/blob-store.ts +60 -8
  197. package/src/session/fallback-chain-controller.ts +35 -0
  198. package/src/session/internal/managed-session-scope.ts +25 -3
  199. package/src/session/internal/managed-session-storage.ts +574 -73
  200. package/src/session/internal/session-memory-sidecar.ts +19 -29
  201. package/src/session/session-manager.ts +2166 -743
  202. package/src/session/session-storage.ts +715 -48
  203. package/src/setup/provider-onboarding.ts +19 -3
  204. package/src/setup/provider-presets.json +3 -1
  205. package/src/slash-commands/builtin-registry.ts +85 -3
  206. package/src/stt/downloader.ts +2 -3
  207. package/src/stt/setup.ts +29 -4
  208. package/src/task/executor.ts +22 -18
  209. package/src/task/index.ts +24 -1
  210. package/src/tools/archive-reader.ts +11 -2
  211. package/src/tools/browser/runtime-diagnostics.ts +249 -0
  212. package/src/tools/browser/tab-protocol.ts +4 -0
  213. package/src/tools/browser/tab-supervisor.ts +5 -0
  214. package/src/tools/browser/tab-worker.ts +36 -1
  215. package/src/tools/browser.ts +7 -0
  216. package/src/tools/image-gen.ts +110 -166
  217. package/src/tools/index.ts +2 -0
  218. package/src/tools/read.ts +1 -1
  219. package/src/tools/resource-gc.ts +6 -1
  220. package/src/tools/tool-catalog.generated.ts +5 -1
  221. package/src/utils/pasted-image-path.ts +93 -0
  222. package/src/web/search/index.ts +3 -3
@@ -6,6 +6,7 @@ export type ControlInput = Record<string, unknown>;
6
6
  * AgentSession and its controllers without exposing those concrete types here.
7
7
  */
8
8
  export interface ControlSurface {
9
+ authorizeElevationClaim?(sdkId: string, input: ControlInput, capability: string): boolean;
9
10
  prompt(text: string, images?: ControlValue, clientRef?: string): Promise<ControlValue> | ControlValue;
10
11
  steer(text: string): Promise<ControlValue> | ControlValue;
11
12
  followUp(text: string): Promise<ControlValue> | ControlValue;
@@ -17,8 +18,14 @@ export interface ControlSurface {
17
18
  response: ControlValue,
18
19
  expectedSessionId?: string,
19
20
  idempotencyKey?: string,
21
+ elevationRequestId?: string,
22
+ ): Promise<ControlValue> | ControlValue;
23
+ approvePlan(
24
+ id: string,
25
+ choice: ControlValue,
26
+ expectedSessionId?: string,
27
+ elevationRequestId?: string,
20
28
  ): Promise<ControlValue> | ControlValue;
21
- approvePlan(id: string, choice: ControlValue, expectedSessionId?: string): Promise<ControlValue> | ControlValue;
22
29
  invokeSkill(name: string, args: ControlValue, clientRef?: string): Promise<ControlValue> | ControlValue;
23
30
  setPlanMode(on: boolean): Promise<ControlValue> | ControlValue;
24
31
  operateGoal(op: string, objective?: string): Promise<ControlValue> | ControlValue;
@@ -1,4 +1,4 @@
1
- import { createHmac, timingSafeEqual } from "node:crypto";
1
+ import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
2
2
  import { MAX_PINNED_REVISIONS, type RevisionStore, RevisionStoreError, SNAPSHOT_TTL_MS } from "./revision-store.js";
3
3
 
4
4
  export const CURSOR_TTL_MS = SNAPSHOT_TTL_MS;
@@ -10,8 +10,14 @@ export interface CursorEnvelope {
10
10
  protocolMajor: 3;
11
11
  sessionId: string;
12
12
  resource: string;
13
+ /** Unique per-grant identity so identical envelopes never alias into one pin. */
14
+ nonce?: string;
13
15
  revision: string;
14
16
  highWatermark?: unknown;
17
+ /** TTL metadata minted by `CursorRegistry.grant`: wall-clock ms when granted. */
18
+ issuedAt?: number;
19
+ /** TTL metadata minted by `CursorRegistry.grant`: wall-clock ms when the cursor expires. */
20
+ expiresAt?: number;
15
21
  position: unknown;
16
22
  direction: string;
17
23
  pageShape: unknown;
@@ -139,6 +145,12 @@ export class CursorRegistry {
139
145
  MAX_CURSORS_PER_CONNECTION
140
146
  )
141
147
  throw new CursorError("snapshot_capacity_exceeded", false);
148
+ // Mint the authoritative TTL inside the registry so the signed envelope
149
+ // and the active-entry expiry can never diverge; callers read
150
+ // `issuedAt`/`expiresAt` back from the envelope they passed in.
151
+ const issuedAt = this.now();
152
+ envelope.issuedAt = issuedAt;
153
+ envelope.expiresAt = issuedAt + CURSOR_TTL_MS;
142
154
  const cursor = signCursor(envelope, this.sessionToken);
143
155
  try {
144
156
  await this.revisions.pin(cursor, resourceKind, resourceId, envelope.revision);
@@ -146,15 +158,47 @@ export class CursorRegistry {
146
158
  if (error instanceof RevisionStoreError) throw new CursorError(error.code, false);
147
159
  throw error;
148
160
  }
149
- this.#active.set(cursor, { connectionId, resourceId, expiresAt: this.now() + CURSOR_TTL_MS });
161
+ this.#active.set(cursor, { connectionId, resourceId, expiresAt: envelope.expiresAt });
150
162
  return cursor;
151
163
  }
152
164
 
165
+ async exchange(
166
+ cursor: string,
167
+ connectionId: string,
168
+ expected: CursorQueryContext,
169
+ resourceKind: string,
170
+ resourceId: string,
171
+ ): Promise<{ cursor: string; envelope: CursorEnvelope }> {
172
+ const envelope = verifyCursor(cursor, this.sessionToken);
173
+ if (!envelope) throw new CursorError("invalid_cursor", false);
174
+ if (envelope.expiresAt === undefined || envelope.expiresAt <= this.now()) throw new CursorError("cursor_expired");
175
+ if (
176
+ envelope.sessionId !== expected.sessionId ||
177
+ (expected.resource !== undefined && envelope.resource !== expected.resource) ||
178
+ envelope.direction !== expected.direction ||
179
+ canonicalJson(envelope.pageShape) !== canonicalJson(expected.pageShape)
180
+ )
181
+ throw new CursorError("invalid_input", false, "cursor does not match query");
182
+ const exchanged: CursorEnvelope = {
183
+ ...envelope,
184
+ nonce: randomUUID(),
185
+ issuedAt: undefined,
186
+ expiresAt: undefined,
187
+ };
188
+ const exchangedCursor = await this.grant(connectionId, exchanged, resourceKind, resourceId);
189
+ return { cursor: exchangedCursor, envelope: exchanged };
190
+ }
191
+
153
192
  consume(cursor: string, connectionId: string, expected: CursorQueryContext): CursorEnvelope {
154
193
  const envelope = verifyCursor(cursor, this.sessionToken);
155
194
  if (!envelope) throw new CursorError("invalid_cursor", false);
156
195
  const active = this.#active.get(cursor);
157
- if (!active || active.connectionId !== connectionId || active.expiresAt <= this.now()) {
196
+ const expired =
197
+ !active ||
198
+ active.connectionId !== connectionId ||
199
+ active.expiresAt <= this.now() ||
200
+ (envelope.expiresAt !== undefined && envelope.expiresAt <= this.now());
201
+ if (expired) {
158
202
  this.release(cursor);
159
203
  throw new CursorError("cursor_expired");
160
204
  }
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { PROMPT_CLIENT_REF_MAX_LENGTH } from "../../prompt-status.js";
2
3
  import type { ActiveProviderDescriptor } from "../../providers.js";
3
4
  import { ActiveProviderResolutionError } from "../../providers.js";
@@ -56,6 +57,12 @@ export interface SessionSurface {
56
57
  }): unknown | Promise<unknown>;
57
58
  /** Q27 effective model-profile catalog from the live session registry. */
58
59
  getModelProfiles?(): unknown[] | Promise<unknown[]>;
60
+ /**
61
+ * Q30 atomic checkpoint capture: the live transcript entries and the
62
+ * event-ring watermark read in ONE synchronous call, so the snapshot
63
+ * revision and the subscribe position can never straddle an append.
64
+ */
65
+ getCheckpointSnapshot?(): { entries: unknown[]; watermark: SdkCheckpointRecord };
59
66
  /** Query rows backed by the session's installed binding map. */
60
67
  installedQueries?: ReadonlySet<string>;
61
68
  }
@@ -81,7 +88,33 @@ export interface QueryResponse {
81
88
  result?: unknown;
82
89
  error?: { code: string; message: string; restartQuery?: boolean; details?: unknown };
83
90
  }
91
+ /**
92
+ * Host-published durable transcript checkpoint (C9). `revision` is the
93
+ * transcript high-watermark (entry count/offset) at checkpoint time and bounds
94
+ * replay to exactly `[checkpointRevision, live)`; `generation`/`seq` anchor the
95
+ * event-ring subscribe position carried by replay cursors.
96
+ */
97
+ export interface SdkCheckpointRecord {
98
+ revision: number;
99
+ generation: number;
100
+ seq: number;
101
+ }
102
+
103
+ function isNonNegativeSafeInteger(value: unknown): value is number {
104
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
105
+ }
84
106
 
107
+ export function isCheckpointRecord(value: unknown): value is SdkCheckpointRecord {
108
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
109
+ const record = value as Record<string, unknown>;
110
+ const keys = Object.keys(record);
111
+ return (
112
+ keys.length === 3 &&
113
+ isNonNegativeSafeInteger(record.revision) &&
114
+ isNonNegativeSafeInteger(record.generation) &&
115
+ isNonNegativeSafeInteger(record.seq)
116
+ );
117
+ }
85
118
  const sources: Record<string, { resource: string; method: keyof SessionSurface; mvcc: boolean }> = {
86
119
  Q01: { resource: "transcript", method: "getTranscriptEntries", mvcc: true },
87
120
  Q03: { resource: "context", method: "getContextSnapshot", mvcc: false },
@@ -138,6 +171,7 @@ const names = [
138
171
  "models.profiles.list",
139
172
  "skill.invoke_status",
140
173
  "providers.list/active",
174
+ "session.checkpoint",
141
175
  ];
142
176
 
143
177
  export class QueryHandlers {
@@ -164,11 +198,32 @@ export class QueryHandlers {
164
198
  false,
165
199
  `${request.query} is not installed for this session.`,
166
200
  );
201
+ // D1 strict raw checkpoint/cursor validation (approved authority contract):
202
+ // `checkpointToken` is a signed checkpoint cursor accepted ONLY on Q01
203
+ // (transcript.list), is mutually exclusive with a top-level cursor, and
204
+ // empty cursors are rejected instead of silently dropped.
205
+ if (request.cursor !== undefined && request.cursor === "")
206
+ return this.#error(request, "invalid_input", false, "cursor must be a non-empty string");
207
+ const checkpointToken = request.input?.checkpointToken;
208
+ if (checkpointToken !== undefined) {
209
+ if (typeof checkpointToken !== "string" || checkpointToken.trim() === "")
210
+ return this.#error(request, "invalid_input", false, "checkpointToken must be a non-empty string");
211
+ if (query !== "Q01" && query !== "Q30")
212
+ return this.#error(
213
+ request,
214
+ "invalid_input",
215
+ false,
216
+ "checkpointToken is only supported on transcript.list and session.checkpoint",
217
+ );
218
+ if (request.cursor !== undefined)
219
+ return this.#error(request, "invalid_input", false, "checkpointToken and cursor are mutually exclusive");
220
+ }
167
221
  if (query === "Q02") return await this.#transcriptBody(request);
168
222
  if (query === "Q23") return await this.#resourceBody(request);
169
223
  if (query === "Q26") return await this.#promptStatus(request);
170
224
  if (query === "Q28") return await this.#skillInvokeStatus(request);
171
225
  if (query === "Q24") return await this.#artifact(request);
226
+ if (query === "Q30") return await this.#checkpoint(request);
172
227
  if (query === "Q27" && request.input && Object.keys(request.input).length > 0)
173
228
  return this.#error(request, "invalid_request", false, "models.profiles.list does not accept input fields.");
174
229
  if (query === "Q27" && typeof this.surface.getModelProfiles !== "function")
@@ -203,8 +258,16 @@ export class QueryHandlers {
203
258
  let position = 0;
204
259
  let byteOffset = 0;
205
260
  let snapshot: unknown;
206
- if (request.cursor) {
207
- const cursor = this.cursors.consume(request.cursor, request.connectionId, {
261
+ // The cursor may arrive either as the top-level continuation cursor or,
262
+ // on Q01 only (validated in `dispatch`), as the `checkpointToken` resume
263
+ // seed. Both are signed checkpoint cursors consumed through the same
264
+ // CursorRegistry authority; neither mints a fresh snapshot.
265
+ const rawCursorToken = request.cursor ?? (queryId === "Q01" ? request.input?.checkpointToken : undefined);
266
+ if (rawCursorToken !== undefined && typeof rawCursorToken !== "string")
267
+ return this.#error(request, "invalid_request", false, "checkpointToken must be a non-empty string.");
268
+ const cursorToken = rawCursorToken;
269
+ if (cursorToken !== undefined) {
270
+ const cursor = this.cursors.consume(cursorToken, request.connectionId, {
208
271
  sessionId: this.sessionId,
209
272
  resource: source.resource,
210
273
  direction: "forward",
@@ -344,6 +407,96 @@ export class QueryHandlers {
344
407
  source.resource === "transcript" ? { highWatermark: lastId(snapshot) } : {},
345
408
  );
346
409
  }
410
+ async #checkpoint(request: QueryRequest): Promise<QueryResponse> {
411
+ if (request.cursor)
412
+ return this.#error(
413
+ request,
414
+ "invalid_request",
415
+ false,
416
+ "session.checkpoint does not accept a cursor; issue a fresh checkpoint on reconnect.",
417
+ );
418
+ const input = request.input ?? {};
419
+ const inputKeys = Object.keys(input);
420
+ if (inputKeys.some(key => key !== "checkpointToken"))
421
+ return this.#error(request, "invalid_request", false, "session.checkpoint accepts only checkpointToken.");
422
+ if (input.checkpointToken !== undefined) {
423
+ if (typeof input.checkpointToken !== "string" || input.checkpointToken.length === 0)
424
+ return this.#error(request, "invalid_request", false, "checkpointToken must be a non-empty string.");
425
+ try {
426
+ const exchanged = await this.cursors.exchange(
427
+ input.checkpointToken,
428
+ request.connectionId,
429
+ {
430
+ sessionId: this.sessionId,
431
+ resource: "transcript",
432
+ direction: "forward",
433
+ pageShape: { targetBytes: TARGET_PAGE_BYTES },
434
+ },
435
+ "transcript",
436
+ "default",
437
+ );
438
+ return {
439
+ id: request.id,
440
+ ok: true,
441
+ result: {
442
+ checkpointToken: exchanged.cursor,
443
+ checkpoint: exchanged.envelope.highWatermark,
444
+ revisionId: exchanged.envelope.revision,
445
+ issuedAt: exchanged.envelope.issuedAt,
446
+ expiresAt: exchanged.envelope.expiresAt,
447
+ },
448
+ };
449
+ } catch (error) {
450
+ if (error instanceof CursorError)
451
+ return this.#error(request, error.code, error.restartQuery, error.message);
452
+ throw error;
453
+ }
454
+ }
455
+ // Expired pins must be removed before payload-hash deduplication; otherwise
456
+ // createRevision can return an expired revision that grant() immediately sweeps.
457
+ this.cursors.sweep();
458
+ // Atomic synchronous capture (C9): entries and event-ring watermark come
459
+ // from the same host-owned call, so the pinned snapshot revision and the
460
+ // subscribe position can never straddle a concurrent append.
461
+ const captured =
462
+ typeof this.surface.getCheckpointSnapshot === "function" ? this.surface.getCheckpointSnapshot() : undefined;
463
+ const entries = captured !== undefined ? captured.entries : await this.surface.getTranscriptEntries();
464
+ const head: SdkCheckpointRecord =
465
+ captured !== undefined
466
+ ? captured.watermark
467
+ : { revision: Array.isArray(entries) ? entries.length : 0, generation: 0, seq: 0 };
468
+ // Mint the snapshot from the exact entries captured with the watermark;
469
+ // entries appended after this point are excluded from replay by
470
+ // construction (append-during-checkpoint), and the returned token is the
471
+ // per-grant signed cursor pinned to this revision — never an unlocked
472
+ // fresh replay authority.
473
+ const snapshot = await this.revisions.createRevision("transcript", "default", entries);
474
+ const nonce = randomUUID();
475
+ const envelope: CursorEnvelope = {
476
+ cursorVersion: 1,
477
+ protocolMajor: 3,
478
+ sessionId: this.sessionId,
479
+ resource: "transcript",
480
+ revision: snapshot,
481
+ highWatermark: head,
482
+ nonce,
483
+ position: { offset: 0, selector: { queryId: "Q01" } },
484
+ direction: "forward",
485
+ pageShape: { targetBytes: TARGET_PAGE_BYTES },
486
+ };
487
+ const cursor = await this.cursors.grant(request.connectionId, envelope, "transcript", "default");
488
+ return {
489
+ id: request.id,
490
+ ok: true,
491
+ result: {
492
+ checkpointToken: cursor,
493
+ checkpoint: head,
494
+ revisionId: snapshot,
495
+ issuedAt: envelope.issuedAt,
496
+ expiresAt: envelope.expiresAt,
497
+ },
498
+ };
499
+ }
347
500
 
348
501
  async #transcriptBody(request: QueryRequest): Promise<QueryResponse> {
349
502
  let selector = selectorFor("Q02", request.input);
@@ -116,6 +116,7 @@ describe("SDK surface parity", () => {
116
116
  expect(createSdkCapabilities(nativePolicy, false)).toEqual(createSdkCapabilities(loopbackPolicy, false));
117
117
  expect(nativePolicy.installedQueries).toContain("turn.prompt_status");
118
118
  expect(nativePolicy.installedQueries).toContain("skill.invoke_status");
119
+ expect(nativePolicy.installedQueries).toContain("session.checkpoint");
119
120
  });
120
121
 
121
122
  test("workflow controls are advertised exactly when a durable gate bridge exists", () => {