@gajae-code/coding-agent 0.11.7 → 0.11.8

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 (180) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/types/cli/args.d.ts +0 -2
  3. package/dist/types/config/keybindings.d.ts +33 -3
  4. package/dist/types/config/model-profile-activation.d.ts +4 -3
  5. package/dist/types/config/model-profile-contract.d.ts +38 -0
  6. package/dist/types/config/settings-schema.d.ts +15 -2
  7. package/dist/types/edit/modes/patch.d.ts +13 -0
  8. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  9. package/dist/types/eval/py/executor.d.ts +1 -1
  10. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  11. package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
  12. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
  13. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
  14. package/dist/types/main.d.ts +8 -0
  15. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  16. package/dist/types/modes/components/tool-status-header.d.ts +3 -2
  17. package/dist/types/modes/components/welcome.d.ts +2 -0
  18. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  19. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  20. package/dist/types/modes/interactive-mode.d.ts +5 -2
  21. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  22. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  23. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  24. package/dist/types/modes/types.d.ts +3 -0
  25. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  26. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  27. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  28. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  29. package/dist/types/sdk/broker/broker.d.ts +2 -0
  30. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  31. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
  32. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  33. package/dist/types/sdk/bus/config.d.ts +2 -0
  34. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  35. package/dist/types/sdk/bus/index.d.ts +25 -0
  36. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  37. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
  38. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -3
  39. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
  41. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  42. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  43. package/dist/types/sdk/host/host.d.ts +1 -1
  44. package/dist/types/sdk/host/query/handlers.d.ts +9 -0
  45. package/dist/types/sdk/index.d.ts +2 -0
  46. package/dist/types/sdk/prompt-status.d.ts +88 -0
  47. package/dist/types/sdk/session-directory.d.ts +1 -1
  48. package/dist/types/sdk/startup-capability.d.ts +3 -0
  49. package/dist/types/session/agent-session.d.ts +2 -0
  50. package/dist/types/session/internal/managed-session-scope.d.ts +11 -2
  51. package/dist/types/session/internal/managed-session-storage.d.ts +17 -2
  52. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  53. package/dist/types/session/session-storage.d.ts +18 -0
  54. package/dist/types/task/index.d.ts +3 -1
  55. package/dist/types/task/receipt.d.ts +2 -0
  56. package/dist/types/task/types.d.ts +94 -0
  57. package/dist/types/tools/ask.d.ts +454 -174
  58. package/dist/types/tools/image-gen.d.ts +47 -1
  59. package/dist/types/tools/index.d.ts +4 -0
  60. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  61. package/package.json +8 -8
  62. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  63. package/scripts/dogfood-repository-binding.ts +209 -0
  64. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  65. package/scripts/run-test-manifest.ts +4 -4
  66. package/src/cli/args.ts +2 -92
  67. package/src/cli/notify-cli.ts +5 -38
  68. package/src/cli.ts +18 -0
  69. package/src/commands/sdk.ts +2 -2
  70. package/src/config/keybindings.ts +157 -24
  71. package/src/config/model-profile-activation.ts +7 -18
  72. package/src/config/model-profile-contract.ts +161 -0
  73. package/src/config/model-registry.ts +11 -1
  74. package/src/config/settings-schema.ts +17 -2
  75. package/src/coordinator-mcp/model-preset.ts +21 -74
  76. package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
  77. package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
  78. package/src/edit/index.ts +3 -6
  79. package/src/edit/modes/patch.ts +38 -1
  80. package/src/edit/modes/replace.ts +42 -0
  81. package/src/edit/path-mutation-lock.ts +112 -0
  82. package/src/eval/py/executor.ts +30 -11
  83. package/src/eval/py/prelude.py +2 -2
  84. package/src/eval/py/tool-bridge.ts +34 -18
  85. package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
  86. package/src/gjc-runtime/launch-tmux.ts +1 -1
  87. package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
  88. package/src/gjc-runtime/ralplan-runtime.ts +75 -3
  89. package/src/gjc-runtime/repository-binding.ts +267 -0
  90. package/src/gjc-runtime/session-state-sidecar.ts +25 -9
  91. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
  92. package/src/gjc-runtime/tmux-sessions.ts +45 -6
  93. package/src/gjc-runtime/ultragoal-guard.ts +5 -5
  94. package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
  95. package/src/internal-urls/docs-index.generated.ts +6 -5
  96. package/src/internal-urls/local-protocol.ts +33 -16
  97. package/src/internal-urls/mcp-protocol.ts +73 -11
  98. package/src/main.ts +25 -10
  99. package/src/modes/DESIGN.md +5 -0
  100. package/src/modes/acp/acp-agent.ts +50 -16
  101. package/src/modes/components/model-selector.ts +42 -8
  102. package/src/modes/components/plan-preview-overlay.ts +1 -1
  103. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  104. package/src/modes/components/tool-status-header.ts +20 -4
  105. package/src/modes/components/welcome.ts +22 -18
  106. package/src/modes/controllers/command-controller.ts +97 -54
  107. package/src/modes/controllers/event-controller.ts +10 -0
  108. package/src/modes/controllers/input-controller.ts +7 -2
  109. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  110. package/src/modes/controllers/selector-controller.ts +46 -54
  111. package/src/modes/interactive-mode.ts +41 -27
  112. package/src/modes/prompt-action-autocomplete.ts +32 -13
  113. package/src/modes/prompt-suggestion-controller.ts +96 -0
  114. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  115. package/src/modes/types.ts +3 -0
  116. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  117. package/src/modes/utils/ui-helpers.ts +5 -3
  118. package/src/prompts/agents/architect.md +0 -1
  119. package/src/prompts/agents/critic.md +0 -1
  120. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  121. package/src/prompts/system/system-prompt.md +6 -4
  122. package/src/prompts/tools/cron.md +3 -1
  123. package/src/runtime-mcp/manager.ts +51 -53
  124. package/src/runtime-mcp/oauth-flow.ts +0 -72
  125. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  126. package/src/runtime-mcp/transports/http.ts +13 -6
  127. package/src/sdk/broker/broker.ts +34 -20
  128. package/src/sdk/broker/lifecycle.ts +185 -36
  129. package/src/sdk/bus/chat-daemon-control.ts +18 -5
  130. package/src/sdk/bus/config-commands.ts +2 -2
  131. package/src/sdk/bus/config.ts +9 -1
  132. package/src/sdk/bus/conversation-store.ts +40 -6
  133. package/src/sdk/bus/index.ts +240 -65
  134. package/src/sdk/bus/lifecycle-commands.ts +47 -3
  135. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  136. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  137. package/src/sdk/bus/notification-orchestration.ts +12 -0
  138. package/src/sdk/bus/notification-service.ts +27 -2
  139. package/src/sdk/bus/prompt-reconciliation.ts +239 -0
  140. package/src/sdk/bus/recent-activity.ts +90 -26
  141. package/src/sdk/bus/telegram-daemon-contract.ts +13 -3
  142. package/src/sdk/bus/telegram-daemon-control.ts +145 -33
  143. package/src/sdk/bus/telegram-daemon.ts +835 -110
  144. package/src/sdk/bus/telegram-reference.ts +20 -2
  145. package/src/sdk/cli/session-cli.ts +15 -2
  146. package/src/sdk/host/control/dispatch.ts +1 -1
  147. package/src/sdk/host/control/operations.ts +1 -1
  148. package/src/sdk/host/host.ts +2 -6
  149. package/src/sdk/host/query/handlers.ts +70 -5
  150. package/src/sdk/index.ts +12 -0
  151. package/src/sdk/prompt-status.ts +97 -0
  152. package/src/sdk/protocol/operation-inventory.generated.json +47 -0
  153. package/src/sdk/protocol/operation-registry.ts +18 -4
  154. package/src/sdk/session-directory.ts +5 -1
  155. package/src/sdk/session.ts +2 -0
  156. package/src/sdk/startup-capability.ts +9 -1
  157. package/src/session/agent-session.ts +52 -5
  158. package/src/session/blob-store.ts +7 -2
  159. package/src/session/internal/managed-session-scope.ts +854 -84
  160. package/src/session/internal/managed-session-storage.ts +251 -199
  161. package/src/session/internal/native-publish-outcome.ts +266 -0
  162. package/src/session/session-manager.ts +310 -327
  163. package/src/session/session-storage.ts +128 -11
  164. package/src/task/gjc-command.ts +2 -2
  165. package/src/task/index.ts +121 -15
  166. package/src/task/receipt.ts +3 -0
  167. package/src/task/types.ts +34 -3
  168. package/src/tools/ask.ts +198 -88
  169. package/src/tools/ast-edit.ts +2 -2
  170. package/src/tools/bash-pty-selection.ts +2 -2
  171. package/src/tools/browser/screenshot-format.ts +6 -1
  172. package/src/tools/cron.ts +1 -1
  173. package/src/tools/image-gen.ts +178 -6
  174. package/src/tools/index.ts +4 -0
  175. package/src/tools/render-utils.ts +7 -1
  176. package/src/utils/edit-mode.ts +2 -2
  177. package/src/utils/prompt-suggestion.ts +265 -0
  178. package/src/utils/sixel.ts +3 -3
  179. package/src/web/insane/url-guard.ts +3 -1
  180. package/src/web/scrapers/docs-rs.ts +30 -5
