@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
@@ -1,40 +1,76 @@
1
+ import { randomBytes, randomUUID } from "node:crypto";
1
2
  import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
2
4
  import { createInterface } from "node:readline/promises";
3
5
  import { getAgentDir } from "@gajae-code/utils";
4
6
  import { ensureBroker } from "../broker/ensure";
5
7
  import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
6
8
  import {
7
- listSdkSessionEndpoints,
8
9
  readSdkBrokerDiscovery,
9
10
  readSdkSessionEndpoint,
10
11
  SdkClient,
11
12
  SdkClientError,
12
13
  SdkDiscoveryError,
13
14
  } from "../client";
15
+ import { PROMPT_CLIENT_REF_MAX_LENGTH } from "../prompt-status.js";
14
16
  import { validateAdapterControl } from "../protocol/adapter-validation";
15
17
  import { adapterDispositionError, findOperation, type OperationKind } from "../protocol/operation-registry";
18
+ import {
19
+ type SdkCheckpointRecordV1,
20
+ type SdkRetentionGapV1,
21
+ type SdkSessionRowV1,
22
+ type SdkTailItemV1,
23
+ SESSION_ROWS_VERSION,
24
+ stripSecretFields,
25
+ toCheckpointRecordV1,
26
+ toOfflineSessionRowV1,
27
+ toRetentionGapV1,
28
+ toSessionRowV1,
29
+ toTailItemV1,
30
+ } from "./rows";
16
31
 
17
- export type SdkSessionCliAction = "list" | "control" | "query" | "global";
32
+ export type SdkSessionCliAction = "list" | "inspect" | "send" | "status" | "tail" | "elevate" | "raw";
33
+ export type SdkSessionCliRawKind = "control" | "query" | "global";
18
34
 
19
35
  export interface SdkSessionCliArgs {
20
36
  action?: string;
37
+ /** Raw dispatch kind when action is "raw" (control | query | global). */
38
+ rawAction?: string;
21
39
  sessionId?: string;
40
+ /** Caller-chosen operation reference (ULID) for `send`; required target for `status`. */
41
+ opRef?: string;
22
42
  operation?: string;
23
43
  query?: string;
44
+ /** Convenience `send` body: forwarded as the `text` field of turn.prompt. */
45
+ text?: string;
24
46
  jsonInput?: string;
25
47
  jsonInputFile?: string;
26
- idempotencyKey?: string;
27
48
  jsonInputStdin?: boolean;
49
+ idempotencyKey?: string;
50
+ elevationRequestId?: string;
51
+ operationKind?: OperationKind;
28
52
  confirm?: boolean;
53
+ /** Raw query continuation cursor, or a saved checkpoint token to resume `tail` from (re-minted per connection). */
29
54
  cursor?: string;
30
55
  showEndpointCredential?: boolean;
31
56
  yes?: boolean;
57
+ /** `send --wait`: poll turn.prompt_status until terminal or the wait window elapses. */
58
+ wait?: boolean;
59
+ /** Bounded wait for `send --wait`, `status`, and live `tail` follow. */
60
+ timeoutMs?: number;
61
+ /** `tail --strict`: fail closed on retention gaps (exit 1 retention_gap). */
62
+ strict?: boolean;
63
+ /** `tail --until-idle`: exit once the observed event stream reaches a terminal turn state. */
64
+ untilIdle?: boolean;
65
+ /** `tail --all-events`: include every event-ring kind instead of the tail subset. */
66
+ allEvents?: boolean;
32
67
  repo?: string;
33
68
  agentDir?: string;
34
69
  }
35
70
 
36
71
  type JsonRecord = Record<string, unknown>;
37
72
  const SECRET_FIELD = /(?:secret|token|password|credential|authorization|api[_-]?key)/i;
73
+ const MAX_SESSION_LIST_PAGES = 10_000;
38
74
 
