@gajae-code/coding-agent 0.11.10 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/cli.d.ts +8 -1
  4. package/dist/types/commands/deep-interview.d.ts +9 -0
  5. package/dist/types/commands/harness.d.ts +22 -0
  6. package/dist/types/commands/read.d.ts +6 -0
  7. package/dist/types/config/model-profile-activation.d.ts +1 -0
  8. package/dist/types/config/model-profile-contract.d.ts +2 -0
  9. package/dist/types/config/model-registry.d.ts +4 -0
  10. package/dist/types/config/provider-auth-health.d.ts +14 -0
  11. package/dist/types/config/provider-ranking.d.ts +59 -0
  12. package/dist/types/config/settings-schema.d.ts +86 -3
  13. package/dist/types/config/settings.d.ts +3 -0
  14. package/dist/types/extensibility/extensions/types.d.ts +43 -2
  15. package/dist/types/extensibility/shared-events.d.ts +6 -0
  16. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  17. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  18. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  19. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  20. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  21. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  22. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  23. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
  24. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  25. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  26. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  28. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  29. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  30. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  31. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  32. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  33. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  34. package/dist/types/main.d.ts +1 -1
  35. package/dist/types/memories/index.d.ts +2 -0
  36. package/dist/types/modes/acp/acp-agent.d.ts +22 -9
  37. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  38. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  39. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  40. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  41. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  42. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  43. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  44. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  45. package/dist/types/modes/components/welcome.d.ts +1 -0
  46. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  47. package/dist/types/modes/interactive-mode.d.ts +7 -1
  48. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  49. package/dist/types/modes/types.d.ts +2 -0
  50. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  51. package/dist/types/runtime/memory-domain.d.ts +9 -0
  52. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  53. package/dist/types/runtime/memory-guard.d.ts +39 -0
  54. package/dist/types/runtime/memory-limit.d.ts +11 -0
  55. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  56. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  57. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  58. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  59. package/dist/types/sdk/broker/lifecycle.d.ts +7 -0
  60. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  61. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  62. package/dist/types/sdk/bus/index.d.ts +18 -2
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  65. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  66. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  67. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
  68. package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
  69. package/dist/types/sdk/host/control/operations.d.ts +2 -1
  70. package/dist/types/sdk/host/host.d.ts +2 -0
  71. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  72. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  73. package/dist/types/sdk/startup-capability.d.ts +6 -0
  74. package/dist/types/session/agent-session.d.ts +51 -19
  75. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  76. package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
  77. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  78. package/dist/types/session/session-manager.d.ts +59 -13
  79. package/dist/types/session/streaming-output.d.ts +60 -5
  80. package/dist/types/skill-state/active-state.d.ts +0 -4
  81. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  82. package/dist/types/task/executor.d.ts +6 -0
  83. package/dist/types/task/types.d.ts +36 -0
  84. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  85. package/dist/types/tools/browser/launch.d.ts +7 -0
  86. package/dist/types/tools/fetch.d.ts +17 -1
  87. package/dist/types/tools/output-meta.d.ts +21 -1
  88. package/dist/types/tools/read-internals.d.ts +12 -0
  89. package/dist/types/tools/read.d.ts +45 -1
  90. package/dist/types/tools/resource-gc.d.ts +23 -0
  91. package/dist/types/tools/tool-result.d.ts +7 -1
  92. package/package.json +9 -13
  93. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  94. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  95. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  96. package/scripts/run-test-manifest.ts +6 -4
  97. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  98. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  99. package/src/capability/index.ts +8 -0
  100. package/src/cli/args.ts +14 -8
  101. package/src/cli/fast-help.ts +1 -1
  102. package/src/cli/read-cli.ts +2 -1
  103. package/src/cli.ts +49 -2
  104. package/src/commands/deep-interview.ts +29 -9
  105. package/src/commands/harness.ts +35 -12
  106. package/src/commands/read.ts +11 -2
  107. package/src/commands/sdk.ts +91 -7
  108. package/src/commands/team.ts +1 -1
  109. package/src/config/model-profile-activation.ts +1 -0
  110. package/src/config/model-profile-contract.ts +13 -0
  111. package/src/config/model-profiles.ts +24 -0
  112. package/src/config/model-registry.ts +21 -2
  113. package/src/config/provider-auth-health.ts +42 -0
  114. package/src/config/provider-ranking.ts +119 -0
  115. package/src/config/settings-schema.ts +84 -3
  116. package/src/config/settings.ts +254 -108
  117. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  118. package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
  119. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  120. package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
  121. package/src/eval/py/executor.ts +7 -2
  122. package/src/extensibility/extensions/runner.ts +4 -0
  123. package/src/extensibility/extensions/types.ts +42 -4
  124. package/src/extensibility/shared-events.ts +5 -0
  125. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
  126. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  127. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  128. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  129. package/src/gjc-runtime/deep-interview-state.ts +12 -921
  130. package/src/gjc-runtime/gc-render.ts +1 -0
  131. package/src/gjc-runtime/gc-runtime.ts +11 -1
  132. package/src/gjc-runtime/linux-proc.ts +70 -36
  133. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  134. package/src/gjc-runtime/ralplan-runtime.ts +979 -71
  135. package/src/gjc-runtime/state-runtime.ts +50 -12
  136. package/src/gjc-runtime/state-writer.ts +70 -222
  137. package/src/gjc-runtime/team-launch.ts +34 -5
  138. package/src/gjc-runtime/team-runtime.ts +970 -33
  139. package/src/gjc-runtime/team-store.ts +32 -0
  140. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  141. package/src/gjc-runtime/team-workers.ts +1 -0
  142. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  143. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  144. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  145. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  146. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  147. package/src/hooks/native-skill-hook.ts +17 -4
  148. package/src/hooks/skill-state.ts +1 -23
  149. package/src/internal-urls/docs-index.generated.ts +10 -11
  150. package/src/internal-urls/local-protocol.ts +23 -4
  151. package/src/internal-urls/local-root-gc.ts +127 -0
  152. package/src/main.ts +9 -5
  153. package/src/memories/index.ts +10 -0
  154. package/src/modes/DESIGN.md +47 -0
  155. package/src/modes/acp/acp-agent.ts +597 -67
  156. package/src/modes/acp/acp-event-mapper.ts +106 -12
  157. package/src/modes/acp/acp-mode.ts +39 -3
  158. package/src/modes/components/assistant-message.ts +123 -22
  159. package/src/modes/components/irc-sidebar.ts +176 -62
  160. package/src/modes/components/model-selector.ts +76 -5
  161. package/src/modes/components/notifications-settings-editor.ts +1 -1
  162. package/src/modes/components/oauth-selector.ts +42 -12
  163. package/src/modes/components/queue-pane.ts +55 -9
  164. package/src/modes/components/queued-message-selector.ts +53 -10
  165. package/src/modes/components/read-tool-group.ts +82 -14
  166. package/src/modes/components/session-observer-overlay.ts +187 -32
  167. package/src/modes/components/settings-selector.ts +60 -18
  168. package/src/modes/components/tool-execution.ts +138 -27
  169. package/src/modes/components/tool-status-header.ts +1 -4
  170. package/src/modes/components/welcome.ts +18 -3
  171. package/src/modes/controllers/command-controller.ts +4 -0
  172. package/src/modes/controllers/event-controller.ts +109 -27
  173. package/src/modes/controllers/extension-ui-controller.ts +23 -12
  174. package/src/modes/controllers/input-controller.ts +62 -6
  175. package/src/modes/controllers/selector-controller.ts +83 -37
  176. package/src/modes/interactive-mode.ts +127 -29
  177. package/src/modes/irc-observation-ledger.ts +71 -15
  178. package/src/modes/runtime-init.ts +20 -2
  179. package/src/modes/types.ts +2 -0
  180. package/src/modes/utils/ui-helpers.ts +94 -1
  181. package/src/prompts/agents/architect.md +9 -1
  182. package/src/prompts/agents/critic.md +9 -0
  183. package/src/prompts/agents/executor.md +1 -1
  184. package/src/prompts/system/system-prompt.md +0 -1
  185. package/src/prompts/tools/read.md +4 -2
  186. package/src/runtime/memory-domain.ts +66 -0
  187. package/src/runtime/memory-guard-contract.ts +73 -0
  188. package/src/runtime/memory-guard.ts +292 -0
  189. package/src/runtime/memory-limit.ts +50 -0
  190. package/src/runtime-mcp/manager.ts +26 -6
  191. package/src/runtime-mcp/smithery-auth.ts +17 -2
  192. package/src/runtime-mcp/smithery-connect.ts +11 -1
  193. package/src/sdk/acp/adapter.ts +8 -1
  194. package/src/sdk/acp/final-text.ts +40 -0
  195. package/src/sdk/acp/mcp.ts +26 -0
  196. package/src/sdk/broker/lifecycle.ts +107 -7
  197. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  198. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  199. package/src/sdk/bus/config.ts +1 -1
  200. package/src/sdk/bus/control-drain-lease.ts +104 -0
  201. package/src/sdk/bus/index.ts +503 -60
  202. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  203. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  204. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  205. package/src/sdk/bus/reconciliation-store.ts +224 -0
  206. package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
  207. package/src/sdk/bus/telegram-daemon.ts +531 -53
  208. package/src/sdk/host/control/dispatch.ts +7 -1
  209. package/src/sdk/host/control/operations.ts +2 -1
  210. package/src/sdk/host/host.ts +25 -3
  211. package/src/sdk/host/query/handlers.ts +55 -0
  212. package/src/sdk/lifecycle-session.ts +19 -3
  213. package/src/sdk/mcp/server.ts +46 -18
  214. package/src/sdk/protocol/operation-inventory.generated.json +76 -0
  215. package/src/sdk/protocol/operation-registry.ts +8 -2
  216. package/src/sdk/session.ts +25 -2
  217. package/src/sdk/startup-capability.ts +6 -0
  218. package/src/session/agent-session.ts +720 -190
  219. package/src/session/fallback-chain-controller.ts +47 -8
  220. package/src/session/internal/managed-session-scope.ts +40 -10
  221. package/src/session/internal/native-publish-outcome.ts +8 -3
  222. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  223. package/src/session/session-manager.ts +1372 -42
  224. package/src/session/streaming-output.ts +262 -43
  225. package/src/setup/provider-onboarding.ts +19 -4
  226. package/src/skill-state/active-state.ts +0 -10
  227. package/src/skill-state/workflow-hud.ts +25 -2
  228. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  229. package/src/task/executor.ts +11 -3
  230. package/src/task/index.ts +2 -0
  231. package/src/task/types.ts +22 -11
  232. package/src/task/ultragoal-redteam-activation.ts +75 -0
  233. package/src/tools/ask.ts +89 -69
  234. package/src/tools/browser/launch.ts +41 -6
  235. package/src/tools/fetch.ts +114 -23
  236. package/src/tools/output-meta.ts +176 -9
  237. package/src/tools/path-utils.ts +5 -1
  238. package/src/tools/read-internals.ts +44 -0
  239. package/src/tools/read.ts +1542 -310
  240. package/src/tools/resource-gc.ts +665 -125
  241. package/src/tools/tool-result.ts +14 -1
  242. package/src/web/search/providers/codex.ts +1 -1
  243. package/src/workflow/workflow-intent-diff.ts +4 -1
  244. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  245. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  246. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  247. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  248. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  249. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
