zcode-acp-server 0.1.0 → 0.3.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 (223) hide show
  1. package/README.md +192 -9
  2. package/README.zh-CN.md +143 -8
  3. package/dist/backend/client.d.ts +12 -4
  4. package/dist/backend/client.d.ts.map +1 -1
  5. package/dist/backend/client.js +72 -7
  6. package/dist/backend/client.js.map +1 -1
  7. package/dist/backend/credentials.d.ts.map +1 -1
  8. package/dist/backend/credentials.js +2 -1
  9. package/dist/backend/credentials.js.map +1 -1
  10. package/dist/backend/listener.d.ts +12 -4
  11. package/dist/backend/listener.d.ts.map +1 -1
  12. package/dist/backend/listener.js +68 -12
  13. package/dist/backend/listener.js.map +1 -1
  14. package/dist/backend/types.d.ts +1 -1
  15. package/dist/backend/types.d.ts.map +1 -1
  16. package/dist/bin/hub.d.ts +16 -0
  17. package/dist/bin/hub.d.ts.map +1 -0
  18. package/dist/bin/hub.js +41 -0
  19. package/dist/bin/hub.js.map +1 -0
  20. package/dist/bin/quota.d.ts +54 -0
  21. package/dist/bin/quota.d.ts.map +1 -0
  22. package/dist/bin/quota.js +333 -0
  23. package/dist/bin/quota.js.map +1 -0
  24. package/dist/config/auto-compact.d.ts +23 -0
  25. package/dist/config/auto-compact.d.ts.map +1 -0
  26. package/dist/config/auto-compact.js +67 -0
  27. package/dist/config/auto-compact.js.map +1 -0
  28. package/dist/config/mcp-discovery.d.ts +34 -0
  29. package/dist/config/mcp-discovery.d.ts.map +1 -0
  30. package/dist/config/mcp-discovery.js +153 -0
  31. package/dist/config/mcp-discovery.js.map +1 -0
  32. package/dist/config/model-cache.d.ts +12 -1
  33. package/dist/config/model-cache.d.ts.map +1 -1
  34. package/dist/config/model-cache.js +30 -8
  35. package/dist/config/model-cache.js.map +1 -1
  36. package/dist/config/options.d.ts +87 -9
  37. package/dist/config/options.d.ts.map +1 -1
  38. package/dist/config/options.js +248 -45
  39. package/dist/config/options.js.map +1 -1
  40. package/dist/config/plugin-commands.d.ts +24 -0
  41. package/dist/config/plugin-commands.d.ts.map +1 -0
  42. package/dist/config/plugin-commands.js +107 -0
  43. package/dist/config/plugin-commands.js.map +1 -0
  44. package/dist/config/provider-registry.d.ts +60 -0
  45. package/dist/config/provider-registry.d.ts.map +1 -0
  46. package/dist/config/provider-registry.js +128 -0
  47. package/dist/config/provider-registry.js.map +1 -0
  48. package/dist/config/runtime-model.d.ts +54 -18
  49. package/dist/config/runtime-model.d.ts.map +1 -1
  50. package/dist/config/runtime-model.js +105 -61
  51. package/dist/config/runtime-model.js.map +1 -1
  52. package/dist/config/skill-discovery.d.ts +35 -0
  53. package/dist/config/skill-discovery.d.ts.map +1 -0
  54. package/dist/config/skill-discovery.js +188 -0
  55. package/dist/config/skill-discovery.js.map +1 -0
  56. package/dist/handlers/account.d.ts +43 -0
  57. package/dist/handlers/account.d.ts.map +1 -0
  58. package/dist/handlers/account.js +59 -0
  59. package/dist/handlers/account.js.map +1 -0
  60. package/dist/handlers/background-tasks.d.ts +72 -0
  61. package/dist/handlers/background-tasks.d.ts.map +1 -0
  62. package/dist/handlers/background-tasks.js +330 -0
  63. package/dist/handlers/background-tasks.js.map +1 -0
  64. package/dist/handlers/dispatch.d.ts.map +1 -1
  65. package/dist/handlers/dispatch.js +134 -22
  66. package/dist/handlers/dispatch.js.map +1 -1
  67. package/dist/handlers/extensions.d.ts +15 -0
  68. package/dist/handlers/extensions.d.ts.map +1 -1
  69. package/dist/handlers/extensions.js +43 -27
  70. package/dist/handlers/extensions.js.map +1 -1
  71. package/dist/handlers/io.d.ts +31 -3
  72. package/dist/handlers/io.d.ts.map +1 -1
  73. package/dist/handlers/io.js +108 -9
  74. package/dist/handlers/io.js.map +1 -1
  75. package/dist/handlers/replay.d.ts +79 -0
  76. package/dist/handlers/replay.d.ts.map +1 -0
  77. package/dist/handlers/replay.js +252 -0
  78. package/dist/handlers/replay.js.map +1 -0
  79. package/dist/handlers/server-requests.d.ts +20 -4
  80. package/dist/handlers/server-requests.d.ts.map +1 -1
  81. package/dist/handlers/server-requests.js +293 -63
  82. package/dist/handlers/server-requests.js.map +1 -1
  83. package/dist/handlers/session.d.ts +113 -10
  84. package/dist/handlers/session.d.ts.map +1 -1
  85. package/dist/handlers/session.js +932 -277
  86. package/dist/handlers/session.js.map +1 -1
  87. package/dist/handlers/slash.d.ts +37 -1
  88. package/dist/handlers/slash.d.ts.map +1 -1
  89. package/dist/handlers/slash.js +143 -8
  90. package/dist/handlers/slash.js.map +1 -1
  91. package/dist/index.js +76 -12
  92. package/dist/index.js.map +1 -1
  93. package/dist/interaction/adapter.d.ts +19 -33
  94. package/dist/interaction/adapter.d.ts.map +1 -1
  95. package/dist/interaction/adapter.js +80 -79
  96. package/dist/interaction/adapter.js.map +1 -1
  97. package/dist/lazy-sessions.d.ts +35 -0
  98. package/dist/lazy-sessions.d.ts.map +1 -0
  99. package/dist/lazy-sessions.js +98 -0
  100. package/dist/lazy-sessions.js.map +1 -0
  101. package/dist/quota/cache.d.ts +18 -0
  102. package/dist/quota/cache.d.ts.map +1 -0
  103. package/dist/quota/cache.js +32 -0
  104. package/dist/quota/cache.js.map +1 -0
  105. package/dist/quota/client.d.ts +30 -0
  106. package/dist/quota/client.d.ts.map +1 -0
  107. package/dist/quota/client.js +57 -0
  108. package/dist/quota/client.js.map +1 -0
  109. package/dist/quota/color.d.ts +58 -0
  110. package/dist/quota/color.d.ts.map +1 -0
  111. package/dist/quota/color.js +95 -0
  112. package/dist/quota/color.js.map +1 -0
  113. package/dist/quota/combined.d.ts +66 -0
  114. package/dist/quota/combined.d.ts.map +1 -0
  115. package/dist/quota/combined.js +179 -0
  116. package/dist/quota/combined.js.map +1 -0
  117. package/dist/quota/format.d.ts +105 -0
  118. package/dist/quota/format.d.ts.map +1 -0
  119. package/dist/quota/format.js +219 -0
  120. package/dist/quota/format.js.map +1 -0
  121. package/dist/quota/index.d.ts +23 -0
  122. package/dist/quota/index.d.ts.map +1 -0
  123. package/dist/quota/index.js +42 -0
  124. package/dist/quota/index.js.map +1 -0
  125. package/dist/quota/opencode-go/cache.d.ts +17 -0
  126. package/dist/quota/opencode-go/cache.d.ts.map +1 -0
  127. package/dist/quota/opencode-go/cache.js +31 -0
  128. package/dist/quota/opencode-go/cache.js.map +1 -0
  129. package/dist/quota/opencode-go/client.d.ts +28 -0
  130. package/dist/quota/opencode-go/client.d.ts.map +1 -0
  131. package/dist/quota/opencode-go/client.js +48 -0
  132. package/dist/quota/opencode-go/client.js.map +1 -0
  133. package/dist/quota/opencode-go/config.d.ts +37 -0
  134. package/dist/quota/opencode-go/config.d.ts.map +1 -0
  135. package/dist/quota/opencode-go/config.js +58 -0
  136. package/dist/quota/opencode-go/config.js.map +1 -0
  137. package/dist/quota/opencode-go/format.d.ts +36 -0
  138. package/dist/quota/opencode-go/format.d.ts.map +1 -0
  139. package/dist/quota/opencode-go/format.js +87 -0
  140. package/dist/quota/opencode-go/format.js.map +1 -0
  141. package/dist/quota/opencode-go/index.d.ts +30 -0
  142. package/dist/quota/opencode-go/index.d.ts.map +1 -0
  143. package/dist/quota/opencode-go/index.js +108 -0
  144. package/dist/quota/opencode-go/index.js.map +1 -0
  145. package/dist/quota/opencode-go/parse.d.ts +41 -0
  146. package/dist/quota/opencode-go/parse.d.ts.map +1 -0
  147. package/dist/quota/opencode-go/parse.js +75 -0
  148. package/dist/quota/opencode-go/parse.js.map +1 -0
  149. package/dist/quota/opencode-go/types.d.ts +48 -0
  150. package/dist/quota/opencode-go/types.d.ts.map +1 -0
  151. package/dist/quota/opencode-go/types.js +11 -0
  152. package/dist/quota/opencode-go/types.js.map +1 -0
  153. package/dist/quota/parse.d.ts +33 -0
  154. package/dist/quota/parse.d.ts.map +1 -0
  155. package/dist/quota/parse.js +200 -0
  156. package/dist/quota/parse.js.map +1 -0
  157. package/dist/quota/types.d.ts +72 -0
  158. package/dist/quota/types.d.ts.map +1 -0
  159. package/dist/quota/types.js +10 -0
  160. package/dist/quota/types.js.map +1 -0
  161. package/dist/remote/broadcast.d.ts +47 -0
  162. package/dist/remote/broadcast.d.ts.map +1 -0
  163. package/dist/remote/broadcast.js +121 -0
  164. package/dist/remote/broadcast.js.map +1 -0
  165. package/dist/remote/config.d.ts +32 -0
  166. package/dist/remote/config.d.ts.map +1 -0
  167. package/dist/remote/config.js +65 -0
  168. package/dist/remote/config.js.map +1 -0
  169. package/dist/remote/endpoint.d.ts +30 -0
  170. package/dist/remote/endpoint.d.ts.map +1 -0
  171. package/dist/remote/endpoint.js +213 -0
  172. package/dist/remote/endpoint.js.map +1 -0
  173. package/dist/remote/hub-server.d.ts +41 -0
  174. package/dist/remote/hub-server.d.ts.map +1 -0
  175. package/dist/remote/hub-server.js +346 -0
  176. package/dist/remote/hub-server.js.map +1 -0
  177. package/dist/server.d.ts +139 -1
  178. package/dist/server.d.ts.map +1 -1
  179. package/dist/server.js +179 -4
  180. package/dist/server.js.map +1 -1
  181. package/dist/tasks-index.d.ts +14 -4
  182. package/dist/tasks-index.d.ts.map +1 -1
  183. package/dist/tasks-index.js +145 -46
  184. package/dist/tasks-index.js.map +1 -1
  185. package/dist/translators/event-translator.d.ts +33 -0
  186. package/dist/translators/event-translator.d.ts.map +1 -1
  187. package/dist/translators/event-translator.js +101 -0
  188. package/dist/translators/event-translator.js.map +1 -1
  189. package/dist/translators/index.d.ts +1 -1
  190. package/dist/translators/index.d.ts.map +1 -1
  191. package/dist/translators/index.js +1 -1
  192. package/dist/translators/index.js.map +1 -1
  193. package/dist/translators/projection-differ.d.ts +8 -0
  194. package/dist/translators/projection-differ.d.ts.map +1 -1
  195. package/dist/translators/projection-differ.js +19 -9
  196. package/dist/translators/projection-differ.js.map +1 -1
  197. package/dist/translators/tool-helpers.d.ts +28 -0
  198. package/dist/translators/tool-helpers.d.ts.map +1 -1
  199. package/dist/translators/tool-helpers.js +104 -0
  200. package/dist/translators/tool-helpers.js.map +1 -1
  201. package/dist/translators/types.d.ts +38 -1
  202. package/dist/translators/types.d.ts.map +1 -1
  203. package/dist/translators/types.js.map +1 -1
  204. package/dist/utils.d.ts +36 -7
  205. package/dist/utils.d.ts.map +1 -1
  206. package/dist/utils.js +69 -5
  207. package/dist/utils.js.map +1 -1
  208. package/docs/ARCHITECTURE.md +130 -40
  209. package/docs/BACKLOG.md +59 -0
  210. package/docs/DEVELOPMENT.md +31 -0
  211. package/docs/PROTOCOL.md +328 -23
  212. package/docs/REMOTE-CLIENTS.md +260 -0
  213. package/docs/REPLAY-GUIDE.md +131 -0
  214. package/docs/TROUBLESHOOTING.md +131 -14
  215. package/docs/adr/0001-bridge-lifetime-follows-primary-client.md +14 -0
  216. package/docs/adr/0002-stateless-hub-over-per-bridge-acp-endpoints.md +23 -0
  217. package/docs/adr/0003-tail-replay-meta-and-cursor-pagination.md +40 -0
  218. package/docs/agents/domain.md +51 -0
  219. package/docs/agents/issue-tracker.md +22 -0
  220. package/docs/agents/triage-labels.md +15 -0
  221. package/docs/proposals/0001-tail-session-replay.md +136 -0
  222. package/docs/proposals/0002-plan-quota-usage.md +81 -0
  223. package/package.json +10 -6
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Session-scoped background-task listener.
3
+ *
4
+ * Background sub-agents (the Agent/Task tool launched with `run_in_background:
5
+ * true`) keep producing events AFTER `session/prompt` has returned: their
6
+ * internal tool calls stream as `tool.updated`, and when they finish the
7
+ * backend auto-triggers a notification turn (`turn.started` with
8
+ * `inputSource:"background_task"`) whose `text_delta` carries the result.
9
+ *
10
+ * The per-prompt turn loop (`runEventTurn`) exits at `turn.completed` and
11
+ * unregisters its listener, so without this module ALL of those post-turn
12
+ * events are silently dropped — the user sees "background task launched" and
13
+ * then nothing.
14
+ *
15
+ * This listener is registered once per session (lives across prompts) and
16
+ * forwards a curated subset to the ACP client as out-of-band `session/update`
17
+ * notifications:
18
+ *
19
+ * - `session.updated` carrying `taskId` → background task status change.
20
+ * First sighting emits a fresh `tool_call` card (`[background] …`);
21
+ * subsequent sightings emit `tool_call_update` with the new status.
22
+ * - `turn.started` with `inputSource:"background_task"` → marks the start
23
+ * of a notification turn; its `model.streaming text_delta` is forwarded
24
+ * as `agent_message_chunk` so the user sees the background result.
25
+ *
26
+ * Everything else is ignored — the turn loop handles normal turns and normal
27
+ * tool calls. Per the design decision, background agents' INTERNAL tool calls
28
+ * are NOT expanded here (only the task-level card + the final result message).
29
+ *
30
+ * All work is best-effort: failures are logged via `warn()` and never thrown
31
+ * into the event loop (would crash the bridge, per AGENTS.md).
32
+ */
33
+ import type { EventListener } from "../backend/client.js";
34
+ import type { ZcodeEvent } from "../backend/types.js";
35
+ import type { ZcodeAcpServer } from "../server.js";
36
+ export declare class BackgroundTaskListener implements EventListener {
37
+ private readonly server;
38
+ readonly zcodeSid: string;
39
+ /** taskId → tracked state. */
40
+ private readonly tasks;
41
+ /**
42
+ * The turnId of the currently-active background notification turn, if any.
43
+ * While set, that turn's `model.streaming text_delta` events are forwarded
44
+ * to the client as `agent_message_chunk`.
45
+ */
46
+ private activeNotifyTurnId;
47
+ constructor(server: ZcodeAcpServer, zcodeSid: string);
48
+ handleEvent(event: ZcodeEvent): void;
49
+ /** Handle a `session.updated` carrying a background task status. */
50
+ private onTaskStatus;
51
+ /**
52
+ * Push a status update for an already-tracked task. For background Bash
53
+ * (reusesLaunchCard), a terminal-state transition also streams the final
54
+ * output via terminal_output and closes the terminal UI with terminal_exit
55
+ * (mirroring dispatchTerminalUpdate's 2-notification split), then clears
56
+ * terminalSentData so the launch card is fully retired.
57
+ */
58
+ private emitStatusUpdate;
59
+ /** Forward a text delta from the active background notification turn. */
60
+ private forwardText;
61
+ private firstMessageId;
62
+ private activeMessageId;
63
+ /**
64
+ * Mark a tracked task as cancelled (used by `session/cancelBackgroundTask`).
65
+ * Emits a final `failed` update with `_meta.backgroundTask.cancelled = true`
66
+ * so the editor card reflects the cancellation, then drops local state. For
67
+ * background Bash reusing a launch terminal card, also emits terminal_exit
68
+ * and clears terminalSentData so the terminal UI closes cleanly.
69
+ */
70
+ markCancelled(taskId: string): Promise<void>;
71
+ }
72
+ //# sourceMappingURL=background-tasks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"background-tasks.d.ts","sourceRoot":"","sources":["../../src/handlers/background-tasks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAwEnD,qBAAa,sBAAuB,YAAW,aAAa;IAC1D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8BAA8B;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkC;IACxD;;;;OAIG;IACH,OAAO,CAAC,kBAAkB,CAAuB;gBAErC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM;IAKpD,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAmDpC,oEAAoE;YACtD,YAAY;IAkE1B;;;;;;OAMG;YACW,gBAAgB;IAiG9B,yEAAyE;YAC3D,WAAW;IAczB,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,eAAe;IAIvB;;;;;;OAMG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CA+BnD"}
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Session-scoped background-task listener.
3
+ *
4
+ * Background sub-agents (the Agent/Task tool launched with `run_in_background:
5
+ * true`) keep producing events AFTER `session/prompt` has returned: their
6
+ * internal tool calls stream as `tool.updated`, and when they finish the
7
+ * backend auto-triggers a notification turn (`turn.started` with
8
+ * `inputSource:"background_task"`) whose `text_delta` carries the result.
9
+ *
10
+ * The per-prompt turn loop (`runEventTurn`) exits at `turn.completed` and
11
+ * unregisters its listener, so without this module ALL of those post-turn
12
+ * events are silently dropped — the user sees "background task launched" and
13
+ * then nothing.
14
+ *
15
+ * This listener is registered once per session (lives across prompts) and
16
+ * forwards a curated subset to the ACP client as out-of-band `session/update`
17
+ * notifications:
18
+ *
19
+ * - `session.updated` carrying `taskId` → background task status change.
20
+ * First sighting emits a fresh `tool_call` card (`[background] …`);
21
+ * subsequent sightings emit `tool_call_update` with the new status.
22
+ * - `turn.started` with `inputSource:"background_task"` → marks the start
23
+ * of a notification turn; its `model.streaming text_delta` is forwarded
24
+ * as `agent_message_chunk` so the user sees the background result.
25
+ *
26
+ * Everything else is ignored — the turn loop handles normal turns and normal
27
+ * tool calls. Per the design decision, background agents' INTERNAL tool calls
28
+ * are NOT expanded here (only the task-level card + the final result message).
29
+ *
30
+ * All work is best-effort: failures are logged via `warn()` and never thrown
31
+ * into the event loop (would crash the bridge, per AGENTS.md).
32
+ */
33
+ import { randomUUID } from "node:crypto";
34
+ import { log, warn } from "../utils.js";
35
+ /** Map a zcode background task status string to an ACP ToolCallStatus. */
36
+ function toAcpStatus(status) {
37
+ if (status === "completed")
38
+ return "completed";
39
+ if (status === "failed" || status === "error" || status === "cancelled")
40
+ return "failed";
41
+ return "in_progress"; // running / pending / unknown
42
+ }
43
+ /** Build the card title for a background task. */
44
+ function cardTitle(description) {
45
+ const d = (description ?? "").trim();
46
+ return d ? `[background] ${d}` : "[background] task";
47
+ }
48
+ export class BackgroundTaskListener {
49
+ server;
50
+ zcodeSid;
51
+ /** taskId → tracked state. */
52
+ tasks = new Map();
53
+ /**
54
+ * The turnId of the currently-active background notification turn, if any.
55
+ * While set, that turn's `model.streaming text_delta` events are forwarded
56
+ * to the client as `agent_message_chunk`.
57
+ */
58
+ activeNotifyTurnId = null;
59
+ constructor(server, zcodeSid) {
60
+ this.server = server;
61
+ this.zcodeSid = zcodeSid;
62
+ }
63
+ handleEvent(event) {
64
+ try {
65
+ if (event.type === "session.updated") {
66
+ const raw = event.payload;
67
+ if (raw && typeof raw.taskId === "string" && raw.taskId) {
68
+ // taskId is verified string above; cast to the full shape (other
69
+ // fields are optional and read defensively inside onTaskStatus).
70
+ void this.onTaskStatus(raw);
71
+ }
72
+ return;
73
+ }
74
+ if (event.type === "turn.started") {
75
+ const inputSource = event.payload?.["inputSource"];
76
+ const turnId = event.payload?.["turnId"] ?? "";
77
+ if (inputSource === "background_task") {
78
+ this.activeNotifyTurnId = turnId || null;
79
+ if (this.activeNotifyTurnId) {
80
+ log(` [bg] background notification turn started (${turnId.slice(-8)})`);
81
+ }
82
+ }
83
+ return;
84
+ }
85
+ // Inside an active background notification turn → forward text deltas.
86
+ if (this.activeNotifyTurnId) {
87
+ if (event.type === "model.streaming") {
88
+ const kind = event.payload?.["kind"];
89
+ if (kind === "text_delta") {
90
+ const delta = event.payload?.["delta"] ?? "";
91
+ if (delta)
92
+ void this.forwardText(delta);
93
+ }
94
+ return;
95
+ }
96
+ if (event.type === "turn.completed" || event.type === "turn.failed") {
97
+ log(` [bg] background notification turn ended (${event.type})`);
98
+ this.activeNotifyTurnId = null;
99
+ // Reset the result messageId so the NEXT background task in this
100
+ // session gets its own — otherwise the editor would merge/overwrite
101
+ // distinct tasks' outputs under one shared messageId.
102
+ this.firstMessageId = null;
103
+ return;
104
+ }
105
+ }
106
+ }
107
+ catch (e) {
108
+ warn(`BackgroundTaskListener: event handling failed: ${e instanceof Error ? e.message : String(e)}`);
109
+ }
110
+ }
111
+ /** Handle a `session.updated` carrying a background task status. */
112
+ async onTaskStatus(p) {
113
+ const taskId = p.taskId;
114
+ const status = p.status ?? "running";
115
+ const acpStatus = toAcpStatus(status);
116
+ let task = this.tasks.get(taskId);
117
+ if (!task) {
118
+ // First sighting → decide whether to reuse the launch card or mint a
119
+ // fresh bg_* card. Background Bash: the dispatcher seeds
120
+ // `terminalSentData[toolCallId]` when the launch card is created, so its
121
+ // presence means the client already has a terminal card for this call —
122
+ // route the lifecycle back to it instead of creating a duplicate. Sub-
123
+ // agents (Agent/Task tool) have no such launch card → fall back to bg_*.
124
+ const launchToolCallId = p.toolCallId;
125
+ const reusesLaunchCard = !!launchToolCallId && this.server.terminalSentData.has(launchToolCallId);
126
+ if (reusesLaunchCard) {
127
+ task = {
128
+ acpCallId: launchToolCallId,
129
+ sourceToolCallId: launchToolCallId,
130
+ reusesLaunchCard: true,
131
+ description: p.description ?? "",
132
+ lastStatus: "",
133
+ };
134
+ this.tasks.set(taskId, task);
135
+ // The launch card already exists (dispatch created it in the main
136
+ // turn). Don't emit a new tool_call — just push the first status update
137
+ // so the card reflects "running in background" with task metadata.
138
+ await this.emitStatusUpdate(task, acpStatus, p);
139
+ log(` [bg] reusing launch card ${launchToolCallId.slice(-12)} for task ${taskId.slice(-12)}`);
140
+ return;
141
+ }
142
+ // No launch card to reuse → emit a fresh [background] tool_call card.
143
+ task = {
144
+ acpCallId: `bg_${randomUUID().slice(0, 12)}`,
145
+ reusesLaunchCard: false,
146
+ description: p.description ?? "",
147
+ lastStatus: "",
148
+ };
149
+ this.tasks.set(taskId, task);
150
+ const meta = {
151
+ backgroundTask: {
152
+ taskId,
153
+ agentId: p.terminalId ?? undefined,
154
+ outputPath: p.outputPath ?? undefined,
155
+ sourceToolCallId: p.toolCallId ?? undefined,
156
+ },
157
+ };
158
+ const ok = await this.server.notifyByZcodeSid(this.zcodeSid, {
159
+ sessionUpdate: "tool_call",
160
+ toolCallId: task.acpCallId,
161
+ title: cardTitle(task.description),
162
+ kind: "other",
163
+ status: acpStatus,
164
+ _meta: meta,
165
+ });
166
+ task.lastStatus = acpStatus;
167
+ if (ok) {
168
+ log(` [bg] task card emitted: ${taskId.slice(-12)} status=${acpStatus}`);
169
+ }
170
+ return;
171
+ }
172
+ await this.emitStatusUpdate(task, acpStatus, p);
173
+ }
174
+ /**
175
+ * Push a status update for an already-tracked task. For background Bash
176
+ * (reusesLaunchCard), a terminal-state transition also streams the final
177
+ * output via terminal_output and closes the terminal UI with terminal_exit
178
+ * (mirroring dispatchTerminalUpdate's 2-notification split), then clears
179
+ * terminalSentData so the launch card is fully retired.
180
+ */
181
+ async emitStatusUpdate(task, acpStatus, p) {
182
+ // Skip no-op status updates (same as last advertised).
183
+ if (task.lastStatus === acpStatus && !task.reusesLaunchCard)
184
+ return;
185
+ const meta = {
186
+ backgroundTask: { taskId: p.taskId },
187
+ };
188
+ if (p.outputPath)
189
+ meta.backgroundTask["outputPath"] = p.outputPath;
190
+ const isTerminal = acpStatus === "completed" || acpStatus === "failed";
191
+ // Background Bash closing a launch terminal card: stream final output +
192
+ // emit terminal_exit so Zed's terminal UI finalises. The terminal_output
193
+ // diff guard mirrors dispatchTerminalUpdate (only emit the suffix beyond
194
+ // what was already streamed; for a background launch the launch text was
195
+ // already sent, so outputTail here is the real command output).
196
+ if (task.reusesLaunchCard && task.sourceToolCallId) {
197
+ // Stream any new command output via terminal_output. The output is
198
+ // tracked on `task.streamedOutput` (NOT server.terminalSentData) because
199
+ // terminalSentData holds the launch acknowledgement text written by the
200
+ // dispatcher, which is unrelated to the command's actual stdout — they
201
+ // aren't prefix-related, so diffing against the launch text would drop
202
+ // the real output entirely. Each session.updated carries a CUMULATIVE
203
+ // outputTail snapshot, so we diff against what we last streamed and emit
204
+ // only the suffix (mirrors dispatchTerminalUpdate's progress diffing).
205
+ const finalOutput = p.outputTail ?? p.stdoutTail ?? "";
206
+ const lastSent = task.streamedOutput ?? "";
207
+ if (finalOutput) {
208
+ const delta = lastSent && finalOutput.startsWith(lastSent)
209
+ ? finalOutput.slice(lastSent.length)
210
+ : finalOutput;
211
+ if (delta) {
212
+ task.streamedOutput = finalOutput;
213
+ await this.server.notifyByZcodeSid(this.zcodeSid, {
214
+ sessionUpdate: "tool_call_update",
215
+ toolCallId: task.sourceToolCallId,
216
+ _meta: { terminal_output: { terminal_id: task.sourceToolCallId, data: delta } },
217
+ });
218
+ }
219
+ }
220
+ // terminal_exit only fires on a terminal status. A non-terminal (running)
221
+ // update has streamed its progress above; also push an in_progress status
222
+ // update with backgroundTask metadata so the card reflects "running in
223
+ // background" on first sighting (the launch card was left in_progress by
224
+ // the dispatcher, but we still advertise the status to attach taskId).
225
+ if (!isTerminal) {
226
+ await this.server.notifyByZcodeSid(this.zcodeSid, {
227
+ sessionUpdate: "tool_call_update",
228
+ toolCallId: task.sourceToolCallId,
229
+ status: acpStatus,
230
+ _meta: meta,
231
+ });
232
+ task.lastStatus = acpStatus;
233
+ return;
234
+ }
235
+ const exitCode = acpStatus === "failed" ? 1 : 0;
236
+ meta.backgroundTask["completed"] = true;
237
+ await this.server.notifyByZcodeSid(this.zcodeSid, {
238
+ sessionUpdate: "tool_call_update",
239
+ toolCallId: task.sourceToolCallId,
240
+ status: acpStatus,
241
+ content: [{ type: "terminal", terminalId: task.sourceToolCallId }],
242
+ _meta: {
243
+ ...meta,
244
+ claudeCode: { toolName: "Bash" },
245
+ terminal_exit: {
246
+ terminal_id: task.sourceToolCallId,
247
+ exit_code: exitCode,
248
+ signal: null,
249
+ },
250
+ },
251
+ });
252
+ this.server.terminalSentData.delete(task.sourceToolCallId);
253
+ task.lastStatus = acpStatus;
254
+ log(` [bg] launch card ${task.sourceToolCallId.slice(-12)} → ${acpStatus} (terminal_exit)`);
255
+ return;
256
+ }
257
+ // Subsequent sighting (bg_* card) or non-terminal → status update only.
258
+ if (task.lastStatus === acpStatus)
259
+ return;
260
+ const ok = await this.server.notifyByZcodeSid(this.zcodeSid, {
261
+ sessionUpdate: "tool_call_update",
262
+ toolCallId: task.acpCallId,
263
+ status: acpStatus,
264
+ _meta: meta,
265
+ });
266
+ task.lastStatus = acpStatus;
267
+ if (ok) {
268
+ log(` [bg] task status update: ${p.taskId.slice(-12)} → ${acpStatus}`);
269
+ }
270
+ }
271
+ /** Forward a text delta from the active background notification turn. */
272
+ async forwardText(delta) {
273
+ // Allocate the messageId lazily so an empty-result turn emits nothing.
274
+ let msgId = this.activeMessageId();
275
+ if (!msgId) {
276
+ msgId = `bg_msg_${randomUUID().slice(0, 12)}`;
277
+ this.firstMessageId = msgId;
278
+ }
279
+ await this.server.notifyByZcodeSid(this.zcodeSid, {
280
+ sessionUpdate: "agent_message_chunk",
281
+ content: { type: "text", text: delta },
282
+ messageId: msgId,
283
+ });
284
+ }
285
+ firstMessageId = null;
286
+ activeMessageId() {
287
+ return this.firstMessageId;
288
+ }
289
+ /**
290
+ * Mark a tracked task as cancelled (used by `session/cancelBackgroundTask`).
291
+ * Emits a final `failed` update with `_meta.backgroundTask.cancelled = true`
292
+ * so the editor card reflects the cancellation, then drops local state. For
293
+ * background Bash reusing a launch terminal card, also emits terminal_exit
294
+ * and clears terminalSentData so the terminal UI closes cleanly.
295
+ */
296
+ async markCancelled(taskId) {
297
+ const task = this.tasks.get(taskId);
298
+ if (!task)
299
+ return;
300
+ if (task.reusesLaunchCard && task.sourceToolCallId) {
301
+ await this.server.notifyByZcodeSid(this.zcodeSid, {
302
+ sessionUpdate: "tool_call_update",
303
+ toolCallId: task.sourceToolCallId,
304
+ status: "failed",
305
+ content: [{ type: "terminal", terminalId: task.sourceToolCallId }],
306
+ _meta: {
307
+ backgroundTask: { taskId, cancelled: true },
308
+ claudeCode: { toolName: "Bash" },
309
+ terminal_exit: {
310
+ terminal_id: task.sourceToolCallId,
311
+ exit_code: 1,
312
+ signal: null,
313
+ },
314
+ },
315
+ });
316
+ this.server.terminalSentData.delete(task.sourceToolCallId);
317
+ }
318
+ else {
319
+ await this.server.notifyByZcodeSid(this.zcodeSid, {
320
+ sessionUpdate: "tool_call_update",
321
+ toolCallId: task.acpCallId,
322
+ status: "failed",
323
+ _meta: { backgroundTask: { taskId, cancelled: true } },
324
+ });
325
+ }
326
+ this.tasks.delete(taskId);
327
+ log(` [bg] task cancelled: ${taskId.slice(-12)}`);
328
+ }
329
+ }
330
+ //# sourceMappingURL=background-tasks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"background-tasks.js","sourceRoot":"","sources":["../../src/handlers/background-tasks.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../src/handlers/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,KAAK,GAAG,MAAM,0BAA0B,CAAC;AAKrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGnD,yEAAyE;AACzE,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,EAAE,EAAE,GAAG,CAAC,YAAY,EACpB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,aAAa,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAmCf"}
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../src/handlers/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,KAAK,GAAG,MAAM,0BAA0B,CAAC;AAcrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAInD,yEAAyE;AACzE,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,EAAE,EAAE,GAAG,CAAC,YAAY,EACpB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,aAAa,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAsCf"}
@@ -9,8 +9,9 @@
9
9
  */
