@gajae-code/coding-agent 0.12.11 → 0.12.12

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 (112) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/types/cli/notify-cli.d.ts +42 -1
  3. package/dist/types/cli/update-cli.d.ts +9 -3
  4. package/dist/types/commands/notify.d.ts +6 -0
  5. package/dist/types/config/file-lock.d.ts +3 -0
  6. package/dist/types/coordinator/contract.d.ts +1 -1
  7. package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
  8. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  9. package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
  10. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
  11. package/dist/types/extensibility/extensions/types.d.ts +2 -0
  12. package/dist/types/main.d.ts +6 -2
  13. package/dist/types/modes/components/session-selector.d.ts +1 -1
  14. package/dist/types/modes/utils/host-status.d.ts +23 -0
  15. package/dist/types/runtime-mcp/types.d.ts +2 -0
  16. package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
  17. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
  18. package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
  19. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
  20. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
  21. package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
  22. package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
  23. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
  24. package/dist/types/sdk/bus/notification-service.d.ts +10 -0
  25. package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
  26. package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
  27. package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
  28. package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
  29. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
  30. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +24 -7
  31. package/dist/types/sdk/bus/telegram-daemon.d.ts +104 -16
  32. package/dist/types/sdk/bus/telegram-reference.d.ts +10 -1
  33. package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
  34. package/dist/types/sdk/host/host.d.ts +52 -0
  35. package/dist/types/sdk/lifecycle-session.d.ts +6 -0
  36. package/dist/types/sdk/session-activation.d.ts +41 -0
  37. package/dist/types/sdk/session.d.ts +18 -0
  38. package/dist/types/sdk/startup-capability.d.ts +8 -1
  39. package/dist/types/session/agent-session.d.ts +7 -0
  40. package/dist/types/session/artifacts.d.ts +2 -2
  41. package/dist/types/session/session-manager.d.ts +23 -5
  42. package/dist/types/tools/index.d.ts +8 -0
  43. package/dist/types/utils/npm-registry.d.ts +71 -0
  44. package/examples/extensions/README.md +38 -4
  45. package/examples/extensions/ooo-bridge.ts +15 -0
  46. package/package.json +8 -8
  47. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  48. package/src/async/job-manager.ts +9 -2
  49. package/src/cli/notify-cli.ts +226 -2
  50. package/src/cli/update-cli.ts +90 -23
  51. package/src/commands/notify.ts +27 -3
  52. package/src/commands/sdk.ts +1 -0
  53. package/src/config/file-lock-gc.ts +18 -0
  54. package/src/config/file-lock.ts +61 -16
  55. package/src/config/model-profiles.ts +31 -15
  56. package/src/coordinator/contract.ts +1 -0
  57. package/src/coordinator-mcp/question-state.ts +2 -0
  58. package/src/coordinator-mcp/server.ts +358 -7
  59. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
  60. package/src/extensibility/extensions/index.ts +1 -0
  61. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
  62. package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
  63. package/src/extensibility/extensions/runner.ts +15 -2
  64. package/src/extensibility/extensions/types.ts +2 -0
  65. package/src/gjc-runtime/launch-tmux.ts +53 -1
  66. package/src/internal-urls/docs-index.generated.ts +11 -10
  67. package/src/main.ts +203 -67
  68. package/src/modes/DESIGN.md +8 -5
  69. package/src/modes/components/footer.ts +10 -18
  70. package/src/modes/components/session-selector.ts +26 -3
  71. package/src/modes/components/status-line/gh.ts +23 -2
  72. package/src/modes/controllers/event-controller.ts +3 -0
  73. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  74. package/src/modes/controllers/input-controller.ts +11 -0
  75. package/src/modes/controllers/selector-controller.ts +8 -2
  76. package/src/modes/interactive-mode.ts +7 -1
  77. package/src/modes/utils/host-status.ts +38 -0
  78. package/src/runtime-mcp/types.ts +2 -0
  79. package/src/sdk/broker/lifecycle.ts +60 -3
  80. package/src/sdk/bus/chat-daemon-cli.ts +42 -0
  81. package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
  82. package/src/sdk/bus/chat-daemon-control.ts +16 -3
  83. package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
  84. package/src/sdk/bus/control-plane-frames.ts +51 -0
  85. package/src/sdk/bus/conversation-store.ts +29 -1
  86. package/src/sdk/bus/existing-thread-readiness.ts +155 -0
  87. package/src/sdk/bus/index.ts +70 -0
  88. package/src/sdk/bus/notification-service.ts +28 -1
  89. package/src/sdk/bus/rich-render.ts +3 -2
  90. package/src/sdk/bus/slack-daemon.ts +99 -4
  91. package/src/sdk/bus/slack-live-provider.ts +32 -2
  92. package/src/sdk/bus/slack-provider.ts +11 -0
  93. package/src/sdk/bus/slack-thread-binding.ts +641 -0
  94. package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
  95. package/src/sdk/bus/telegram-daemon-contract.ts +24 -7
  96. package/src/sdk/bus/telegram-daemon.ts +1961 -497
  97. package/src/sdk/bus/telegram-reference.ts +42 -6
  98. package/src/sdk/bus/topic-registry.ts +674 -181
  99. package/src/sdk/host/host.ts +180 -1
  100. package/src/sdk/lifecycle-session.ts +8 -2
  101. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  102. package/src/sdk/session-activation.ts +127 -0
  103. package/src/sdk/session.ts +139 -28
  104. package/src/sdk/startup-capability.ts +10 -1
  105. package/src/session/agent-session.ts +220 -9
  106. package/src/session/artifacts.ts +56 -13
  107. package/src/session/internal/managed-session-storage.ts +206 -7
  108. package/src/session/session-manager.ts +148 -38
  109. package/src/setup/provider-presets.json +1 -1
  110. package/src/task/index.ts +92 -44
  111. package/src/tools/index.ts +8 -0
  112. package/src/utils/npm-registry.ts +660 -0
