@sayknow-cli/coding-agent 0.3.12 → 0.3.15

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 (248) hide show
  1. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  2. package/dist/types/autoresearch/tools/log-experiment.d.ts +2 -2
  3. package/dist/types/cli/classify-install-target.d.ts +0 -10
  4. package/dist/types/cli/daemon-cli.d.ts +2 -0
  5. package/dist/types/cli/initial-message.d.ts +1 -1
  6. package/dist/types/cli/malloc-env-guard.d.ts +65 -0
  7. package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
  8. package/dist/types/cli/notify-cli.d.ts +3 -1
  9. package/dist/types/cli/session-picker.d.ts +4 -3
  10. package/dist/types/cli/update-cli.d.ts +22 -3
  11. package/dist/types/cli.d.ts +5 -0
  12. package/dist/types/commands/daemon.d.ts +6 -2
  13. package/dist/types/commands/launch.d.ts +1 -0
  14. package/dist/types/commands/notify.d.ts +6 -0
  15. package/dist/types/commands/web-search.d.ts +1 -1
  16. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +4 -4
  17. package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -9
  18. package/dist/types/commit/agentic/tools/schemas.d.ts +9 -9
  19. package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -9
  20. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  21. package/dist/types/commit/analysis/summary.d.ts +1 -1
  22. package/dist/types/commit/changelog/generate.d.ts +3 -3
  23. package/dist/types/commit/changelog/index.d.ts +2 -2
  24. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  25. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  26. package/dist/types/commit/shared-llm.d.ts +9 -9
  27. package/dist/types/config/keybindings.d.ts +134 -129
  28. package/dist/types/config/model-profile-activation.d.ts +1 -0
  29. package/dist/types/config/model-registry.d.ts +35 -35
  30. package/dist/types/config/models-config-schema.d.ts +111 -111
  31. package/dist/types/config/settings-schema.d.ts +56 -16
  32. package/dist/types/config/settings.d.ts +2 -0
  33. package/dist/types/coordinator/contract.d.ts +1 -1
  34. package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
  35. package/dist/types/coordinator-mcp/policy.d.ts +3 -0
  36. package/dist/types/coordinator-mcp/server.d.ts +79 -6
  37. package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
  38. package/dist/types/daemon/control-types.d.ts +10 -0
  39. package/dist/types/daemon/operator-contract.d.ts +47 -0
  40. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
  41. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/errors.d.ts +12 -12
  42. package/dist/types/discovery/helpers.d.ts +1 -1
  43. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  44. package/dist/types/edit/modes/patch.d.ts +2 -2
  45. package/dist/types/extensibility/extensions/types.d.ts +3 -1
  46. package/dist/types/goals/tools/goal-tool.d.ts +6 -6
  47. package/dist/types/i18n/messages/en.d.ts +8 -8
  48. package/dist/types/internal-urls/local-protocol.d.ts +13 -9
  49. package/dist/types/lsp/index.d.ts +9 -9
  50. package/dist/types/lsp/types.d.ts +9 -9
  51. package/dist/types/main.d.ts +66 -2
  52. package/dist/types/modes/acp/acp-agent.d.ts +19 -2
  53. package/dist/types/modes/acp/acp-mode.d.ts +12 -0
  54. package/dist/types/modes/components/assistant-message.d.ts +1 -1
  55. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  56. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  57. package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
  58. package/dist/types/modes/components/pet-selector.d.ts +11 -0
  59. package/dist/types/modes/components/sayknow-pet-widget.d.ts +59 -0
  60. package/dist/types/modes/components/session-selector.d.ts +22 -9
  61. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  62. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  63. package/dist/types/modes/components/status-line.d.ts +0 -10
  64. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  65. package/dist/types/modes/components/user-message.d.ts +2 -1
  66. package/dist/types/modes/controllers/event-controller.d.ts +4 -0
  67. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  68. package/dist/types/modes/interactive-mode.d.ts +24 -2
  69. package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
  70. package/dist/types/modes/print-mode.d.ts +3 -4
  71. package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
  72. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
  73. package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
  74. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
  75. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
  76. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
  77. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
  78. package/dist/types/modes/theme/defaults/index.d.ts +568 -568
  79. package/dist/types/modes/types.d.ts +28 -2
  80. package/dist/types/modes/utils/abort-message.d.ts +1 -1
  81. package/dist/types/modes/utils/context-usage.d.ts +4 -19
  82. package/dist/types/modes/utils/irc-message.d.ts +38 -0
  83. package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
  84. package/dist/types/notifications/config.d.ts +34 -0
  85. package/dist/types/notifications/daemon-paths.d.ts +7 -0
  86. package/dist/types/notifications/index.d.ts +27 -0
  87. package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
  88. package/dist/types/notifications/notification-service.d.ts +144 -0
  89. package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
  90. package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
  91. package/dist/types/notifications/telegram-daemon.d.ts +130 -3
  92. package/dist/types/notifications/telegram-reference.d.ts +19 -2
  93. package/dist/types/notifications/topic-registry.d.ts +24 -4
  94. package/dist/types/registry/agent-registry.d.ts +2 -0
  95. package/dist/types/rlm/index.d.ts +1 -1
  96. package/dist/types/sdk.d.ts +2 -0
  97. package/dist/types/secrets/obfuscator.d.ts +1 -1
  98. package/dist/types/session/agent-session.d.ts +40 -3
  99. package/dist/types/session/cache-economics.d.ts +79 -0
  100. package/dist/types/session/context-estimation.d.ts +31 -0
  101. package/dist/types/session/provider-safety-stop.d.ts +5 -0
  102. package/dist/types/session/session-manager.d.ts +124 -4
  103. package/dist/types/session/session-storage.d.ts +155 -13
  104. package/dist/types/setup/credential-auto-import.d.ts +2 -2
  105. package/dist/types/skc-runtime/session-state-sidecar.d.ts +34 -3
  106. package/dist/types/skc-runtime/state-schema.d.ts +19 -19
  107. package/dist/types/skc-runtime/tmux-common.d.ts +6 -0
  108. package/dist/types/skc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
  109. package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +309 -0
  110. package/dist/types/skc-runtime/tmux-sessions.d.ts +49 -17
  111. package/dist/types/task/agents.d.ts +2 -2
  112. package/dist/types/task/executor.d.ts +3 -0
  113. package/dist/types/task/receipt.d.ts +3 -1
  114. package/dist/types/task/types.d.ts +35 -27
  115. package/dist/types/tools/ask-answer-registry.d.ts +5 -0
  116. package/dist/types/tools/bash.d.ts +1 -1
  117. package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
  118. package/dist/types/tools/browser.d.ts +28 -28
  119. package/dist/types/tools/computer.d.ts +20 -20
  120. package/dist/types/tools/cron.d.ts +2 -2
  121. package/dist/types/tools/debug.d.ts +40 -40
  122. package/dist/types/tools/gh.d.ts +10 -10
  123. package/dist/types/tools/image-gen.d.ts +2 -2
  124. package/dist/types/tools/index.d.ts +58 -6
  125. package/dist/types/tools/monitor.d.ts +6 -6
  126. package/dist/types/tools/skill-discovery.d.ts +4 -4
  127. package/dist/types/tools/subagent.d.ts +12 -12
  128. package/dist/types/tools/telegram-send.d.ts +7 -0
  129. package/dist/types/tools/todo-write.d.ts +8 -8
  130. package/dist/types/utils/external-editor.d.ts +2 -0
  131. package/dist/types/web/insane/bridge.d.ts +9 -9
  132. package/dist/types/web/search/index.d.ts +4 -4
  133. package/package.json +8 -8
  134. package/scripts/g004-tmux-smoke.ts +196 -36
  135. package/src/cli/daemon-cli.ts +19 -29
  136. package/src/cli/malloc-env-guard.test.ts +104 -0
  137. package/src/cli/malloc-env-guard.ts +149 -0
  138. package/src/cli/notify-cli.ts +78 -14
  139. package/src/cli/session-picker.ts +23 -40
  140. package/src/cli/update-cli.ts +56 -37
  141. package/src/cli.ts +51 -10
  142. package/src/commands/daemon.ts +19 -5
  143. package/src/commands/harness.ts +515 -47
  144. package/src/commands/launch.ts +11 -3
  145. package/src/commands/notify.ts +5 -1
  146. package/src/commands/session.ts +6 -1
  147. package/src/config/keybindings.ts +5 -0
  148. package/src/config/model-profile-activation.ts +1 -1
  149. package/src/config/model-profiles.ts +14 -14
  150. package/src/config/settings-schema.ts +41 -1
  151. package/src/config/settings.ts +96 -45
  152. package/src/coordinator/contract.ts +1 -0
  153. package/src/coordinator-mcp/model-preset.ts +100 -0
  154. package/src/coordinator-mcp/policy.ts +30 -0
  155. package/src/coordinator-mcp/server.ts +2958 -430
  156. package/src/coordinator-mcp/session-reaper.ts +129 -0
  157. package/src/daemon/control-types.ts +11 -0
  158. package/src/daemon/operator-contract.ts +121 -0
  159. package/src/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
  160. package/src/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
  161. package/src/defaults/skc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
  162. package/src/extensibility/extensions/runner.ts +1 -1
  163. package/src/extensibility/extensions/types.ts +3 -1
  164. package/src/internal-urls/docs-index.generated.ts +10 -9
  165. package/src/internal-urls/local-protocol.ts +29 -10
  166. package/src/main.ts +301 -80
  167. package/src/modes/acp/acp-agent.ts +730 -100
  168. package/src/modes/acp/acp-mode.ts +29 -2
  169. package/src/modes/components/assistant-message.ts +32 -3
  170. package/src/modes/components/bash-execution.ts +43 -13
  171. package/src/modes/components/custom-editor.ts +5 -0
  172. package/src/modes/components/footer.ts +3 -3
  173. package/src/modes/components/hook-selector.ts +12 -0
  174. package/src/modes/components/irc-sidebar.ts +206 -0
  175. package/src/modes/components/pet-selector.ts +55 -0
  176. package/src/modes/components/sayknow-pet-widget.ts +360 -0
  177. package/src/modes/components/session-selector.ts +222 -221
  178. package/src/modes/components/settings-selector.ts +10 -0
  179. package/src/modes/components/status-line/segments.ts +14 -7
  180. package/src/modes/components/status-line/types.ts +1 -1
  181. package/src/modes/components/status-line.ts +4 -171
  182. package/src/modes/components/tool-execution.ts +47 -6
  183. package/src/modes/components/user-message.ts +26 -9
  184. package/src/modes/controllers/command-controller.ts +28 -2
  185. package/src/modes/controllers/event-controller.ts +159 -36
  186. package/src/modes/controllers/extension-ui-controller.ts +37 -18
  187. package/src/modes/controllers/input-controller.ts +8 -2
  188. package/src/modes/controllers/selector-controller.ts +62 -15
  189. package/src/modes/interactive-mode.ts +199 -7
  190. package/src/modes/irc-observation-ledger.ts +127 -0
  191. package/src/modes/print-mode.ts +228 -92
  192. package/src/modes/rpc/rpc-client.ts +58 -1
  193. package/src/modes/rpc/rpc-mode.ts +286 -85
  194. package/src/modes/rpc/rpc-types.ts +15 -1
  195. package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
  196. package/src/modes/shared/agent-wire/command-validation.ts +10 -0
  197. package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
  198. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  199. package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
  200. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
  201. package/src/modes/types.ts +34 -2
  202. package/src/modes/utils/context-usage.ts +56 -92
  203. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  204. package/src/modes/utils/irc-message.ts +138 -0
  205. package/src/modes/utils/ui-helpers.ts +216 -50
  206. package/src/notifications/config.ts +44 -0
  207. package/src/notifications/daemon-paths.ts +8 -0
  208. package/src/notifications/index.ts +535 -75
  209. package/src/notifications/lifecycle-control-runtime.ts +930 -77
  210. package/src/notifications/notification-service.ts +710 -0
  211. package/src/notifications/rate-limit-pool.ts +46 -12
  212. package/src/notifications/telegram-daemon-control.ts +27 -2
  213. package/src/notifications/telegram-daemon.ts +635 -94
  214. package/src/notifications/telegram-reference.ts +73 -13
  215. package/src/notifications/topic-registry.ts +86 -9
  216. package/src/prompts/system/irc-peer-roster.md +1 -0
  217. package/src/prompts/system/subagent-system-prompt.md +1 -0
  218. package/src/prompts/system/system-prompt.md +1 -0
  219. package/src/prompts/tools/irc.md +1 -1
  220. package/src/registry/agent-registry.ts +3 -0
  221. package/src/sdk.ts +52 -1
  222. package/src/secrets/obfuscator.ts +4 -2
  223. package/src/session/agent-session.ts +670 -217
  224. package/src/session/cache-economics.ts +275 -0
  225. package/src/session/context-estimation.ts +91 -0
  226. package/src/session/provider-safety-stop.ts +9 -0
  227. package/src/session/session-dump-format.ts +6 -0
  228. package/src/session/session-manager.ts +809 -39
  229. package/src/session/session-storage.ts +582 -38
  230. package/src/skc-runtime/session-state-sidecar.ts +756 -135
  231. package/src/skc-runtime/tmux-common.ts +26 -7
  232. package/src/skc-runtime/tmux-gc.ts +52 -9
  233. package/src/skc-runtime/tmux-owner-isolation-cli.ts +185 -0
  234. package/src/skc-runtime/tmux-owner-isolation.ts +2060 -0
  235. package/src/skc-runtime/tmux-sessions.ts +871 -68
  236. package/src/slash-commands/builtin-registry.ts +153 -9
  237. package/src/slash-commands/helpers/context-report.ts +26 -4
  238. package/src/task/executor.ts +12 -3
  239. package/src/task/index.ts +8 -0
  240. package/src/task/receipt.ts +7 -2
  241. package/src/task/types.ts +26 -0
  242. package/src/tools/ask-answer-registry.ts +25 -0
  243. package/src/tools/browser/tab-supervisor.ts +63 -6
  244. package/src/tools/browser.ts +8 -3
  245. package/src/tools/index.ts +81 -6
  246. package/src/tools/sqlite-reader.ts +8 -4
  247. package/src/tui/output-block.ts +18 -6
  248. package/src/utils/external-editor.ts +6 -1