10
10
  import { randomUUID } from "node:crypto";
11
11
  import { currentModelCached } from "../config/model-cache.js";
12
- import { modelContextWindow } from "../config/options.js";
13
- import { extractExitCode, TOOL_KIND_MAP } from "../translators/tool-helpers.js";
12
+ import { buildConfigOptions, formatModelValue, modelContextWindow, parseModelValue, } from "../config/options.js";
13
+ import { extractExitCode, parseSubagentMetadata, TOOL_KIND_MAP, } from "../translators/tool-helpers.js";
14
+ import { warn } from "../utils.js";
14
15
  import { sendSessionUpdate } from "./io.js";
15
16
  /** Dispatch one internal event to the ACP client as a session/update. */
16
17
  export async function dispatchEvent(server, cx, acpSid, ev, chunkMsgId) {
@@ -47,6 +48,52 @@ export async function dispatchEvent(server, cx, acpSid, ev, chunkMsgId) {
47
48
  case "FilesChanged":
48
49
  await dispatchFilesChanged(cx, acpSid, ev);
49
50
  break;
51
+ case "ConfigChanged":
52
+ await dispatchConfigChanged(server, cx, acpSid, ev);
53
+ break;
54
+ }
55
+ }
56
+ /**
57
+ * Session settings changed (model/mode/thoughtLevel switch). Push the rebuilt
58
+ * configOptions (+ current_mode_update for mode) so the editor UI follows the
59
+ * switch immediately — even mid-turn, without waiting for turn completion.
60
+ *
61
+ * Builds the option structures from the session's authoritative settings
62
+ * (`session/read` via buildConfigOptions), then overlays the values from the
63
+ * event patch. A state.updated patch only carries the fields that actually
64
+ * changed, so building from the null defaults instead would reset every
65
+ * untouched field — most visibly the model dropdown jumping back to the
66
+ * default model mid-conversation.
67
+ * Best-effort: failures are logged and swallowed, never thrown into the loop.
68
+ */
69
+ async function dispatchConfigChanged(server, cx, acpSid, ev) {
70
+ try {
71
+ // Fall back to null (defaults) only if the session mapping isn't live yet —
72
+ // events routed through a registered turn loop always have it.
73
+ const zcodeSid = server.resolveSid(acpSid) ?? null;
74
+ const options = await buildConfigOptions(server, zcodeSid);
75
+ if (ev.model)
76
+ options[0].currentValue = formatModelValue(ev.model.providerId, ev.model.modelId);
77
+ if (ev.mode !== undefined)
78
+ options[1].currentValue = ev.mode;
79
+ if (ev.thought !== undefined)
80
+ options[2].currentValue = ev.thought;
81
+ await sendSessionUpdate(cx, acpSid, {
82
+ sessionUpdate: "config_option_update",
83
+ configOptions: options,
84
+ });
85
+ if (ev.mode !== undefined) {
86
+ // Mirror the advertised mode so turn-completion reconciliation
87
+ // (emitModeIfChanged) doesn't re-emit the same value.
88
+ server.lastMode.set(acpSid, ev.mode);
89
+ await sendSessionUpdate(cx, acpSid, {
90
+ sessionUpdate: "current_mode_update",
91
+ currentModeId: ev.mode,
92
+ });
93
+ }
94
+ }
95
+ catch (e) {
96
+ warn(`dispatch: ConfigChanged failed (${e instanceof Error ? e.message : String(e)})`);
50
97
  }
51
98
  }
