@gajae-code/coding-agent 0.11.9 → 0.11.11

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 (150) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/dist/types/cli.d.ts +8 -1
  3. package/dist/types/config/model-profile-activation.d.ts +1 -0
  4. package/dist/types/config/model-profile-contract.d.ts +2 -0
  5. package/dist/types/config/model-registry.d.ts +2 -0
  6. package/dist/types/config/settings-schema.d.ts +57 -2
  7. package/dist/types/config/settings.d.ts +1 -0
  8. package/dist/types/extensibility/extensions/types.d.ts +21 -0
  9. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  10. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +60 -0
  11. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  12. package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
  13. package/dist/types/modes/acp/acp-agent.d.ts +14 -9
  14. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  15. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  16. package/dist/types/modes/components/welcome.d.ts +1 -0
  17. package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
  18. package/dist/types/modes/interactive-mode.d.ts +5 -1
  19. package/dist/types/runtime/memory-domain.d.ts +9 -0
  20. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  21. package/dist/types/runtime/memory-guard.d.ts +39 -0
  22. package/dist/types/runtime/memory-limit.d.ts +11 -0
  23. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  24. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  25. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  26. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  27. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  28. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  29. package/dist/types/sdk/bus/index.d.ts +11 -2
  30. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
  31. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  32. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
  33. package/dist/types/sdk/bus/telegram-daemon.d.ts +97 -8
  34. package/dist/types/sdk/host/control/operations.d.ts +1 -0
  35. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  36. package/dist/types/sdk/session.d.ts +7 -0
  37. package/dist/types/sdk/startup-capability.d.ts +6 -0
  38. package/dist/types/session/agent-session.d.ts +11 -11
  39. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  40. package/dist/types/session/internal/managed-session-scope.d.ts +28 -1
  41. package/dist/types/session/session-manager.d.ts +34 -0
  42. package/dist/types/task/executor.d.ts +6 -0
  43. package/dist/types/task/provider-retry-status.d.ts +13 -0
  44. package/dist/types/task/types.d.ts +39 -0
  45. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  46. package/dist/types/tools/index.d.ts +3 -1
  47. package/dist/types/tools/resource-gc.d.ts +19 -0
  48. package/package.json +8 -8
  49. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  50. package/scripts/run-test-manifest.ts +6 -4
  51. package/src/cli/args.ts +14 -8
  52. package/src/cli/fast-help.ts +1 -1
  53. package/src/cli/notify-cli.ts +1 -0
  54. package/src/cli.ts +48 -2
  55. package/src/commands/sdk.ts +91 -7
  56. package/src/config/model-profile-activation.ts +1 -0
  57. package/src/config/model-profile-contract.ts +13 -0
  58. package/src/config/model-profiles.ts +31 -7
  59. package/src/config/model-registry.ts +4 -2
  60. package/src/config/settings-schema.ts +61 -2
  61. package/src/config/settings.ts +8 -0
  62. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -2
  63. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  64. package/src/eval/js/tool-bridge.ts +1 -1
  65. package/src/extensibility/extensions/runner.ts +4 -0
  66. package/src/extensibility/extensions/types.ts +14 -0
  67. package/src/gjc-runtime/deep-interview-ambiguity.ts +16 -2
  68. package/src/gjc-runtime/deep-interview-draft.ts +2 -1
  69. package/src/gjc-runtime/deep-interview-recorder.ts +16 -1
  70. package/src/gjc-runtime/deep-interview-state.ts +25 -3
  71. package/src/gjc-runtime/linux-proc.ts +70 -36
  72. package/src/gjc-runtime/ralplan-runtime.ts +252 -1
  73. package/src/gjc-runtime/state-runtime.ts +48 -11
  74. package/src/gjc-runtime/state-writer.ts +1 -1
  75. package/src/hooks/skill-state.ts +23 -1
  76. package/src/internal-urls/docs-index.generated.ts +7 -7
  77. package/src/internal-urls/local-protocol.ts +23 -4
  78. package/src/modes/acp/acp-agent.ts +554 -60
  79. package/src/modes/acp/acp-event-mapper.ts +93 -6
  80. package/src/modes/components/model-selector.ts +15 -1
  81. package/src/modes/components/notifications-settings-editor.ts +1 -1
  82. package/src/modes/components/queue-pane.ts +55 -9
  83. package/src/modes/components/queued-message-selector.ts +53 -10
  84. package/src/modes/components/tool-status-header.ts +1 -4
  85. package/src/modes/components/welcome.ts +18 -3
  86. package/src/modes/controllers/command-controller.ts +4 -0
  87. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  88. package/src/modes/controllers/input-controller.ts +44 -3
  89. package/src/modes/controllers/selector-controller.ts +38 -8
  90. package/src/modes/interactive-mode.ts +65 -26
  91. package/src/modes/runtime-init.ts +1 -0
  92. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  93. package/src/prompts/agents/architect.md +8 -1
  94. package/src/prompts/agents/critic.md +8 -1
  95. package/src/prompts/agents/executor.md +1 -1
  96. package/src/prompts/agents/planner.md +8 -1
  97. package/src/runtime/memory-domain.ts +66 -0
  98. package/src/runtime/memory-guard-contract.ts +73 -0
  99. package/src/runtime/memory-guard.ts +292 -0
  100. package/src/runtime/memory-limit.ts +50 -0
  101. package/src/runtime-mcp/manager.ts +26 -6
  102. package/src/sdk/acp/adapter.ts +8 -1
  103. package/src/sdk/acp/final-text.ts +40 -0
  104. package/src/sdk/acp/mcp.ts +26 -0
  105. package/src/sdk/broker/identity.ts +22 -12
  106. package/src/sdk/broker/lifecycle.ts +82 -1
  107. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  108. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  109. package/src/sdk/bus/config.ts +1 -1
  110. package/src/sdk/bus/daemon-paths.ts +2 -0
  111. package/src/sdk/bus/index.ts +222 -27
  112. package/src/sdk/bus/notification-service.ts +19 -5
  113. package/src/sdk/bus/operator-runtime.ts +31 -5
  114. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  115. package/src/sdk/bus/telegram-cli.ts +6 -13
  116. package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
  117. package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
  118. package/src/sdk/bus/telegram-daemon-control.ts +14 -5
  119. package/src/sdk/bus/telegram-daemon.ts +1191 -164
  120. package/src/sdk/host/control/dispatch.ts +2 -0
  121. package/src/sdk/host/control/operations.ts +1 -0
  122. package/src/sdk/host/host.ts +2 -2
  123. package/src/sdk/lifecycle-session.ts +19 -3
  124. package/src/sdk/protocol/operation-inventory.generated.json +58 -0
  125. package/src/sdk/protocol/operation-registry.ts +1 -0
  126. package/src/sdk/session.ts +24 -1
  127. package/src/sdk/startup-capability.ts +6 -0
  128. package/src/session/agent-session.ts +290 -132
  129. package/src/session/fallback-chain-controller.ts +47 -8
  130. package/src/session/internal/managed-session-scope.ts +173 -127
  131. package/src/session/internal/native-publish-outcome.ts +8 -3
  132. package/src/session/session-manager.ts +608 -25
  133. package/src/skill-state/workflow-mutation-guard.ts +41 -7
  134. package/src/task/executor.ts +152 -42
  135. package/src/task/index.ts +2 -0
  136. package/src/task/provider-retry-status.ts +88 -0
  137. package/src/task/render.ts +45 -26
  138. package/src/task/types.ts +25 -11
  139. package/src/task/ultragoal-redteam-activation.ts +75 -0
  140. package/src/tools/bash-allowed-prefixes.ts +45 -1
  141. package/src/tools/index.ts +3 -1
  142. package/src/tools/resource-gc.ts +496 -120
  143. package/src/tools/subagent-render.ts +74 -16
  144. package/src/tools/subagent.ts +2 -0
  145. package/src/workflow/workflow-intent-diff.ts +4 -1
  146. package/vendor/insane-search/MANIFEST.json +4 -1
  147. package/vendor/insane-search/engine/bias_check.py +5 -0
  148. package/vendor/insane-search/engine/fetch_chain.py +67 -1
  149. package/vendor/insane-search/engine/transport.py +13 -2
  150. package/vendor/insane-search/engine/validators.py +4 -1