@@ -193,8 +193,20 @@ export const CLIENT_PING_PONG_CAPABILITY = "client_ping_pong";
193
193
  /** Capability required for typed controls and semantic Selected acknowledgement frames. */
194
194
  export const ASK_SELECTED_ACK_CAPABILITY = "ask_selected_ack_v1";
195
195
  export const ASK_CONTROLS_CAPABILITY = "ask_controls_v1";
196
- /** Capability required for tool lifecycle and reasoning-summary frames. */
197
- export const TOOL_ACTIVITY_CAPABILITY = "tool_activity_v1";
196
+ /** Capability for the closed tool phase set: started, completed, failed, and cancelled. */
197
+ export const TOOL_ACTIVITY_CAPABILITY = "tool_activity_v2";
198
+ /** Receive-only compatibility capability for pre-v2 hosts. */
199
+ export const LEGACY_TOOL_ACTIVITY_CAPABILITY = "tool_activity_v1";
200
+ type ToolActivityCapability = "v1" | "v2";
201
+
202
+ function negotiateToolActivityCapability(
203
+ current: ToolActivityCapability | undefined,
204
+ capabilities: unknown[],
205
+ ): ToolActivityCapability | undefined {
206
+ if (current === "v2" || capabilities.includes(TOOL_ACTIVITY_CAPABILITY)) return "v2";
207
+ if (current === "v1" || capabilities.includes(LEGACY_TOOL_ACTIVITY_CAPABILITY)) return "v1";
208
+ return undefined;
209
+ }
198
210
 