@@ -31,6 +31,7 @@ import { ThinkingLevel } from "@gajae-code/agent-core";
31
31
  import type { ImageContent, TextContent, Tool } from "@gajae-code/ai";
32
32
  import { NotificationServer, nativeBuildInfo } from "@gajae-code/natives";
33
33
  import { logger, postmortem, VERSION } from "@gajae-code/utils";
34
+ import { projectModelProfileCatalog } from "../../config/model-profile-contract";
34
35
  import { Settings } from "../../config/settings";
35
36
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
36
37
  import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
@@ -59,13 +60,13 @@ import { SessionSdkHost, shouldHostSdk } from "../host";
59
60
  import { type ControlSurface, dispatchControl } from "../host/control";
60
61
  import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "../host/query";
61
62
  import { projectQ10Models } from "../models.js";
63
+ import { PROMPT_CLIENT_REF_MAX_LENGTH } from "../prompt-status";
62
64
  import { OPERATIONS } from "../protocol/operation-registry";
63
65
  import {
64
66
  lifecycleStartupCapabilityForApi,
65
67
  normalizeSdkStartupFailure,
66
68
  type SdkStartupFailure,
67
69
  } from "../startup-capability";
68
-
69
70
  import { registerTelegramFileSink } from "./attachment-registry";
70
71
  import { ensureDiscordDaemon, ensureSlackDaemon } from "./chat-daemon-control";