@@ -67,7 +67,7 @@ async function loadConfig(agentDir: string, kind: ChatDaemonKind): Promise<ChatD
67
67
  idleTimeoutMs: 60_000,
68
68
  rich: { enabled: true },
69
69
  richDraft: { enabled: false },
70
- toolActivity: { enabled: true },
70
+ toolActivity: { enabled: false },
71
71
  topics: { nameTemplate: undefined },
72
72
  btw: { enabled: true },
73
73
  streaming: { enabled: true },
@@ -102,7 +102,7 @@ async function loadConfig(agentDir: string, kind: ChatDaemonKind): Promise<ChatD
102
102
  idleTimeoutMs: 60_000,
103
103
  rich: { enabled: true },
104
104
  richDraft: { enabled: false },
105
- toolActivity: { enabled: true },
105
+ toolActivity: { enabled: false },
106
106
  topics: { nameTemplate: undefined },
107
107
  btw: { enabled: true },
108
108
  streaming: { enabled: true },
@@ -34,11 +34,12 @@ export type ChatDaemonAction = "stop" | "reload";
34
34
  * concrete detached quarantine plus a proven-absent canonical lock pathname —
35
35
  * when deleting an observed owner-lock lease. Generation 12 refreshes retained
36
36
  * native path and process authority semantics. Generation 13 rejects special
37
- * files before retained native authority opens.
37
+ * files before retained native authority opens. Generation 14 reloads shared
38
+ * chat daemons after notification configuration parsing changes.
38
39
  */
39
40
  export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
40
- discord: 13,
41
- slack: 13,
41
+ discord: 14,
42
+ slack: 14,
42
43
  };
43
44
 
44
45
  export function chatDaemonGeneration(kind: ChatDaemonKind): number {
@@ -170,7 +170,7 @@ export function parseNotificationSettingsSnapshot(rawConfig?: unknown): Notifica
170
170
  enabled: notificationSettingsBoolean(richDraft.enabled, false),
171
171
  },
172
172
  toolActivity: {
173
- enabled: notificationSettingsBoolean(toolActivity.enabled, true),
173
+ enabled: notificationSettingsBoolean(toolActivity.enabled, false),
174
174
  },