39
75
  class SdkSessionCliError extends Error {
40
76
  constructor(
@@ -70,6 +106,10 @@ function containsSecretField(value: unknown): boolean {
70
106
  return Object.entries(value).some(([key, nested]) => SECRET_FIELD.test(key) || containsSecretField(nested));
71
107
  }
72
108
 
109
+ function object(value: unknown): JsonRecord | undefined {
110
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : undefined;
111
+ }
112
+
73
113
  async function inputFromArgs(args: SdkSessionCliArgs): Promise<JsonRecord> {
74
114
  const sources = [
75
115
  args.jsonInput !== undefined,
@@ -88,20 +128,15 @@ async function inputFromArgs(args: SdkSessionCliArgs): Promise<JsonRecord> {
88
128
  return input;
89
129
  }
90
130
  if (args.jsonInputFile !== undefined) {
91
- let stat: Awaited<ReturnType<typeof fs.stat>>;
92
131
  try {
93
- stat = await fs.stat(args.jsonInputFile);
94
- } catch {
95
- throw new SdkSessionCliError("input_file_unavailable", "Unable to read --json-input-file.", 2);
96
- }
97
- if (!stat.isFile() || (stat.mode & 0o077) !== 0)
98
- throw new SdkSessionCliError(
99
- "input_file_permissions",
100
- "--json-input-file must be a regular file with 0600 permissions.",
101
- 2,
102
- );
103
- try {
104
- return parseInput(await fs.readFile(args.jsonInputFile, "utf8"), "--json-input-file");
132
+ const stat = await fs.stat(args.jsonInputFile);
133
+ if (!stat.isFile() || (stat.mode & 0o077) !== 0)
134
+ throw new SdkSessionCliError(
135
+ "input_file_permissions",
136
+ "--json-input-file must be a regular file with 0600 permissions.",
137
+ 2,
138
+ );
139
+ return parseInput(await Bun.file(args.jsonInputFile).text(), "--json-input-file");
105
140
  } catch (error) {
106
141
  if (error instanceof SdkSessionCliError) throw error;
107
142
  throw new SdkSessionCliError("input_file_unavailable", "Unable to read --json-input-file.", 2);
@@ -119,6 +154,15 @@ function isEndpointOperation(operation: string): boolean {
119
154
  return operation === "session.get_endpoint";
120
155
  }
121
156
 
157
+ function isCredentialBearingOperation(operation: string): boolean {
158
+ return (
159
+ isEndpointOperation(operation) ||
160
+ operation === "session.create" ||
161
+ operation === "session.fork" ||
162
+ operation === "session.resume"
163
+ );
164
+ }
165
+
122
166
  function cliOperationError(kind: OperationKind, operation: string): { code: string; message: string } | undefined {
123
167
  const row = findOperation(kind, operation);
124
168
  const error = adapterDispositionError("daemonCli", kind, operation);
@@ -157,11 +201,10 @@ async function connectBroker(agentDir: string): Promise<SdkClient> {
157
201
  return await SdkClient.connect(discovery.url, discovery.token);
158
202
  }
159
203
 
160
- async function connectSession(repo: string, sessionId: string): Promise<SdkClient> {
161
- const endpoint = await readSdkSessionEndpoint(repo, sessionId);
162
- if (!endpoint)
163
- throw new SdkSessionCliError("session_unavailable", `SDK endpoint for session ${sessionId} is unavailable.`, 1);
164
- return await SdkClient.connect(endpoint.url, endpoint.token);
204
+ async function connectExistingBroker(agentDir: string): Promise<SdkClient> {
205
+ const discovery = await readSdkBrokerDiscovery(agentDir);
206
+ if (!discovery) throw new SdkSessionCliError("broker_unavailable", "SDK broker discovery is unavailable.", 1);
207
+ return await SdkClient.connect(discovery.url, discovery.token);
165
208
  }
166
209
 
167
210
  function brokerAbsent(error: unknown): boolean {
@@ -175,28 +218,808 @@ function brokerAbsent(error: unknown): boolean {
175
218
  );
176
219
  }
177
220
 
178
- async function runList(repo: string, agentDir: string): Promise<unknown> {
221
+ async function paginatedSessionList(client: SdkClient, input: JsonRecord = {}): Promise<unknown> {
222
+ const aggregate: JsonRecord = {};
223
+ const sessions: unknown[] = [];
224
+ let firstResponse: JsonRecord | undefined;
225
+ let cursor: string | undefined;
226
+ for (let pageCount = 0; pageCount < MAX_SESSION_LIST_PAGES; pageCount++) {
227
+ const response = object(
228
+ await client.global("session.list", { ...input, ...(cursor === undefined ? {} : { cursor }) }),
229
+ );
230
+ firstResponse ??= response;
231
+ if (response?.ok === false) {
232
+ const failure = object(response.error);
233
+ throw new SdkClientError(
234
+ typeof failure?.code === "string" ? failure.code : "broker_error",
235
+ typeof failure?.message === "string" ? failure.message : "session.list failed",
236
+ );
237
+ }
238
+ const listing = object(response?.result) ?? response;
239
+ if (listing) {
240
+ for (const [key, value] of Object.entries(listing)) {
241
+ if (key !== "sessions" && key !== "continuationCursor") aggregate[key] = value;
242
+ }
243
+ if (Array.isArray(listing.sessions)) sessions.push(...listing.sessions);
244
+ const nextCursor =
245
+ typeof listing.continuationCursor === "string" && listing.continuationCursor.length > 0
246
+ ? listing.continuationCursor
247
+ : undefined;
248
+ if (nextCursor) {
249
+ cursor = nextCursor;
250
+ continue;
251
+ }
252
+ }
253
+ const result = { ...aggregate, sessions };
254
+ return firstResponse && "result" in firstResponse ? { ...firstResponse, result } : result;
255
+ }
256
+ throw new SdkClientError("protocol_error", "session.list exceeded the page budget.");
257
+ }
258
+
259
+ /**
260
+ * Broker-bound session endpoint resolution (C10): the broker validates the
261
+ * indexed session against its durable endpoint record and returns the
262
+ * credential, which the CLI uses to connect and never renders. Both clients
263
+ * (broker + session) are returned so callers close exactly what they opened.
264
+ */
265
+ async function connectSessionViaBroker(
266
+ agentDir: string,
267
+ sessionId: string,
268
+ ): Promise<{ broker: SdkClient; session: SdkClient; endpoint: unknown }> {
269
+ const broker = await connectBroker(agentDir);
179
270
  try {
180
- const client = await connectBroker(agentDir);
271
+ const response = await broker.global("session.get_endpoint", { sessionId });
272
+ const endpoint = (response as { result?: unknown } | undefined)?.result;
273
+ const url = (endpoint as { url?: unknown } | undefined)?.url;
274
+ const token = (endpoint as { token?: unknown } | undefined)?.token;
275
+ if (typeof url !== "string" || !url || typeof token !== "string")
276
+ throw new SdkSessionCliError(
277
+ "session_unavailable",
278
+ `SDK endpoint for session ${sessionId} is unavailable.`,
279
+ 1,
280
+ );
281
+ const session = await SdkClient.connect(url, token);
282
+ return { broker, session, endpoint };
283
+ } catch (error) {
284
+ await broker.close();
285
+ throw error;
286
+ }
287
+ }
288
+
289
+ function resultObject(response: unknown): Record<string, unknown> | undefined {
290
+ const result = (response as { result?: unknown } | undefined)?.result;
291
+ return result && typeof result === "object" && !Array.isArray(result)
292
+ ? (result as Record<string, unknown>)
293
+ : undefined;
294
+ }
295
+
296
+ function arrayOf(value: unknown): unknown[] {
297
+ return Array.isArray(value) ? value : [];
298
+ }
299
+
300
+ const TERMINAL_TURN_KINDS = new Set(["turn_end", "agent_end", "agent_failed"]);
301
+ const CLOSE_EVENT_KINDS = new Set(["session_closed", "session_terminated"]);
302
+ /** Default tail emission subset; `--all-events` widens to every ring kind. */
303
+ const DEFAULT_TAIL_KINDS = new Set([
304
+ "session_ready",
305
+ "session_prepared",
306
+ "session_closed",
307
+ "session_terminated",
308
+ "turn_start",
309
+ "turn_end",
310
+ "agent_start",
311
+ "agent_end",
312
+ "agent_failed",
313
+ ]);
314
+ const TAIL_OFFLINE_MAX_ENTRIES = 200;
315
+ const TAIL_STATUS_POLL_MS = 100;
316
+
317
+ /**
318
+ * Time-ordered operation reference (ULID): 48-bit millisecond timestamp plus
319
+ * 80 bits of randomness, Crockford base32, lowercase. The ref is used as the
320
+ * `turn.prompt` clientRef so `send`/`status` correlate without a broker round
321
+ * trip, and matches the plan's durable op-ref ledger shape (C7).
322
+ */
323
+ export function createOperationRef(now: number = Date.now()): string {
324
+ const crockford = "0123456789abcdefghjkmnpqrstvwxyz";
325
+ let random = 0n;
326
+ for (const byte of randomBytes(10)) random = (random << 8n) | BigInt(byte);
327
+ const value = (BigInt(now) << 80n) | random;
328
+ let encoded = "";
329
+ for (let shift = 125n; shift >= 0n; shift -= 5n) encoded += crockford[Number((value >> shift) & 0x1fn)];
330
+ return encoded;
331
+ }
332
+
333
+ function clientRefFromInput(input: JsonRecord): string | undefined {
334
+ const raw = typeof input.clientRef === "string" ? input.clientRef.trim() : undefined;
335
+ return raw;
336
+ }
337
+
338
+ function assertClientRef(clientRef: string): void {
339
+ if (!clientRef || clientRef.length > PROMPT_CLIENT_REF_MAX_LENGTH)
340
+ throw new SdkSessionCliError(
341
+ "invalid_input",
342
+ `clientRef must be a non-empty string of at most ${PROMPT_CLIENT_REF_MAX_LENGTH} characters.`,
343
+ 2,
344
+ );
345
+ }
346
+
347
+ function isRecord(value: unknown): value is Record<string, unknown> {
348
+ return typeof value === "object" && value !== null && !Array.isArray(value);
349
+ }
350
+
351
+ /** Renders the broker `session.list` result as a versioned, credential-free DTO. */
352
+ async function runList(agentDir: string): Promise<unknown> {
353
+ const client = await connectBroker(agentDir);
354
+ try {
355
+ const response = await paginatedSessionList(client);
356
+ const result = resultObject(response) ?? {};
357
+ const sessions = arrayOf(result.sessions).map(toSessionRowV1);
358
+ return {
359
+ ok: true,
360
+ result: {
361
+ version: SESSION_ROWS_VERSION,
362
+ source: "broker",
363
+ indexSeq: typeof result.indexSeq === "number" ? result.indexSeq : undefined,
364
+ sessions,
365
+ warnings: arrayOf(result.warnings),
366
+ },
367
+ };
368
+ } finally {
369
+ await client.close();
370
+ }
371
+ }
372
+
373
+ async function runInspect(repo: string, agentDir: string, sessionId: string): Promise<unknown> {
374
+ try {
375
+ const client = await connectExistingBroker(agentDir);
181
376
  try {
182
- return await client.global("session.list", {});
377
+ const response = await paginatedSessionList(client);
378
+ const sessions = arrayOf(resultObject(response)?.sessions).map(toSessionRowV1);
379
+ const row = sessions.find(candidate => candidate.sessionId === sessionId);
380
+ if (!row)
381
+ throw new SdkSessionCliError(
382
+ "session_unavailable",
383
+ `Session ${sessionId} is not indexed by the broker.`,
384
+ 1,
385
+ );
386
+ return { ok: true, result: { version: SESSION_ROWS_VERSION, source: "broker", session: row } };
183
387
  } finally {
184
388
  await client.close();
185
389
  }
186
390
  } catch (error) {
391
+ // Broker absent: offline inspect-only (principle 1) from the local
392
+ // endpoint discovery record, still projected without credentials.
187
393
  if (!brokerAbsent(error)) throw error;
188
- const { endpoints, warnings } = await listSdkSessionEndpoints(repo);
394
+ const endpoint = await readSdkSessionEndpoint(repo, sessionId);
395
+ if (!endpoint)
396
+ throw new SdkSessionCliError(
397
+ "session_unavailable",
398
+ `SDK endpoint for session ${sessionId} is unavailable.`,
399
+ 1,
400
+ );
401
+ return {
402
+ ok: true,
403
+ result: { version: SESSION_ROWS_VERSION, source: "offline", session: toOfflineSessionRowV1(endpoint) },
404
+ };
405
+ }
406
+ }
407
+
408
+ async function waitForTerminalStatus(
409
+ session: SdkClient,
410
+ clientRef: string,
411
+ timeoutMs: number | undefined,
412
+ ): Promise<{ terminal: boolean; status: string; detail: unknown }> {
413
+ const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
414
+ for (;;) {
415
+ const response = await session.query("turn.prompt_status", { clientRef });
416
+ const result = resultObject(response);
417
+ const status = typeof result?.status === "string" ? result.status : "unknown";
418
+ if (status === "terminal_ok" || status === "failed") return { terminal: true, status, detail: result };
419
+ if (deadline !== undefined && Date.now() >= deadline) return { terminal: false, status, detail: undefined };
420
+ await Bun.sleep(TAIL_STATUS_POLL_MS);
421
+ }
422
+ }
423
+
424
+ async function runSend(agentDir: string, sessionId: string, args: SdkSessionCliArgs): Promise<unknown> {
425
+ const generatedRef = createOperationRef();
426
+ const input = await inputFromArgs(args);
427
+ if (args.text !== undefined && Object.keys(input).length > 0)
428
+ throw new SdkSessionCliError("usage", "Use either --text or one JSON input source for the prompt, not both.", 2);
429
+ const promptInput: JsonRecord = args.text !== undefined ? { text: args.text } : { ...input };
430
+ const inputRef = clientRefFromInput(promptInput);
431
+ const clientRef = inputRef ?? args.opRef?.trim() ?? generatedRef;
432
+ if (args.opRef !== undefined && inputRef !== undefined && inputRef !== args.opRef.trim())
433
+ throw new SdkSessionCliError("usage", "--op-ref must match the clientRef in the JSON input.", 2);
434
+ assertClientRef(clientRef);
435
+ if (inputRef === undefined) promptInput.clientRef = clientRef;
436
+ const invalid = validateAdapterControl("turn.prompt", promptInput);
437
+ if (invalid) throw new SdkSessionCliError(invalid.code, invalid.message, 2);
438
+ const { broker, session } = await connectSessionViaBroker(agentDir, sessionId);
439
+ try {
440
+ const response = await broker.global("session.control", {
441
+ sessionId,
442
+ operation: "turn.prompt",
443
+ input: promptInput,
444
+ confirm: args.confirm === true,
445
+ });
446
+ if (isRecord(response) && response.ok === false) {
447
+ const responseError = isRecord(response.error) ? response.error : {};
448
+ throw new SdkSessionCliError(
449
+ typeof responseError.code === "string" ? responseError.code : "unavailable",
450
+ typeof responseError.message === "string" ? responseError.message : "Prompt dispatch failed.",
451
+ 1,
452
+ );
453
+ }
454
+ const receipt = resultObject(response) ?? response;
455
+ const result: JsonRecord = {
456
+ version: SESSION_ROWS_VERSION,
457
+ operationRef: clientRef,
458
+ status: "accepted",
459
+ receipt,
460
+ };
461
+ if (args.wait === true) {
462
+ const outcome = await waitForTerminalStatus(session, clientRef, args.timeoutMs ?? 30_000);
463
+ if (!outcome.terminal)
464
+ throw new SdkSessionCliError(
465
+ "wait_timeout",
466
+ `Prompt ${clientRef} did not reach a terminal state within the wait window.`,
467
+ 1,
468
+ { operationRef: clientRef, status: outcome.status },
469
+ );
470
+ result.status = outcome.status;
471
+ result.statusDetail = outcome.detail;
472
+ }
473
+ return { ok: true, result: stripSecretFields(result) };
474
+ } finally {
475
+ await session.close();
476
+ await broker.close();
477
+ }
478
+ }
479
+
480
+ async function runStatus(agentDir: string, sessionId: string, opRef: string): Promise<unknown> {
481
+ assertClientRef(opRef);
482
+ const { broker, session } = await connectSessionViaBroker(agentDir, sessionId);
483
+ try {
484
+ const response = await session.query("turn.prompt_status", { clientRef: opRef });
485
+ const status = resultObject(response);
486
+ const record = status ?? (isRecord(response) ? response : {});
487
+ const raw = typeof record.status === "string" ? record.status : "unknown";
488
+ return {
489
+ ok: true,
490
+ result: stripSecretFields({
491
+ version: SESSION_ROWS_VERSION,
492
+ operationRef: opRef,
493
+ status: record,
494
+ summary: { completed: raw === "terminal_ok" || raw === "failed" },
495
+ }),
496
+ };
497
+ } finally {
498
+ await session.close();
499
+ await broker.close();
500
+ }
501
+ }
502
+
503
+ type CheckpointExtraction = {
504
+ record?: SdkCheckpointRecordV1;
505
+ cursor?: string;
506
+ gap?: SdkRetentionGapV1;
507
+ };
508
+
509
+ function extractCheckpoint(response: unknown): CheckpointExtraction {
510
+ const result = resultObject(response);
511
+ if (!result) return {};
512
+ const gap = toRetentionGapV1(result.gap);
513
+ if (gap !== undefined) return { gap };
514
+ const record = toCheckpointRecordV1(result.checkpoint ?? result);
515
+ // The Q30 result's `cursor`/`checkpointToken` is the signed checkpoint
516
+ // cursor: `cursor` is the legacy alias kept for wire compatibility, and the
517
+ // signed token is the resume artifact (`--cursor`) consumed by Q01.
518
+ const cursor =
519
+ typeof result.cursor === "string" && result.cursor
520
+ ? result.cursor
521
+ : typeof result.checkpointToken === "string" && result.checkpointToken
522
+ ? result.checkpointToken
523
+ : undefined;
524
+ return { record, cursor };
525
+ }
526
+
527
+ function extractTranscriptPage(response: unknown): { items: unknown[]; complete: boolean; cursor?: string } {
528
+ const page = (response as { page?: unknown } | undefined)?.page;
529
+ if (!isRecord(page)) return { items: arrayOf(resultObject(response)?.items), complete: true };
530
+ return {
531
+ items: arrayOf(page.items),
532
+ complete: page.complete === true,
533
+ cursor: typeof page.continuationCursor === "string" ? page.continuationCursor : undefined,
534
+ };
535
+ }
536
+
537
+ function eventKindOf(frame: Record<string, unknown>): string | undefined {
538
+ if (typeof frame.kind === "string" && frame.kind) return frame.kind;
539
+ if (typeof frame.name === "string" && frame.name) return frame.name;
540
+ return undefined;
541
+ }
542
+
543
+ function tailItemKey(item: SdkTailItemV1): string {
544
+ // Ring events carry a synchronous generation/seq identity; keying on it (not
545
+ // kind+optional id) keeps repeated same-kind no-id events distinct and
546
+ // dedupes the replay/live overlap exactly once.
547
+ if (item.generation !== undefined && item.seq !== undefined)
548
+ return `${item.kind}\u0000${item.generation}\u0000${item.seq}`;
549
+ // Retained transcript entries have stable ids (or none); never silently drop
550
+ // distinct identity-less items, so fall back to the payload itself.
551
+ if (item.id !== undefined) return `${item.kind}\u0000${item.id}`;
552
+ return `${item.kind}\u0000${JSON.stringify(item.payload)}`;
553
+ }
554
+
555
+ function mergeTailItems(
556
+ target: SdkTailItemV1[],
557
+ seen: Set<string>,
558
+ items: SdkTailItemV1[],
559
+ include: (kind: string) => boolean,
560
+ ): void {
561
+ for (const item of items) {
562
+ const key = tailItemKey(item);
563
+ if (seen.has(key)) continue;
564
+ seen.add(key);
565
+ if (include(item.kind)) target.push(item);
566
+ }
567
+ }
568
+
569
+ function eventGapToRetentionGap(
570
+ value: unknown,
571
+ frame: Record<string, unknown>,
572
+ record: SdkCheckpointRecordV1 | undefined,
573
+ ): SdkRetentionGapV1 | undefined {
574
+ const existing = toRetentionGapV1(value);
575
+ if (existing !== undefined) return existing;
576
+ if (!isRecord(value)) return undefined;
577
+ const revision = record?.revision ?? 0;
578
+ if (value.kind === "sequence_gap" && typeof value.fromSeq === "number" && typeof value.toSeq === "number") {
189
579
  return {
190
- sessions: endpoints.map(({ sessionId, path }) => ({ sessionId, path })),
191
- warnings: [
192
- { code: "broker_unavailable", message: "Listed endpoint files because the broker is unavailable." },
193
- ...warnings,
194
- ],
580
+ code: "retention_gap",
581
+ missing: { from: value.fromSeq, to: value.toSeq },
582
+ resync: {
583
+ revision,
584
+ generation: typeof frame.generation === "number" ? frame.generation : (record?.generation ?? 0),
585
+ seq: typeof frame.lastSeq === "number" ? frame.lastSeq : value.toSeq,
586
+ },
195
587
  };
196
588
  }
589
+ if (value.kind === "generation_reset" && typeof value.toGeneration === "number")
590
+ return { code: "retention_gap", resync: { revision, generation: value.toGeneration, seq: 0 } };
591
+ return undefined;
197
592
  }
198
593
 
199
- /** Runs the pure-SDK `gjc daemon session` command family. */
594
+ async function requestEventReplay(
595
+ session: SdkClient,
596
+ record: SdkCheckpointRecordV1 | undefined,
597
+ timeoutMs: number,
598
+ ): Promise<{ events: SdkTailItemV1[]; gap?: SdkRetentionGapV1; closed: boolean }> {
599
+ const id = randomUUID();
600
+ const { promise, resolve, reject } = Promise.withResolvers<{
601
+ events: SdkTailItemV1[];
602
+ gap?: SdkRetentionGapV1;
603
+ closed: boolean;
604
+ }>();
605
+ const bufferedLiveEvents: SdkTailItemV1[] = [];
606
+ const off = session.onFrame(frame => {
607
+ if (frame.type === "event") {
608
+ bufferedLiveEvents.push(toTailItemV1(frame, { kind: eventKindOf(frame) ?? "event" }));
609
+ return;
610
+ }
611
+ if (frame.type !== "event_replay_result" || frame.id !== id) return;
612
+ const replayFrame = frame as Record<string, unknown>;
613
+ const events = [
614
+ ...arrayOf(replayFrame.events).map(event => toTailItemV1(event, { kind: "event" })),
615
+ ...bufferedLiveEvents,
616
+ ];
617
+ const gap = eventGapToRetentionGap(replayFrame.gap, replayFrame, record);
618
+ resolve({ events, ...(gap !== undefined ? { gap } : {}), closed: false });
619
+ });
620
+ const offFailed = session.onReconnectFailed(() => resolve({ events: [], closed: true }));
621
+ const timer = setTimeout(
622
+ () => reject(new SdkSessionCliError("tail_timeout", "Timed out waiting for the event replay response.", 1)),
623
+ timeoutMs,
624
+ );
625
+ try {
626
+ session.send({
627
+ type: "event_replay",
628
+ id,
629
+ // Resume strictly after the checkpoint sequence: the checkpoint event
630
+ // was already observed, so re-emitting it would let a terminal
631
+ // checkpoint event satisfy --until-idle without any new turn.
632
+ ...(record !== undefined ? { sinceGeneration: record.generation, sinceSeq: record.seq } : {}),
633
+ });
634
+ return await promise;
635
+ } finally {
636
+ clearTimeout(timer);
637
+ off();
638
+ offFailed();
639
+ }
640
+ }
641
+
642
+ async function followLiveEvents(
643
+ session: SdkClient,
644
+ items: SdkTailItemV1[],
645
+ seen: Set<string>,
646
+ args: SdkSessionCliArgs,
647
+ include: (kind: string) => boolean,
648
+ timeoutMs: number,
649
+ ): Promise<{ reason: "idle" | "close" | "timeout" | "reconnect" | "reconnect_failed" }> {
650
+ const { promise, resolve } = Promise.withResolvers<{
651
+ reason: "idle" | "close" | "timeout" | "reconnect" | "reconnect_failed";
652
+ }>();
653
+ const off = session.onFrame(frame => {
654
+ if (!isRecord(frame) || frame.type !== "event") return;
655
+ const kind = eventKindOf(frame);
656
+ if (kind === undefined) return;
657
+ const item = toTailItemV1(frame, { kind });
658
+ const key = tailItemKey(item);
659
+ if (!seen.has(key)) {
660
+ seen.add(key);
661
+ if (include(kind)) items.push(item);
662
+ }
663
+ if (TERMINAL_TURN_KINDS.has(kind) && args.untilIdle === true) resolve({ reason: "idle" });
664
+ if (CLOSE_EVENT_KINDS.has(kind)) resolve({ reason: "close" });
665
+ });
666
+ const offClosed = session.onReconnectFailed(() => resolve({ reason: "reconnect_failed" }));
667
+ const offReconnect = session.onReconnect(() => resolve({ reason: "reconnect" }));
668
+ const timer = setTimeout(() => resolve({ reason: "timeout" }), timeoutMs);
669
+ try {
670
+ const result = await promise;
671
+ return result;
672
+ } finally {
673
+ clearTimeout(timer);
674
+ off();
675
+ offClosed();
676
+ offReconnect();
677
+ }
678
+ }
679
+
680
+ /** Read-only retained-history replay for a stopped/terminal session (offline, no endpoint). */
681
+ async function offlineTailReplay(
682
+ repo: string,
683
+ agentDir: string,
684
+ sessionId: string,
685
+ row: SdkSessionRowV1,
686
+ ): Promise<unknown> {
687
+ const broker = await connectBroker(agentDir);
688
+ try {
689
+ const response = await broker.global("session.list", { resolveSessionId: sessionId, cwd: repo });
690
+ const savedSession = resultObject(response)?.savedSession;
691
+ const savedPath = isRecord(savedSession) && typeof savedSession.path === "string" ? savedSession.path : undefined;
692
+ if (!savedPath) {
693
+ throw new SdkSessionCliError(
694
+ "session_unavailable",
695
+ `Session ${sessionId} is stopped and has no retained transcript replay.`,
696
+ 1,
697
+ );
698
+ }
699
+ const text = await fs.readFile(savedPath, "utf8");
700
+ const parsedEntries: unknown[] = [];
701
+ const malformed: number[] = [];
702
+ const lines = text.split("\n");
703
+ for (let index = 0; index < lines.length; index++) {
704
+ const line = lines[index]!;
705
+ if (!line) continue;
706
+ try {
707
+ parsedEntries.push(JSON.parse(line));
708
+ } catch {
709
+ malformed.push(index);
710
+ }
711
+ }
712
+ // Retained-history corruption is never silently skipped: a torn or
713
+ // unparseable line means the retained replay is not trustworthy, so tail
714
+ // fails closed with a typed retention error instead of reporting success.
715
+ if (malformed.length > 0)
716
+ throw new SdkSessionCliError(
717
+ "retention_gap",
718
+ "Retained transcript history contains unparseable entries; refusing to replay corrupted history.",
719
+ 1,
720
+ { code: "retention_gap", lines: malformed },
721
+ );
722
+ const entries = parsedEntries.slice(-TAIL_OFFLINE_MAX_ENTRIES);
723
+ const items = entries.map((entry, index) => toTailItemV1(entry, { kind: "transcript", seq: index }));
724
+ return {
725
+ ok: true,
726
+ result: {
727
+ version: SESSION_ROWS_VERSION,
728
+ source: "offline",
729
+ session: row,
730
+ items,
731
+ terminal: true,
732
+ },
733
+ };
734
+ } finally {
735
+ await broker.close();
736
+ }
737
+ }
738
+
739
+ export async function runTail(
740
+ repo: string,
741
+ agentDir: string,
742
+ sessionId: string,
743
+ args: SdkSessionCliArgs,
744
+ ): Promise<unknown> {
745
+ const broker = await connectBroker(agentDir);
746
+ let row: SdkSessionRowV1 | undefined;
747
+ try {
748
+ const response = await paginatedSessionList(broker);
749
+ row = arrayOf(resultObject(response)?.sessions ?? undefined)
750
+ .map(toSessionRowV1)
751
+ .find(candidate => candidate.sessionId === sessionId);
752
+ } finally {
753
+ await broker.close();
754
+ }
755
+ if (!row)
756
+ throw new SdkSessionCliError("session_unavailable", `Session ${sessionId} is not indexed by the broker.`, 1);
757
+ if (row.deleted)
758
+ throw new SdkSessionCliError("session_deleted", `Session ${sessionId} was deleted and has no tail.`, 1);
759
+
760
+ const include = (kind: string): boolean =>
761
+ kind === "transcript" || args.allEvents === true || DEFAULT_TAIL_KINDS.has(kind);
762
+ const items: SdkTailItemV1[] = [];
763
+ const seen = new Set<string>();
764
+ let checkpoint: SdkCheckpointRecordV1 | undefined;
765
+ let gap: SdkRetentionGapV1 | undefined;
766
+ const timeoutMs = args.timeoutMs ?? 10_000;
767
+
768
+ let brokerClient: SdkClient | undefined;
769
+ let session: SdkClient | undefined;
770
+ try {
771
+ try {
772
+ const connected = await connectSessionViaBroker(agentDir, sessionId);
773
+ brokerClient = connected.broker;
774
+ session = connected.session;
775
+ } catch (error) {
776
+ if (row.live === false && brokerAbsent(error) === false)
777
+ return await offlineTailReplay(repo, agentDir, sessionId, row);
778
+ throw error;
779
+ }
780
+
781
+ let cursor: string | undefined;
782
+ // Q30 exchanges a still-valid signed checkpoint claim into a fresh
783
+ // connection-owned pin; direct cross-connection cursor consumption remains rejected.
784
+ const checkpointResponse = await session.query(
785
+ "session.checkpoint",
786
+ args.cursor === undefined ? {} : { checkpointToken: args.cursor },
787
+ );
788
+ const extraction = extractCheckpoint(checkpointResponse);
789
+ checkpoint = extraction.record;
790
+ gap = extraction.gap;
791
+ cursor = extraction.cursor;
792
+ if (gap !== undefined && args.strict === true)
793
+ throw new SdkSessionCliError(
794
+ "retention_gap",
795
+ "Retained history is missing entries before the checkpoint (strict mode).",
796
+ 1,
797
+ gap,
798
+ );
799
+
800
+ // Continue paginating the retained transcript from the checkpoint cursor
801
+ // (the resume page above may itself be incomplete).
802
+ while (cursor !== undefined) {
803
+ const page = extractTranscriptPage(await session.query("transcript.list", {}, cursor));
804
+ mergeTailItems(
805
+ items,
806
+ seen,
807
+ page.items.map(item => toTailItemV1(item, { kind: "transcript" })),
808
+ include,
809
+ );
810
+ if (page.complete || !page.cursor) break;
811
+ cursor = page.cursor;
812
+ }
813
+
814
+ // Stopped/terminal sessions replay retained history and exit; live
815
+ // sessions follow the event ring from the checkpoint position. A
816
+ // lifecycle-terminal row is treated as terminal for exit purposes even
817
+ // when its process is still alive (uncertain stop), so `tail` never
818
+ // hangs on a session whose lifecycle already ended.
819
+ if (row.live === false || row.terminalUncertain === true) {
820
+ return {
821
+ ok: true,
822
+ result: {
823
+ version: SESSION_ROWS_VERSION,
824
+ source: "session",
825
+ session: row,
826
+ ...(checkpoint !== undefined ? { checkpoint } : {}),
827
+ ...(gap !== undefined ? { gap } : {}),
828
+ items,
829
+ terminal: true,
830
+ },
831
+ };
832
+ }
833
+
834
+ let outcome: { reason: "idle" | "close" | "timeout" | "reconnect" | "reconnect_failed" } = { reason: "close" };
835
+ for (let reconnectAttempt = 0; reconnectAttempt <= 3; reconnectAttempt++) {
836
+ const replay = await requestEventReplay(session, checkpoint, timeoutMs);
837
+ if (replay.closed)
838
+ throw new SdkSessionCliError(
839
+ "reconnect_failed",
840
+ "Tail connection exhausted reconnect attempts before replay continuity was restored.",
841
+ 1,
842
+ );
843
+ if (replay.gap !== undefined) {
844
+ gap = replay.gap;
845
+ if (args.strict === true)
846
+ throw new SdkSessionCliError(
847
+ "retention_gap",
848
+ "The event ring dropped entries before the checkpoint (strict mode).",
849
+ 1,
850
+ replay.gap,
851
+ );
852
+ }
853
+ mergeTailItems(items, seen, replay.events, include);
854
+ const terminal = replay.events.some(item => TERMINAL_TURN_KINDS.has(item.kind)) && args.untilIdle === true;
855
+ if (args.untilIdle === true && terminal) {
856
+ return {
857
+ ok: true,
858
+ result: {
859
+ version: SESSION_ROWS_VERSION,
860
+ source: "session",
861
+ session: row,
862
+ ...(checkpoint !== undefined ? { checkpoint } : {}),
863
+ items,
864
+ terminal: true,
865
+ },
866
+ };
867
+ }
868
+ outcome = await followLiveEvents(session, items, seen, args, include, timeoutMs);
869
+ if (outcome.reason !== "reconnect") break;
870
+ if (reconnectAttempt === 3)
871
+ throw new SdkSessionCliError(
872
+ "reconnect_failed",
873
+ "Tail could not restore replay continuity after reconnect.",
874
+ 1,
875
+ );
876
+ const lastEvent = [...items].reverse().find(item => item.generation !== undefined && item.seq !== undefined);
877
+ if (lastEvent?.generation !== undefined && lastEvent.seq !== undefined)
878
+ checkpoint = {
879
+ revision: checkpoint?.revision ?? 0,
880
+ generation: lastEvent.generation,
881
+ seq: lastEvent.seq,
882
+ };
883
+ }
884
+ if (outcome.reason === "reconnect_failed")
885
+ throw new SdkSessionCliError(
886
+ "reconnect_failed",
887
+ "Tail connection exhausted reconnect attempts before continuity was restored.",
888
+ 1,
889
+ );
890
+ if (outcome.reason === "timeout")
891
+ throw new SdkSessionCliError(
892
+ "tail_timeout",
893
+ `Tail did not reach an exit condition within the wait window.`,
894
+ 1,
895
+ { sessionId, timeoutMs },
896
+ );
897
+ return {
898
+ ok: true,
899
+ result: {
900
+ version: SESSION_ROWS_VERSION,
901
+ source: "session",
902
+ session: row,
903
+ ...(checkpoint !== undefined ? { checkpoint } : {}),
904
+ ...(gap !== undefined ? { gap } : {}),
905
+ items,
906
+ terminal: outcome.reason === "idle" || outcome.reason === "close",
907
+ },
908
+ };
909
+ } finally {
910
+ if (session !== undefined) await session.close();
911
+ if (brokerClient !== undefined) await brokerClient.close();
912
+ }
913
+ }
914
+
915
+ async function runRawControl(
916
+ agentDir: string,
917
+ sessionId: string,
918
+ operation: string,
919
+ input: JsonRecord,
920
+ args: SdkSessionCliArgs,
921
+ ): Promise<unknown> {
922
+ const invalid = validateAdapterControl(operation, input);
923
+ if (invalid) throw new SdkSessionCliError(invalid.code, invalid.message, 2);
924
+ const broker = await connectBroker(agentDir);
925
+ try {
926
+ return await broker.global("session.control", {
927
+ sessionId,
928
+ operation,
929
+ input,
930
+ confirm: args.confirm === true,
931
+ ...(args.elevationRequestId === undefined ? {} : { elevationRequestId: args.elevationRequestId }),
932
+ });
933
+ } finally {
934
+ await broker.close();
935
+ }
936
+ }
937
+
938
+ async function runRawQuery(
939
+ agentDir: string,
940
+ sessionId: string,
941
+ operation: string,
942
+ input: JsonRecord,
943
+ args: SdkSessionCliArgs,
944
+ ): Promise<unknown> {
945
+ const { broker, session } = await connectSessionViaBroker(agentDir, sessionId);
946
+ try {
947
+ return await session.query(operation, input, args.cursor);
948
+ } finally {
949
+ await session.close();
950
+ await broker.close();
951
+ }
952
+ }
953
+
954
+ async function runRawGlobal(
955
+ agentDir: string,
956
+ operation: string,
957
+ input: JsonRecord,
958
+ args: SdkSessionCliArgs,
959
+ ): Promise<unknown> {
960
+ const idempotencyKey = args.idempotencyKey;
961
+ if (isLifecycleOperation(operation) && !idempotencyKey)
962
+ throw new SdkSessionCliError("invalid_input", "--idempotency-key is required for lifecycle operations.", 2);
963
+ const client = await connectBroker(agentDir);
964
+ try {
965
+ const timeoutMs = lifecycleRequestTimeoutMs(operation, input);
966
+ const response = await client.global(operation, input, {
967
+ idempotencyKey,
968
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
969
+ ...(args.elevationRequestId === undefined ? {} : { elevationRequestId: args.elevationRequestId }),
970
+ });
971
+ return args.showEndpointCredential ? response : stripSecretFields(response);
972
+ } finally {
973
+ await client.close();
974
+ }
975
+ }
976
+
977
+ async function runElevate(agentDir: string, sessionId: string, args: SdkSessionCliArgs): Promise<unknown> {
978
+ if (args.confirm !== true || !process.stdin.isTTY)
979
+ throw new SdkSessionCliError(
980
+ "operator_confirmation_required",
981
+ "Elevation approval requires an attended TTY and --confirm.",
982
+ 1,
983
+ );
984
+ const kind = args.operationKind;
985
+ if (kind !== "control" && kind !== "global")
986
+ throw new SdkSessionCliError("usage", "elevate requires --kind control|global.", 2);
987
+ const operation = requireValue(args.operation, "--op");
988
+ const input = await inputFromArgs(args);
989
+ const broker = await connectBroker(agentDir);
990
+ try {
991
+ const issued = await broker.global("elevation.issue", {
992
+ sessionId,
993
+ operation: { kind, sdkId: operation },
994
+ input,
995
+ requester: { source: "broker_connection", connectionId: `operator-cli:${process.pid}` },
996
+ });
997
+ if (isRecord(issued) && issued.ok === false) return issued;
998
+ const result = resultObject(issued);
999
+ const elevationRequestId = typeof result?.elevationRequestId === "string" ? result.elevationRequestId : undefined;
1000
+ const presentedDigest = typeof result?.requestDigest === "string" ? result.requestDigest : undefined;
1001
+ if (!elevationRequestId || !presentedDigest)
1002
+ throw new SdkSessionCliError("unavailable", "Broker returned a malformed elevation request.", 1);
1003
+ const directory = path.join(agentDir, "sdk", "elevation", "operator");
1004
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
1005
+ const target = path.join(directory, `${elevationRequestId}.json`);
1006
+ const temporary = `${target}.${randomUUID()}.tmp`;
1007
+ await Bun.write(
1008
+ temporary,
1009
+ `${JSON.stringify({ version: 1, elevationRequestId, answer: "approve", presentedDigest, createdAt: Date.now() })}\n`,
1010
+ );
1011
+ await fs.chmod(temporary, 0o600);
1012
+ await fs.rename(temporary, target);
1013
+ return { ok: true, result: { elevationRequestId, requestDigest: presentedDigest, state: "approval_submitted" } };
1014
+ } finally {
1015
+ await broker.close();
1016
+ }
1017
+ }
1018
+
1019
+ /**
1020
+ * Runs the `gjc sdk session` command family: semantic verbs (list, inspect,
1021
+ * send, status, tail) plus the explicit raw control/query/global hatch.
1022
+ */
200
1023
  export async function runSdkSessionCli(
201
1024
  args: SdkSessionCliArgs,
202
1025
  writeOutput: (value: unknown) => void = writeJson,
@@ -206,65 +1029,93 @@ export async function runSdkSessionCli(
206
1029
  ): Promise<void> {
207
1030
  try {
208
1031
  const action = args.action;
209
- if (action !== "list" && action !== "control" && action !== "query" && action !== "global")
210
- throw new SdkSessionCliError("usage", "Expected one of: list, control, query, global.", 2);
1032
+ if (
1033
+ action !== "list" &&
1034
+ action !== "inspect" &&
1035
+ action !== "send" &&
1036
+ action !== "status" &&
1037
+ action !== "tail" &&
1038
+ action !== "elevate" &&
1039
+ action !== "raw"
1040
+ )
1041
+ throw new SdkSessionCliError(
1042
+ "usage",
1043
+ "Expected one of: list, inspect, send, status, tail, elevate, raw (control|query|global).",
1044
+ 2,
1045
+ );
211
1046
  const repo = args.repo ?? process.cwd();
212
1047
  const agentDir = args.agentDir ?? getAgentDir();
1048
+
213
1049
  if (action === "list") {
214
- writeOutput(await runList(repo, agentDir));
1050
+ writeOutput(await runList(agentDir));
215
1051
  return;
216
1052
  }
217
- const operation = action === "query" ? requireValue(args.query, "--query") : requireValue(args.operation, "--op");
218
- const kind: OperationKind = action === "query" ? "query" : action === "global" ? "global" : "control";
1053
+ const sessionId =
1054
+ action === "raw" && args.rawAction === "global" ? undefined : requireValue(args.sessionId, "<sessionId>");
1055
+ if (action === "inspect") {
1056
+ writeOutput(await runInspect(repo, agentDir, requireValue(sessionId, "<sessionId>")));
1057
+ return;
1058
+ }
1059
+ if (action === "send") {
1060
+ writeOutput(await runSend(agentDir, requireValue(sessionId, "<sessionId>"), args));
1061
+ return;
1062
+ }
1063
+ if (action === "status") {
1064
+ writeOutput(
1065
+ await runStatus(agentDir, requireValue(sessionId, "<sessionId>"), requireValue(args.opRef, "<opRef>")),
1066
+ );
1067
+ return;
1068
+ }
1069
+ if (action === "tail") {
1070
+ writeOutput(await runTail(repo, agentDir, requireValue(sessionId, "<sessionId>"), args));
1071
+ return;
1072
+ }
1073
+ if (action === "elevate") {
1074
+ writeOutput(await runElevate(agentDir, requireValue(sessionId, "<sessionId>"), args));
1075
+ return;
1076
+ }
1077
+ const rawAction = args.rawAction;
1078
+ if (rawAction !== "control" && rawAction !== "query" && rawAction !== "global")
1079
+ throw new SdkSessionCliError("usage", "raw requires one of: control, query, global.", 2);
1080
+ const operation =
1081
+ rawAction === "query" ? requireValue(args.query, "--query") : requireValue(args.operation, "--op");
1082
+ const kind: OperationKind = rawAction === "query" ? "query" : rawAction === "global" ? "global" : "control";
219
1083
  const dispositionError = cliOperationError(kind, operation);
220
1084
  if (dispositionError) throw new SdkSessionCliError(dispositionError.code, dispositionError.message, 1);
221
- if (isEndpointOperation(operation)) {
222
- if (!args.showEndpointCredential)
223
- throw new SdkSessionCliError(
224
- "endpoint_credential_forbidden",
225
- "session.get_endpoint requires --show-endpoint-credential.",
226
- 1,
227
- );
228
- if (process.stdout.isTTY && !args.yes && !(await confirmEndpointCredentialOutput()))
229
- throw new SdkSessionCliError(
230
- "endpoint_credential_confirmation_required",
231
- "Endpoint credential output was not confirmed.",
232
- 1,
233
- );
234
- }
1085
+ if (isEndpointOperation(operation) && !args.showEndpointCredential)
1086
+ throw new SdkSessionCliError(
1087
+ "endpoint_credential_forbidden",
1088
+ "session.get_endpoint requires --show-endpoint-credential.",
1089
+ 1,
1090
+ );
1091
+ if (
1092
+ isCredentialBearingOperation(operation) &&
1093
+ args.showEndpointCredential &&
1094
+ process.stdout.isTTY &&
1095
+ !args.yes &&
1096
+ !(await confirmEndpointCredentialOutput())
1097
+ )
1098
+ throw new SdkSessionCliError(
1099
+ "endpoint_credential_confirmation_required",
1100
+ "Endpoint credential output was not confirmed.",
1101
+ 1,
1102
+ );
235
1103
  const input = await inputFromArgs(args);
236
- if (kind === "control") {
237
- const invalid = validateAdapterControl(operation, input);
238
- if (invalid) throw new SdkSessionCliError(invalid.code, invalid.message, 2);
239
- }
240
- if (action === "global") {
241
- const idempotencyKey = args.idempotencyKey;
242
- if (isLifecycleOperation(operation) && !idempotencyKey)
243
- throw new SdkSessionCliError("invalid_input", "--idempotency-key is required for lifecycle operations.", 2);
244
- const client = await connectBroker(agentDir);
245
- try {
246
- const timeoutMs = lifecycleRequestTimeoutMs(operation, input);
247
- writeOutput(
248
- await client.global(operation, input, {
249
- idempotencyKey,
250
- ...(timeoutMs === undefined ? {} : { timeoutMs }),
251
- }),
252
- );
253
- } finally {
254
- await client.close();
255
- }
1104
+ if (rawAction === "global") {
1105
+ writeOutput(await runRawGlobal(agentDir, operation, input, args));
256
1106
  return;
257
1107
  }
258
- const sessionId = requireValue(args.sessionId, "<sessionId>");
259
- const client = await connectSession(repo, sessionId);
260
- try {
261
- if (action === "control")
262
- writeOutput(await client.control(operation, input, { confirm: args.confirm === true }));
263
- else writeOutput(await client.query(operation, input, args.cursor));
264
- } finally {
265
- await client.close();
1108
+ if (rawAction === "control") {
1109
+ writeOutput(await runRawControl(agentDir, requireValue(sessionId, "<sessionId>"), operation, input, args));
1110
+ return;
266
1111
  }
1112
+ writeOutput(await runRawQuery(agentDir, requireValue(sessionId, "<sessionId>"), operation, input, args));
267
1113
  } catch (error) {
1114
+ if (brokerAbsent(error)) {
1115
+ writeOutput({ ok: false, error: { code: "broker_unavailable", message: "SDK broker is unavailable." } });
1116
+ setExitCode(1);
1117
+ return;
1118
+ }
268
1119
  const cliError =
269
1120
  error instanceof SdkSessionCliError
270
1121
  ? error
@@ -287,7 +1138,7 @@ export async function runSdkSessionCli(
287
1138
  error: {
288
1139
  code: cliError.code,
289
1140
  message: cliError.message,
290
- ...(cliError.details ? { details: cliError.details } : {}),
1141
+ ...(cliError.details ? { details: stripSecretFields(cliError.details) } : {}),
291
1142
  },
292
1143
  });
293
1144
  setExitCode(cliError.exitCode);