@@ -4,6 +4,42 @@ import type { BrokerIndexWriter, HostEndpointAdapters, SdkFrame } from "./types"
4
4
 
5
5
  export type SdkRequestObserver = (kind: "control" | "query", connectionId: string, frame: SdkFrame) => void;
6
6
 
7
+ /**
8
+ * When a session publishes its replayable readiness signal.
9
+ *
10
+ * `immediate` is the stock contract: `start()` publishes `session_ready` at
11
+ * once, so a chat daemon that attaches (or replays late) surfaces the session
12
+ * and creates its stock root. `deferred` prepares the session instead: the
13
+ * session id, endpoint, and broker registration become discoverable authority,
14
+ * but no readiness exists for any consumer to act on until `activate()` runs.
15
+ * That ordering is what lets an operator adopt an existing chat root before the
16
+ * daemon would otherwise have published one of its own.
17
+ */
18
+ export type SessionReadinessMode = "immediate" | "deferred";
19
+
20
+ /**
21
+ * Replayable signal that a deferred session is fully initialized and holding
22
+ * endpoint authority while its readiness stays withheld.
23
+ *
24
+ * It is deliberately not `session_ready`: a broker lifecycle wait can
25
+ * authenticate it as the child's semantic completion receipt, while every
26
+ * readiness consumer — chat daemons above all — keeps ignoring it and publishes
27
+ * no root until the session is explicitly activated.
28
+ */
29
+ export const SESSION_PREPARED_EVENT = "session_prepared";
30
+
31
+ /** Every terminal answer an activation attempt can produce. */
32
+ export type SessionActivationOutcome =
33
+ | "activated"
34
+ | "already"
35
+ | "not_prepared"
36
+ | "generation_changed"
37
+ | "not_authorized"
38
+ | "authority_unavailable";
39
+
40
+ /** Proves that a prepared session may publish readiness at this exact generation. */
41
+ export type SessionActivationGate = (input: { sessionId: string; generation: number }) => boolean | Promise<boolean>;
42
+
7
43
  export interface SessionSdkHostOptions extends HostEndpointAdapters {
8
44
  control?: (connectionId: string, frame: SdkFrame) => unknown | Promise<unknown>;
9
45
  query?: (connectionId: string, frame: SdkFrame) => unknown | Promise<unknown>;
@@ -23,12 +59,32 @@ export interface SessionSdkHostOptions extends HostEndpointAdapters {
23
59
  onReverseCancel?: (requestId: string, reason: "provider_disconnected" | "lease_released") => void;
24
60
  /** Best-effort capabilities mirrored from the native transport for out-of-band consumers. */
25
61
  connectionCapabilities?: (connectionId: string) => ReadonlySet<string> | undefined;
62
+ /** Readiness publication mode; defaults to the stock immediate contract. */
63
+ readiness?: SessionReadinessMode;
64
+ /**
65
+ * Authorization for a deferred activation. It is consulted on every attempt
66
+ * that would publish readiness and never on an idempotent replay, and a gate
67
+ * that fails is never read as authorization.
68
+ */
69
+ activationGate?: SessionActivationGate;
26
70
  }
27
71
 
28
72
  const TOOL_ACTIVITY_CAPABILITY = "tool_activity_v2";
29
73
  const CAP_GATED_FRAME_KINDS = new Set(["tool_activity", "reasoning_summary"]);
30
74
  const EMPTY_CAPABILITIES: ReadonlySet<string> = new Set();
31
75
 
76
+ /** Safe, identifier-free explanations for every refused activation status. */
77
+ const ACTIVATION_MESSAGES: Record<
78
+ Exclude<SessionActivationOutcome, "activated" | "already"> | "session_mismatch",
79
+ string
80
+ > = {
81
+ not_prepared: "The session is not prepared for activation.",
82
+ generation_changed: "The session endpoint generation changed before activation.",
83
+ not_authorized: "Session activation is not authorized at this endpoint generation.",
84
+ authority_unavailable: "Session activation authority could not be read.",
85
+ session_mismatch: "The activation request addresses a different session.",
86
+ };
87
+
32
88
  /** SDK hosting is independent of notification configuration. Only root sessions host an endpoint. */
33
89
  export function shouldHostSdk(_settings: unknown, isTopLevel: boolean, env: NodeJS.ProcessEnv = process.env): boolean {
34
90
  return isTopLevel && env.GJC_SDK_DISABLE !== "1";
@@ -117,9 +173,14 @@ export class SessionSdkHost {
117
173
  readonly reverse: ReverseLeaseRuntime;
118
174
  readonly #options: SessionSdkHostOptions;
119
175
  #started = false;
176
+ #stopping = false;
120
177
  #stopPromise?: Promise<"stopped">;
121
178
  #unsubscribe?: () => void;
122
179
  #registration?: { writer: BrokerIndexWriter; generation: number };
180
+ /** The generation whose readiness signal has already been published. */
181
+ #readyGeneration?: number;
182
+ /** Serializes activation attempts so a concurrent pair cannot both publish. */
183
+ #activation: Promise<SessionActivationOutcome> = Promise.resolve("not_prepared");
123
184
 
124
185
  constructor(options: SessionSdkHostOptions) {
125
186
  this.#options = options;
@@ -137,6 +198,14 @@ export class SessionSdkHost {
137
198
  get generation(): number {
138
199
  return this.events.generation;
139
200
  }
201
+ /** True while the session holds endpoint authority but has published no readiness. */
202
+ get prepared(): boolean {
203
+ return this.#started && this.#readyGeneration !== this.events.generation;
204
+ }
205
+ /** True once readiness for the current generation has been published. */
206
+ get ready(): boolean {
207
+ return this.#started && this.#readyGeneration === this.events.generation;
208
+ }
140
209
  /** Current installed definitions for a live provider capability. */
141
210
  getProviderDefinitions(capability: string): unknown | undefined {
142
211
  return this.reverse.getInstalledDefinitions(capability);
@@ -154,7 +223,13 @@ export class SessionSdkHost {
154
223
  async start(): Promise<"started" | "already"> {
155
224
  if (this.#started) return "already";
156
225
  this.events.restart();
157
- this.emitEvent({ name: "session_ready", sessionId: this.#options.sessionId, generation: this.events.generation });
226
+ if (this.#options.readiness !== "deferred") this.#publishReadiness();
227
+ else
228
+ this.emitEvent({
229
+ name: SESSION_PREPARED_EVENT,
230
+ sessionId: this.#options.sessionId,
231
+ generation: this.events.generation,
232
+ });
158
233
  const disposer = this.#options.onFrame((connectionId, frame) => {
159
234
  void this.#onFrame(connectionId, frame);
160
235
  });
@@ -169,6 +244,61 @@ export class SessionSdkHost {
169
244
  return "started";
170
245
  }
171
246
 
247
+ #publishReadiness(): void {
248
+ this.emitEvent({ name: "session_ready", sessionId: this.#options.sessionId, generation: this.events.generation });
249
+ this.#readyGeneration = this.events.generation;
250
+ }
251
+
252
+ /**
253
+ * Publish the readiness signal a prepared session withheld.
254
+ *
255
+ * The attempt is refused unless the session is still started at exactly
256
+ * `expectedGeneration` (when supplied) and the activation gate authorizes it
257
+ * at that same generation. Authority is re-proved after the gate resolves,
258
+ * because a stop or an endpoint roll can land while it is in flight, and an
259
+ * exact retry after a successful activation is answered `already` instead of
260
+ * publishing a second readiness signal.
261
+ */
262
+ activate(expectedGeneration?: number): Promise<SessionActivationOutcome> {
263
+ const attempt = this.#activation.then(
264
+ () => this.#activateOnce(expectedGeneration),
265
+ () => this.#activateOnce(expectedGeneration),
266
+ );
267
+ this.#activation = attempt.then(
268
+ outcome => outcome,
269
+ () => "authority_unavailable" as const,
270
+ );
271
+ return attempt;
272
+ }
273
+
274
+ async #activateOnce(expectedGeneration?: number): Promise<SessionActivationOutcome> {
275
+ if (!this.#started) return "not_prepared";
276
+ const generation = this.events.generation;
277
+ if (expectedGeneration !== undefined && expectedGeneration !== generation) return "generation_changed";
278
+ if (this.#readyGeneration === generation) return "already";
279
+ const gate = this.#options.activationGate;
280
+ if (gate) {
281
+ let authorized: boolean;
282
+ try {
283
+ authorized = await gate({ sessionId: this.#options.sessionId, generation });
284
+ } catch {
285
+ // An unreadable authority is never an authorization.
286
+ return "authority_unavailable";
287
+ }
288
+ if (!authorized) return "not_authorized";
289
+ // The gate is asynchronous, so the session may have stopped or rolled
290
+ // while it ran; nothing published below may rest on the earlier proof.
291
+ // `#started` stays true across the unregister await inside stop(), so an
292
+ // activation gate that resolves during shutdown would otherwise pass this
293
+ // re-check and publish readiness after teardown began.
294
+ if (!this.#started || this.#stopping) return "not_prepared";
295
+ if (this.events.generation !== generation) return "generation_changed";
296
+ if (this.#readyGeneration === generation) return "already";
297
+ }
298
+ this.#publishReadiness();
299
+ return "activated";
300
+ }
301
+
172
302
  async stop(): Promise<"stopped" | "already"> {
173
303
  if (this.#stopPromise) return this.#stopPromise;
174
304
  if (!this.#started) return "already";
@@ -182,6 +312,9 @@ export class SessionSdkHost {
182
312
  }
183
313
 
184
314
  async #stopStartedHost(): Promise<"stopped"> {
315
+ // Fence before the first await: everything after this point is teardown,
316
+ // and no in-flight activation may publish readiness across it.
317
+ this.#stopping = true;
185
318
  this.#unsubscribe?.();
186
319
  this.#unsubscribe = undefined;
187
320
  if (this.#registration?.writer.unregister)
@@ -191,6 +324,7 @@ export class SessionSdkHost {
191
324
  endpointGeneration: this.events.generation,
192
325
  });
193
326
  this.#started = false;
327
+ this.#stopping = false;
194
328
  return "stopped";
195
329
  }
196
330
 
@@ -226,6 +360,22 @@ export class SessionSdkHost {
226
360
  switch (frame.type) {
227
361
  case "control_request": {
228
362
  this.#observeRequest("control", connectionId, frame);
363
+ // Deferred readiness withholds `session_ready`, but the control
364
+ // dispatcher was still reachable, so the pre-activation interval made
365
+ // activation optional for input admission. `session_activate` is a
366
+ // separate frame type and is unaffected by this gate.
367
+ if (this.prepared) {
368
+ await this.#send(connectionId, {
369
+ type: "control_response",
370
+ id: requiredString(frame, "id"),
371
+ ok: false,
372
+ error: {
373
+ code: "not_activated",
374
+ message: "The session is prepared and must be activated before it accepts controls.",
375
+ },
376
+ });
377
+ break;
378
+ }
229
379
  const result = await this.#options.control?.(connectionId, frame);
230
380
  if (result !== undefined) {
231
381
  const response = { type: "control_response", ...(result as SdkFrame) };
@@ -241,6 +391,35 @@ export class SessionSdkHost {
241
391
  }
242
392
  break;
243
393
  }
394
+ case "session_activate": {
395
+ const id = requiredString(frame, "id");
396
+ const requestedSession = optionalString(frame, "sessionId");
397
+ const requestedGeneration = frame.endpointGeneration;
398
+ if (
399
+ requestedGeneration !== undefined &&
400
+ (typeof requestedGeneration !== "number" ||
401
+ !Number.isSafeInteger(requestedGeneration) ||
402
+ requestedGeneration <= 0)
403
+ )
404
+ throw invalidFrame("endpointGeneration must be a positive integer.");
405
+ // A request addressed to another session is refused before any
406
+ // activation authority is consulted.
407
+ const status: SessionActivationOutcome | "session_mismatch" =
408
+ requestedSession !== undefined && requestedSession !== this.#options.sessionId
409
+ ? "session_mismatch"
410
+ : await this.activate(requestedGeneration as number | undefined);
411
+ const ok = status === "activated" || status === "already";
412
+ await this.#send(connectionId, {
413
+ type: "session_activate_result",
414
+ id,
415
+ ok,
416
+ status,
417
+ sessionId: this.#options.sessionId,
418
+ generation: this.events.generation,
419
+ ...(ok ? {} : { error: { code: status, message: ACTIVATION_MESSAGES[status] } }),
420
+ });
421
+ break;
422
+ }
244
423
  case "event_replay": {
245
424
  const id = requiredString(frame, "id");
246
425
  const rawGeneration =
@@ -24,6 +24,12 @@ export type CreateLifecycleAgentSessionOptions = CreateAgentSessionOptions & {
24
24
  * the manager's short default ceiling.
25
25
  */
26
26
  mcpStartupTimeoutMs?: number;
27
+ /**
28
+ * The broker-issued readiness intent from this session's launch request.
29
+ * `deferred` prepares the session: it holds endpoint authority and publishes
30
+ * a prepared signal instead of readiness until it is explicitly activated.
31
+ */
32
+ readiness?: "immediate" | "deferred";
27
33
  };
28
34
 
29
35
  /** Internal lifecycle-only session construction with an owner-bound SDK startup result. */
@@ -31,9 +37,9 @@ export async function createLifecycleAgentSession(
31
37
  options: CreateLifecycleAgentSessionOptions = {},
32
38
  ): Promise<CreateLifecycleAgentSessionResult> {
33
39
  const rollback = new SdkStartupRollbackTracker();
34
- const capability = new SdkStartupCapability(rollback);
40
+ const capability = new SdkStartupCapability(rollback, options.readiness ?? "immediate");
35
41
  try {
36
- const { mcpStartupTimeoutMs, ...sessionOptions } = options;
42
+ const { mcpStartupTimeoutMs, readiness: _readiness, ...sessionOptions } = options;
37
43
  const internalOptions = {
38
44
  ...sessionOptions,
39
45
  [lifecycleStartupCapabilityOption]: capability,
@@ -2329,6 +2329,17 @@
2329
2329
  "testIds": "not_applicable"
2330
2330
  }
2331
2331
  },
2332
+ {
2333
+ "sourceId": "agent_session:extendStartupTurnBarrier",
2334
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
2335
+ "sourceKind": "agent_session",
2336
+ "decision": "exclude",
2337
+ "rationale": "internal CLI startup-readiness fence composition, not a user-facing SDK control seam",
2338
+ "exclusionMetadata": {
2339
+ "adapterMappings": "not_applicable",
2340
+ "testIds": "not_applicable"
2341
+ }
2342
+ },
2332
2343
  {
2333
2344
  "sourceId": "agent_session:constructor",
2334
2345
  "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
@@ -2560,6 +2571,17 @@
2560
2571
  "testIds": "not_applicable"
2561
2572
  }
2562
2573
  },
2574
+ {
2575
+ "sourceId": "agent_session:registerToolSessionTransitionCleanup",
2576
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
2577
+ "sourceKind": "agent_session",
2578
+ "decision": "exclude",
2579
+ "rationale": "internal tool transition cleanup registration for shared artifact-manager ownership, not a user-facing SDK control seam",
2580
+ "exclusionMetadata": {
2581
+ "adapterMappings": "not_applicable",
2582
+ "testIds": "not_applicable"
2583
+ }
2584
+ },
2563
2585
  {
2564
2586
  "sourceId": "agent_session:getAsyncJobSnapshot",
2565
2587
  "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
@@ -0,0 +1,127 @@
1
+ /**
2
+ * The activation exchange a prepared session answers, and nothing else.
3
+ *
4
+ * A prepared session holds discoverable endpoint authority while its readiness
5
+ * stays withheld; activation is the one request that asks it to publish that
6
+ * readiness. Every caller shares this exchange whatever proved the endpoint
7
+ * authority beforehand: the `gjc notify activate-thread` CLI resolves it from
8
+ * the local session index, and the Coordinator resolves it from the broker's
9
+ * incarnation-bound binding.
10
+ *
11
+ * It lives outside `sdk/bus` on purpose. The existing-thread module reaches the
12
+ * chat-daemon and settings graph to prove a Slack binding, and the Coordinator
13
+ * MCP entrypoint may not reach that graph at all, so the exchange the machine
14
+ * entrypoint needs carries no notification, daemon, or settings dependency.
15
+ */
16
+
17
+ import { SdkClientError } from "./client/client";
18
+
19
+ export type SessionActivationErrorCode =
20
+ | "session_not_live"
21
+ | "not_prepared"
22
+ | "not_bound"
23
+ | "activation_unavailable"
24
+ | "activation_outcome_unknown";
25
+
26
+ /** A fail-closed rejection while activating a prepared session. */
27
+ export class SessionActivationError extends Error {
28
+ constructor(
29
+ readonly code: SessionActivationErrorCode,
30
+ message: string,
31
+ ) {
32
+ super(message);
33
+ this.name = "SessionActivationError";
34
+ }
35
+ }
36
+
37
+ /** Safe confirmation of an activation. It carries identifiers only. */
38
+ export interface ActivatedPreparedSession {
39
+ sessionId: string;
40
+ endpointGeneration: number;
41
+ status: "activated" | "already";
42
+ }
43
+
44
+ /** The narrow transport surface activation needs from an SDK connection. */
45
+ export interface PreparedSessionActivationClient {
46
+ request(frame: Record<string, unknown>): Promise<Record<string, unknown>>;
47
+ close(): Promise<void>;
48
+ }
49
+
50
+ /**
51
+ * Ask one already-connected session to publish the readiness it withheld.
52
+ *
53
+ * The frame names the exact session and endpoint generation, and the answer is
54
+ * accepted only when it proves both, so a replacement session can never be
55
+ * activated in place of the requested one. Activation is idempotent: an exact
56
+ * retry answers `already` without a second readiness signal.
57
+ */
58
+ export async function requestPreparedSessionActivation(
59
+ client: PreparedSessionActivationClient,
60
+ sessionId: string,
61
+ endpointGeneration: number,
62
+ ): Promise<ActivatedPreparedSession> {
63
+ try {
64
+ const answer = await client.request({
65
+ type: "session_activate",
66
+ sessionId,
67
+ endpointGeneration,
68
+ });
69
+ return interpretActivationAnswer(answer, sessionId, endpointGeneration);
70
+ } catch (error) {
71
+ throw activationFailure(error);
72
+ }
73
+ }
74
+
75
+ function interpretActivationAnswer(
76
+ answer: Record<string, unknown>,
77
+ sessionId: string,
78
+ endpointGeneration: number,
79
+ ): ActivatedPreparedSession {
80
+ const status = answer.status;
81
+ if (
82
+ answer.type !== "session_activate_result" ||
83
+ answer.ok !== true ||
84
+ answer.sessionId !== sessionId ||
85
+ answer.generation !== endpointGeneration ||
86
+ (status !== "activated" && status !== "already")
87
+ )
88
+ throw new SessionActivationError(
89
+ "activation_outcome_unknown",
90
+ "The session answered an activation that does not match the request; rerun it to observe the settled state.",
91
+ );
92
+ return { sessionId, endpointGeneration, status };
93
+ }
94
+
95
+ /**
96
+ * Translate a failed activation by what it proves.
97
+ *
98
+ * A refused status is definitive: the session decided and published nothing. A
99
+ * transport failure after the request was sent is not, because the session may
100
+ * have activated before the answer was lost; an exact retry then answers
101
+ * `already` rather than publishing a second readiness signal.
102
+ */
103
+ function activationFailure(error: unknown): SessionActivationError {
104
+ if (error instanceof SessionActivationError) return error;
105
+ const code = error instanceof SdkClientError ? error.code : undefined;
106
+ if (code === "not_authorized")
107
+ return new SessionActivationError(
108
+ "not_bound",
109
+ "The session is prepared for an existing thread but no binding has been applied yet.",
110
+ );
111
+ if (code === "not_prepared")
112
+ return new SessionActivationError("not_prepared", "The session is not prepared for activation.");
113
+ if (code === "generation_changed" || code === "session_mismatch")
114
+ return new SessionActivationError(
115
+ "session_not_live",
116
+ "The session endpoint generation changed before activation; rerun it against the current session.",
117
+ );
118
+ if (code === "authority_unavailable")
119
+ return new SessionActivationError(
120
+ "activation_unavailable",
121
+ "The session could not read its activation authority.",
122
+ );
123
+ return new SessionActivationError(
124
+ "activation_outcome_unknown",
125
+ "The session did not answer the activation; rerun it to observe the settled state.",
126
+ );
127
+ }