175
175
  streaming: {
176
176
  enabled: notificationSettingsBoolean(streaming.enabled, true),
@@ -12,6 +12,7 @@ export interface DaemonPaths {
12
12
  dir: string;
13
13
  lock: string;
14
14
  state: string;
15
+ heartbeat: string;
15
16
  ownership: string;
16
17
  roots: string;
17
18
  steal: string;
@@ -26,6 +27,7 @@ export function daemonPaths(agentDir: string): DaemonPaths {
26
27
  lock: path.join(dir, "telegram-daemon.lock"),
27
28
  state: path.join(dir, "telegram-daemon.state.json"),
28
29
  ownership: path.join(dir, "telegram-daemon.ownership"),
30
+ heartbeat: path.join(dir, "telegram-daemon.heartbeat.json"),
29
31
  roots: path.join(dir, "telegram-daemon.roots.json"),
30
32
  steal: path.join(dir, "telegram-daemon.steal"),
31
33
  aliases: path.join(dir, "telegram-callback-aliases.json"),
@@ -31,7 +31,8 @@ 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
+ import { isModelProfileProviderAvailable, projectModelProfileCatalog } from "../../config/model-profile-contract";
35
+ import { isAuthenticated, kNoAuth } from "../../config/model-registry";
35
36
  import { Settings } from "../../config/settings";
36
37
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
37
38
  import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
@@ -54,6 +55,7 @@ import type {
54
55
  AskSettlementResult,
55
56
  } from "../../tools";
56
57
  import { registerAskAnswerSource, registerWorkflowGateEmitterListener } from "../../tools/ask-answer-registry";
58
+ import { acpFinalTextFromMessage } from "../acp/final-text";
57
59
  import { ensureBroker } from "../broker/ensure";
58
60
  import { SessionIndex } from "../broker/session-index";
59
61
  import { SessionSdkHost, shouldHostSdk } from "../host";
@@ -89,6 +91,8 @@ import {
89
91
  type EnsureDaemonResult,
90
92
  endpointAuthorityDigest,
91
93
  ensureTelegramDaemonRunningDetailed,
94
+ type RegisterNotificationRootResult,
95
+ unregisterNotificationRoot,
92
96
  } from "./telegram-daemon";
93
97
 
94
98
  // ===========================================================================
@@ -909,6 +913,8 @@ interface SessionRuntime {
909
913
  hostStopped: boolean;
910
914
  serverStopped: boolean;
911
915
  brokerRegistrationReleased: boolean;
916
+ /** Managed Telegram root registration released during terminal teardown. */
917
+ notificationRootRegistration?: { settings: Settings; cwd: string; registrationToken?: string };
912
918
  verbosity: "lean" | "verbose";
913
919
  sessionTag: string;
914
920
  /** Whether the agent loop is currently running (drives the typing indicator). */
@@ -931,7 +937,13 @@ interface SessionRuntime {
931
937
  emitPromptLifecycle: (
932
938
  correlation: { commandId: string; turnId: string } | undefined,
933
939
  frame:
934
- | { type: "agent_start" | "agent_end"; sessionId: string; commandId?: string; turnId?: string }
940
+ | {
941
+ type: "agent_start" | "agent_end";
942
+ sessionId: string;
943
+ commandId?: string;
944
+ turnId?: string;
945
+ finalText?: string;
946
+ }
935
947
  | {
936
948
  type: "agent_failed";
937
949
  sessionId: string;
@@ -971,7 +983,10 @@ interface SessionRuntime {
971
983
  /** SDK control frames received during provisional ownership; replayed only after stable activation. */
972
984
  deferredInboundControls: Array<() => void>;
973
985
  /** Started tool calls awaiting a terminal activity frame, keyed by tool call id. */
974
- inFlightTools: Map<string, { toolName: string; args: unknown }>;
986
+ inFlightTools: Map<
987
+ string,
988
+ { toolName: string; args?: unknown; pendingPhase?: "completed" | "failed" | "cancelled" }
989
+ >;
975
990
  /** Cancels the postmortem cleanup that emits `session_closed` on process teardown. */
976
991
  cancelPostmortemCleanup: () => void;
977
992
  /** Disposes side-turn resources when their owning logical session becomes unavailable. */
@@ -1101,7 +1116,7 @@ const defaultConfig: NotificationConfig = {
1101
1116
  idleTimeoutMs: 60_000,
1102
1117
  rich: { enabled: true },
1103
1118
  richDraft: { enabled: false },
1104
- toolActivity: { enabled: true },
1119
+ toolActivity: { enabled: false },
1105
1120
  streaming: { enabled: true },
1106
1121
  topics: {},
1107
1122
  btw: { enabled: true },
@@ -1694,6 +1709,7 @@ const UNINSTALLED_CONTROL_OPERATIONS = new Set(["auth.login", "host_tools.regist
1694
1709
 
1695
1710
  const CONTROL_BINDINGS: Readonly<Record<string, string | undefined>> = {
1696
1711
  "model.cycle": "cycleModel",
1712
+ "model.profile.set": "setModelProfile",
1697
1713
  "thinking.cycle": "cycleThinkingLevel",
1698
1714
  "queue.steering_mode.set": "setQueueMode",
1699
1715
  "queue.follow_up_mode.set": "setQueueMode",
@@ -1858,8 +1874,26 @@ function sdkQuerySurface(
1858
1874
  const currentThinkingLevel = api.getThinkingLevel();
1859
1875
  return projectQ10Models({ models, currentModel, currentThinkingLevel });
1860
1876
  },
1861
- getModelProfiles: () =>
1862
- projectModelProfileCatalog(ctx.modelRegistry.getModelProfiles(), ctx.modelRegistry.getError()),
1877
+ getModelProfiles: async () => {
1878
+ const profiles = ctx.modelRegistry.getModelProfiles();
1879
+ const catalog = projectModelProfileCatalog(profiles, ctx.modelRegistry.getError());
1880
+ const providers = new Set([...profiles.values()].flatMap(profile => profile.requiredProviders));
1881
+ const authenticatedProviders = new Set<string>();
1882
+ await Promise.all(
1883
+ [...providers].map(async provider => {
1884
+ try {
1885
+ const credential = await ctx.modelRegistry.getApiKeyForProvider(provider, id);
1886
+ if (credential === kNoAuth || isAuthenticated(credential)) authenticatedProviders.add(provider);
1887
+ } catch {
1888
+ // A provider whose credential state cannot be read is not currently configurable.
1889
+ }
1890
+ }),
1891
+ );
1892
+ return catalog.map(item => ({
1893
+ ...item,
1894
+ available: isModelProfileProviderAvailable(profiles.get(item.id)!, authenticatedProviders),
1895
+ }));
1896
+ },
1863
1897
  getSkillState: () => ctx.getSkillState(),
1864
1898
  getGates: () => {
1865
1899
  const workflowGate = ctx.workflowGate;
@@ -2329,6 +2363,14 @@ function sdkControlSurface(
2329
2363
  );
2330
2364
  return typed("model.set", { id: `${model.provider}/${model.id}`, thinkingLevel });
2331
2365
  },
2366
+ setModelProfile: async id => {
2367
+ if (!bindings.has("setModelProfile") || !ctx.setModelProfile)
2368
+ return unavailable("model.profile.set", "no model-profile activation seam is installed")();
2369
+ if (!id) throw Object.assign(new Error("model.profile.set requires a profile id."), { code: "invalid_input" });
2370
+ if (!ctx.modelRegistry.getModelProfile(id))
2371
+ throw Object.assign(new Error(`Model profile ${id} was not found.`), { code: "invalid_input" });
2372
+ return { changed: await ctx.setModelProfile(id), id };
2373
+ },
2332
2374
  cycleModel: async () => {
2333
2375
  if (!bindings.has("cycleModel"))
2334
2376
  return unavailable("model.cycle", "no session model-cycle seam is installed")();
@@ -2832,6 +2874,7 @@ export function createNotificationsExtension(
2832
2874
  settings: Settings;
2833
2875
  cwd: string;
2834
2876
  sessionId: string;
2877
+ onRegistered?: (registration: RegisterNotificationRootResult) => void;
2835
2878
  }) => Promise<EnsureDaemonResult>;
2836
2879
  ensureProviderDaemon?: (provider: "discord" | "slack", settings: Settings) => Promise<unknown>;
2837
2880
  /** Suppress auto-delivery for a GJC-spawned child under `sessionScope=primary`. */
@@ -2873,13 +2916,24 @@ export function createNotificationsExtension(
2873
2916
  settings: Settings,
2874
2917
  cwd: string,
2875
2918
  id: string,
2919
+ onRegistered?: (registrationToken: string) => void,
2876
2920
  ): Promise<"ready" | "blocked_identity"> {
2877
2921
  if (options.ensureTelegramDaemon) {
2878
- return (await options.ensureTelegramDaemon({ settings, cwd, sessionId: id })) === "blocked"
2922
+ return (await options.ensureTelegramDaemon({
2923
+ settings,
2924
+ cwd,
2925
+ sessionId: id,
2926
+ onRegistered: registration => onRegistered?.(registration.token),
2927
+ })) === "blocked"
2879
2928
  ? "blocked_identity"
2880
2929
  : "ready";
2881
2930
  }
2882
- return (await ensureTelegramDaemonRunningDetailed({ settings, cwd, sessionId: id })) === "blocked_identity"
2931
+ return (await ensureTelegramDaemonRunningDetailed({
2932
+ settings,
2933
+ cwd,
2934
+ sessionId: id,
2935
+ onRegistered: registration => onRegistered?.(registration.token),
2936
+ })) === "blocked_identity"
2883
2937
  ? "blocked_identity"
2884
2938
  : "ready";
2885
2939
  }
@@ -2888,9 +2942,10 @@ export function createNotificationsExtension(
2888
2942
  cfg: NotificationConfig,
2889
2943
  cwd: string,
2890
2944
  id: string,
2945
+ onRegistered?: (registrationToken: string) => void,
2891
2946
  ): Promise<boolean> {
2892
2947
  if (isTelegramConfigured(cfg)) {
2893
- if ((await ensureTelegramOwner(settings, cwd, id)) === "blocked_identity") return false;
2948
+ if ((await ensureTelegramOwner(settings, cwd, id, onRegistered)) === "blocked_identity") return false;
2894
2949
  }
2895
2950
  await ensureConfiguredProviderDaemons(settings, cfg, options.ensureProviderDaemon);
2896
2951
  return true;
@@ -3040,6 +3095,20 @@ export function createNotificationsExtension(
3040
3095
  hostStopped: rt.hostStopped && rt.serverStopped,
3041
3096
  brokerRegistrationReleased: rt.brokerRegistrationReleased,
3042
3097
  });
3098
+ if (rt.notificationRootRegistration) {
3099
+ try {
3100
+ await unregisterNotificationRoot({
3101
+ settings: rt.notificationRootRegistration.settings,
3102
+ cwd: rt.notificationRootRegistration.cwd,
3103
+ sessionId: id,
3104
+ registrationToken: rt.notificationRootRegistration.registrationToken,
3105
+ });
3106
+ rt.notificationRootRegistration = undefined;
3107
+ } catch (e) {
3108
+ ownerReleaseFailures.push(e);
3109
+ logger.warn(`notifications: Telegram root unregister failed: ${String(e)}`);
3110
+ }
3111
+ }
3043
3112
  if (ownerReleaseFailures.length > 0) {
3044
3113
  cleanupRetries.set(id, rt);
3045
3114
  throw new AggregateError(ownerReleaseFailures, `SDK notification runtime ${id} owner release failed.`);
@@ -3188,7 +3257,13 @@ export function createNotificationsExtension(
3188
3257
  const promptSubmissionKey = (correlation: { commandId: string; turnId: string }) =>
3189
3258
  `${correlation.commandId}:${correlation.turnId}`;
3190
3259
  type PromptLifecycleFrame =
3191
- | { type: "agent_start" | "agent_end"; sessionId: string; commandId?: string; turnId?: string }
3260
+ | {
3261
+ type: "agent_start" | "agent_end";
3262
+ sessionId: string;
3263
+ commandId?: string;
3264
+ turnId?: string;
3265
+ finalText?: string;
3266
+ }
3192
3267
  | {
3193
3268
  type: "agent_failed";
3194
3269
  sessionId: string;
@@ -3638,9 +3713,13 @@ export function createNotificationsExtension(
3638
3713
  emitPromptLifecycle,
3639
3714
  emitPromptEvent,
3640
3715
  pendingInbound: new Set<number>(),
3641
- inFlightTools: new Map<string, { toolName: string; args: unknown }>(),
3716
+ inFlightTools: new Map<
3717
+ string,
3718
+ { toolName: string; args?: unknown; pendingPhase?: "completed" | "failed" | "cancelled" }
3719
+ >(),
3642
3720
  deferredGatePresentations: [],
3643
3721
  deferredInboundControls: [],
3722
+ notificationRootRegistration: undefined,
3644
3723
  };
3645
3724
  const initializedRuntime = runtime;
3646
3725
  runtimes.set(id, initializedRuntime);
@@ -4071,7 +4150,7 @@ export function createNotificationsExtension(
4071
4150
  }
4072
4151
  if (typeof inbound.redact === "boolean") {
4073
4152
  if (inbound.redact && !runtime.committedRedact) {
4074
- terminalizeInFlightTools(runtime, runtime.id, "unknown");
4153
+ terminalizeInFlightTools(runtime, runtime.id, "cancelled");
4075
4154
  }
4076
4155
  runtime.committedRedact = inbound.redact;
4077
4156
  runtime.redact = inbound.redact;
@@ -4130,12 +4209,19 @@ export function createNotificationsExtension(
4130
4209
  }
4131
4210
  if (notificationsEnabledForSession && settingsAvailable && settings) {
4132
4211
  try {
4133
- if (!(await ensureConfiguredDaemonOwners(settings, cfg, ctx.cwd, id))) {
4212
+ let registrationToken: string | undefined;
4213
+ if (
4214
+ !(await ensureConfiguredDaemonOwners(settings, cfg, ctx.cwd, id, token => {
4215
+ registrationToken = token;
4216
+ }))
4217
+ ) {
4134
4218
  const result = failLifecycleStartup("failed", "Telegram daemon ownership is blocked.");
4135
4219
  finishStartup(result);
4136
4220
  await cleanupAbandonedStartup();
4137
4221
  return result;
4138
4222
  }
4223
+ if (isTelegramConfigured(cfg))
4224
+ runtime.notificationRootRegistration = { settings, cwd: ctx.cwd, registrationToken };
4139
4225
  } catch (error) {
4140
4226
  const result = failLifecycleStartup("failed", error);
4141
4227
  finishStartup(result);
@@ -4411,8 +4497,15 @@ export function createNotificationsExtension(
4411
4497
  runtime.redact = true;
4412
4498
  runtime.verbosity = "lean";
4413
4499
  runtime.stream = false;
4500
+ for (const [toolCallId, tool] of runtime.inFlightTools) {
4501
+ runtime.inFlightTools.set(toolCallId, {
4502
+ toolName: tool.toolName,
4503
+ ...(tool.pendingPhase ? { pendingPhase: tool.pendingPhase } : {}),
4504
+ });
4505
+ }
4414
4506
  return;
4415
4507
  }
4508
+ const wasPolicySuspended = runtime.policySuspended;
4416
4509
  const redactionEnabled = policy.redact && !runtime.committedRedact;
4417
4510
  runtime.policyGeneration++;
4418
4511
  runtime.committedRedact = policy.redact;
@@ -4420,7 +4513,8 @@ export function createNotificationsExtension(
4420
4513
  runtime.redact = policy.redact;
4421
4514
  runtime.verbosity = policy.verbosity;
4422
4515
  runtime.stream = policy.stream;
4423
- if (redactionEnabled) terminalizeInFlightTools(runtime, runtime.id, "unknown");
4516
+ if (redactionEnabled) terminalizeInFlightTools(runtime, runtime.id, "cancelled", true);
4517
+ else if (wasPolicySuspended && !policy.redact) settleProvisionalToolTerminals(runtime, runtime.id);
4424
4518
  },
4425
4519
  activate: binding => {
4426
4520
  const runtime = runtimes.get(binding.sessionId);
@@ -4434,7 +4528,23 @@ export function createNotificationsExtension(
4434
4528
  const { settings, settingsAvailable } = resolveSettings(options.settings);
4435
4529
  if (!settingsAvailable || !settings) return "blocked_identity";
4436
4530
  try {
4437
- return await ensureTelegramOwner(settings, binding.cwd, binding.sessionId);
4531
+ let registrationToken: string | undefined;
4532
+ const result = await ensureTelegramOwner(settings, binding.cwd, binding.sessionId, token => {
4533
+ registrationToken = token;
4534
+ });
4535
+ const runtime = runtimes.get(binding.sessionId);
4536
+ const configured = isTelegramConfigured(resolveSettings(options.settings).cfg);
4537
+ if (result === "ready" && runtime && !runtime.stopping && configured) {
4538
+ runtime.notificationRootRegistration = { settings, cwd: binding.cwd, registrationToken };
4539
+ } else if (registrationToken !== undefined) {
4540
+ await unregisterNotificationRoot({
4541
+ settings,
4542
+ cwd: binding.cwd,
4543
+ sessionId: binding.sessionId,
4544
+ registrationToken,
4545
+ });
4546
+ }
4547
+ return result;
4438
4548
  } catch {
4439
4549
  return "failed";
4440
4550
  }
@@ -4647,8 +4757,22 @@ export function createNotificationsExtension(
4647
4757
  await rotateSessionAuthority(event, ctx, false);
4648
4758
  });
4649
4759
 
4650
- const terminalizeInFlightTools = (rt: SessionRuntime, id: string, phase: "cancelled" | "unknown"): void => {
4651
- if (rt.notificationsActive && !rt.redact) {
4760
+ const terminalizeInFlightTools = (
4761
+ rt: SessionRuntime,
4762
+ id: string,
4763
+ phase: "cancelled" | "failed",
4764
+ allowSafeRedactedFrame = false,
4765
+ ): void => {
4766
+ if (rt.policySuspended && !allowSafeRedactedFrame) {
4767
+ for (const [toolCallId, tool] of rt.inFlightTools) {
4768
+ rt.inFlightTools.set(toolCallId, {
4769
+ toolName: tool.toolName,
4770
+ pendingPhase: tool.pendingPhase ?? phase,
4771
+ });
4772
+ }
4773
+ return;
4774
+ }
4775
+ if (rt.notificationsActive && (!rt.redact || allowSafeRedactedFrame)) {
4652
4776
  for (const [toolCallId, { toolName }] of rt.inFlightTools) {
4653
4777
  try {
4654
4778
  pushSessionFrame(rt, { type: "tool_activity", sessionId: id, toolCallId, toolName, phase });
@@ -4660,6 +4784,27 @@ export function createNotificationsExtension(
4660
4784
  rt.inFlightTools.clear();
4661
4785
  };
4662
4786
 
4787
+ const settleProvisionalToolTerminals = (rt: SessionRuntime, id: string): void => {
4788
+ for (const [toolCallId, tool] of rt.inFlightTools) {
4789
+ if (!tool.pendingPhase) continue;
4790
+ try {
4791
+ if (rt.notificationsActive && !rt.redact) {
4792
+ pushSessionFrame(rt, {
4793
+ type: "tool_activity",
4794
+ sessionId: id,
4795
+ toolCallId,
4796
+ toolName: tool.toolName,
4797
+ phase: tool.pendingPhase,
4798
+ });
4799
+ }
4800
+ } catch (e) {
4801
+ logger.warn(`notifications: provisional tool_activity settlement failed: ${String(e)}`);
4802
+ } finally {
4803
+ rt.inFlightTools.delete(toolCallId);
4804
+ }
4805
+ }
4806
+ };
4807
+
4663
4808
  const resetTurnStreamState = (rt: SessionRuntime): void => {
4664
4809
  rt.currentTurnText = undefined;
4665
4810
  rt.preAskFlushedText = undefined;
@@ -4796,14 +4941,28 @@ export function createNotificationsExtension(
4796
4941
  });
4797
4942
  else {
4798
4943
  rt.notePromptReconciliation(correlation, { type: "agent_end" });
4799
- if (rt.recordPromptTerminal(correlation))
4800
- rt.emitPromptLifecycle(correlation, { type: "agent_end", sessionId: id, ...correlation });
4944
+ if (rt.recordPromptTerminal(correlation)) {
4945
+ const finalAssistant = (Array.isArray(event.messages) ? [...event.messages].reverse() : []).find(
4946
+ message =>
4947
+ message &&
4948
+ typeof message === "object" &&
4949
+ (message as { role?: unknown }).role === "assistant" &&
4950
+ (message as { stopReason?: unknown }).stopReason === "stop",
4951
+ );
4952
+ const finalText = finalAssistant ? acpFinalTextFromMessage(finalAssistant).text : "";
4953
+ rt.emitPromptLifecycle(correlation, {
4954
+ type: "agent_end",
4955
+ sessionId: id,
4956
+ ...correlation,
4957
+ ...(finalText ? { finalText } : {}),
4958
+ });
4959
+ }
4801
4960
  }
4802
4961
  } else {
4803
4962
  rt.emitPromptLifecycle(undefined, { type: "agent_end", sessionId: id });
4804
4963
  }
4805
4964
  rt.activePromptCorrelation = undefined;
4806
- terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "unknown");
4965
+ terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "failed");
4807
4966
  try {
4808
4967
  pushSessionFrame(rt, { type: "activity", sessionId: id, state: "idle" });
4809
4968
  } catch (e) {
@@ -4921,14 +5080,13 @@ export function createNotificationsExtension(
4921
5080
  // path and ordered before it — unlike message_update, which is queued async),
4922
5081
  // then flushed here before the ask tool's execute calls registerAsk.
4923
5082
  api.on("tool_execution_start", (event, ctx) => {
5083
+ const id = sessionId(ctx);
5084
+ const rt = runtimes.get(id);
5085
+ rt?.emitPromptEvent(event);
4924
5086
  if (event.toolName === "ask") {
4925
- const id = sessionId(ctx);
4926
- const rt = runtimes.get(id);
4927
5087
  if (!rt?.notificationsActive || rt.redact) return;
4928
5088
  flushTurnText(rt, id, rt.currentTurnText, false);
4929
5089
  }
4930
- const id = sessionId(ctx);
4931
- const rt = runtimes.get(id);
4932
5090
  if (!rt?.notificationsActive || rt.redact) return;
4933
5091
  rt.inFlightTools.set(event.toolCallId, { toolName: event.toolName, args: event.args });
4934
5092
  try {
@@ -4944,12 +5102,32 @@ export function createNotificationsExtension(
4944
5102
  }
4945
5103
  });
4946
5104
 
5105
+ api.on("tool_execution_update", (event, ctx) => {
5106
+ const rt = runtimes.get(sessionId(ctx));
5107
+ rt?.emitPromptEvent(event);
5108
+ });
5109
+
4947
5110
  api.on("tool_execution_end", (event, ctx) => {
4948
5111
  const id = sessionId(ctx);
4949
5112
  const rt = runtimes.get(id);
4950
5113
  if (!rt) return;
5114
+ rt.emitPromptEvent(event);
4951
5115
  const inFlight = rt.inFlightTools.get(event.toolCallId);
4952
- if (!rt.notificationsActive || rt.redact) {
5116
+ if (!inFlight) return;
5117
+ if (!rt.notificationsActive) {
5118
+ rt.inFlightTools.delete(event.toolCallId);
5119
+ return;
5120
+ }
5121
+ if (rt.policySuspended) {
5122
+ if (!inFlight.pendingPhase) {
5123
+ rt.inFlightTools.set(event.toolCallId, {
5124
+ toolName: inFlight.toolName,
5125
+ pendingPhase: event.isError ? "failed" : "completed",
5126
+ });
5127
+ }
5128
+ return;
5129
+ }
5130
+ if (rt.redact) {
4953
5131
  rt.inFlightTools.delete(event.toolCallId);
4954
5132
  return;
4955
5133
  }
@@ -5117,10 +5295,22 @@ export function createNotificationsExtension(
5117
5295
  await Promise.allSettled([...branchStartupTasks]);
5118
5296
  const id = sessionId(ctx);
5119
5297
  const rt = runtimes.get(id);
5120
- if (rt) terminalizeInFlightTools(rt, id, "unknown");
5298
+ if (rt) terminalizeInFlightTools(rt, id, "cancelled");
5299
+ // Startup is only genuinely in flight when a `sessionStartPromises` entry
5300
+ // exists. Once startup has settled, the host is broker-visible and its
5301
+ // post-start `reconcileCurrentSession` may already have minted a
5302
+ // replacement notification-root token whose unregister is still awaiting
5303
+ // its file lock and atomic registry write. Returning before that settles
5304
+ // leaves a stale `sessions[id]` row that the retained older token is
5305
+ // correctly fenced from removing, so shutdown must join it.
5306
+ const startupWasPending = sessionStartPromises.has(id);
5121
5307
  const controllerStop =
5122
5308
  typeof ctx.sessionManager.getCwd === "function" ? controller.stopCurrentSession(ctx) : Promise.resolve(false);
5123
- void controllerStop.catch(error => logger.warn(`notifications: controller shutdown failed: ${String(error)}`));
5309
+ const settledControllerStop = controllerStop.catch(error => {
5310
+ logger.warn(`notifications: controller shutdown failed: ${String(error)}`);
5311
+ return false;
5312
+ });
5313
+ if (startupWasPending) void settledControllerStop;
5124
5314
  try {
5125
5315
  await stopSession(id);
5126
5316
  } catch (error) {
@@ -5131,5 +5321,10 @@ export function createNotificationsExtension(
5131
5321
  // error severity (matching the postmortem cleanup precedent).
5132
5322
  logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
5133
5323
  }
5324
+ // Keep shutdown nonblocking only while native startup is genuinely
5325
+ // pending (the `/notify on` path); otherwise await the controller queue so
5326
+ // completed-start reconciliation and its replacement-token cleanup are
5327
+ // joined before lifecycle shutdown returns.
5328
+ if (!startupWasPending) await settledControllerStop;
5134
5329
  });
5135
5330
  }
@@ -28,6 +28,7 @@ import {
28
28
  tokenFingerprint,
29
29
  } from "./config";
30
30
  import { type DaemonPaths, daemonPaths, HEARTBEAT_TTL_MS } from "./daemon-paths";
31
+ import { type OwnerFreshnessSnapshot, readOwnerFreshnessSnapshot, type TelegramDaemonFs } from "./telegram-daemon";
31
32
  import { DAEMON_GENERATION } from "./telegram-daemon-contract";
32
33
 
33
34
  const DEFAULT_API_BASE = "https://api.telegram.org";
@@ -578,10 +579,21 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
578
579
  checks.push({ name: "config", level: "ok", detail: "enabled with at least one configured adapter" });
579
580
  }
580
581
 
581
- // Daemon ownership state (offline; read the persisted state file directly).
582
- const paths = daemonPaths(opts.settings.getAgentDir());
583
- const state = await readDaemonStateFile(fs, paths.state);
584
- const heartbeatAt = state?.heartbeatAt;
582
+ // Daemon ownership state (offline; corrupt or unreadable state degrades to a
583
+ // warning instead of making the health command itself fail).
584
+ let daemonStateUnreadable = false;
585
+ let snapshot: OwnerFreshnessSnapshot;
586
+ try {
587
+ snapshot = await readOwnerFreshnessSnapshot({
588
+ settings: opts.settings,
589
+ fs: fs as unknown as TelegramDaemonFs,
590
+ });
591
+ } catch {
592
+ daemonStateUnreadable = true;
593
+ snapshot = { ownerTag: null, effectiveHeartbeatAt: undefined, legacyEmbedded: false, state: undefined };
594
+ }
595
+ const state = snapshot.state ? parseDaemonState(JSON.stringify(snapshot.state)) : undefined;
596
+ const heartbeatAt = finiteNonNegativeNumber(snapshot.effectiveHeartbeatAt);
585
597
  const daemon: DaemonHealth = {
586
598
  present: Boolean(state),
587
599
  ownerId: state?.ownerId,
@@ -600,7 +612,9 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
600
612
  currentGeneration: DAEMON_GENERATION,
601
613
  generationRelation: daemonGenerationRelation(state),
602
614
  };
603
- if (!state) {
615
+ if (daemonStateUnreadable) {
616
+ checks.push({ name: "daemon", level: "warn", detail: "daemon ownership record is corrupt or unreadable" });
617
+ } else if (!state) {
604
618
  checks.push({ name: "daemon", level: "ok", detail: "no daemon ownership record (none running)" });
605
619
  } else if (!daemon.alive) {
606
620
  checks.push({
@@ -74,7 +74,7 @@ export class NotificationOperatorRuntime {
74
74
  #stopRequested = false;
75
75
  #activeAbort: AbortController | undefined;
76
76
  #intervals = new Map<string, OperatorIntervalHandle>();
77
- #exclusive = new Set<string>();
77
+ #exclusive = new Map<string, Promise<void>>();
78
78
 
79
79
  #deps: NotificationOperatorTimerDeps;
80
80
 
@@ -130,7 +130,7 @@ export class NotificationOperatorRuntime {
130
130
 
131
131
  stopInterval(name: string): void {
132
132
  const timer = this.#intervals.get(name);
133
- if (!timer) return;
133
+ if (timer === undefined) return;
134
134
  const clearIntervalImpl = this.#deps.clearIntervalImpl ?? clearInterval;
135
135
  clearIntervalImpl(timer);
136
136
  this.#intervals.delete(name);
@@ -142,14 +142,40 @@ export class NotificationOperatorRuntime {
142
142
 
143
143
  async runExclusive(name: string, fn: () => Promise<void>): Promise<void> {
144
144
  if (this.#exclusive.has(name)) return;
145
- this.#exclusive.add(name);
145
+ const completion = Promise.withResolvers<void>();
146
+ this.#exclusive.set(name, completion.promise);
146
147
  try {
147
- await fn();
148
+ try {
149
+ void fn().then(completion.resolve, completion.reject);
150
+ } catch (error) {
151
+ completion.reject(error);
152
+ }
153
+ await completion.promise;
148
154
  } finally {
149
- this.#exclusive.delete(name);
155
+ if (this.#exclusive.get(name) === completion.promise) this.#exclusive.delete(name);
150
156
  }
151
157
  }
152
158
 
159
+ async joinExclusive(name: string, timeoutMs: number): Promise<boolean> {
160
+ const completion = this.#exclusive.get(name);
161
+ if (!completion) return true;
162
+ const setTimeoutImpl = this.#deps.setTimeoutImpl ?? setTimeout;
163
+ const clearTimeoutImpl = this.#deps.clearTimeoutImpl ?? clearTimeout;
164
+ const { promise, resolve } = Promise.withResolvers<boolean>();
165
+ const timer = setTimeoutImpl(() => resolve(false), timeoutMs);
166
+ void completion.then(
167
+ () => {
168
+ clearTimeoutImpl(timer);
169
+ resolve(true);
170
+ },
171
+ () => {
172
+ clearTimeoutImpl(timer);
173
+ resolve(true);
174
+ },
175
+ );
176
+ return await promise;
177
+ }
178
+
153
179
  sleep(ms: number, signal?: AbortSignal): Promise<void> {
154
180
  return new Promise<void>(resolve => {
155
181
  if (signal?.aborted) return resolve();
@@ -116,6 +116,14 @@ export class RateLimitPool<T = unknown> {
116
116
  return total;
117
117
  }
118
118
 
119
+ /** Whether any queued item currently matches the predicate. */
120
+ someQueued(predicate: (item: RateLimitItem<T>) => boolean): boolean {
121
+ for (const queue of this.lanes.values()) {
122
+ if (queue.some(entry => predicate(entry.item))) return true;
123
+ }
124
+ return false;
125
+ }
126
+
119
127
  /** Current available token count (after refill at `now`). */
120
128
  availableTokens(nowMs: number = this.now()): number {
121
129
  this.refill(nowMs);