52
99
  function dispatchToolCallNew(server, cx, acpSid, ev) {
@@ -54,6 +101,9 @@ function dispatchToolCallNew(server, cx, acpSid, ev) {
54
101
  const meta = { claudeCode: { toolName: ev.tool } };
55
102
  if (termSupported)
56
103
  meta["terminal_info"] = { terminal_id: ev.callId };
104
+ // Mark sub-agent dispatch cards so editors can badge them from creation.
105
+ if (ev.tool === "Agent" || ev.tool === "Task")
106
+ meta["subagent"] = true;
57
107
  const update = {
58
108
  sessionUpdate: "tool_call",
59
109
  toolCallId: ev.callId,
@@ -80,8 +130,18 @@ function dispatchToolCallUpdate(server, cx, acpSid, ev) {
80
130
  const toolName = ev.tool ?? "";
81
131
  const termSupported = server.supportsTerminalOutput() && (toolName === "Bash" || toolName === "bash");
82
132
  if (termSupported) {
83
- // Bash terminal protocol (full 2-notification split arrives in Commit 5).
84
- // For now emit the terminal_output data + the standard update with status.
133
+ // Background Bash: the `result` event carries the launch acknowledgement
134
+ // ("Command running in background with ID: exec_…"), NOT the final output.
135
+ // Closing the card now (terminal_exit + status:completed) would make it
136
+ // indistinguishable from a normal Bash call and steal the lifecycle from
137
+ // the BackgroundTaskListener, which owns the real completion via
138
+ // out-of-band `session.updated` events. So stream any launch text via
139
+ // terminal_output and leave the card in_progress.
140
+ if (ev.background) {
141
+ return dispatchTerminalUpdate(server, cx, acpSid, ev, toolName, {
142
+ skipExit: true,
143
+ });
144
+ }
85
145
  return dispatchTerminalUpdate(server, cx, acpSid, ev, toolName);
86
146
  }
87
147
  const update = {
@@ -92,6 +152,15 @@ function dispatchToolCallUpdate(server, cx, acpSid, ev) {
92
152
  const meta = {};
93
153
  if (toolName)
94
154
  meta["claudeCode"] = { toolName };
155
+ // Sub-agent (Agent/Task tool) result: surface structured metadata so editors
156
+ // can badge the card (agentId, background flag, token/tool/time usage). The
157
+ // raw result text is left in `content` for the user-facing view.
158
+ if ((toolName === "Agent" || toolName === "Task") &&
159
+ (ev.status === "completed" || ev.status === "failed")) {
160
+ const sub = parseSubagentMetadata(ev.rawResult);
161
+ if (sub)
162
+ meta["subagent"] = sub;
163
+ }
95
164
  if (ev.output !== undefined)
96
165
  update.rawOutput = ev.output;
97
166
  if (ev.diffContent && ev.diffContent.length > 0) {
@@ -110,30 +179,69 @@ function dispatchToolCallUpdate(server, cx, acpSid, ev) {
110
179
  * Bash terminal update — the 2-notification split (matches acp-agent.ts:5061-5094
111
180
  * and the Python bridge's _dispatch_event). Zed correlates by terminal_id, so
112
181
  * the two notifications MUST be separate:
113
- * ① terminal_output — pure data, no status/content. Sent on BOTH progress and
114
- * result states (whenever there's data) so live output streams.
182
+ * ① terminal_output — pure data, no status/content. Streams live output.
115
183
  * ② terminal_exit — terminal state only: status + content[type:terminal] +
116
- * _meta.terminal_exit (with exitCode) + rawOutput text fallback. Sent ONLY
117
- * on completed/failed.
184
+ * _meta.terminal_exit (with exitCode). Sent ONLY on completed/failed.
118
185
  *
119
186
  * Merging them into one notification causes Zed to clear the content once the
120
187
  * turn completes (the original bug); splitting keeps the output visible.
188
+ *
189
+ * OUTPUT DEDUP (two distinct hazards, both fixed here):
190
+ * - Progress replay: zcode's `stdoutTail` is a CUMULATIVE snapshot of the tail,
191
+ * but terminal_output.data is APPEND semantics. Without diffing, each progress
192
+ * event replays the whole tail → N× duplication. We track the last-sent
193
+ * snapshot per callId and emit only the suffix beyond it.
194
+ * - Result replay: zcode's result payload re-sends the COMPLETE output. Once
195
+ * progress has streamed it, emitting it again would duplicate every line.
196
+ * So on completed/failed we SKIP terminal_output IF any progress already
197
+ * streamed data for this callId; for short commands (scheduled → result, no
198
+ * progress) nothing was sent yet, so we emit the full output once.
121
199
  */
122
- async function dispatchTerminalUpdate(_server, cx, acpSid, ev, toolName) {
123
- // terminal_output (pure data) progress and result both emit it.
200
+ async function dispatchTerminalUpdate(server, cx, acpSid, ev, toolName, opts = {}) {
201
+ // Extract the textual output from whichever payload field carries it.
124
202
  let termData = ev.rawOutput ?? ev.rawResult;
125
203
  if (termData && typeof termData === "object" && !Array.isArray(termData)) {
126
204
  termData = termData["content"] ?? "";
127
205
  }
128
- if (termData) {
129
- await sendSessionUpdate(cx, acpSid, {
130
- sessionUpdate: "tool_call_update",
131
- toolCallId: ev.callId,
132
- _meta: { terminal_output: { terminal_id: ev.callId, data: String(termData) } },
133
- });
206
+ const full = termData ? String(termData) : "";
207
+ const lastSent = server.terminalSentData.get(ev.callId) ?? "";
208
+ // ① terminal_output (pure data, append semantics).
209
+ // - progress (in_progress): zcode's stdoutTail is a CUMULATIVE snapshot of
210
+ // the tail, but terminal_output.data is APPEND semantics, so we diff and
211
+ // emit only the suffix beyond the last-sent snapshot.
212
+ // - result (completed/failed): the full output was already streamed during
213
+ // progress, and zcode's result re-sends the complete tail → skip, UNLESS
214
+ // no progress ever fired (short command: scheduled → result). In that
215
+ // case nothing was streamed yet, so emit the full output once.
216
+ const isResult = ev.status === "completed" || ev.status === "failed";
217
+ const alreadyStreamed = server.terminalSentData.has(ev.callId);
218
+ if (full && !(isResult && alreadyStreamed)) {
219
+ const delta = lastSent && full.startsWith(lastSent) ? full.slice(lastSent.length) : full;
220
+ if (delta) {
221
+ server.terminalSentData.set(ev.callId, full);
222
+ await sendSessionUpdate(cx, acpSid, {
223
+ sessionUpdate: "tool_call_update",
224
+ toolCallId: ev.callId,
225
+ _meta: { terminal_output: { terminal_id: ev.callId, data: delta } },
226
+ });
227
+ }
228
+ }
229
+ // Background Bash launch: leave the card in_progress — BackgroundTaskListener
230
+ // owns the final status + terminal_exit via out-of-band session.updated. We
231
+ // MUST still seed terminalSentData with an empty entry (even when no launch
232
+ // text was streamed) so the listener can recognise this callId as a tracked
233
+ // launch card and route its updates back to the same card.
234
+ if (opts.skipExit) {
235
+ if (!server.terminalSentData.has(ev.callId)) {
236
+ server.terminalSentData.set(ev.callId, "");
237
+ }
238
+ return;
134
239
  }
135
240
  // ② terminal_exit (terminal state) — only on completed/failed.
136
- if (ev.status === "completed" || ev.status === "failed") {
241
+ // Deliberately omits rawOutput: the output was already streamed via
242
+ // terminal_output.data above, and Zed renders BOTH the terminal buffer and
243
+ // the rawOutput fallback — including rawOutput here duplicates every line.
244
+ if (isResult) {
137
245
  const exitCode = extractExitCode(ev.rawResult, ev.status === "failed");
138
246
  const exitUpdate = {
139
247
  sessionUpdate: "tool_call_update",
@@ -145,10 +253,10 @@ async function dispatchTerminalUpdate(_server, cx, acpSid, ev, toolName) {
145
253
  terminal_exit: { terminal_id: ev.callId, exit_code: exitCode, signal: null },
146
254
  },
147
255
  };
148
- // rawOutput gives Zed a text fallback outside the terminal render path.
149
- if (ev.output !== undefined)
150
- exitUpdate.rawOutput = ev.output;
151
256
  await sendSessionUpdate(cx, acpSid, exitUpdate);
257
+ // Terminal session ended — clear the snapshot so a future tool reusing this
258
+ // callId (shouldn't happen, but defensively) starts fresh.
259
+ server.terminalSentData.delete(ev.callId);
152
260
  }
153
261
  }
154
262
  async function dispatchUsageDelta(server, cx, acpSid, ev) {
@@ -156,8 +264,12 @@ async function dispatchUsageDelta(server, cx, acpSid, ev) {
156
264
  // config.json limit.context so the editor can render the context bar.
157
265
  let size = ev.size;
158
266
  if (!size) {
159
- const modelId = await currentModelCached(server, acpSid);
160
- size = modelContextWindow(modelId);
267
+ // Resolve to the real backend session id — `acpSid` may be a lazy
268
+ // session/new placeholder that the backend rejects with "Session is not
269
+ // active", wasting a 5s request timeout on every usage_update.
270
+ const zcodeSid = server.resolveSid(acpSid) ?? acpSid;
271
+ const { providerId, modelId } = parseModelValue(await currentModelCached(server, zcodeSid));
272
+ size = modelContextWindow(providerId, modelId);
161
273
  }
162
274
  await sendSessionUpdate(cx, acpSid, {
163
275
  sessionUpdate: "usage_update",