@@ -30,8 +30,22 @@ import { NotificationServer } from "@sayknow-cli/natives";
30
30
  import { logger, postmortem } from "@sayknow-cli/utils";
31
31
  import { Settings } from "../config/settings";
32
32
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../extensibility/extensions";
33
+ import type {
34
+ WorkflowGateEmitter,
35
+ WorkflowGateTerminalController,
36
+ } from "../modes/shared/agent-wire/unattended-session";
33
37
  import { parseThinkingLevel } from "../thinking";
34
- import { registerAskAnswerSource } from "../tools/ask-answer-registry";
38
+ import type {
39
+ AskAnswerRequest,
40
+ AskAnswerSourceResult,
41
+ AskRemoteControl,
42
+ AskRemoteInteraction,
43
+ AskRemoteReceipt,
44
+ AskSelectedAckOutcome,
45
+ AskSettlement,
46
+ AskSettlementResult,
47
+ } from "../tools";
48
+ import { registerAskAnswerSource, registerWorkflowGateEmitterListener } from "../tools/ask-answer-registry";
35
49
  import { registerTelegramFileSink } from "./attachment-registry";
36
50
  import {
37
51
  getNotificationConfig,
@@ -327,8 +341,128 @@ async function readGitDiffStat(cwd: string): Promise<string | undefined> {
327
341
  }
328
342
 
329
343
  interface PendingInteractiveAsk {
330
- resolve: (label: string | undefined) => void;
344
+ resolve: (result: AskAnswerSourceResult) => void;
331
345
  options: string[];
346
+ controls: readonly AskRemoteControl[];
347
+ reissue: () => boolean;
348
+ }
349
+
350
+ interface UnattendedGatePresentation {
351
+ gateId: string;
352
+ sessionId: string;
353
+ question: string;
354
+ options: string[];
355
+ controls: readonly AskRemoteControl[];
356
+ multi: boolean;
357
+ allowEmpty: boolean;
358
+ navigationLabel?: "Next" | "Done";
359
+ selectedOptions: string[];
360
+ }
361
+
362
+ /** Keeps transport interaction ids separate from durable workflow gate ids. */
363
+ class GatePresentationRegistry {
364
+ private readonly presentations = new Map<string, UnattendedGatePresentation>();
365
+ private readonly routes = new Map<string, string>();
366
+
367
+ constructor(
368
+ private readonly server: NotificationServer,
369
+ private readonly redact: () => boolean,
370
+ private readonly tag: string,
371
+ ) {}
372
+
373
+ retain(presentation: UnattendedGatePresentation): void {
374
+ this.presentations.set(presentation.gateId, presentation);
375
+ }
376
+
377
+ routeFor(actionId: string): string | undefined {
378
+ return this.routes.get(actionId);
379
+ }
380
+
381
+ presentationFor(actionId: string): UnattendedGatePresentation | undefined {
382
+ const gateId = this.routes.get(actionId);
383
+ return gateId ? this.presentations.get(gateId) : undefined;
384
+ }
385
+
386
+ toggle(actionId: string, label: string): boolean {
387
+ const presentation = this.presentationFor(actionId);
388
+ if (!presentation?.multi || !presentation.options.includes(label)) return false;
389
+ this.closeInteraction(actionId, "toggle");
390
+ const selected = new Set(presentation.selectedOptions);
391
+ if (selected.has(label)) selected.delete(label);
392
+ else selected.add(label);
393
+ presentation.selectedOptions = [...selected];
394
+ this.reissue(presentation.gateId);
395
+ return true;
396
+ }
397
+
398
+ reissue(gateId: string): string | undefined {
399
+ const presentation = this.presentations.get(gateId);
400
+ if (!presentation) return undefined;
401
+ const actionId = `gate-interaction:${crypto.randomUUID()}`;
402
+ this.routes.set(actionId, gateId);
403
+ try {
404
+ this.server.registerAsk(
405
+ JSON.stringify(
406
+ notificationActionPayload(
407
+ {
408
+ id: actionId,
409
+ kind: "ask",
410
+ sessionId: presentation.sessionId,
411
+ question:
412
+ presentation.selectedOptions.length > 0
413
+ ? `(${presentation.selectedOptions.length} selected) ${presentation.question}`
414
+ : presentation.question,
415
+ options: presentation.options,
416
+ controls: presentation.multi
417
+ ? [
418
+ {
419
+ id: "navigation_forward",
420
+ kind: "navigation",
421
+ label: presentation.navigationLabel ?? "Done",
422
+ enabled: presentation.allowEmpty || presentation.selectedOptions.length > 0,
423
+ },
424
+ ]
425
+ : [],
426
+ },
427
+ { redact: this.redact(), sessionTag: this.tag },
428
+ ),
429
+ ),
430
+ true,
431
+ );
432
+ return actionId;
433
+ } catch (error) {
434
+ this.routes.delete(actionId);
435
+ logger.warn(`notifications: registerAsk (gate interaction) failed: ${String(error)}`);
436
+ return undefined;
437
+ }
438
+ }
439
+
440
+ closeInteraction(actionId: string, reason: string): void {
441
+ this.routes.delete(actionId);
442
+ try {
443
+ this.server.resolveLocal(actionId, undefined);
444
+ } catch {
445
+ // The native claim close already terminalizes this action when applicable.
446
+ }
447
+ void reason;
448
+ }
449
+
450
+ complete(gateId: string): void {
451
+ for (const [actionId, routeGateId] of this.routes) {
452
+ if (routeGateId !== gateId) continue;
453
+ this.closeInteraction(actionId, "gate_complete");
454
+ }
455
+ this.presentations.delete(gateId);
456
+ }
457
+
458
+ cancel(gateId: string, reason: string): void {
459
+ this.complete(gateId);
460
+ void reason;
461
+ }
462
+
463
+ dispose(): void {
464
+ for (const gateId of [...this.presentations.keys()]) this.cancel(gateId, "session_shutdown");
465
+ }
332
466
  }
333
467
 
334
468
  interface SessionRuntime {
@@ -342,6 +476,12 @@ interface SessionRuntime {
342
476
  disposeFileSink: () => void;
343
477
  /** Deregisters this session's unattended workflow-gate listener. */
344
478
  disposeGateListener: () => void;
479
+ /** Deregisters canonical workflow-gate terminal cleanup. */
480
+ disposeGateTerminalController: () => void;
481
+ disposeAckRecoveryParticipant: () => void;
482
+ disposeGateEmitterListener: () => void;
483
+ workflowGate?: WorkflowGateEmitter;
484
+ gatePresentations?: GatePresentationRegistry;
345
485
  redact: boolean;
346
486
  verbosity: "lean" | "verbose";
347
487
  sessionTag: string;
@@ -389,9 +529,11 @@ const defaultConfig: NotificationConfig = {
389
529
  },
390
530
  redact: false,
391
531
  verbosity: "lean",
532
+ sessionScope: "all",
392
533
  idleTimeoutMs: 60_000,
393
534
  rich: { enabled: true },
394
535
  richDraft: { enabled: false },
536
+ topics: {},
395
537
  };
396
538
 
397
539
  export function notificationsEnabled(): boolean {
@@ -463,18 +605,41 @@ function mapAnswerToLabel(answerJson: string, options: string[]): string | undef
463
605
  return undefined;
464
606
  }
465
607
 
466
- /** Map a client answer to the workflow-gate answer shape (unattended mode). */
467
- function mapAnswerToGate(
468
- answerJson: string,
469
- options: string[],
470
- ): { selected: string[]; other?: boolean; custom?: string } {
608
+ /** Workflow-gate answer shape (unattended mode). */
609
+ interface GateAnswer {
610
+ selected: string[];
611
+ other?: boolean;
612
+ custom?: string;
613
+ }
614
+
615
+ /**
616
+ * Discriminated result of mapping a client answer to a workflow-gate answer.
617
+ * `ok: false` means the reply is invalid and the caller must close the exact
618
+ * claim/receipt and reissue the interaction rather than durably accepting it.
619
+ */
620
+ type GateAnswerResult = { ok: true; answer: GateAnswer } | { ok: false; reason: string };
621
+
622
+ /**
623
+ * Map a client answer to the workflow-gate answer shape (unattended mode).
624
+ *
625
+ * The protocol defines a numeric reply as an option index, so a number outside
626
+ * `options` is invalid (issue #2030): it must NOT be converted into a free-text
627
+ * `Other` that passes the ask schema and triggers a misleading success ack.
628
+ * Only JSON strings enter the free-text/Other path.
629
+ */
630
+ export function mapAnswerToGate(answerJson: string, options: string[]): GateAnswerResult {
471
631
  const answer = parseAnswer(answerJson);
472
632
  if (typeof answer === "number") {
473
633
  const label = options[answer];
474
- return label === undefined ? { selected: [], other: true, custom: String(answer) } : { selected: [label] };
634
+ return label === undefined
635
+ ? { ok: false, reason: "numeric_selector_out_of_range" }
636
+ : { ok: true, answer: { selected: [label] } };
475
637
  }
476
638
  if (typeof answer === "string") {
477
- return options.includes(answer) ? { selected: [answer] } : { selected: [], other: true, custom: answer };
639
+ return {
640
+ ok: true,
641
+ answer: options.includes(answer) ? { selected: [answer] } : { selected: [], other: true, custom: answer },
642
+ };
478
643
  }
479
644
  if (answer && typeof answer === "object") {
480
645
  const obj = answer as { selected?: unknown; custom?: unknown };
@@ -482,9 +647,9 @@ function mapAnswerToGate(
482
647
  ? obj.selected.map(s => (typeof s === "number" ? (options[s] ?? String(s)) : String(s)))
483
648
  : [];
484
649
  const custom = typeof obj.custom === "string" ? obj.custom : undefined;
485
- return { selected, other: custom !== undefined, custom };
650
+ return { ok: true, answer: { selected, other: custom !== undefined, custom } };
486
651
  }
487
- return { selected: [] };
652
+ return { ok: true, answer: { selected: [] } };
488
653
  }
489
654
 
490
655
  interface NotificationControlCommandPayload {
@@ -602,6 +767,92 @@ export async function executeNotificationControlCommand(
602
767
  }
603
768
  }
604
769
 
770
+ function selectedAckOutcome(value: { status: string; messageId?: number; reason?: string }): AskSelectedAckOutcome {
771
+ if (value.status === "delivered" && typeof value.messageId === "number") {
772
+ return { status: "delivered", messageId: value.messageId };
773
+ }
774
+ if (value.status === "failed") {
775
+ switch (value.reason) {
776
+ case "unsupported":
777
+ case "no_participant":
778
+ case "ambiguous_participant":
779
+ case "route_missing":
780
+ case "expired":
781
+ case "cancelled":
782
+ case "telegram_rejected":
783
+ case "session_closed":
784
+ return { status: "failed", reason: value.reason };
785
+ default:
786
+ return { status: "failed", reason: "session_closed" };
787
+ }
788
+ }
789
+ switch (value.reason) {
790
+ case "transport_ambiguous":
791
+ case "origin_disconnected":
792
+ case "host_timeout":
793
+ case "shutdown":
794
+ return { status: "unknown", reason: value.reason };
795
+ default:
796
+ return { status: "unknown", reason: "host_timeout" };
797
+ }
798
+ }
799
+
800
+ async function requestLiveSelectedAck(
801
+ native: {
802
+ requestAskSelectedAck(
803
+ replyReceiptId: string,
804
+ requestJson: string,
805
+ ): Promise<{ status: string; messageId?: number; reason?: string }>;
806
+ },
807
+ input: { replyReceiptId: string; actionId: string; commitKey: string; deadlineAt: number },
808
+ ): Promise<AskSelectedAckOutcome> {
809
+ const requestId = `ack:${crypto.randomUUID()}`;
810
+ try {
811
+ return selectedAckOutcome(
812
+ await native.requestAskSelectedAck(
813
+ input.replyReceiptId,
814
+ JSON.stringify({
815
+ mode: "live",
816
+ requestId,
817
+ commitKey: input.commitKey,
818
+ actionId: input.actionId,
819
+ deadlineAt: input.deadlineAt,
820
+ }),
821
+ ),
822
+ );
823
+ } catch (error) {
824
+ logger.warn(`notifications: Selected acknowledgement failed: ${String(error)}`);
825
+ return { status: "unknown", reason: "host_timeout" };
826
+ }
827
+ }
828
+
829
+ async function requestRecoveredSelectedAck(
830
+ native: {
831
+ requestRecoveredAskSelectedAck(
832
+ requestJson: string,
833
+ ): Promise<{ status: string; messageId?: number; reason?: string }>;
834
+ },
835
+ input: { sessionId: string; actionId: string; commitKey: string; deadlineAt: number },
836
+ ): Promise<AskSelectedAckOutcome> {
837
+ try {
838
+ return selectedAckOutcome(
839
+ await native.requestRecoveredAskSelectedAck(
840
+ JSON.stringify({
841
+ mode: "recovery",
842
+ requestId: `ack:${crypto.randomUUID()}`,
843
+ commitKey: input.commitKey,
844
+ sessionId: input.sessionId,
845
+ actionId: input.actionId,
846
+ deadlineAt: input.deadlineAt,
847
+ }),
848
+ ),
849
+ );
850
+ } catch (error) {
851
+ logger.warn(`notifications: recovered Selected acknowledgement failed: ${String(error)}`);
852
+ return { status: "unknown", reason: "host_timeout" };
853
+ }
854
+ }
855
+
605
856
  /** Register the interactive `ask` answer source for a session (the ask tool
606
857
  * races the local UI against a remote reply). Returns the deregister disposer. */
607
858
  function registerInteractiveAnswerSource(
@@ -613,29 +864,59 @@ function registerInteractiveAnswerSource(
613
864
  ): () => void {
614
865
  return registerAskAnswerSource(id, {
615
866
  awaitAnswer(question, options, signal) {
867
+ const result = this.awaitAnswerRequest?.({ question, options, interaction: "selector", controls: [] }, signal);
868
+ if (!result) return Promise.resolve(undefined);
869
+ return result.then(answer => {
870
+ if (!answer || typeof answer === "string") return answer;
871
+ return answer.interaction.kind === "value" ? answer.interaction.value : undefined;
872
+ });
873
+ },
874
+ awaitAnswerRequest(request: AskAnswerRequest, signal?: AbortSignal): Promise<AskAnswerSourceResult> {
616
875
  if (signal?.aborted) return Promise.resolve(undefined);
617
- const askId = `ask:${crypto.randomUUID()}`;
618
- try {
619
- server.registerAsk(
620
- JSON.stringify(
621
- notificationActionPayload(
622
- { id: askId, kind: "ask", sessionId: id, question, options },
623
- { redact: getRedact(), sessionTag: tag },
876
+ const register = (askId: string): boolean => {
877
+ try {
878
+ server.registerAsk(
879
+ JSON.stringify(
880
+ notificationActionPayload(
881
+ {
882
+ id: askId,
883
+ kind: "ask",
884
+ sessionId: id,
885
+ question: request.question,
886
+ options: request.options,
887
+ controls: request.controls,
888
+ },
889
+ { redact: getRedact(), sessionTag: tag },
890
+ ),
624
891
  ),
625
- ),
626
- true,
627
- );
628
- } catch (e) {
629
- logger.warn(`notifications: registerAsk failed: ${String(e)}`);
630
- return Promise.resolve(undefined);
631
- }
632
- return new Promise<string | undefined>(resolve => {
633
- pendingInteractive.set(askId, { resolve, options });
892
+ true,
893
+ );
894
+ return true;
895
+ } catch (error) {
896
+ logger.warn(`notifications: registerAsk failed: ${String(error)}`);
897
+ return false;
898
+ }
899
+ };
900
+ let activeAskId = `ask:${crypto.randomUUID()}`;
901
+ if (!register(activeAskId)) return Promise.resolve(undefined);
902
+ return new Promise<AskAnswerSourceResult>(resolve => {
903
+ const pending: PendingInteractiveAsk = {
904
+ resolve,
905
+ options: request.options,
906
+ controls: request.controls,
907
+ reissue: () => {
908
+ const nextAskId = `ask:${crypto.randomUUID()}`;
909
+ if (!register(nextAskId)) return false;
910
+ activeAskId = nextAskId;
911
+ pendingInteractive.set(nextAskId, pending);
912
+ return true;
913
+ },
914
+ };
915
+ pendingInteractive.set(activeAskId, pending);
634
916
  signal?.addEventListener("abort", () => {
635
- if (!pendingInteractive.delete(askId)) return;
636
- // Local UI answered: mark the remote action resolved-locally.
917
+ if (!pendingInteractive.delete(activeAskId)) return;
637
918
  try {
638
- server.resolveLocal(askId, undefined);
919
+ server.resolveLocal(activeAskId, undefined);
639
920
  } catch {}
640
921
  resolve(undefined);
641
922
  });
@@ -674,6 +955,16 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
674
955
  try {
675
956
  rt.disposeGateListener();
676
957
  } catch {}
958
+ try {
959
+ rt.disposeGateTerminalController();
960
+ } catch {}
961
+ try {
962
+ rt.disposeAckRecoveryParticipant();
963
+ } catch {}
964
+ try {
965
+ rt.disposeGateEmitterListener();
966
+ } catch {}
967
+ rt.gatePresentations?.dispose();
677
968
  // Resolve any still-pending interactive asks so the ask tool is not left hanging.
678
969
  for (const pending of rt.pendingInteractive.values()) pending.resolve(undefined);
679
970
  rt.pendingInteractive.clear();
@@ -708,11 +999,6 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
708
999
  if (runtimes.has(id)) return "already";
709
1000
 
710
1001
  const stateRoot = path.join(ctx.cwd, ".skc", "state");
711
- const gate = ctx.workflowGate;
712
- const unattended =
713
- gate?.isUnattended?.() === true &&
714
- typeof gate.onGateEmitted === "function" &&
715
- typeof gate.resolveGate === "function";
716
1002
  const gateOptions = new Map<string, string[]>();
717
1003
  const pendingInteractive = new Map<string, PendingInteractiveAsk>();
718
1004
  const tag = sessionTag(id);
@@ -723,41 +1009,161 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
723
1009
  // The SDK can always answer now (interactive via the answer source, or the
724
1010
  // unattended gate), so the endpoint advertises a resolver.
725
1011
  const server = new NotificationServer(id, resolveToken(), stateRoot, true);
1012
+ const gatePresentations = new GatePresentationRegistry(server, () => runtime?.redact ?? redact, tag);
726
1013
 
727
1014
  server.onReply((err, reply) => {
728
1015
  if (err || !reply) return;
729
- // 1) Interactive ask awaiting a remote answer.
1016
+ const native = server as unknown as {
1017
+ resolveClaim(receiptId: string, answerJson?: string, idempotencyKey?: string): void;
1018
+ closeClaimInvalid(receiptId: string, reason: string): void;
1019
+ requestAskSelectedAck(
1020
+ receiptId: string,
1021
+ requestJson: string,
1022
+ ): Promise<{ status: string; messageId?: number; reason?: string }>;
1023
+ };
730
1024
  const pending = pendingInteractive.get(reply.id);
731
1025
  if (pending) {
732
1026
  pendingInteractive.delete(reply.id);
733
- const label = mapAnswerToLabel(reply.answerJson, pending.options);
1027
+ let interaction: AskRemoteInteraction | undefined;
734
1028
  try {
735
- server.resolveClient(reply.id, reply.answerJson, reply.idempotencyKey ?? undefined);
736
- } catch (e) {
737
- logger.warn(`notifications: resolveClient failed: ${String(e)}`);
1029
+ const answer = JSON.parse(reply.answerJson) as unknown;
1030
+ if (typeof answer === "object" && answer && "controlId" in answer) {
1031
+ const controlId = (answer as { controlId?: unknown }).controlId;
1032
+ if (
1033
+ controlId === "navigation_forward" &&
1034
+ pending.controls.some(control => control.id === controlId && control.enabled)
1035
+ ) {
1036
+ interaction = { kind: "control", controlId };
1037
+ }
1038
+ } else {
1039
+ const value = mapAnswerToLabel(reply.answerJson, pending.options);
1040
+ if (value !== undefined) interaction = { kind: "value", value };
1041
+ }
1042
+ } catch {}
1043
+ if (!interaction) {
1044
+ try {
1045
+ native.closeClaimInvalid(reply.replyReceiptId, "invalid_answer");
1046
+ } catch {}
1047
+ if (!pending.reissue()) pending.resolve(undefined);
1048
+ return;
738
1049
  }
739
- pending.resolve(label);
1050
+ let settled: Promise<AskSettlementResult> | undefined;
1051
+ const receipt: AskRemoteReceipt = {
1052
+ source: "remote",
1053
+ interaction,
1054
+ settle(settlement: AskSettlement): Promise<AskSettlementResult> {
1055
+ if (settled) return settled;
1056
+ settled = Promise.resolve().then(async () => {
1057
+ if (settlement.kind === "invalid") {
1058
+ native.closeClaimInvalid(reply.replyReceiptId, settlement.reason);
1059
+ return { kind: "invalid_closed" };
1060
+ }
1061
+ if (settlement.kind === "resolve_without_commit") {
1062
+ native.resolveClaim(reply.replyReceiptId, reply.answerJson, reply.idempotencyKey ?? undefined);
1063
+ return { kind: "resolved_without_commit" };
1064
+ }
1065
+ const ack = await requestLiveSelectedAck(native, {
1066
+ replyReceiptId: reply.replyReceiptId,
1067
+ actionId: reply.id,
1068
+ commitKey: `${reply.id}:${reply.idempotencyKey ?? reply.replyReceiptId}`,
1069
+ deadlineAt: Date.now() + 8_000,
1070
+ });
1071
+ native.resolveClaim(reply.replyReceiptId, reply.answerJson, reply.idempotencyKey ?? undefined);
1072
+ return { kind: "committed", ack };
1073
+ });
1074
+ return settled;
1075
+ },
1076
+ };
1077
+ pending.resolve(receipt);
740
1078
  return;
741
1079
  }
742
- // 2) Unattended workflow gate: resolve the real gate, then confirm.
743
- if (unattended && gate?.resolveGate) {
744
- const answer = mapAnswerToGate(reply.answerJson, gateOptions.get(reply.id) ?? []);
745
- gate
746
- .resolveGate({ gate_id: reply.id, answer, idempotency_key: reply.idempotencyKey ?? undefined })
747
- .then(() => server.resolveClient(reply.id, reply.answerJson, reply.idempotencyKey ?? undefined))
748
- .catch(e => {
749
- logger.warn(`notifications: resolveGate failed: ${String(e)}`);
750
- try {
751
- server.reject(reply.id, "invalid_answer");
752
- } catch {}
753
- });
1080
+ const gate = runtime?.workflowGate;
1081
+ const unattended =
1082
+ gate?.isUnattended?.() === true &&
1083
+ typeof gate.onGateEmitted === "function" &&
1084
+ typeof gate.resolveGate === "function";
1085
+ const gateId = gatePresentations.routeFor(reply.id);
1086
+ if (unattended && gateId && gate?.resolveGateFromNotification) {
1087
+ const presentation = gatePresentations.presentationFor(reply.id);
1088
+ const rawAnswer = parseAnswer(reply.answerJson);
1089
+ if (presentation?.multi) {
1090
+ const option =
1091
+ typeof rawAnswer === "number"
1092
+ ? presentation.options[rawAnswer]
1093
+ : typeof rawAnswer === "string" && presentation.options.includes(rawAnswer)
1094
+ ? rawAnswer
1095
+ : undefined;
1096
+ if (option !== undefined) {
1097
+ native.resolveClaim(reply.replyReceiptId, reply.answerJson, reply.idempotencyKey ?? undefined);
1098
+ if (!gatePresentations.toggle(reply.id, option)) gatePresentations.reissue(gateId);
1099
+ return;
1100
+ }
1101
+ }
1102
+ let answer: unknown;
1103
+ if (
1104
+ presentation?.multi &&
1105
+ typeof rawAnswer === "object" &&
1106
+ rawAnswer !== null &&
1107
+ (rawAnswer as { controlId?: unknown }).controlId === "navigation_forward"
1108
+ ) {
1109
+ if (!presentation.allowEmpty && presentation.selectedOptions.length === 0) {
1110
+ native.closeClaimInvalid(reply.replyReceiptId, "invalid_control");
1111
+ gatePresentations.closeInteraction(reply.id, "invalid_control");
1112
+ gatePresentations.reissue(gateId);
1113
+ return;
1114
+ }
1115
+ answer = { selected: presentation.selectedOptions };
1116
+ } else if (
1117
+ typeof rawAnswer === "object" &&
1118
+ rawAnswer !== null &&
1119
+ (rawAnswer as { action?: unknown }).action === "clarify"
1120
+ ) {
1121
+ answer = rawAnswer;
1122
+ } else if (presentation?.multi && typeof rawAnswer === "string") {
1123
+ answer = { selected: presentation.selectedOptions, other: true, custom: rawAnswer };
1124
+ } else {
1125
+ const mapped = mapAnswerToGate(reply.answerJson, gateOptions.get(gateId) ?? []);
1126
+ if (!mapped.ok) {
1127
+ // A numeric selector outside options is invalid (issue #2030): close the
1128
+ // exact claim/receipt and reissue the interaction — never a success ack.
1129
+ native.closeClaimInvalid(reply.replyReceiptId, mapped.reason);
1130
+ gatePresentations.closeInteraction(reply.id, mapped.reason);
1131
+ gatePresentations.reissue(gateId);
1132
+ return;
1133
+ }
1134
+ answer = mapped.answer;
1135
+ }
1136
+ void gate
1137
+ .resolveGateFromNotification(
1138
+ { gate_id: gateId, answer, idempotency_key: reply.idempotencyKey ?? undefined },
1139
+ {
1140
+ interactionActionId: reply.id,
1141
+ replyReceiptId: reply.replyReceiptId,
1142
+ answerJson: reply.answerJson,
1143
+ idempotencyKey: reply.idempotencyKey ?? undefined,
1144
+ resolveClaim: () =>
1145
+ native.resolveClaim(reply.replyReceiptId, reply.answerJson, reply.idempotencyKey ?? undefined),
1146
+ closeClaimInvalid: reason => {
1147
+ native.closeClaimInvalid(reply.replyReceiptId, reason);
1148
+ gatePresentations.closeInteraction(reply.id, reason);
1149
+ gatePresentations.reissue(gateId);
1150
+ },
1151
+ requestSelectedAck: input =>
1152
+ requestLiveSelectedAck(native, {
1153
+ replyReceiptId: input.replyReceiptId,
1154
+ actionId: input.actionId,
1155
+ commitKey: input.commitKey,
1156
+ deadlineAt: input.daemonDeadlineAt,
1157
+ }),
1158
+ },
1159
+ )
1160
+ .catch(error => logger.warn(`notifications: resolveGateFromNotification failed: ${String(error)}`));
754
1161
  return;
755
1162
  }
756
- // 3) No matching pending ask.
757
1163
  try {
758
- server.reject(reply.id, "unknown_action");
759
- } catch (e) {
760
- logger.warn(`notifications: reject failed: ${String(e)}`);
1164
+ server.closeClaimInvalid(reply.replyReceiptId, "unknown_action");
1165
+ } catch (error) {
1166
+ logger.warn(`notifications: closeClaimInvalid failed: ${String(error)}`);
761
1167
  }
762
1168
  });
763
1169
 
@@ -892,6 +1298,11 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
892
1298
  disposeAnswerSource,
893
1299
  disposeFileSink,
894
1300
  disposeGateListener: () => {},
1301
+ disposeGateTerminalController: () => {},
1302
+ disposeAckRecoveryParticipant: () => {},
1303
+ disposeGateEmitterListener: () => {},
1304
+ workflowGate: undefined,
1305
+ gatePresentations,
895
1306
  cancelPostmortemCleanup: () => {},
896
1307
  redact,
897
1308
  verbosity,
@@ -901,6 +1312,7 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
901
1312
  pendingInbound: new Set<number>(),
902
1313
  };
903
1314
  runtimes.set(id, runtime);
1315
+ const activeRuntime = runtime;
904
1316
  // A native terminal close (SIGHUP), SIGTERM, Ctrl+C exit, or fatal error
905
1317
  // skips AgentSession.dispose(), so the `session_shutdown` extension event
906
1318
  // never fires and the daemon-side topic would be orphaned. postmortem
@@ -910,7 +1322,7 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
910
1322
  runtime.cancelPostmortemCleanup = postmortem.register(`notifications-session-closed:${id}`, async () => {
911
1323
  await stopSession(id);
912
1324
  });
913
- logger.info(`notifications: serving session ${id} at ${endpoint.url} (unattended=${unattended})`);
1325
+ logger.info(`notifications: serving session ${id} at ${endpoint.url}`);
914
1326
 
915
1327
  if (settingsAvailable && settings && isTelegramConfigured(cfg)) {
916
1328
  try {
@@ -934,9 +1346,35 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
934
1346
  logger.warn(`notifications: identity_header failed: ${String(e)}`);
935
1347
  }
936
1348
 
937
- // Unattended: a real ask emits a workflow gate; register it repliable by gate_id.
938
- if (unattended && gate?.onGateEmitted) {
939
- runtime.disposeGateListener = gate.onGateEmitted(g => {
1349
+ // Workflow-gate installation occurs after session_start in RPC/bridge modes.
1350
+ // Attach dynamically so those sessions receive the same Telegram gate surface.
1351
+ const attachWorkflowGate = (gate: WorkflowGateEmitter | undefined): void => {
1352
+ if (activeRuntime.workflowGate === gate) return;
1353
+ activeRuntime.disposeGateListener();
1354
+ activeRuntime.disposeGateTerminalController();
1355
+ activeRuntime.disposeAckRecoveryParticipant();
1356
+ activeRuntime.disposeGateListener = () => {};
1357
+ activeRuntime.disposeGateTerminalController = () => {};
1358
+ activeRuntime.disposeAckRecoveryParticipant = () => {};
1359
+ activeRuntime.workflowGate = undefined;
1360
+ gateOptions.clear();
1361
+ gatePresentations.dispose();
1362
+ if (typeof gate?.onGateEmitted !== "function" || typeof gate.resolveGate !== "function") {
1363
+ return;
1364
+ }
1365
+ activeRuntime.workflowGate = gate;
1366
+ if (gate.registerGateTerminalController) {
1367
+ const controller: WorkflowGateTerminalController = {
1368
+ completeGateInteractions: gateId => gatePresentations.complete(gateId),
1369
+ cancelGateInteractions: (gateId, reason) => gatePresentations.cancel(gateId, reason),
1370
+ };
1371
+ try {
1372
+ activeRuntime.disposeGateTerminalController = gate.registerGateTerminalController(controller);
1373
+ } catch (error) {
1374
+ logger.warn(`notifications: gate terminal controller unavailable: ${String(error)}`);
1375
+ }
1376
+ }
1377
+ activeRuntime.disposeGateListener = gate.onGateEmitted(g => {
940
1378
  const options = (g.options ?? []).map(o => String((o as { label?: unknown }).label ?? ""));
941
1379
  gateOptions.set(g.gate_id, options);
942
1380
  const promptCtx = g.context as { prompt?: unknown; title?: unknown } | undefined;
@@ -944,21 +1382,43 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
944
1382
  (typeof promptCtx?.prompt === "string" && promptCtx.prompt) ||
945
1383
  (typeof promptCtx?.title === "string" && promptCtx.title) ||
946
1384
  "Question";
947
- try {
948
- server.registerAsk(
949
- JSON.stringify(
950
- notificationActionPayload(
951
- { id: g.gate_id, kind: "ask", sessionId: id, question, options },
952
- { redact, sessionTag: tag },
953
- ),
954
- ),
955
- true,
956
- );
957
- } catch (e) {
958
- logger.warn(`notifications: registerAsk (gate) failed: ${String(e)}`);
959
- }
1385
+ const stageState =
1386
+ typeof g.context?.stage_state === "object" && g.context.stage_state !== null
1387
+ ? (g.context.stage_state as Record<string, unknown>)
1388
+ : {};
1389
+ gatePresentations.retain({
1390
+ gateId: g.gate_id,
1391
+ sessionId: id,
1392
+ question,
1393
+ options,
1394
+ controls: [],
1395
+ multi: stageState.multi === true,
1396
+ allowEmpty: stageState.allow_empty === true,
1397
+ navigationLabel: stageState.navigation_label === "Next" ? "Next" : "Done",
1398
+ selectedOptions: [],
1399
+ });
1400
+ gatePresentations.reissue(g.gate_id);
960
1401
  });
961
- }
1402
+ if (gate.setAckRecoveryParticipant) {
1403
+ const native = server as unknown as {
1404
+ requestRecoveredAskSelectedAck(
1405
+ requestJson: string,
1406
+ ): Promise<{ status: string; messageId?: number; reason?: string }>;
1407
+ };
1408
+ gate.setAckRecoveryParticipant({
1409
+ requestRecoveredAskSelectedAck: input =>
1410
+ requestRecoveredSelectedAck(native, {
1411
+ sessionId: input.sessionId,
1412
+ actionId: input.actionId,
1413
+ commitKey: input.commitKey,
1414
+ deadlineAt: input.deadlineAt,
1415
+ }),
1416
+ });
1417
+ activeRuntime.disposeAckRecoveryParticipant = () => gate.setAckRecoveryParticipant?.(null);
1418
+ }
1419
+ };
1420
+ activeRuntime.disposeGateEmitterListener = registerWorkflowGateEmitterListener(id, attachWorkflowGate);
1421
+ if (ctx.workflowGate) attachWorkflowGate(ctx.workflowGate);
962
1422
  return "started";
963
1423
  } catch (e) {
964
1424
  logger.warn(`notifications: failed to start server: ${String(e)}`);