71
72
  import {
@@ -81,6 +82,7 @@ import {
81
82
  import { telegramControlCommandUsage } from "./config-commands";
82
83
  import { imageAttachmentsFromMessage, notificationActionPayload, summaryFromMessage, truncate } from "./helpers";
83
84
  import { assertNativeRuntimeCompatibility } from "./native-runtime-compatibility";
85
+ import { createPromptReconciliation, sanitizePromptFailure } from "./prompt-reconciliation";
84
86
  import { NotificationSessionController, type NotificationSessionRuntime } from "./session-control";
85
87
  import {
86
88
  ASK_SELECTED_ACK_CAPABILITY,
@@ -401,6 +403,7 @@ interface UnattendedGatePresentation {
401
403
  question: string;
402
404
  options: string[];
403
405
  controls: readonly AskRemoteControl[];
406
+ recommendedIndex?: number;
404
407
  multi: boolean;
405
408
  allowEmpty: boolean;
406
409
  navigationLabel?: "Next" | "Done";
@@ -409,6 +412,14 @@ interface UnattendedGatePresentation {
409
412
  onActivated?: (actionId: string, lease: { actionId: string; registrationEpoch: number }) => void;
410
413
  onClosed?: () => void;
411
414
  }
415
+ function recommendedIndexFromGateOptions(options: readonly unknown[]): number | undefined {
416
+ const descriptions = options.map(option => (option as { description?: unknown }).description);
417
+ const recommended = descriptions.filter(description => description === "recommended");
418
+ return recommended.length === 1 &&
419
+ descriptions.every(description => description === undefined || description === "recommended")
420
+ ? descriptions.indexOf("recommended")
421
+ : undefined;
422
+ }
412
423
 
413
424
  type RetireStatus = "retired" | "already_terminal" | "claimed" | "stale";
414
425
  type DirectControlOutcome = "accepted" | "rejected" | "unknown";
@@ -739,6 +750,9 @@ export class PresentationArbiter {
739
750
  ? `(${presentation.selectedOptions.length} selected) ${presentation.question}`
740
751
  : presentation.question,
741
752
  options: presentation.options,
753
+ ...(presentation.recommendedIndex === undefined
754
+ ? {}
755
+ : { recommendedIndex: presentation.recommendedIndex }),
742
756
  controls: presentation.multi
743
757
  ? [
744
758
  {
@@ -906,6 +920,13 @@ interface SessionRuntime {
906
920
  /** Records a correlated prompt terminal boundary after agent unwind. */
907
921
  /** Atomically claims a correlated prompt terminal boundary after agent unwind. */
908
922
  recordPromptTerminal: (correlation: { commandId: string; turnId: string } | undefined) => boolean;
923
+ /** Transitions the authoritative reconciliation record at lifecycle ingress; terminal outcomes settle once. */
924
+ notePromptReconciliation: (
925
+ correlation: { commandId: string; turnId: string } | undefined,
926
+ frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
927
+ ) => void;
928
+ /** Settles and emits one sanitized correlated prompt failure. */
929
+ emitPromptFailure: (correlation: { commandId: string; turnId: string }, error: unknown) => void;
909
930
  /** Records correlated lifecycle frames for replay and delivers them only to the accepted requester after acknowledgement. */
910
931
  emitPromptLifecycle: (
911
932
  correlation: { commandId: string; turnId: string } | undefined,
@@ -939,6 +960,12 @@ interface SessionRuntime {
939
960
  turnClosed?: boolean;
940
961
  /** Finalized while provisional policy was held; flush exactly once on stable activation. */
941
962
  pendingFinal?: { text?: string; messageRef?: string };
963
+ /**
964
+ * Lean-mode deferred final answer: latest assistant text observed at `turn_end`,
965
+ * emitted once at `agent_end` so intermediate tool-turn narration does not flood
966
+ * remote clients. Cleared after flush or when replaced by a newer turn.
967
+ */
968
+ pendingSettled?: { text: string; messageRef?: string };
942
969
  /** Durable gates emitted while ownership is provisional; presented only after stable activation. */
943
970
  deferredGatePresentations: Array<() => void>;
944
971
  /** SDK control frames received during provisional ownership; replayed only after stable activation. */
@@ -954,7 +981,6 @@ interface SessionRuntime {
954
981
  const SENSITIVE_MODEL_LABEL =
955
982
  /(?:\b(?:https?|wss?):\/\/|\b[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}\b|\b(?:api[-_ ]?key|access[-_ ]?token|bearer|secret|password|account(?:\s*id)?|email|exception|stack trace)\b|\b(?:sk|pk|rk)-[A-Za-z0-9_-]{12,}\b)/i;
956
983
  const TOOL_SUMMARY_MAX = 280;
957
- const EMPTY_CAPABILITIES: ReadonlySet<string> = new Set();
958
984
 
959
985
  /** Stable projection of the tool-owned safe-display seam (never the full Tool surface). */
960
986
  type SafeSummaryTool = Pick<Tool, "safeSummary" | "safeSummaryFields">;
@@ -1019,6 +1045,14 @@ function pushSessionFrame(
1019
1045
  runtime.server.pushFrame(JSON.stringify(frame));
1020
1046
  }
1021
1047
 
1048
+ async function pushTerminalSessionFrame(
1049
+ runtime: Pick<SessionRuntime, "server" | "host">,
1050
+ frame: { type: "session_closed"; sessionId: string },
1051
+ ): Promise<boolean> {
1052
+ runtime.host.emitEvent({ kind: frame.type, payload: frame });
1053
+ return await runtime.server.pushFrameAndWait(JSON.stringify(frame), 1_000);
1054
+ }
1055
+
1022
1056
  function pushFileAttachment(
1023
1057
  runtime: Pick<SessionRuntime, "server" | "host">,
1024
1058
  frame: { type: "file_attachment"; sessionId: string; name: string; mime?: string; caption?: string },
@@ -1593,6 +1627,7 @@ function registerInteractiveAnswerSource(
1593
1627
  question: request.question,
1594
1628
  options: request.options,
1595
1629
  controls: request.controls,
1630
+ recommendedIndex: request.recommendedIndex,
1596
1631
  multi: false,
1597
1632
  allowEmpty: false,
1598
1633
  selectedOptions: [],
@@ -1755,6 +1790,7 @@ function sdkQuerySurface(
1755
1790
  followupQueueDepth: 0,
1756
1791
  }),
1757
1792
  configOverrides: ReadonlyMap<string, unknown> = new Map(),
1793
+ promptStatusLookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown,
1758
1794
  ): SessionSurface {
1759
1795
  const metadata = () => ({
1760
1796
  sessionId: id,
@@ -1822,6 +1858,8 @@ function sdkQuerySurface(
1822
1858
  const currentThinkingLevel = api.getThinkingLevel();
1823
1859
  return projectQ10Models({ models, currentModel, currentThinkingLevel });
1824
1860
  },
1861
+ getModelProfiles: () =>
1862
+ projectModelProfileCatalog(ctx.modelRegistry.getModelProfiles(), ctx.modelRegistry.getError()),
1825
1863
  getSkillState: () => ctx.getSkillState(),
1826
1864
  getGates: () => {
1827
1865
  const workflowGate = ctx.workflowGate;
@@ -1861,6 +1899,8 @@ function sdkQuerySurface(
1861
1899
  getExtensions: () => ctx.getExtensions(),
1862
1900
  getArtifactRange: (id, offset, length) => ctx.getArtifactRange?.(id, offset, length),
1863
1901
  getJobs: () => ctx.getJobs(),
1902
+ getPromptStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
1903
+ promptStatusLookup(selector),
1864
1904
  installedQueries: installedOperations(ctx, "query"),
1865
1905
  };
1866
1906
  }
@@ -1886,10 +1926,14 @@ function sdkControlSurface(
1886
1926
  onPromptAccepted: (
1887
1927
  correlation: { commandId: string; turnId: string },
1888
1928
  requesterConnectionId?: string,
1929
+ clientRef?: string,
1930
+ trackReconciliation?: boolean,
1889
1931
  ) => void = () => {},
1890
1932
  onPromptFailed: (correlation: { commandId: string; turnId: string }, error: unknown) => void = () => {},
1891
1933
  acceptGateResolution: () => boolean,
1892
1934
  trackGateResolution: <T>(resolution: Promise<T>) => Promise<T>,
1935
+ admitPrompt: (clientRef?: string) => void,
1936
+ releasePromptAdmission: (clientRef?: string) => void,
1893
1937
  settings?: Settings,
1894
1938
  configOverrides: Map<string, unknown> = new Map(),
1895
1939
  configRevision: { current: number } = { current: 0 },
@@ -1966,19 +2010,35 @@ function sdkControlSurface(
1966
2010
  deliverAs?: "steer" | "followUp",
1967
2011
  rejectWhenBusy = false,
1968
2012
  requesterConnectionId?: string,
2013
+ clientRef?: string,
2014
+ trackReconciliation = false,
1969
2015
  ) => {
1970
- if (forceFresh && isSessionBusy()) {
1971
- throw Object.assign(new Error("Previous turn did not finish aborting before replacement prompt submission."), {
1972
- code: "busy",
2016
+ const trimmedClientRef = typeof clientRef === "string" ? clientRef.trim() : undefined;
2017
+ if (clientRef !== undefined && (!trimmedClientRef || trimmedClientRef.length > PROMPT_CLIENT_REF_MAX_LENGTH))
2018
+ throw Object.assign(new Error("clientRef must be a non-empty string of at most 128 characters."), {
2019
+ code: "invalid_input",
1973
2020
  });
2021
+ if (trackReconciliation) admitPrompt(trimmedClientRef);
2022
+ try {
2023
+ if (forceFresh && isSessionBusy()) {
2024
+ throw Object.assign(
2025
+ new Error("Previous turn did not finish aborting before replacement prompt submission."),
2026
+ {
2027
+ code: "busy",
2028
+ },
2029
+ );
2030
+ }
2031
+ if (rejectWhenBusy && isSessionBusy())
2032
+ throw Object.assign(
2033
+ new Error("turn.prompt is unavailable while the agent is busy; use turn.steer explicitly."),
2034
+ {
2035
+ code: "busy",
2036
+ },
2037
+ );
2038
+ } catch (error) {
2039
+ if (trackReconciliation) releasePromptAdmission(trimmedClientRef);
2040
+ throw error;
1974
2041
  }
1975
- if (rejectWhenBusy && isSessionBusy())
1976
- throw Object.assign(
1977
- new Error("turn.prompt is unavailable while the agent is busy; use turn.steer explicitly."),
1978
- {
1979
- code: "busy",
1980
- },
1981
- );
1982
2042
  const promptImages = Array.isArray(images) ? (images as { data: string; mimeType?: string }[]) : [];
1983
2043
  const content: string | (TextContent | ImageContent)[] =
1984
2044
  promptImages.length > 0
@@ -2010,7 +2070,7 @@ function sdkControlSurface(
2010
2070
  const onPreflightAccepted = () => {
2011
2071
  if (preflightSettled) return;
2012
2072
  accepted = true;
2013
- onPromptAccepted(correlation, requesterConnectionId);
2073
+ onPromptAccepted(correlation, requesterConnectionId, trimmedClientRef, trackReconciliation);
2014
2074
  settlePreflight({ status: "accepted" });
2015
2075
  };
2016
2076
  // Do not acknowledge the prompt until AgentSession's async preflight
@@ -2047,13 +2107,17 @@ function sdkControlSurface(
2047
2107
  try {
2048
2108
  const result = await preflight.promise;
2049
2109
  if (result.status === "rejected") throw result.error;
2050
- return { commandId, turnId, accepted: true };
2110
+ return { commandId, turnId, accepted: true, ...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}) };
2111
+ } catch (error) {
2112
+ if (trackReconciliation) releasePromptAdmission(trimmedClientRef);
2113
+ throw error;
2051
2114
  } finally {
2052
2115
  pendingPreflightCancellations.delete(cancelPreflight);
2053
2116
  }
2054
2117
  };
2055
2118
  const surface: ControlSurface & { cancelPendingPreflights(): void } = {
2056
- prompt: (text, images) => submitPrompt(text, images, false, undefined, true, controlRequesterContext.getStore()),
2119
+ prompt: (text, images, clientRef) =>
2120
+ submitPrompt(text, images, false, undefined, true, controlRequesterContext.getStore(), clientRef, true),
2057
2121
  steer: text => sendSteer(text),
2058
2122
  followUp: text => submitPrompt(text, undefined, false, "followUp", false, controlRequesterContext.getStore()),
2059
2123
  abort: () => {
@@ -2839,7 +2903,6 @@ export function createNotificationsExtension(
2839
2903
  "session.branch",
2840
2904
  ]);
2841
2905
  const sessionId = (ctx: ExtensionContext): string => ctx.sessionManager.getSessionId();
2842
- const sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
2843
2906
 
2844
2907
  async function stopSession(
2845
2908
  id: string,
@@ -2914,6 +2977,12 @@ export function createNotificationsExtension(
2914
2977
  try {
2915
2978
  rt.workflowGate?.setRuntimeTurnProvider?.(null);
2916
2979
  } catch {}
2980
+ try {
2981
+ const delivered = await pushTerminalSessionFrame(rt, { type: "session_closed", sessionId: id });
2982
+ if (!delivered) logger.warn("notifications: session_closed socket delivery was not acknowledged");
2983
+ } catch (e) {
2984
+ logger.warn(`notifications: session_closed failed: ${String(e)}`);
2985
+ }
2917
2986
  await rt.waitForGateResolutionQuiescence();
2918
2987
  try {
2919
2988
  rt.disposeAckRecoveryParticipant();
@@ -2955,14 +3024,6 @@ export function createNotificationsExtension(
2955
3024
  ownerReleaseFailures.push(e);
2956
3025
  logger.warn(`sdk query snapshots: close failed: ${String(e)}`);
2957
3026
  }
2958
- let closeFrameSent = false;
2959
- try {
2960
- pushSessionFrame(rt, { type: "session_closed", sessionId: id });
2961
- closeFrameSent = true;
2962
- } catch (e) {
2963
- logger.warn(`notifications: session_closed failed: ${String(e)}`);
2964
- }
2965
- if (closeFrameSent) await sleep(100);
2966
3027
  let serverStopped = rt.serverStopped;
2967
3028
  if (!serverStopped) {
2968
3029
  try {
@@ -3142,11 +3203,20 @@ export function createNotificationsExtension(
3142
3203
  failed: boolean;
3143
3204
  error: unknown;
3144
3205
  terminal: boolean;
3206
+ retainCorrelation: boolean;
3145
3207
  createdAt: number;
3146
3208
  bufferedFrames: Array<PromptLifecycleFrame | Record<string, unknown>>;
3147
3209
  };
3148
3210
  const promptSubmissions = new Map<string, PromptSubmission>();
3149
3211
  const promptTerminalTombstones = new Map<string, number>();
3212
+ // Authoritative bounded reconciliation state for Q26 turn.prompt_status
3213
+ // (contract documented in ./prompt-reconciliation and ../prompt-status).
3214
+ // Active records never age into terminal; documented TTL/capacity
3215
+ // eviction is the only removal, after which lookups report `unknown`.
3216
+ const reconciliation = createPromptReconciliation();
3217
+ const admitPromptSubmission = reconciliation.admit;
3218
+ const notePromptReconciliationAccepted = reconciliation.noteAccepted;
3219
+ const lookupPromptStatus = reconciliation.lookup;
3150
3220
  const removePendingPromptCorrelation = (correlation: { commandId: string; turnId: string }) => {
3151
3221
  const pendingIndex = pendingPromptCorrelations.findIndex(
3152
3222
  candidate => candidate.commandId === correlation.commandId && candidate.turnId === correlation.turnId,
@@ -3164,14 +3234,20 @@ export function createNotificationsExtension(
3164
3234
  removePendingPromptCorrelation(correlation);
3165
3235
  addTerminalTombstone(key);
3166
3236
  };
3237
+ const expirePromptDelivery = (key: string, submission: PromptSubmission) => {
3238
+ promptSubmissions.delete(key);
3239
+ if (!submission.terminal && submission.retainCorrelation) return;
3240
+ const [commandId, turnId] = key.split(":", 2);
3241
+ if (!commandId || !turnId) return;
3242
+ removePendingPromptCorrelation({ commandId, turnId });
3243
+ addTerminalTombstone(key);
3244
+ };
3167
3245
  const cleanupPromptRecords = (now = Date.now()) => {
3246
+ reconciliation.cleanup();
3168
3247
  for (const [key, expiresAt] of promptTerminalTombstones)
3169
3248
  if (expiresAt <= now) promptTerminalTombstones.delete(key);
3170
3249
  for (const [key, submission] of promptSubmissions)
3171
- if (submission.createdAt + PROMPT_SUBMISSION_TTL_MS <= now) {
3172
- const [commandId, turnId] = key.split(":", 2);
3173
- if (commandId && turnId) finalizePrompt(key, { commandId, turnId });
3174
- }
3250
+ if (submission.createdAt + PROMPT_SUBMISSION_TTL_MS <= now) expirePromptDelivery(key, submission);
3175
3251
  };
3176
3252
  const abandonPrompt = (submission: PromptSubmission) => {
3177
3253
  submission.abandoned = true;
@@ -3182,6 +3258,7 @@ export function createNotificationsExtension(
3182
3258
  frame: PromptLifecycleFrame,
3183
3259
  ) => {
3184
3260
  cleanupPromptRecords();
3261
+
3185
3262
  if (!correlation || !runtime) {
3186
3263
  emitAgentLifecycle(runtime!, frame as Extract<PromptLifecycleFrame, { type: "agent_start" | "agent_end" }>);
3187
3264
  return;
@@ -3247,15 +3324,21 @@ export function createNotificationsExtension(
3247
3324
  const recordPromptAccepted = (
3248
3325
  correlation: { commandId: string; turnId: string },
3249
3326
  requesterConnectionId?: string,
3327
+ clientRef?: string,
3328
+ trackReconciliation = false,
3250
3329
  ) => {
3251
- if (!requesterConnectionId) return;
3330
+ if (!requesterConnectionId) {
3331
+ // No delivery owner: tracked prompts cannot be reconciled. Release
3332
+ // their admission reservation instead of leaking the active slot.
3333
+ if (trackReconciliation) reconciliation.releaseAdmission(clientRef);
3334
+ return;
3335
+ }
3336
+ if (trackReconciliation) notePromptReconciliationAccepted(correlation, clientRef);
3252
3337
  cleanupPromptRecords();
3253
3338
  while (promptSubmissions.size >= PROMPT_SUBMISSION_CAPACITY) {
3254
3339
  const oldest = promptSubmissions.entries().next().value as [string, PromptSubmission] | undefined;
3255
3340
  if (!oldest) break;
3256
- const [key] = oldest;
3257
- const [commandId, turnId] = key.split(":", 2);
3258
- if (commandId && turnId) finalizePrompt(key, { commandId, turnId });
3341
+ expirePromptDelivery(oldest[0], oldest[1]);
3259
3342
  }
3260
3343
  pendingPromptCorrelations.push(correlation);
3261
3344
  promptSubmissions.set(promptSubmissionKey(correlation), {
@@ -3265,6 +3348,7 @@ export function createNotificationsExtension(
3265
3348
  failed: false,
3266
3349
  error: undefined,
3267
3350
  terminal: false,
3351
+ retainCorrelation: trackReconciliation,
3268
3352
  createdAt: Date.now(),
3269
3353
  bufferedFrames: [],
3270
3354
  });
@@ -3280,17 +3364,15 @@ export function createNotificationsExtension(
3280
3364
  return true;
3281
3365
  };
3282
3366
  const emitPromptFailure = (correlation: { commandId: string; turnId: string }, error: unknown) => {
3367
+ const sanitized = sanitizePromptFailure(error);
3368
+ reconciliation.noteTransition(correlation, { type: "agent_failed", error: sanitized });
3283
3369
  const submission = promptSubmissions.get(promptSubmissionKey(correlation));
3284
3370
  if (!submission || !runtime || !recordPromptTerminal(correlation)) return;
3285
- const candidate = error as { code?: unknown; message?: unknown };
3286
3371
  emitPromptLifecycle(correlation, {
3287
3372
  type: "agent_failed",
3288
3373
  sessionId: runtime.id,
3289
3374
  ...correlation,
3290
- error: {
3291
- code: typeof candidate.code === "string" ? candidate.code : "internal",
3292
- message: typeof candidate.message === "string" ? candidate.message : "Prompt submission failed.",
3293
- },
3375
+ error: sanitized,
3294
3376
  });
3295
3377
  };
3296
3378
  const recordPromptFailure = (correlation: { commandId: string; turnId: string }, error: unknown) => {
@@ -3333,6 +3415,7 @@ export function createNotificationsExtension(
3333
3415
  };
3334
3416
  },
3335
3417
  configOverrides,
3418
+ lookupPromptStatus,
3336
3419
  ),
3337
3420
  id,
3338
3421
  revisions,
@@ -3348,6 +3431,8 @@ export function createNotificationsExtension(
3348
3431
  recordPromptFailure,
3349
3432
  () => runtime?.stopping !== true,
3350
3433
  trackGateResolution,
3434
+ admitPromptSubmission,
3435
+ reconciliation.releaseAdmission,
3351
3436
  settings,
3352
3437
  configOverrides,
3353
3438
  configRevision,
@@ -3406,7 +3491,7 @@ export function createNotificationsExtension(
3406
3491
  stateRoot,
3407
3492
  token,
3408
3493
  sendFrame: (connectionId, frame) => sendSdkFrame(connectionId, frame),
3409
- connectionCapabilities: connectionId => hostCapCache.get(connectionId) ?? EMPTY_CAPABILITIES,
3494
+ connectionCapabilities: connectionId => hostCapCache.get(connectionId),
3410
3495
  installProviderDefinitions,
3411
3496
  onProviderDefinitionsRemoved: removeProviderDefinitions,
3412
3497
  onFrame: handler => {
@@ -3478,6 +3563,7 @@ export function createNotificationsExtension(
3478
3563
  },
3479
3564
  ),
3480
3565
  );
3566
+
3481
3567
  if (rotatesIdentity && response.ok !== true) {
3482
3568
  identityControlInFlight = false;
3483
3569
  deferredIdentityRotation = undefined;
@@ -3547,6 +3633,8 @@ export function createNotificationsExtension(
3547
3633
  pendingPromptCorrelations,
3548
3634
  activePromptCorrelation: undefined,
3549
3635
  recordPromptTerminal,
3636
+ notePromptReconciliation: reconciliation.noteTransition,
3637
+ emitPromptFailure,
3550
3638
  emitPromptLifecycle,
3551
3639
  emitPromptEvent,
3552
3640
  pendingInbound: new Set<number>(),
@@ -3608,6 +3696,13 @@ export function createNotificationsExtension(
3608
3696
  if (!frame || typeof frame !== "object") return;
3609
3697
  const typedFrame = frame as Record<string, unknown>;
3610
3698
  if (typedFrame.type === "ephemeral_turn" || typedFrame.type === "ephemeral_turn_cancel") return;
3699
+ if (typedFrame.type === "event_replay") {
3700
+ const capabilities = Array.isArray(typedFrame.capabilities) ? typedFrame.capabilities : [];
3701
+ hostCapCache.set(
3702
+ inbound.connectionId,
3703
+ new Set(capabilities.filter((capability): capability is string => typeof capability === "string")),
3704
+ );
3705
+ }
3611
3706
  inboundSdkFrame?.(inbound.connectionId, typedFrame);
3612
3707
  } catch {}
3613
3708
  });
@@ -3748,7 +3843,7 @@ export function createNotificationsExtension(
3748
3843
  }
3749
3844
  const gate = runtime?.workflowGate;
3750
3845
  const workflowGateActive =
3751
- gate?.isUnattended?.() === true &&
3846
+ gate?.supportsRemoteGateAnswers?.() === true &&
3752
3847
  typeof gate.onGateEmitted === "function" &&
3753
3848
  typeof gate.resolveGateFromNotification === "function";
3754
3849
  const gateId = gatePresentations.routeFor(reply.id);
@@ -4201,7 +4296,9 @@ export function createNotificationsExtension(
4201
4296
  ? Gate
4202
4297
  : never,
4203
4298
  ): void => {
4204
- const options = (g.options ?? []).map(o => String((o as { label?: unknown }).label ?? ""));
4299
+ const rawGateOptions = g.options ?? [];
4300
+ const options = rawGateOptions.map(o => String((o as { label?: unknown }).label ?? ""));
4301
+ const recommendedIndex = recommendedIndexFromGateOptions(rawGateOptions);
4205
4302
  gateOptions.set(g.gate_id, options);
4206
4303
  const promptCtx = g.context as { prompt?: unknown; title?: unknown } | undefined;
4207
4304
  const question =
@@ -4218,6 +4315,7 @@ export function createNotificationsExtension(
4218
4315
  sessionId: id,
4219
4316
  question,
4220
4317
  options,
4318
+ ...(recommendedIndex === undefined ? {} : { recommendedIndex }),
4221
4319
  controls: [],
4222
4320
  multi: stageState.multi === true,
4223
4321
  allowEmpty: stageState.allow_empty === true,
@@ -4576,22 +4674,45 @@ export function createNotificationsExtension(
4576
4674
  if (!pending) return;
4577
4675
  rt.pendingFinal = undefined;
4578
4676
  if (pending.text && rt.notificationsActive && !rt.redact) {
4579
- try {
4580
- pushSessionFrame(rt, {
4581
- type: "turn_stream",
4582
- sessionId: id,
4583
- phase: "finalized",
4584
- finalAnswer: true,
4677
+ // Under lean, hold intermediate finals until agent_end when the agent is still
4678
+ // running so provisional-policy activation cannot reintroduce per-turn spam.
4679
+ if (rt.verbosity === "lean" && rt.busy) {
4680
+ rt.pendingSettled = {
4585
4681
  text: pending.text,
4586
4682
  ...(pending.messageRef ? { messageRef: pending.messageRef } : {}),
4587
- });
4588
- } catch (error) {
4589
- logger.warn(`notifications: pushFrame (pending turn) failed: ${String(error)}`);
4683
+ };
4684
+ } else {
4685
+ try {
4686
+ pushSessionFrame(rt, {
4687
+ type: "turn_stream",
4688
+ sessionId: id,
4689
+ phase: "finalized",
4690
+ finalAnswer: true,
4691
+ text: pending.text,
4692
+ ...(pending.messageRef ? { messageRef: pending.messageRef } : {}),
4693
+ });
4694
+ } catch (error) {
4695
+ logger.warn(`notifications: pushFrame (pending turn) failed: ${String(error)}`);
4696
+ }
4590
4697
  }
4591
4698
  }
4592
4699
  resetTurnStreamState(rt);
4593
4700
  };
4594
4701
 
4702
+ /** Emit the deferred lean settled answer exactly once (agent_end / idle). */
4703
+ const flushPendingSettled = (rt: SessionRuntime, id: string): void => {
4704
+ const settled = rt.pendingSettled;
4705
+ rt.pendingSettled = undefined;
4706
+ if (!settled?.text || !rt.notificationsActive || rt.redact || rt.policySuspended) return;
4707
+ const previousLiveRef = rt.liveRef;
4708
+ if (settled.messageRef) rt.liveRef = settled.messageRef;
4709
+ try {
4710
+ flushTurnText(rt, id, settled.text, true);
4711
+ } finally {
4712
+ if (!settled.messageRef) rt.liveRef = previousLiveRef;
4713
+ }
4714
+ };
4715
+
4595
4716
  // Drive the live typing indicator: mark busy when the agent loop starts so
4596
4717
  // the daemon shows "typing…" in the thread while the agent is thinking,
4597
4718
  // before any turn output exists. Cleared on `agent_end` below.
@@ -4604,6 +4725,7 @@ export function createNotificationsExtension(
4604
4725
  rt.busy = true;
4605
4726
  const correlation = rt.pendingPromptCorrelations.shift();
4606
4727
  rt.activePromptCorrelation = correlation;
4728
+ rt.notePromptReconciliation(correlation, { type: "agent_start" });
4607
4729
  rt.emitPromptLifecycle(correlation, { type: "agent_start", sessionId: id, ...correlation });
4608
4730
  try {
4609
4731
  // `activity` is the native live-host lifecycle surface. The separately
@@ -4648,8 +4770,35 @@ export function createNotificationsExtension(
4648
4770
  rt.busy = false;
4649
4771
  const correlation = rt.activePromptCorrelation;
4650
4772
  if (correlation) {
4651
- if (rt.recordPromptTerminal(correlation))
4652
- rt.emitPromptLifecycle(correlation, { type: "agent_end", sessionId: id, ...correlation });
4773
+ const terminalAssistant = (Array.isArray(event.messages) ? [...event.messages].reverse() : []).find(
4774
+ message =>
4775
+ message &&
4776
+ typeof message === "object" &&
4777
+ (message as { role?: unknown }).role === "assistant" &&
4778
+ ((message as { stopReason?: unknown }).stopReason === "error" ||
4779
+ (message as { stopReason?: unknown }).stopReason === "aborted"),
4780
+ ) as { stopReason?: "error" | "aborted"; errorMessage?: unknown } | undefined;
4781
+ const failureCode =
4782
+ event.stopReason === "cancelled"
4783
+ ? "cancelled"
4784
+ : terminalAssistant?.stopReason === "error"
4785
+ ? "agent_error"
4786
+ : terminalAssistant?.stopReason === "aborted"
4787
+ ? "aborted"
4788
+ : undefined;
4789
+ if (failureCode)
4790
+ rt.emitPromptFailure(correlation, {
4791
+ code: failureCode,
4792
+ message:
4793
+ typeof terminalAssistant?.errorMessage === "string"
4794
+ ? terminalAssistant.errorMessage
4795
+ : "Prompt submission failed.",
4796
+ });
4797
+ else {
4798
+ rt.notePromptReconciliation(correlation, { type: "agent_end" });
4799
+ if (rt.recordPromptTerminal(correlation))
4800
+ rt.emitPromptLifecycle(correlation, { type: "agent_end", sessionId: id, ...correlation });
4801
+ }
4653
4802
  } else {
4654
4803
  rt.emitPromptLifecycle(undefined, { type: "agent_end", sessionId: id });
4655
4804
  }
@@ -4693,9 +4842,14 @@ export function createNotificationsExtension(
4693
4842
  logger.warn(`notifications: noteIdle failed: ${String(e)}`);
4694
4843
  }
4695
4844
 
4845
+ // Lean: emit the latest deferred assistant answer exactly once at idle.
4846
+ // Intermediate tool-turn narration was held on turn_end; ask lead-ins were
4847
+ // already flushed immediately and deduped via preAskFlushedText.
4848
+ flushPendingSettled(rt, id);
4849
+
4696
4850
  // On idle, stream a context update with metadata (token/model usage +
4697
- // working-tree diff) unless redaction is on. The agent's last message is
4698
- // NOT repeated here — it is already streamed once via `turn_stream`.
4851
+ // working-tree diff) unless redaction is on. Under verbose the agent's last
4852
+ // message is already streamed per turn_end; lean flushes it just above.
4699
4853
  if (!rt.redact && rt.verbosity === "verbose") {
4700
4854
  const usage = (
4701
4855
  ctx as { getContextUsage?: () => { tokens: number | null; contextWindow: number } | undefined }
@@ -4724,17 +4878,20 @@ export function createNotificationsExtension(
4724
4878
  }
4725
4879
  });
4726
4880
 
4727
- // Stream viable agent output per turn (the live thread mirror). Unlike idle,
4728
- // turn output is expected to be multiple messages — one per turn that
4729
- // produced assistant text. Tool-only turns yield no text and are skipped.
4730
- // Redaction suppresses streamed content (only the one-time identity header
4731
- // survives redaction). The daemon coalesces/throttles these via its shared
4732
- // rate-limit pool before sending to Telegram.
4881
+ // Stream viable agent output. Verbose mirrors each turn that produced assistant
4882
+ // text. Lean defers the latest answer until agent_end (idle) so tool-heavy runs
4883
+ // do not flood remote clients with intermediate narration; ask lead-ins still
4884
+ // flush immediately. Tool-only turns yield no text and are skipped. Redaction
4885
+ // suppresses streamed content (only the one-time identity header survives).
4886
+ // The daemon coalesces/throttles these via its shared rate-limit pool.
4733
4887
  // Push the in-flight turn's assistant text as a finalized turn_stream, deduped
4734
4888
  // against what was already flushed for this turn (the pre-ask lead-in).
4735
4889
  const flushTurnText = (rt: SessionRuntime, id: string, text: string | undefined, finalAnswer: boolean): void => {
4736
4890
  if (!text || text === rt.preAskFlushedText || !rt.notificationsActive || rt.policySuspended) return;
4737
4891
  rt.preAskFlushedText = text;
4892
+ // Ask lead-ins supersede any deferred lean settled answer from earlier turns
4893
+ // so agent_end does not re-emit stale intermediate narration (#2863 review).
4894
+ if (!finalAnswer) rt.pendingSettled = undefined;
4738
4895
  // Decision A: a stream-enabled turn must finalize as an in-place edit of ONE
4739
4896
  // live message, never a fresh (rich-promotable) send. If live frames were
4740
4897
  // async-queued and none landed before this flush, reuse the per-turn ref
@@ -4875,19 +5032,37 @@ export function createNotificationsExtension(
4875
5032
  rt.turnClosed = true;
4876
5033
  return;
4877
5034
  }
4878
- if (text) flushTurnText(rt, id, text, true);
5035
+ if (text) {
5036
+ if (rt.verbosity === "verbose") {
5037
+ // Verbose: one finalized turn_stream per turn with assistant text.
5038
+ flushTurnText(rt, id, text, true);
5039
+ } else if (text !== rt.preAskFlushedText) {
5040
+ // Lean: hold the latest answer until agent_end. Skip when this turn
5041
+ // already flushed the same text as an ask lead-in (no duplicate at idle).
5042
+ rt.pendingSettled = {
5043
+ text,
5044
+ ...(rt.liveRef ? { messageRef: rt.liveRef } : {}),
5045
+ };
5046
+ } else {
5047
+ // Lead-in already flushed: drop any older deferred settled text so idle
5048
+ // does not re-emit intermediate narration after the ask prompt (#2863).
5049
+ rt.pendingSettled = undefined;
5050
+ }
5051
+ }
4879
5052
  resetTurnStreamState(rt);
4880
5053
  });
4881
5054
 
4882
- // Live streaming (opt-in): push throttled in-progress assistant text as
4883
- // non-finalized turn_stream frames so remote clients edit one message as the
4884
- // turn streams. The finalized frame (turn_end) carries the same messageRef and
4885
- // lands the authoritative text. Suppressed under redaction.
5055
+ // Live streaming (opt-in + verbose only): push throttled in-progress assistant
5056
+ // text as non-finalized turn_stream frames so remote clients edit one message
5057
+ // as the turn streams. Lean keeps settled-answer-only delivery; live frames
5058
+ // are suppressed even when the stream preference is on. The finalized frame
5059
+ // (turn_end / agent_end) carries the same messageRef and lands the authoritative
5060
+ // text. Suppressed under redaction.
4886
5061
  api.on("message_update", (event, ctx) => {
4887
5062
  const id = sessionId(ctx);
4888
5063
  const rt = runtimes.get(id);
4889
5064
  rt?.emitPromptEvent(event);
4890
- if (!rt?.notificationsActive || !rt.stream || rt.redact || rt.turnClosed) return;
5065
+ if (!rt?.notificationsActive || !rt.stream || rt.redact || rt.turnClosed || rt.verbosity !== "verbose") return;
4891
5066
  if ((event.message as { role?: unknown }).role !== "assistant") return;
4892
5067
  if (rt.liveRef === undefined && rt.turnSeq !== undefined) {
4893
5068
  rt.liveRef = String(rt.turnSeq);