199
211
  const nodeFs: TelegramDaemonFs = {
200
212
  ...(fs.promises as unknown as TelegramDaemonFs),
@@ -3834,7 +3846,7 @@ export interface TelegramDaemonOptions {
3834
3846
  rich?: { enabled: boolean };
3835
3847
  /** Opt-in rich-draft streaming of live turn previews (off by default; see rich-draft.ts). */
3836
3848
  richDraft?: { enabled: boolean };
3837
- /** Tool start/completion messages (enabled by default). */
3849
+ /** Tool start/completion messages (off by default; explicit opt-in only). */
3838
3850
  toolActivity?: { enabled: boolean };
3839
3851
  /**
3840
3852
  * Per-session Telegram forum-topic naming. `nameTemplate` supports the
@@ -3863,6 +3875,8 @@ interface SessionSocket {
3863
3875
  /** True once the server advertised the `client_ping_pong` capability. */
3864
3876
  capable: boolean;
3865
3877
  ephemeralCapable: boolean;
3878
+ /** Tool activity contract advertised by the connected host. */
3879
+ toolActivityCapability?: "v1" | "v2";
3866
3880
  /** Timestamp (via opts.now) of the last received pong; seeds the TTL window. */
3867
3881
  lastPongAt: number;
3868
3882
  /** Nonce of the most recent in-flight ping, if any. */
@@ -3913,6 +3927,19 @@ interface ToolActivityOwner {
3913
3927
  session: SessionSocket;
3914
3928
  phase: "started" | "terminal";
3915
3929
  policyEpoch?: number;
3930
+ summaryFreeSend?: ThreadedSend;
3931
+ }
3932
+ type LegacyToolStartOutcome = "visible" | "failed" | "cancelled" | "terminal";
3933
+ interface LegacyToolStartSettlement {
3934
+ key: string;
3935
+ owner: ToolActivityOwner;
3936
+ policyEpoch: number;
3937
+ phase: "admitted" | "pending_identity" | "queued" | "dispatching" | LegacyToolStartOutcome;
3938
+ itemId?: string;
3939
+ settled: Promise<LegacyToolStartOutcome>;
3940
+ resolve: (outcome: LegacyToolStartOutcome) => void;
3941
+ settledOutcome?: LegacyToolStartOutcome;
3942
+ terminalClaimed?: boolean;
3916
3943
  }
3917
3944
 
3918
3945
  interface PendingThreadedFrame {
@@ -3988,6 +4015,8 @@ interface TelegramQueuePayload {
3988
4015
  selectedAck?: SelectedAckQueueItem;
3989
4016
  btwDelivery?: BtwQueuedDelivery;
3990
4017
  toolActivity?: ToolActivityOwner;
4018
+ /** Exact admitted legacy-v1 start authorized for this terminal delivery. */
4019
+ legacyToolStart?: LegacyToolStartSettlement;
3991
4020
  }
3992
4021
 
3993
4022
  interface PendingBtwTurn {
@@ -4085,6 +4114,9 @@ export class TelegramNotificationDaemon {
4085
4114
  /** Endpoint-bound ownership for visible or dispatching tool bubbles. */
4086
4115
  private readonly toolActivityOwners = new Map<string, ToolActivityOwner>();
4087
4116
  private readonly revokedToolEndpoints = new Set<string>();
4117
+ /** Exact settlement of each admitted legacy-v1 start; retained only while visible. */
4118
+ private readonly legacyToolStarts = new Map<string, LegacyToolStartSettlement>();
4119
+ private nextLegacyToolStartId = 1;
4088
4120
  private readonly unresolvedToolTerminalizations = new Map<string, { messageId: number; owner: ToolActivityOwner }>();
4089
4121
  private toolTerminalizationChain: Promise<void> = Promise.resolve();
4090
4122
  private toolActivityPolicyEpoch = 0;
@@ -4654,6 +4686,10 @@ export class TelegramNotificationDaemon {
4654
4686
  matches: msg => msg.type === "hello",
4655
4687
  handle: (session, msg) => {
4656
4688
  const caps = Array.isArray(msg.capabilities) ? msg.capabilities : [];
4689
+ const previousToolActivityCapability = session.toolActivityCapability;
4690
+ session.toolActivityCapability = negotiateToolActivityCapability(previousToolActivityCapability, caps);
4691
+ if (previousToolActivityCapability === "v1" && session.toolActivityCapability === "v2")
4692
+ this.cleanLegacyToolStartsForCapabilityUpgrade(session);
4657
4693
  if (caps.includes("ephemeral_turn_v1")) session.ephemeralCapable = true;
4658
4694
  if (caps.includes(CLIENT_PING_PONG_CAPABILITY)) {
4659
4695
  session.capable = true;
@@ -5186,6 +5222,7 @@ export class TelegramNotificationDaemon {
5186
5222
  ASK_CONTROLS_CAPABILITY,
5187
5223
  ASK_SELECTED_ACK_CAPABILITY,
5188
5224
  TOOL_ACTIVITY_CAPABILITY,
5225
+ LEGACY_TOOL_ACTIVITY_CAPABILITY,
5189
5226
  "ephemeral_turn_v1",
5190
5227
  ],
5191
5228
  }),
@@ -5279,12 +5316,12 @@ export class TelegramNotificationDaemon {
5279
5316
  for (const item of claimedItems) {
5280
5317
  const { messageId, owner } = item;
5281
5318
  const backlogKey = `${owner.endpointDigest}\0${owner.sessionId}\0${owner.toolCallId}\0${messageId}`;
5282
- const send = renderThreadedFrame({
5319
+ const send = this.renderThreadedFrame({
5283
5320
  type: "tool_activity",
5284
5321
  sessionId: owner.sessionId,
5285
5322
  toolCallId: owner.toolCallId,
5286
5323
  toolName: owner.toolName,
5287
- phase: "unknown",
5324
+ phase: "cancelled",
5288
5325
  });
5289
5326
  if (!send?.text) continue;
5290
5327
  let failure: unknown;
@@ -5342,6 +5379,9 @@ export class TelegramNotificationDaemon {
5342
5379
  this.toolActivityOwners.delete(key);
5343
5380
  const messageId = this.liveMessages.get(key);
5344
5381
  this.liveMessages.delete(key);
5382
+ const legacyStart = this.legacyToolStarts.get(key);
5383
+ if (legacyStart !== undefined && legacyStart.owner === owner)
5384
+ this.settleLegacyToolStart(legacyStart, "terminal");
5345
5385
  if (messageId !== undefined) {
5346
5386
  const backlogKey = `${owner.endpointDigest}\0${owner.sessionId}\0${owner.toolCallId}\0${messageId}`;
5347
5387
  claimedByKey.set(backlogKey, { messageId, owner });
@@ -5369,6 +5409,7 @@ export class TelegramNotificationDaemon {
5369
5409
  this.toolActivityStopping = true;
5370
5410
  this.toolActivityPolicyEpoch++;
5371
5411
  this.toolShutdownBarrier = (async () => {
5412
+ await this.cancelLegacyToolStartsForPolicyTransition();
5372
5413
  await this.flushChain;
5373
5414
  const failures: Error[] = [];
5374
5415
  if (this.toolActivityAmbiguous) {
@@ -5393,6 +5434,7 @@ export class TelegramNotificationDaemon {
5393
5434
  if (session.recoveryLease) session.recoveryLease = { ...session.recoveryLease, state: "rejected" };
5394
5435
  const isCurrentSession = this.sessions.get(session.sessionId) === session;
5395
5436
  if (isCurrentSession) this.droppedSessions.add(session);
5437
+ if (isCurrentSession) this.cancelLegacyToolStartsForSession(session);
5396
5438
  if (isCurrentSession) this.scheduleVisibleToolTerminalization(session.endpointDigest).catch(() => undefined);
5397
5439
  const clearIntervalImpl = this.opts.clearIntervalImpl ?? clearInterval;
5398
5440
  if (session.pingTimer) {
@@ -5923,7 +5965,7 @@ export class TelegramNotificationDaemon {
5923
5965
  typeof msg.sessionId !== "string" ||
5924
5966
  !msg.sessionId.trim() ||
5925
5967
  msg.sessionId === this.#logicalSessionId(session) ||
5926
- (msg.type !== "config_update" && !renderThreadedFrame(msg))
5968
+ (msg.type !== "config_update" && !this.renderThreadedFrame(msg))
5927
5969
  )
5928
5970
  return;
5929
5971
  if (!session.logicalSessionIdTrusted) return;
@@ -6185,12 +6227,151 @@ export class TelegramNotificationDaemon {
6185
6227
  return undefined;
6186
6228
  }
6187
6229
 
6230
+ private admitLegacyToolStart(owner: ToolActivityOwner): LegacyToolStartSettlement | undefined {
6231
+ if (owner.session.toolActivityCapability !== "v1") return undefined;
6232
+ const key = `${owner.sessionId}:tool:${owner.toolCallId}`;
6233
+ if (this.legacyToolStarts.has(key)) return undefined;
6234
+ const deferred = Promise.withResolvers<LegacyToolStartOutcome>();
6235
+ const state: LegacyToolStartSettlement = {
6236
+ key,
6237
+ owner,
6238
+ policyEpoch: owner.policyEpoch ?? this.toolActivityPolicyEpoch,
6239
+ phase: "admitted",
6240
+ settled: deferred.promise,
6241
+ resolve: deferred.resolve,
6242
+ };
6243
+ this.legacyToolStarts.set(key, state);
6244
+ return state;
6245
+ }
6246
+ private legacyToolStartForTerminal(owner: ToolActivityOwner): LegacyToolStartSettlement | undefined {
6247
+ if (owner.phase !== "terminal") return undefined;
6248
+ const key = `${owner.sessionId}:tool:${owner.toolCallId}`;
6249
+ const state = this.legacyToolStarts.get(key);
6250
+ if (
6251
+ state === undefined ||
6252
+ state.key !== key ||
6253
+ state.owner.session !== owner.session ||
6254
+ state.owner.endpointDigest !== owner.endpointDigest ||
6255
+ state.owner.toolName !== owner.toolName ||
6256
+ state.policyEpoch !== owner.policyEpoch
6257
+ )
6258
+ return undefined;
6259
+ return state;
6260
+ }
6261
+
6262
+ private settleLegacyToolStart(state: LegacyToolStartSettlement, outcome: LegacyToolStartOutcome): void {
6263
+ if (state.settledOutcome === undefined) {
6264
+ state.settledOutcome = outcome;
6265
+ state.resolve(outcome);
6266
+ }
6267
+ state.phase = outcome;
6268
+ if (outcome !== "visible" && this.legacyToolStarts.get(state.key) === state)
6269
+ this.legacyToolStarts.delete(state.key);
6270
+ if (outcome !== "visible" && !this.liveMessages.has(state.key)) {
6271
+ const owner = this.toolActivityOwners.get(state.key);
6272
+ if (owner === state.owner) this.toolActivityOwners.delete(state.key);
6273
+ }
6274
+ }
6275
+
6276
+ private cancelUnsentLegacyToolStart(state: LegacyToolStartSettlement): boolean {
6277
+ if (state.phase !== "admitted" && state.phase !== "pending_identity" && state.phase !== "queued") return false;
6278
+ if (state.itemId) this.pool.removeById(state.itemId);
6279
+ for (const [sessionId, frames] of this.pendingThreadedFrames) {
6280
+ const retained = frames.filter(frame => frame.toolActivity !== state.owner);
6281
+ if (retained.length === 0) this.pendingThreadedFrames.delete(sessionId);
6282
+ else if (retained.length !== frames.length) this.pendingThreadedFrames.set(sessionId, retained);
6283
+ }
6284
+ this.settleLegacyToolStart(state, "cancelled");
6285
+ return true;
6286
+ }
6287
+
6288
+ private failLegacyToolStart(toolActivity: ToolActivityOwner | undefined): void {
6289
+ if (toolActivity?.phase !== "started") return;
6290
+ const state = this.legacyToolStarts.get(`${toolActivity.sessionId}:tool:${toolActivity.toolCallId}`);
6291
+ if (state !== undefined && state.owner === toolActivity && state.phase !== "visible")
6292
+ this.settleLegacyToolStart(state, "failed");
6293
+ }
6294
+
6295
+ private settleRejectedLegacyToolSubmission(
6296
+ toolActivity: ToolActivityOwner | undefined,
6297
+ legacyToolStart: LegacyToolStartSettlement | undefined,
6298
+ ): void {
6299
+ if (toolActivity?.phase === "started") {
6300
+ this.failLegacyToolStart(toolActivity);
6301
+ return;
6302
+ }
6303
+ if (
6304
+ toolActivity?.phase !== "terminal" ||
6305
+ legacyToolStart === undefined ||
6306
+ this.legacyToolStarts.get(legacyToolStart.key) !== legacyToolStart
6307
+ )
6308
+ return;
6309
+ this.settleLegacyToolStart(legacyToolStart, "terminal");
6310
+ this.liveMessages.delete(legacyToolStart.key);
6311
+ if (this.toolActivityOwners.get(legacyToolStart.key) === legacyToolStart.owner)
6312
+ this.toolActivityOwners.delete(legacyToolStart.key);
6313
+ }
6314
+
6315
+ private cancelLegacyToolStartsForSession(session: SessionSocket): void {
6316
+ for (const state of [...this.legacyToolStarts.values()]) {
6317
+ if (state.owner.session !== session) continue;
6318
+ if (!this.cancelUnsentLegacyToolStart(state)) this.settleLegacyToolStart(state, "cancelled");
6319
+ }
6320
+ }
6321
+
6322
+ private cleanLegacyToolStartsForCapabilityUpgrade(session: SessionSocket): void {
6323
+ for (const state of [...this.legacyToolStarts.values()]) {
6324
+ if (state.owner.session !== session || state.phase === "dispatching") continue;
6325
+ if (state.phase === "visible") this.settleLegacyToolStart(state, "terminal");
6326
+ else this.cancelUnsentLegacyToolStart(state);
6327
+ }
6328
+ }
6329
+ private cancelLegacyToolStartsForPolicyTransition(): Promise<LegacyToolStartOutcome[]> {
6330
+ const dispatching: Promise<LegacyToolStartOutcome>[] = [];
6331
+ const visibleTerminalizations: Array<{ messageId: number; owner: ToolActivityOwner }> = [];
6332
+ for (const state of [...this.legacyToolStarts.values()]) {
6333
+ if (state.phase === "dispatching")
6334
+ dispatching.push(
6335
+ state.settled.then(outcome => {
6336
+ if (outcome === "visible" && this.legacyToolStarts.get(state.key) === state) {
6337
+ const messageId = this.liveMessages.get(state.key);
6338
+ this.liveMessages.delete(state.key);
6339
+ if (this.toolActivityOwners.get(state.key) === state.owner)
6340
+ this.toolActivityOwners.delete(state.key);
6341
+ this.settleLegacyToolStart(state, "terminal");
6342
+ if (messageId !== undefined) {
6343
+ void this.enqueueToolTerminalization([{ messageId, owner: state.owner }], false);
6344
+ }
6345
+ }
6346
+ return outcome;
6347
+ }),
6348
+ );
6349
+ else if (state.phase === "visible") {
6350
+ const messageId = this.liveMessages.get(state.key);
6351
+ this.liveMessages.delete(state.key);
6352
+ if (this.toolActivityOwners.get(state.key) === state.owner) this.toolActivityOwners.delete(state.key);
6353
+ this.settleLegacyToolStart(state, "terminal");
6354
+ if (messageId !== undefined) {
6355
+ visibleTerminalizations.push({ messageId, owner: state.owner });
6356
+ }
6357
+ } else this.cancelUnsentLegacyToolStart(state);
6358
+ }
6359
+ if (visibleTerminalizations.length > 0) {
6360
+ void this.enqueueToolTerminalization(visibleTerminalizations, false);
6361
+ }
6362
+ return Promise.all(dispatching);
6363
+ }
6188
6364
  private toolActivityOwner(session: SessionSocket, msg: Record<string, unknown>): ToolActivityOwner | undefined {
6189
6365
  if (msg.type !== "tool_activity") return undefined;
6190
6366
  const toolCallId = typeof msg.toolCallId === "string" ? msg.toolCallId : undefined;
6191
6367
  const toolName = typeof msg.toolName === "string" ? msg.toolName : undefined;
6192
6368
  const phase = typeof msg.phase === "string" ? msg.phase : undefined;
6193
- if (!toolCallId || !toolName || !phase) return undefined;
6369
+ if (
6370
+ !toolCallId ||
6371
+ !toolName ||
6372
+ (phase !== "started" && phase !== "completed" && phase !== "failed" && phase !== "cancelled")
6373
+ )
6374
+ return undefined;
6194
6375
  return {
6195
6376
  sessionId: this.#logicalSessionId(session),
6196
6377
  toolCallId,
@@ -6200,6 +6381,30 @@ export class TelegramNotificationDaemon {
6200
6381
  phase: phase === "started" ? "started" : "terminal",
6201
6382
  };
6202
6383
  }
6384
+ private renderThreadedFrame(frame: Record<string, unknown>): ThreadedSend | undefined {
6385
+ if (
6386
+ frame.type === "tool_activity" &&
6387
+ frame.phase !== "started" &&
6388
+ frame.phase !== "completed" &&
6389
+ frame.phase !== "failed" &&
6390
+ frame.phase !== "cancelled"
6391
+ )
6392
+ return undefined;
6393
+ return renderThreadedFrame(frame);
6394
+ }
6395
+ private toolActivityFrameWithoutSummaries(frame: Record<string, unknown>): Record<string, unknown> {
6396
+ const summaryFree = { ...frame };
6397
+ delete summaryFree.argsSummary;
6398
+ delete summaryFree.resultSummary;
6399
+ return summaryFree;
6400
+ }
6401
+ private toolActivitySummariesAreCurrent(toolActivity: ToolActivityOwner): boolean {
6402
+ return (
6403
+ this.opts.toolActivity?.enabled === true &&
6404
+ toolActivity.policyEpoch === this.toolActivityPolicyEpoch &&
6405
+ this.toolActivityAuthorityIsCurrent(toolActivity)
6406
+ );
6407
+ }
6203
6408
 
6204
6409
  private toolActivityAuthorityIsCurrent(toolActivity: ToolActivityOwner): boolean {
6205
6410
  if (this.revokedToolEndpoints.has(toolActivity.endpointDigest)) return false;
@@ -6208,6 +6413,34 @@ export class TelegramNotificationDaemon {
6208
6413
  if (toolActivity.endpointDigest === undefined) return session === undefined;
6209
6414
  return session === toolActivity.session && session.endpointDigest === toolActivity.endpointDigest;
6210
6415
  }
6416
+ private toolActivityDeliveryIsCurrent(toolActivity: ToolActivityOwner): boolean {
6417
+ const key = `${toolActivity.sessionId}:tool:${toolActivity.toolCallId}`;
6418
+ const owner = this.toolActivityOwners.get(key);
6419
+ if (owner && owner.session !== toolActivity.session) return false;
6420
+ if (toolActivity.phase === "started") {
6421
+ const legacyStart =
6422
+ toolActivity.session.toolActivityCapability === "v1" ? this.legacyToolStarts.get(key) : undefined;
6423
+ return (
6424
+ (toolActivity.session.toolActivityCapability !== "v1" ||
6425
+ (legacyStart !== undefined &&
6426
+ legacyStart.owner === toolActivity &&
6427
+ legacyStart.phase !== "cancelled" &&
6428
+ legacyStart.phase !== "failed" &&
6429
+ legacyStart.phase !== "terminal")) &&
6430
+ this.toolActivityAuthorityIsCurrent(toolActivity) &&
6431
+ !this.toolActivityStopping &&
6432
+ this.opts.toolActivity?.enabled === true &&
6433
+ toolActivity.policyEpoch === this.toolActivityPolicyEpoch
6434
+ );
6435
+ }
6436
+ if (
6437
+ this.toolActivityAuthorityIsCurrent(toolActivity) &&
6438
+ this.opts.toolActivity?.enabled === true &&
6439
+ toolActivity.policyEpoch === this.toolActivityPolicyEpoch
6440
+ )
6441
+ return true;
6442
+ return this.liveMessages.has(key) && owner?.session === toolActivity.session;
6443
+ }
6211
6444
  private async submitThreadedFrame(
6212
6445
  sessionId: string,
6213
6446
  send: ThreadedSend,
@@ -6215,17 +6448,45 @@ export class TelegramNotificationDaemon {
6215
6448
  toolActivity?: ToolActivityOwner,
6216
6449
  socketLease?: { session: SessionSocket; token: number; logicalSessionId: string },
6217
6450
  ): Promise<void> {
6218
- this.submitPool({
6451
+ const legacyStart =
6452
+ toolActivity?.phase === "started"
6453
+ ? this.legacyToolStarts.get(`${toolActivity.sessionId}:tool:${toolActivity.toolCallId}`)
6454
+ : toolActivity?.phase === "terminal"
6455
+ ? this.legacyToolStartForTerminal(toolActivity)
6456
+ : undefined;
6457
+ if (legacyStart !== undefined && legacyStart.owner === toolActivity) {
6458
+ if (toolActivity!.phase === "started") {
6459
+ legacyStart.phase = "queued";
6460
+ legacyStart.itemId ??= `legacy-tool-start:${this.nextLegacyToolStartId++}`;
6461
+ }
6462
+ }
6463
+ // Terminal frames get a distinct pool item identity so they cannot collide
6464
+ // with an in-flight start for the same legacy settlement.
6465
+ const poolItemId =
6466
+ legacyStart !== undefined && legacyStart.owner === toolActivity
6467
+ ? toolActivity!.phase === "terminal"
6468
+ ? `legacy-tool-terminal:${this.nextLegacyToolStartId++}`
6469
+ : legacyStart.itemId
6470
+ : undefined;
6471
+ const submitted = this.submitPool({
6219
6472
  sessionId,
6220
6473
  lane: send.lane,
6221
6474
  coalesceKey: send.coalesceKey,
6475
+ ...(poolItemId !== undefined ? { itemId: poolItemId } : {}),
6222
6476
  payload: {
6223
6477
  send,
6224
6478
  topicLease,
6225
6479
  ...(socketLease ? { socketLease } : {}),
6226
6480
  ...(toolActivity ? { toolActivity } : {}),
6481
+ ...(legacyStart !== undefined && toolActivity?.phase === "terminal"
6482
+ ? { legacyToolStart: legacyStart }
6483
+ : {}),
6227
6484
  },
6228
6485
  });
6486
+ if (!submitted) {
6487
+ this.settleRejectedLegacyToolSubmission(toolActivity, legacyStart);
6488
+ return;
6489
+ }
6229
6490
  await this.flushPool();
6230
6491
  }
6231
6492
 
@@ -6295,8 +6556,16 @@ export class TelegramNotificationDaemon {
6295
6556
  ): void {
6296
6557
  const logicalSessionId = this.#logicalSessionId(session);
6297
6558
  const socketLease = this.#socketLease(session, logicalSessionId);
6298
- if (!socketLease) return;
6559
+ if (!socketLease) {
6560
+ this.failLegacyToolStart(toolActivity);
6561
+ return;
6562
+ }
6299
6563
  const frames = this.pendingThreadedFrames.get(logicalSessionId) ?? [];
6564
+ const legacyStart =
6565
+ toolActivity?.phase === "started"
6566
+ ? this.legacyToolStarts.get(`${toolActivity.sessionId}:tool:${toolActivity.toolCallId}`)
6567
+ : undefined;
6568
+ if (legacyStart !== undefined && legacyStart.owner === toolActivity) legacyStart.phase = "pending_identity";
6300
6569
  frames.push({
6301
6570
  send,
6302
6571
  msg,
@@ -6304,7 +6573,10 @@ export class TelegramNotificationDaemon {
6304
6573
  socketLease,
6305
6574
  ...(toolActivity ? { toolActivity } : {}),
6306
6575
  });
6307
- if (frames.length > PENDING_TOPIC_FRAME_LIMIT) frames.shift();
6576
+ if (frames.length > PENDING_TOPIC_FRAME_LIMIT) {
6577
+ const evicted = frames.shift();
6578
+ this.failLegacyToolStart(evicted?.toolActivity);
6579
+ }
6308
6580
  this.pendingThreadedFrames.set(logicalSessionId, frames);
6309
6581
  }
6310
6582
 
@@ -6313,8 +6585,14 @@ export class TelegramNotificationDaemon {
6313
6585
  if (!frames || frames.length === 0) return;
6314
6586
  this.pendingThreadedFrames.delete(sessionId);
6315
6587
  for (const frame of frames) {
6316
- if (frame.logicalSessionId !== sessionId || !this.#leaseTokenAllows(frame.socketLease)) continue;
6317
- if (frame.msg.type === "tool_activity" && this.opts.toolActivity?.enabled === false) continue;
6588
+ if (
6589
+ frame.logicalSessionId !== sessionId ||
6590
+ !this.#leaseTokenAllows(frame.socketLease) ||
6591
+ (frame.msg.type === "tool_activity" && this.opts.toolActivity?.enabled !== true)
6592
+ ) {
6593
+ this.failLegacyToolStart(frame.toolActivity);
6594
+ continue;
6595
+ }
6318
6596
  await this.submitThreadedFrame(sessionId, frame.send, topicLease, frame.toolActivity, frame.socketLease);
6319
6597
  }
6320
6598
  }
@@ -6937,6 +7215,7 @@ export class TelegramNotificationDaemon {
6937
7215
  this.finishSelectedAck(expiredItem.payload.selectedAck, { status: "failed", reason: "expired" });
6938
7216
  }
6939
7217
  expiredItem.payload.btwDelivery?.finish("not_delivered");
7218
+ this.failLegacyToolStart(expiredItem.payload.toolActivity);
6940
7219
  }
6941
7220
  // Within a batch a finalized frame supersedes any still-queued live frame for
6942
7221
  // the same streamed message (finalized outranks live), so drop the stale live
@@ -6961,17 +7240,18 @@ export class TelegramNotificationDaemon {
6961
7240
  }
6962
7241
  for (const item of batch) {
6963
7242
  const toolActivity = item.payload.toolActivity;
6964
- if (
6965
- toolActivity?.phase === "started" &&
6966
- (this.toolActivityStopping ||
6967
- this.opts.toolActivity?.enabled === false ||
6968
- toolActivity.policyEpoch !== this.toolActivityPolicyEpoch ||
6969
- !this.toolActivityAuthorityIsCurrent(toolActivity))
6970
- ) {
7243
+ const legacyStart =
7244
+ toolActivity?.phase === "started"
7245
+ ? this.legacyToolStarts.get(`${toolActivity.sessionId}:tool:${toolActivity.toolCallId}`)
7246
+ : undefined;
7247
+ if (legacyStart !== undefined && legacyStart.owner === toolActivity && legacyStart.phase === "queued")
7248
+ legacyStart.phase = "dispatching";
7249
+ if (toolActivity && !this.toolActivityDeliveryIsCurrent(toolActivity)) {
6971
7250
  const key = `${toolActivity.sessionId}:tool:${toolActivity.toolCallId}`;
6972
7251
  const owner = this.toolActivityOwners.get(key);
6973
7252
  if (!this.liveMessages.has(key) && owner?.session === toolActivity.session)
6974
7253
  this.toolActivityOwners.delete(key);
7254
+ this.failLegacyToolStart(toolActivity);
6975
7255
  this.pool.settle(item.itemId!, "removed");
6976
7256
  continue;
6977
7257
  }
@@ -7102,17 +7382,20 @@ export class TelegramNotificationDaemon {
7102
7382
  }
7103
7383
  continue;
7104
7384
  }
7105
- const { send, topicLease, socketLease } = item.payload;
7385
+ let { send } = item.payload;
7386
+ const { topicLease, socketLease } = item.payload;
7106
7387
  if (
7107
7388
  (socketLease && !this.#leaseTokenAllows(socketLease)) ||
7108
7389
  (topicLease && !this.topicLeaseIsCurrent(topicLease))
7109
7390
  ) {
7110
7391
  this.pool.settle(item.itemId!, "rejected");
7392
+ this.failLegacyToolStart(toolActivity);
7111
7393
  continue;
7112
7394
  }
7113
7395
  const topicId = topicLease?.topicId;
7114
7396
  if (topicId && !(await this.pairedChatIsPrivate())) {
7115
7397
  this.pool.settle(item.itemId!, "rejected");
7398
+ this.failLegacyToolStart(toolActivity);
7116
7399
  continue;
7117
7400
  }
7118
7401
  if (
@@ -7120,12 +7403,20 @@ export class TelegramNotificationDaemon {
7120
7403
  (topicLease && !this.topicLeaseIsCurrent(topicLease))
7121
7404
  ) {
7122
7405
  this.pool.settle(item.itemId!, "rejected");
7406
+ this.failLegacyToolStart(toolActivity);
7123
7407
  continue;
7124
7408
  }
7125
7409
  if (item.payload.toolActivity && !this.toolActivityAuthorityIsCurrent(item.payload.toolActivity)) {
7126
7410
  this.pool.settle(item.itemId!, "removed");
7411
+ this.failLegacyToolStart(toolActivity);
7127
7412
  continue;
7128
7413
  }
7414
+ if (
7415
+ toolActivity?.phase === "terminal" &&
7416
+ !this.toolActivitySummariesAreCurrent(toolActivity) &&
7417
+ toolActivity.summaryFreeSend
7418
+ )
7419
+ send = toolActivity.summaryFreeSend;
7129
7420
  // Threaded topic when available; otherwise deliver flat to the paired chat.
7130
7421
  const threadField = topicId ? { message_thread_id: Number(topicId) } : {};
7131
7422
  const ckey = send.editable ? item.coalesceKey : undefined;
@@ -7362,23 +7653,74 @@ export class TelegramNotificationDaemon {
7362
7653
  const rejected = itemOutcomes.some(outcome => outcome.kind === "rejected");
7363
7654
  if (retryable) {
7364
7655
  if (!accepted) {
7365
- this.submitPool({
7366
- sessionId: item.sessionId,
7367
- lane: item.lane,
7368
- coalesceKey: item.coalesceKey,
7369
- deadlineAt: item.deadlineAt,
7370
- payload: item.payload,
7371
- });
7656
+ let retryItemId: string | undefined;
7657
+ let retryAllowed = true;
7658
+ if (toolActivity?.phase === "started") {
7659
+ retryAllowed =
7660
+ !this.toolActivityStopping &&
7661
+ !this.stopRequested &&
7662
+ this.opts.toolActivity?.enabled === true &&
7663
+ toolActivity.policyEpoch === this.toolActivityPolicyEpoch &&
7664
+ this.toolActivityAuthorityIsCurrent(toolActivity);
7665
+ if (toolActivity.session.toolActivityCapability === "v1") {
7666
+ const state = this.legacyToolStarts.get(
7667
+ `${toolActivity.sessionId}:tool:${toolActivity.toolCallId}`,
7668
+ );
7669
+ retryAllowed =
7670
+ retryAllowed &&
7671
+ state !== undefined &&
7672
+ state.owner === toolActivity &&
7673
+ !state.terminalClaimed;
7674
+ if (state !== undefined && state.owner === toolActivity) {
7675
+ if (retryAllowed) {
7676
+ state.phase = "queued";
7677
+ state.itemId = `legacy-tool-start:${this.nextLegacyToolStartId++}`;
7678
+ retryItemId = state.itemId;
7679
+ } else {
7680
+ this.settleLegacyToolStart(state, "cancelled");
7681
+ }
7682
+ }
7683
+ } else if (retryAllowed) {
7684
+ retryItemId = `tool-activity-start:${this.nextLegacyToolStartId++}`;
7685
+ }
7686
+ } else if (toolActivity?.phase === "terminal" && item.payload.legacyToolStart !== undefined) {
7687
+ retryItemId = `legacy-tool-terminal:${this.nextLegacyToolStartId++}`;
7688
+ }
7689
+ const newerCoalescedItem =
7690
+ toolActivity === undefined &&
7691
+ item.coalesceKey !== undefined &&
7692
+ this.pool.someQueued(
7693
+ queued =>
7694
+ queued.sessionId === item.sessionId &&
7695
+ queued.lane === item.lane &&
7696
+ queued.coalesceKey === item.coalesceKey,
7697
+ );
7698
+ if (retryAllowed && !newerCoalescedItem) {
7699
+ this.submitPool({
7700
+ sessionId: item.sessionId,
7701
+ lane: item.lane,
7702
+ coalesceKey: item.coalesceKey,
7703
+ deadlineAt: item.deadlineAt,
7704
+ ...(retryItemId !== undefined ? { itemId: retryItemId } : {}),
7705
+ payload: item.payload,
7706
+ });
7707
+ }
7372
7708
  }
7373
7709
  disposition = "ambiguous";
7374
7710
  } else if (disposition === "accepted" && !accepted && rejected) {
7375
7711
  disposition = "rejected";
7376
7712
  }
7377
7713
  this.pool.settle(item.itemId!, disposition);
7714
+ if (toolActivity?.phase === "started" && !retryable) this.failLegacyToolStart(toolActivity);
7715
+ if (toolActivity?.phase === "terminal" && !retryable) {
7716
+ const state = item.payload.legacyToolStart;
7717
+ if (state !== undefined && this.legacyToolStarts.get(state.key) === state)
7718
+ this.settleLegacyToolStart(state, "terminal");
7719
+ }
7378
7720
  // A terminal tool frame owns the end of this coalescing key even when both
7379
7721
  // edit and fallback delivery fail. Retaining the old message id would leak
7380
7722
  // one entry per failure and let a later reused key edit stale Telegram state.
7381
- if (send.terminal && editKey) {
7723
+ if (send.terminal && editKey && !retryable) {
7382
7724
  this.liveMessages.delete(editKey);
7383
7725
  const owner = this.toolActivityOwners.get(editKey);
7384
7726
  if (!item.payload.toolActivity || owner?.session === item.payload.toolActivity.session)
@@ -7414,6 +7756,18 @@ export class TelegramNotificationDaemon {
7414
7756
  }
7415
7757
  }
7416
7758
  if (toolActivity) {
7759
+ const legacyStart = this.legacyToolStarts.get(mapKey);
7760
+ if (
7761
+ legacyStart !== undefined &&
7762
+ legacyStart.owner === toolActivity &&
7763
+ (toolActivity.session.toolActivityCapability !== "v1" ||
7764
+ legacyStart.policyEpoch !== this.toolActivityPolicyEpoch ||
7765
+ !this.toolActivityAuthorityIsCurrent(toolActivity))
7766
+ ) {
7767
+ this.settleLegacyToolStart(legacyStart, "terminal");
7768
+ void this.enqueueToolTerminalization([{ messageId, owner: toolActivity }], false);
7769
+ return;
7770
+ }
7417
7771
  const owner = this.toolActivityOwners.get(mapKey);
7418
7772
  if (this.revokedToolEndpoints.has(toolActivity.endpointDigest) || owner?.session !== toolActivity.session) {
7419
7773
  void this.enqueueToolTerminalization([{ messageId, owner: toolActivity }], false);
@@ -7422,6 +7776,10 @@ export class TelegramNotificationDaemon {
7422
7776
  }
7423
7777
  this.liveMessages.set(mapKey, messageId);
7424
7778
  if (toolActivity) this.toolActivityOwners.set(mapKey, toolActivity);
7779
+ if (toolActivity) {
7780
+ const state = this.legacyToolStarts.get(mapKey);
7781
+ if (state !== undefined && state.owner === toolActivity) this.settleLegacyToolStart(state, "visible");
7782
+ }
7425
7783
  }
7426
7784
 
7427
7785
  /**
@@ -7440,16 +7798,50 @@ export class TelegramNotificationDaemon {
7440
7798
  socketLease?: { session: SessionSocket; token: number; logicalSessionId: string },
7441
7799
  ): Promise<void> {
7442
7800
  if ((socketLease && !this.#leaseTokenAllows(socketLease)) || !(await this.pairedChatIsPrivate())) return;
7801
+ if (toolActivity && !this.toolActivityDeliveryIsCurrent(toolActivity)) return;
7443
7802
  if (socketLease && !this.#leaseTokenAllows(socketLease)) return;
7444
7803
  await this.notifyThreadedFallback(socketLease);
7804
+ if (toolActivity && !this.toolActivityDeliveryIsCurrent(toolActivity)) return;
7445
7805
  if (socketLease && !this.#leaseTokenAllows(socketLease)) return;
7806
+ if (toolActivity && !this.toolActivityDeliveryIsCurrent(toolActivity)) return;
7446
7807
  if (send.identity && this.flatIdentitySent.has(sessionId)) return;
7447
- this.submitPool({
7808
+ if (toolActivity && !this.toolActivityDeliveryIsCurrent(toolActivity)) return;
7809
+ const legacyToolStart =
7810
+ toolActivity?.phase === "started"
7811
+ ? this.legacyToolStarts.get(`${toolActivity.sessionId}:tool:${toolActivity.toolCallId}`)
7812
+ : toolActivity?.phase === "terminal"
7813
+ ? this.legacyToolStartForTerminal(toolActivity)
7814
+ : undefined;
7815
+ if (
7816
+ legacyToolStart !== undefined &&
7817
+ legacyToolStart.owner === toolActivity &&
7818
+ toolActivity?.phase === "started"
7819
+ ) {
7820
+ legacyToolStart.phase = "queued";
7821
+ legacyToolStart.itemId ??= `legacy-tool-start:${this.nextLegacyToolStartId++}`;
7822
+ }
7823
+ const poolItemId =
7824
+ legacyToolStart !== undefined && legacyToolStart.owner === toolActivity
7825
+ ? toolActivity!.phase === "terminal"
7826
+ ? `legacy-tool-terminal:${this.nextLegacyToolStartId++}`
7827
+ : legacyToolStart.itemId
7828
+ : undefined;
7829
+ const submitted = this.submitPool({
7448
7830
  sessionId,
7449
7831
  lane: send.lane,
7450
7832
  coalesceKey: send.coalesceKey,
7451
- payload: { send, ...(socketLease ? { socketLease } : {}), ...(toolActivity ? { toolActivity } : {}) },
7833
+ ...(poolItemId !== undefined ? { itemId: poolItemId } : {}),
7834
+ payload: {
7835
+ send,
7836
+ ...(socketLease ? { socketLease } : {}),
7837
+ ...(toolActivity ? { toolActivity } : {}),
7838
+ ...(legacyToolStart !== undefined && toolActivity?.phase === "terminal" ? { legacyToolStart } : {}),
7839
+ },
7452
7840
  });
7841
+ if (!submitted) {
7842
+ this.settleRejectedLegacyToolSubmission(toolActivity, legacyToolStart);
7843
+ return;
7844
+ }
7453
7845
  await this.flushPool();
7454
7846
  if (socketLease && !this.#leaseTokenAllows(socketLease)) return;
7455
7847
  if (send.identity) this.flatIdentitySent.add(sessionId);
@@ -7708,6 +8100,10 @@ export class TelegramNotificationDaemon {
7708
8100
  async handleSessionMessage(session: SessionSocket, msg: any): Promise<void> {
7709
8101
  if (msg?.type === "hello") {
7710
8102
  const capabilities = Array.isArray(msg.capabilities) ? msg.capabilities : [];
8103
+ const previousToolActivityCapability = session.toolActivityCapability;
8104
+ session.toolActivityCapability = negotiateToolActivityCapability(previousToolActivityCapability, capabilities);
8105
+ if (previousToolActivityCapability === "v1" && session.toolActivityCapability === "v2")
8106
+ this.cleanLegacyToolStartsForCapabilityUpgrade(session);
7711
8107
  if (capabilities.includes("ephemeral_turn_v1")) {
7712
8108
  session.ephemeralCapable = true;
7713
8109
  this.#resumeBtwTurnsForSession(session);
@@ -7719,7 +8115,7 @@ export class TelegramNotificationDaemon {
7719
8115
  return;
7720
8116
  }
7721
8117
  if (session.replayPending) {
7722
- if (msg?.type === "tool_activity" && (this.opts.toolActivity?.enabled === false || this.toolActivityStopping))
8118
+ if (msg?.type === "tool_activity" && (this.opts.toolActivity?.enabled !== true || this.toolActivityStopping))
7723
8119
  return;
7724
8120
  const matchingReplay = msg?.type === "event_replay_result" && msg.id === session.replayId;
7725
8121
  if (!matchingReplay) {
@@ -8073,34 +8469,76 @@ export class TelegramNotificationDaemon {
8073
8469
  return;
8074
8470
  }
8075
8471
  if (typeof msg?.type === "string" && TelegramNotificationDaemon.THREADED_FRAMES.has(msg.type)) {
8076
- const threadedFrame = msg as Record<string, unknown>;
8472
+ let threadedFrame = msg as Record<string, unknown>;
8473
+ let legacyUnknownStart: LegacyToolStartSettlement | undefined;
8474
+ if (threadedFrame.type === "tool_activity" && threadedFrame.phase === "unknown") {
8475
+ const toolCallId = typeof threadedFrame.toolCallId === "string" ? threadedFrame.toolCallId : undefined;
8476
+ const toolName = typeof threadedFrame.toolName === "string" ? threadedFrame.toolName : undefined;
8477
+ const liveKey = toolCallId ? `${this.#logicalSessionId(session)}:tool:${toolCallId}` : undefined;
8478
+ const state = liveKey ? this.legacyToolStarts.get(liveKey) : undefined;
8479
+ if (
8480
+ session.toolActivityCapability !== "v1" ||
8481
+ !state ||
8482
+ state.owner.session !== session ||
8483
+ state.owner.toolName !== toolName ||
8484
+ state.terminalClaimed
8485
+ )
8486
+ return;
8487
+ state.terminalClaimed = true;
8488
+ if (this.cancelUnsentLegacyToolStart(state)) return;
8489
+ if (state.phase === "dispatching") await state.settled;
8490
+ if (
8491
+ state.settledOutcome !== "visible" ||
8492
+ this.legacyToolStarts.get(state.key) !== state ||
8493
+ session.toolActivityCapability !== "v1" ||
8494
+ state.owner.session !== session ||
8495
+ state.policyEpoch !== this.toolActivityPolicyEpoch ||
8496
+ !this.toolActivityAuthorityIsCurrent(state.owner) ||
8497
+ this.toolActivityOwners.get(state.key) !== state.owner ||
8498
+ !this.liveMessages.has(state.key)
8499
+ )
8500
+ return;
8501
+ legacyUnknownStart = state;
8502
+ threadedFrame = { ...this.toolActivityFrameWithoutSummaries(threadedFrame), phase: "cancelled" };
8503
+ }
8077
8504
  const toolActivity = this.toolActivityOwner(session, threadedFrame);
8505
+ if (threadedFrame.type === "tool_activity" && !toolActivity) return;
8078
8506
  const toolAdmissionEpoch = toolActivity
8079
8507
  ? (this.replayToolActivityEpochs.get(threadedFrame) ?? this.toolActivityPolicyEpoch)
8080
8508
  : undefined;
8081
8509
  if (toolActivity) toolActivity.policyEpoch = toolAdmissionEpoch;
8082
- const toolStartIsCurrent = (): boolean =>
8083
- toolActivity?.phase !== "started" ||
8084
- (this.toolActivityAuthorityIsCurrent(toolActivity) &&
8085
- !this.toolActivityStopping &&
8086
- this.opts.toolActivity?.enabled !== false &&
8087
- toolAdmissionEpoch === this.toolActivityPolicyEpoch);
8510
+ if (toolActivity?.phase === "started" && toolActivity.session.toolActivityCapability === "v1") {
8511
+ const state = this.admitLegacyToolStart(toolActivity);
8512
+ if (!state) return;
8513
+ }
8514
+ if (legacyUnknownStart && toolActivity?.phase === "terminal") {
8515
+ if (
8516
+ legacyUnknownStart.owner.session !== toolActivity.session ||
8517
+ legacyUnknownStart.key !== `${toolActivity.sessionId}:tool:${toolActivity.toolCallId}`
8518
+ )
8519
+ return;
8520
+ }
8521
+ const toolFrameIsCurrent = (): boolean => !toolActivity || this.toolActivityDeliveryIsCurrent(toolActivity);
8088
8522
  const abandonStaleToolStart = (): void => {
8089
8523
  if (toolActivity?.phase !== "started") return;
8090
8524
  const key = `${toolActivity.sessionId}:tool:${toolActivity.toolCallId}`;
8091
8525
  const owner = this.toolActivityOwners.get(key);
8092
8526
  if (!this.liveMessages.has(key) && owner?.session === toolActivity.session)
8093
8527
  this.toolActivityOwners.delete(key);
8528
+ this.failLegacyToolStart(toolActivity);
8094
8529
  };
8095
8530
  if (toolActivity) {
8096
8531
  const liveKey = `${toolActivity.sessionId}:tool:${toolActivity.toolCallId}`;
8097
8532
  const currentOwner = this.toolActivityOwners.get(liveKey);
8098
8533
  if (toolActivity.phase === "started") {
8099
- if (!toolStartIsCurrent()) return;
8534
+ if (!toolFrameIsCurrent()) {
8535
+ this.failLegacyToolStart(toolActivity);
8536
+ return;
8537
+ }
8100
8538
  this.toolActivityOwners.set(liveKey, toolActivity);
8101
8539
  } else {
8102
8540
  if (currentOwner && currentOwner.session !== session) return;
8103
- if (this.opts.toolActivity?.enabled === false) {
8541
+ if (this.opts.toolActivity?.enabled !== true) {
8104
8542
  if (!currentOwner) return;
8105
8543
  if (!this.liveMessages.has(liveKey)) {
8106
8544
  // A start may already be granted to the serialized dispatcher but not
@@ -8116,8 +8554,21 @@ export class TelegramNotificationDaemon {
8116
8554
  }
8117
8555
  }
8118
8556
  }
8119
- const send = renderThreadedFrame(msg);
8120
- if (!send) return;
8557
+ const summaryFreeToolFrame =
8558
+ toolActivity?.phase === "terminal" ? this.toolActivityFrameWithoutSummaries(threadedFrame) : threadedFrame;
8559
+ const renderedFrame =
8560
+ toolActivity?.phase === "terminal" && !this.toolActivitySummariesAreCurrent(toolActivity)
8561
+ ? summaryFreeToolFrame
8562
+ : threadedFrame;
8563
+ const send = this.renderThreadedFrame(renderedFrame);
8564
+ if (toolActivity?.phase === "terminal") {
8565
+ const summaryFreeSend = this.renderThreadedFrame(summaryFreeToolFrame);
8566
+ if (summaryFreeSend) toolActivity.summaryFreeSend = summaryFreeSend;
8567
+ }
8568
+ if (!send) {
8569
+ this.failLegacyToolStart(toolActivity);
8570
+ return;
8571
+ }
8121
8572
  const transportLogicalSessionId = this.#logicalSessionId(session);
8122
8573
  // Preserve legacy identity routing for direct/non-replay session callers.
8123
8574
  // Authenticated transports never infer topic ownership from display identity:
@@ -8126,19 +8577,27 @@ export class TelegramNotificationDaemon {
8126
8577
  send.identity && !session.logicalSessionIdTrusted && this.sessions.get(session.sessionId) !== session
8127
8578
  ? (this.topicOwnerForIdentity(msg) ?? transportLogicalSessionId)
8128
8579
  : transportLogicalSessionId;
8129
- if (!this.#leaseAllows(session, logicalSessionId)) return;
8580
+ if (!this.#leaseAllows(session, logicalSessionId)) {
8581
+ this.failLegacyToolStart(toolActivity);
8582
+ return;
8583
+ }
8130
8584
  const socketLease = this.#socketLease(session, logicalSessionId);
8131
- if (!socketLease) return;
8585
+ if (!socketLease) {
8586
+ this.failLegacyToolStart(toolActivity);
8587
+ return;
8588
+ }
8132
8589
  const existingTopic = await this.existingTopicForPrivateChat(logicalSessionId);
8133
- if (!toolStartIsCurrent()) {
8590
+ if (!toolFrameIsCurrent()) {
8134
8591
  abandonStaleToolStart();
8135
8592
  return;
8136
8593
  }
8137
8594
  if (
8138
8595
  this.topics.get(logicalSessionId)?.authorityState === "delete_pending" ||
8139
8596
  this.topics.get(logicalSessionId)?.bindingMalformed
8140
- )
8597
+ ) {
8598
+ this.failLegacyToolStart(toolActivity);
8141
8599
  return;
8600
+ }
8142
8601
  if (!send.identity && !existingTopic && !this.flatIdentitySent.has(logicalSessionId)) {
8143
8602
  this.rememberPendingThreadedFrame(session, send, threadedFrame, toolActivity);
8144
8603
  return;
@@ -8151,8 +8610,14 @@ export class TelegramNotificationDaemon {
8151
8610
  if (
8152
8611
  this.topics.get(logicalSessionId)?.authorityState === "delete_pending" ||
8153
8612
  this.topics.get(logicalSessionId)?.bindingMalformed
8154
- )
8613
+ ) {
8614
+ this.failLegacyToolStart(toolActivity);
8155
8615
  return;
8616
+ }
8617
+ if (!toolFrameIsCurrent()) {
8618
+ abandonStaleToolStart();
8619
+ return;
8620
+ }
8156
8621
  await this.deliverFlatFallback(logicalSessionId, send, toolActivity, socketLease);
8157
8622
  return;
8158
8623
  }
@@ -8194,7 +8659,7 @@ export class TelegramNotificationDaemon {
8194
8659
  await this.reconcileUserTopicName(topicLease);
8195
8660
  return;
8196
8661
  }
8197
- if (!toolStartIsCurrent()) {
8662
+ if (!toolFrameIsCurrent()) {
8198
8663
  abandonStaleToolStart();
8199
8664
  return;
8200
8665
  }
@@ -8590,6 +9055,7 @@ export class TelegramNotificationDaemon {
8590
9055
  ? "notifications.telegram.rich.enabled"
8591
9056
  : "notifications.telegram.toolActivity.enabled";
8592
9057
  const label = isRichCommand ? "Rich messages" : "Tool activity";
9058
+ const toolActivityChanged = isRichCommand || this.opts.toolActivity?.enabled !== desired;
8593
9059
  try {
8594
9060
  await this.opts.settings.set(settingPath, desired);
8595
9061
  // Confirm only after the global config write is durable.
@@ -8603,13 +9069,26 @@ export class TelegramNotificationDaemon {
8603
9069
  }
8604
9070
  if (isRichCommand) {
8605
9071
  this.opts.rich = { enabled: desired };
8606
- } else {
9072
+ } else if (toolActivityChanged) {
8607
9073
  this.toolActivityPolicyEpoch++;
8608
9074
  this.opts.toolActivity = { enabled: desired };
9075
+ const legacySettlements = this.cancelLegacyToolStartsForPolicyTransition();
8609
9076
  if (!desired) {
8610
- const removedTools = this.pool.removeWhere(
8611
- item => item.lane === "live" && item.coalesceKey?.startsWith("tool:") === true,
8612
- );
9077
+ // Unsent legacy starts are cancelled by exact item/frame identity; a
9078
+ // dispatch already inside Bot API is joined through its own settlement.
9079
+ await legacySettlements;
9080
+ await this.flushChain;
9081
+ const removedTools = this.pool.removeWhere(item => {
9082
+ const toolActivity = item.payload.toolActivity;
9083
+ if (!toolActivity) return false;
9084
+ if (toolActivity.phase === "started") return true;
9085
+ // Keep a terminal only when its exact start is already visible. A
9086
+ // queued or pending start is cancelled above and cannot retain a
9087
+ // terminal merely because its owner record has not been swept yet.
9088
+ const key = `${toolActivity.sessionId}:tool:${toolActivity.toolCallId}`;
9089
+ const owner = this.toolActivityOwners.get(key);
9090
+ return !this.liveMessages.has(key) || owner?.session !== toolActivity.session;
9091
+ });
8613
9092
  for (const item of removedTools) {
8614
9093
  const toolActivity = item.payload.toolActivity;
8615
9094
  if (!toolActivity) continue;
@@ -8639,7 +9118,6 @@ export class TelegramNotificationDaemon {
8639
9118
  // future starts are rejected immediately, queued starts are removed,
8640
9119
  // and any already-granted Bot API effect settles before the off
8641
9120
  // acknowledgement. Visible starts may still receive their terminal edit.
8642
- await this.flushChain;
8643
9121
  }
8644
9122
  await reply(`${label}: ${desired ? "on" : "off"}`);
8645
9123
  return;