@oh-my-pi/pi-coding-agent 13.18.0 → 14.0.2

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 (235) hide show
  1. package/CHANGELOG.md +316 -1
  2. package/package.json +86 -24
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +116 -30
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +123 -178
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -8
  28. package/src/commit/agentic/index.ts +22 -26
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/tools/git-file-diff.ts +3 -6
  31. package/src/commit/agentic/tools/git-hunk.ts +3 -3
  32. package/src/commit/agentic/tools/git-overview.ts +6 -9
  33. package/src/commit/agentic/tools/index.ts +6 -8
  34. package/src/commit/agentic/tools/propose-commit.ts +4 -7
  35. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  36. package/src/commit/agentic/tools/split-commit.ts +4 -4
  37. package/src/commit/agentic/validation.ts +1 -1
  38. package/src/commit/analysis/conventional.ts +4 -4
  39. package/src/commit/analysis/summary.ts +3 -3
  40. package/src/commit/changelog/generate.ts +4 -4
  41. package/src/commit/changelog/index.ts +5 -9
  42. package/src/commit/map-reduce/map-phase.ts +4 -4
  43. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  44. package/src/commit/pipeline.ts +13 -16
  45. package/src/config/keybindings.ts +7 -6
  46. package/src/config/prompt-templates.ts +44 -226
  47. package/src/config/resolve-config-value.ts +4 -2
  48. package/src/config/settings-schema.ts +98 -2
  49. package/src/config/settings.ts +25 -26
  50. package/src/dap/client.ts +674 -0
  51. package/src/dap/config.ts +150 -0
  52. package/src/dap/defaults.json +211 -0
  53. package/src/dap/index.ts +4 -0
  54. package/src/dap/session.ts +1255 -0
  55. package/src/dap/types.ts +600 -0
  56. package/src/debug/log-viewer.ts +3 -2
  57. package/src/discovery/builtin.ts +1 -2
  58. package/src/discovery/codex.ts +2 -2
  59. package/src/discovery/github.ts +2 -1
  60. package/src/discovery/helpers.ts +2 -2
  61. package/src/discovery/opencode.ts +2 -2
  62. package/src/edit/diff.ts +818 -0
  63. package/src/edit/index.ts +309 -0
  64. package/src/edit/line-hash.ts +67 -0
  65. package/src/edit/modes/chunk.ts +454 -0
  66. package/src/{patch → edit/modes}/hashline.ts +741 -361
  67. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  68. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  69. package/src/{patch → edit}/normalize.ts +97 -76
  70. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  71. package/src/exec/bash-executor.ts +4 -2
  72. package/src/exec/idle-timeout-watchdog.ts +126 -0
  73. package/src/exec/non-interactive-env.ts +5 -0
  74. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
  75. package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
  76. package/src/extensibility/custom-commands/loader.ts +1 -2
  77. package/src/extensibility/custom-tools/loader.ts +34 -11
  78. package/src/extensibility/custom-tools/types.ts +1 -1
  79. package/src/extensibility/extensions/loader.ts +9 -4
  80. package/src/extensibility/extensions/runner.ts +24 -1
  81. package/src/extensibility/extensions/types.ts +4 -2
  82. package/src/extensibility/hooks/loader.ts +5 -6
  83. package/src/extensibility/hooks/types.ts +2 -2
  84. package/src/extensibility/plugins/doctor.ts +2 -1
  85. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
  86. package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
  87. package/src/extensibility/slash-commands.ts +3 -7
  88. package/src/index.ts +3 -1
  89. package/src/internal-urls/docs-index.generated.ts +11 -11
  90. package/src/ipy/executor.ts +58 -17
  91. package/src/ipy/gateway-coordinator.ts +6 -4
  92. package/src/ipy/kernel.ts +45 -22
  93. package/src/ipy/runtime.ts +2 -2
  94. package/src/lsp/client.ts +7 -4
  95. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  96. package/src/lsp/config.ts +2 -2
  97. package/src/lsp/defaults.json +688 -154
  98. package/src/lsp/index.ts +234 -45
  99. package/src/lsp/lspmux.ts +2 -2
  100. package/src/lsp/startup-events.ts +13 -0
  101. package/src/lsp/types.ts +12 -1
  102. package/src/lsp/utils.ts +8 -1
  103. package/src/main.ts +125 -47
  104. package/src/memories/index.ts +4 -5
  105. package/src/modes/acp/acp-agent.ts +563 -163
  106. package/src/modes/acp/acp-event-mapper.ts +9 -1
  107. package/src/modes/acp/acp-mode.ts +4 -2
  108. package/src/modes/components/agent-dashboard.ts +3 -4
  109. package/src/modes/components/diff.ts +6 -7
  110. package/src/modes/components/footer.ts +9 -29
  111. package/src/modes/components/hook-editor.ts +3 -3
  112. package/src/modes/components/hook-selector.ts +6 -1
  113. package/src/modes/components/read-tool-group.ts +6 -12
  114. package/src/modes/components/session-observer-overlay.ts +472 -0
  115. package/src/modes/components/settings-defs.ts +24 -0
  116. package/src/modes/components/status-line.ts +15 -61
  117. package/src/modes/components/tool-execution.ts +1 -1
  118. package/src/modes/components/welcome.ts +1 -1
  119. package/src/modes/controllers/btw-controller.ts +2 -2
  120. package/src/modes/controllers/command-controller.ts +4 -2
  121. package/src/modes/controllers/event-controller.ts +59 -2
  122. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  123. package/src/modes/controllers/input-controller.ts +15 -8
  124. package/src/modes/controllers/selector-controller.ts +26 -0
  125. package/src/modes/index.ts +20 -2
  126. package/src/modes/interactive-mode.ts +278 -69
  127. package/src/modes/rpc/host-tools.ts +186 -0
  128. package/src/modes/rpc/rpc-client.ts +178 -13
  129. package/src/modes/rpc/rpc-mode.ts +73 -3
  130. package/src/modes/rpc/rpc-types.ts +53 -1
  131. package/src/modes/session-observer-registry.ts +146 -0
  132. package/src/modes/shared.ts +0 -42
  133. package/src/modes/theme/theme.ts +80 -8
  134. package/src/modes/types.ts +4 -2
  135. package/src/modes/utils/keybinding-matchers.ts +9 -0
  136. package/src/prompts/system/custom-system-prompt.md +5 -0
  137. package/src/prompts/system/system-prompt.md +8 -1
  138. package/src/prompts/tools/chunk-edit.md +219 -0
  139. package/src/prompts/tools/debug.md +43 -0
  140. package/src/prompts/tools/grep.md +3 -0
  141. package/src/prompts/tools/lsp.md +5 -5
  142. package/src/prompts/tools/read-chunk.md +17 -0
  143. package/src/prompts/tools/read.md +19 -5
  144. package/src/sdk.ts +216 -165
  145. package/src/secrets/index.ts +1 -1
  146. package/src/secrets/obfuscator.ts +25 -17
  147. package/src/session/agent-session.ts +381 -286
  148. package/src/session/agent-storage.ts +12 -12
  149. package/src/session/compaction/branch-summarization.ts +3 -3
  150. package/src/session/compaction/compaction.ts +5 -6
  151. package/src/session/compaction/utils.ts +3 -3
  152. package/src/session/history-storage.ts +62 -19
  153. package/src/session/messages.ts +3 -3
  154. package/src/session/session-dump-format.ts +203 -0
  155. package/src/session/session-manager.ts +15 -5
  156. package/src/session/session-storage.ts +4 -2
  157. package/src/session/streaming-output.ts +1 -1
  158. package/src/session/tool-choice-queue.ts +213 -0
  159. package/src/slash-commands/builtin-registry.ts +56 -8
  160. package/src/ssh/connection-manager.ts +2 -2
  161. package/src/ssh/sshfs-mount.ts +5 -5
  162. package/src/stt/downloader.ts +4 -4
  163. package/src/stt/recorder.ts +4 -4
  164. package/src/stt/transcriber.ts +2 -2
  165. package/src/system-prompt.ts +25 -13
  166. package/src/task/agents.ts +5 -6
  167. package/src/task/commands.ts +2 -5
  168. package/src/task/executor.ts +32 -4
  169. package/src/task/index.ts +91 -82
  170. package/src/task/template.ts +2 -2
  171. package/src/task/types.ts +25 -0
  172. package/src/task/worktree.ts +131 -149
  173. package/src/tools/ask.ts +2 -3
  174. package/src/tools/ast-edit.ts +7 -7
  175. package/src/tools/ast-grep.ts +7 -7
  176. package/src/tools/auto-generated-guard.ts +36 -41
  177. package/src/tools/await-tool.ts +2 -2
  178. package/src/tools/bash.ts +5 -23
  179. package/src/tools/browser.ts +4 -5
  180. package/src/tools/calculator.ts +2 -3
  181. package/src/tools/cancel-job.ts +2 -2
  182. package/src/tools/checkpoint.ts +3 -3
  183. package/src/tools/debug.ts +1007 -0
  184. package/src/tools/exit-plan-mode.ts +3 -3
  185. package/src/tools/fetch.ts +67 -3
  186. package/src/tools/find.ts +4 -5
  187. package/src/tools/fs-cache-invalidation.ts +5 -0
  188. package/src/tools/gemini-image.ts +13 -5
  189. package/src/tools/gh.ts +130 -308
  190. package/src/tools/grep.ts +57 -9
  191. package/src/tools/index.ts +44 -22
  192. package/src/tools/inspect-image.ts +4 -4
  193. package/src/tools/output-meta.ts +1 -1
  194. package/src/tools/python.ts +19 -6
  195. package/src/tools/read.ts +211 -146
  196. package/src/tools/render-mermaid.ts +2 -3
  197. package/src/tools/render-utils.ts +20 -6
  198. package/src/tools/renderers.ts +3 -1
  199. package/src/tools/report-tool-issue.ts +80 -0
  200. package/src/tools/resolve.ts +70 -39
  201. package/src/tools/search-tool-bm25.ts +2 -2
  202. package/src/tools/ssh.ts +2 -2
  203. package/src/tools/todo-write.ts +2 -2
  204. package/src/tools/tool-timeouts.ts +1 -0
  205. package/src/tools/write.ts +5 -6
  206. package/src/tui/tree-list.ts +3 -1
  207. package/src/utils/clipboard.ts +80 -0
  208. package/src/utils/commit-message-generator.ts +2 -3
  209. package/src/utils/edit-mode.ts +49 -0
  210. package/src/utils/external-editor.ts +11 -5
  211. package/src/utils/file-display-mode.ts +6 -5
  212. package/src/utils/file-mentions.ts +8 -7
  213. package/src/utils/git.ts +1400 -0
  214. package/src/utils/image-loading.ts +98 -0
  215. package/src/utils/title-generator.ts +2 -3
  216. package/src/utils/tools-manager.ts +6 -6
  217. package/src/web/scrapers/choosealicense.ts +1 -1
  218. package/src/web/search/index.ts +3 -3
  219. package/src/web/search/render.ts +6 -4
  220. package/src/autoresearch/command-initialize.md +0 -34
  221. package/src/commit/git/errors.ts +0 -9
  222. package/src/commit/git/index.ts +0 -210
  223. package/src/commit/git/operations.ts +0 -54
  224. package/src/patch/diff.ts +0 -433
  225. package/src/patch/index.ts +0 -888
  226. package/src/patch/parser.ts +0 -532
  227. package/src/patch/types.ts +0 -292
  228. package/src/prompts/agents/oracle.md +0 -77
  229. package/src/tools/gh-cli.ts +0 -125
  230. package/src/tools/pending-action.ts +0 -49
  231. package/src/utils/child-process.ts +0 -88
  232. package/src/utils/frontmatter.ts +0 -117
  233. package/src/utils/image-input.ts +0 -274
  234. package/src/utils/mime.ts +0 -53
  235. package/src/utils/prompt-format.ts +0 -170
@@ -5,6 +5,10 @@ import {
5
5
  type AuthenticateRequest,
6
6
  type AuthenticateResponse,
7
7
  type AvailableCommand,
8
+ type CloseSessionRequest,
9
+ type CloseSessionResponse,
10
+ type ForkSessionRequest,
11
+ type ForkSessionResponse,
8
12
  type InitializeRequest,
9
13
  type InitializeResponse,
10
14
  type ListSessionsRequest,
@@ -17,15 +21,21 @@ import {
17
21
  PROTOCOL_VERSION,
18
22
  type PromptRequest,
19
23
  type PromptResponse,
24
+ type ResumeSessionRequest,
25
+ type ResumeSessionResponse,
20
26
  type SessionConfigOption,
21
27
  type SessionInfo,
28
+ type SessionModelState,
22
29
  type SessionModeState,
23
30
  type SessionNotification,
24
31
  type SessionUpdate,
25
32
  type SetSessionConfigOptionRequest,
26
33
  type SetSessionConfigOptionResponse,
34
+ type SetSessionModelRequest,
35
+ type SetSessionModelResponse,
27
36
  type SetSessionModeRequest,
28
37
  type SetSessionModeResponse,
38
+ type Usage,
29
39
  } from "@agentclientprotocol/sdk";
30
40
  import type { Model } from "@oh-my-pi/pi-ai";
31
41
  import { logger, VERSION } from "@oh-my-pi/pi-utils";
@@ -35,7 +45,11 @@ import { MCPManager } from "../../mcp/manager";
35
45
  import type { MCPServerConfig } from "../../mcp/types";
36
46
  import { theme } from "../../modes/theme/theme";
37
47
  import type { AgentSession, AgentSessionEvent } from "../../session/agent-session";
38
- import { SessionManager, type SessionInfo as StoredSessionInfo } from "../../session/session-manager";
48
+ import {
49
+ SessionManager,
50
+ type SessionInfo as StoredSessionInfo,
51
+ type UsageStatistics,
52
+ } from "../../session/session-manager";
39
53
  import { parseThinkingLevel } from "../../thinking";
40
54
  import { mapAgentSessionEventToAcpSessionUpdates, mapToolKind } from "./acp-event-mapper";
41
55
 
@@ -53,14 +67,23 @@ type AgentImageContent = {
53
67
  };
54
68
 
55
69
  type PromptTurnState = {
56
- messageId: string | null;
70
+ userMessageId: string;
57
71
  cancelRequested: boolean;
58
72
  settled: boolean;
73
+ usageBaseline: UsageStatistics;
59
74
  unsubscribe: (() => void) | undefined;
60
75
  resolve: (value: PromptResponse) => void;
61
76
  reject: (reason?: unknown) => void;
62
77
  };
63
78
 
79
+ type ManagedSessionRecord = {
80
+ session: AgentSession;
81
+ mcpManager: MCPManager | undefined;
82
+ promptTurn: PromptTurnState | undefined;
83
+ liveMessageIds: WeakMap<object, string>;
84
+ extensionsConfigured: boolean;
85
+ };
86
+
64
87
  type ReplayableMessage = {
65
88
  role: string;
66
89
  content?: unknown;
@@ -86,6 +109,8 @@ type MCPSourceMap = {
86
109
  [name: string]: MCPSource;
87
110
  };
88
111
 
112
+ type CreateAcpSession = (cwd: string) => Promise<AgentSession>;
113
+
89
114
  const acpExtensionUiContext: ExtensionUIContext = {
90
115
  select: async () => undefined,
91
116
  confirm: async () => false,
@@ -118,17 +143,20 @@ const acpExtensionUiContext: ExtensionUIContext = {
118
143
 
119
144
  export class AcpAgent implements Agent {
120
145
  #connection: AgentSideConnection;
121
- #session: AgentSession;
122
- #mcpManager: MCPManager | undefined;
123
- #promptTurn: PromptTurnState | undefined;
124
- #hasOpenedSession = false;
146
+ #initialSession: AgentSession | undefined;
147
+ #createSession: CreateAcpSession;
148
+ #sessions = new Map<string, ManagedSessionRecord>();
149
+ #disposePromise: Promise<void> | undefined;
150
+ #cleanupRegistered = false;
125
151
 
126
- constructor(connection: AgentSideConnection, session: AgentSession) {
152
+ constructor(connection: AgentSideConnection, initialSession: AgentSession, createSession: CreateAcpSession) {
127
153
  this.#connection = connection;
128
- this.#session = session;
154
+ this.#initialSession = initialSession;
155
+ this.#createSession = createSession;
129
156
  }
130
157
 
131
158
  async initialize(_params: InitializeRequest): Promise<InitializeResponse> {
159
+ this.#registerConnectionCleanup();
132
160
  return {
133
161
  protocolVersion: PROTOCOL_VERSION,
134
162
  agentInfo: {
@@ -155,6 +183,9 @@ export class AcpAgent implements Agent {
155
183
  },
156
184
  sessionCapabilities: {
157
185
  list: {},
186
+ fork: {},
187
+ resume: {},
188
+ close: {},
158
189
  },
159
190
  },
160
191
  };
@@ -166,50 +197,27 @@ export class AcpAgent implements Agent {
166
197
 
167
198
  async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
168
199
  this.#assertAbsoluteCwd(params.cwd);
169
- await this.#session.sessionManager.flush();
170
- await this.#session.sessionManager.moveTo(params.cwd);
171
- if (this.#hasOpenedSession) {
172
- const success = await this.#session.newSession();
173
- if (!success) {
174
- throw new Error("ACP session creation was cancelled");
175
- }
176
- }
177
- this.#hasOpenedSession = true;
178
- await this.#session.sessionManager.ensureOnDisk();
179
- await this.#configureExtensions();
180
- await this.#configureMcpServers(params.mcpServers);
200
+ const record = await this.#createNewSessionRecord(params.cwd, params.mcpServers);
181
201
  const response: NewSessionResponse = {
182
- sessionId: this.#sessionId,
183
- configOptions: this.#buildConfigOptions(),
202
+ sessionId: record.session.sessionId,
203
+ configOptions: this.#buildConfigOptions(record.session),
204
+ models: this.#buildModelState(record.session),
184
205
  modes: this.#buildModeState(),
185
206
  };
186
- this.#scheduleBootstrapUpdates(this.#sessionId);
207
+ this.#scheduleBootstrapUpdates(record.session.sessionId);
187
208
  return response;
188
209
  }
189
210
 
190
211
  async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
191
212
  this.#assertAbsoluteCwd(params.cwd);
192
- await this.#session.sessionManager.flush();
193
- const storedSession = await this.#findStoredSession(params.sessionId, params.cwd);
194
- if (!storedSession) {
195
- throw new Error(`ACP session not found: ${params.sessionId}`);
196
- }
197
- const currentSessionFile = this.#session.sessionManager.getSessionFile();
198
- if (currentSessionFile !== storedSession.path) {
199
- const success = await this.#session.switchSession(storedSession.path);
200
- if (!success) {
201
- throw new Error(`ACP session load was cancelled: ${params.sessionId}`);
202
- }
203
- }
204
- this.#hasOpenedSession = true;
205
- await this.#configureExtensions();
206
- await this.#configureMcpServers(params.mcpServers);
207
- await this.#replaySessionHistory();
213
+ const record = await this.#loadManagedSession(params.sessionId, params.cwd, params.mcpServers);
214
+ await this.#replaySessionHistory(record);
208
215
  const response: LoadSessionResponse = {
209
- configOptions: this.#buildConfigOptions(),
216
+ configOptions: this.#buildConfigOptions(record.session),
217
+ models: this.#buildModelState(record.session),
210
218
  modes: this.#buildModeState(),
211
219
  };
212
- this.#scheduleBootstrapUpdates(this.#sessionId);
220
+ this.#scheduleBootstrapUpdates(record.session.sessionId);
213
221
  return response;
214
222
  }
215
223
 
@@ -217,7 +225,9 @@ export class AcpAgent implements Agent {
217
225
  if (params.cwd) {
218
226
  this.#assertAbsoluteCwd(params.cwd);
219
227
  }
220
- await this.#session.sessionManager.flush();
228
+ for (const record of this.#sessions.values()) {
229
+ await record.session.sessionManager.flush();
230
+ }
221
231
  const sessions = await this.#listStoredSessions(params.cwd ?? undefined);
222
232
  const offset = this.#parseCursor(params.cursor ?? undefined);
223
233
  const paged = sessions.slice(offset, offset + SESSION_PAGE_SIZE);
@@ -228,20 +238,54 @@ export class AcpAgent implements Agent {
228
238
  };
229
239
  }
230
240
 
241
+ async unstable_resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
242
+ this.#assertAbsoluteCwd(params.cwd);
243
+ const record = await this.#resumeManagedSession(params.sessionId, params.cwd, params.mcpServers ?? []);
244
+ const response: ResumeSessionResponse = {
245
+ configOptions: this.#buildConfigOptions(record.session),
246
+ models: this.#buildModelState(record.session),
247
+ modes: this.#buildModeState(),
248
+ };
249
+ this.#scheduleBootstrapUpdates(record.session.sessionId);
250
+ return response;
251
+ }
252
+
253
+ async unstable_forkSession(params: ForkSessionRequest): Promise<ForkSessionResponse> {
254
+ this.#assertAbsoluteCwd(params.cwd);
255
+ const record = await this.#forkManagedSession(params);
256
+ const response: ForkSessionResponse = {
257
+ sessionId: record.session.sessionId,
258
+ configOptions: this.#buildConfigOptions(record.session),
259
+ models: this.#buildModelState(record.session),
260
+ modes: this.#buildModeState(),
261
+ };
262
+ this.#scheduleBootstrapUpdates(record.session.sessionId);
263
+ return response;
264
+ }
265
+
266
+ async unstable_closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
267
+ const record = this.#sessions.get(params.sessionId);
268
+ if (!record) {
269
+ return {};
270
+ }
271
+ await this.#closeManagedSession(params.sessionId, record);
272
+ return {};
273
+ }
274
+
231
275
  async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse> {
232
- this.#assertSameSession(params.sessionId);
276
+ const record = this.#getSessionRecord(params.sessionId);
233
277
  if (params.modeId !== ACP_MODE_ID) {
234
278
  throw new Error(`Unsupported ACP mode: ${params.modeId}`);
235
279
  }
236
280
  await this.#connection.sessionUpdate({
237
- sessionId: this.#sessionId,
281
+ sessionId: record.session.sessionId,
238
282
  update: this.#buildCurrentModeUpdate(),
239
283
  });
240
284
  return {};
241
285
  }
242
286
 
243
287
  async setSessionConfigOption(params: SetSessionConfigOptionRequest): Promise<SetSessionConfigOptionResponse> {
244
- this.#assertSameSession(params.sessionId);
288
+ const record = this.#getSessionRecord(params.sessionId);
245
289
  if (typeof params.value === "boolean") {
246
290
  throw new Error(`Unsupported boolean ACP config option: ${params.configId}`);
247
291
  }
@@ -253,18 +297,18 @@ export class AcpAgent implements Agent {
253
297
  }
254
298
  break;
255
299
  case MODEL_CONFIG_ID:
256
- await this.#setModelById(params.value);
300
+ await this.#setModelById(record.session, params.value);
257
301
  break;
258
302
  case THINKING_CONFIG_ID:
259
- this.#setThinkingLevelById(params.value);
303
+ this.#setThinkingLevelById(record.session, params.value);
260
304
  break;
261
305
  default:
262
306
  throw new Error(`Unknown ACP config option: ${params.configId}`);
263
307
  }
264
308
 
265
- const configOptions = this.#buildConfigOptions();
309
+ const configOptions = this.#buildConfigOptions(record.session);
266
310
  await this.#connection.sessionUpdate({
267
- sessionId: this.#sessionId,
311
+ sessionId: record.session.sessionId,
268
312
  update: {
269
313
  sessionUpdate: "config_option_update",
270
314
  configOptions,
@@ -273,49 +317,64 @@ export class AcpAgent implements Agent {
273
317
  return { configOptions };
274
318
  }
275
319
 
320
+ async unstable_setSessionModel(params: SetSessionModelRequest): Promise<SetSessionModelResponse> {
321
+ const record = this.#getSessionRecord(params.sessionId);
322
+ await this.#setModelById(record.session, params.modelId);
323
+ await this.#connection.sessionUpdate({
324
+ sessionId: record.session.sessionId,
325
+ update: {
326
+ sessionUpdate: "config_option_update",
327
+ configOptions: this.#buildConfigOptions(record.session),
328
+ },
329
+ });
330
+ return {};
331
+ }
332
+
276
333
  async prompt(params: PromptRequest): Promise<PromptResponse> {
277
- this.#assertSameSession(params.sessionId);
278
- if (this.#promptTurn && !this.#promptTurn.settled) {
334
+ const record = this.#getSessionRecord(params.sessionId);
335
+ if (record.promptTurn && !record.promptTurn.settled) {
279
336
  throw new Error("ACP prompt already in progress for this session");
280
337
  }
281
338
 
282
339
  const converted = this.#convertPromptBlocks(params.prompt);
283
340
  const pendingPrompt = Promise.withResolvers<PromptResponse>();
284
- this.#promptTurn = {
285
- messageId: params.messageId ?? null,
341
+ record.promptTurn = {
342
+ userMessageId: params.messageId ?? crypto.randomUUID(),
286
343
  cancelRequested: false,
287
344
  settled: false,
345
+ usageBaseline: this.#cloneUsageStatistics(record.session.sessionManager.getUsageStatistics()),
288
346
  unsubscribe: undefined,
289
347
  resolve: pendingPrompt.resolve,
290
348
  reject: pendingPrompt.reject,
291
349
  };
292
350
 
293
- this.#promptTurn.unsubscribe = this.#session.subscribe(event => {
294
- void this.#handlePromptEvent(event);
351
+ record.promptTurn.unsubscribe = record.session.subscribe(event => {
352
+ void this.#handlePromptEvent(record, event);
295
353
  });
296
354
 
297
- this.#session.prompt(converted.text, { images: converted.images }).catch((error: unknown) => {
298
- this.#finishPrompt(undefined, error);
355
+ record.session.prompt(converted.text, { images: converted.images }).catch((error: unknown) => {
356
+ this.#finishPrompt(record, undefined, error);
299
357
  });
300
358
 
301
359
  return await pendingPrompt.promise;
302
360
  }
303
361
 
304
362
  async cancel(params: { sessionId: string }): Promise<void> {
305
- this.#assertSameSession(params.sessionId);
306
- const promptTurn = this.#promptTurn;
363
+ const record = this.#getSessionRecord(params.sessionId);
364
+ const promptTurn = record.promptTurn;
307
365
  if (!promptTurn || promptTurn.settled) {
308
366
  return;
309
367
  }
310
368
  promptTurn.cancelRequested = true;
311
369
  try {
312
- await this.#session.abort();
313
- this.#finishPrompt({
370
+ await record.session.abort();
371
+ this.#finishPrompt(record, {
314
372
  stopReason: "cancelled",
315
- userMessageId: promptTurn.messageId,
373
+ usage: this.#buildTurnUsage(promptTurn.usageBaseline, record.session.sessionManager.getUsageStatistics()),
374
+ userMessageId: promptTurn.userMessageId,
316
375
  });
317
376
  } catch (error: unknown) {
318
- this.#finishPrompt(undefined, error);
377
+ this.#finishPrompt(record, undefined, error);
319
378
  }
320
379
  }
321
380
 
@@ -333,37 +392,203 @@ export class AcpAgent implements Agent {
333
392
  return this.#connection.closed;
334
393
  }
335
394
 
336
- get #sessionId(): string {
337
- return this.#session.sessionId;
395
+ #registerConnectionCleanup(): void {
396
+ if (this.#cleanupRegistered) {
397
+ return;
398
+ }
399
+ this.#cleanupRegistered = true;
400
+ this.#connection.signal.addEventListener(
401
+ "abort",
402
+ () => {
403
+ void this.#disposeAllSessions();
404
+ },
405
+ { once: true },
406
+ );
407
+ }
408
+
409
+ async #createNewSessionRecord(cwd: string, mcpServers: McpServer[]): Promise<ManagedSessionRecord> {
410
+ const session = await this.#createSession(path.resolve(cwd));
411
+ try {
412
+ await session.sessionManager.ensureOnDisk();
413
+ } catch (error) {
414
+ await this.#disposeStandaloneSession(session);
415
+ throw error;
416
+ }
417
+ return await this.#registerPreparedSession(session, mcpServers);
418
+ }
419
+
420
+ async #loadManagedSession(sessionId: string, cwd: string, mcpServers: McpServer[]): Promise<ManagedSessionRecord> {
421
+ const existing = this.#sessions.get(sessionId);
422
+ if (existing) {
423
+ this.#assertMatchingCwd(existing.session, cwd);
424
+ await this.#configureMcpServers(existing, mcpServers);
425
+ return existing;
426
+ }
427
+
428
+ const storedSession = await this.#findStoredSession(sessionId, cwd);
429
+ if (!storedSession) {
430
+ throw new Error(`ACP session not found: ${sessionId}`);
431
+ }
432
+ return await this.#openStoredSession(storedSession.path, cwd, mcpServers, sessionId);
433
+ }
434
+
435
+ async #resumeManagedSession(sessionId: string, cwd: string, mcpServers: McpServer[]): Promise<ManagedSessionRecord> {
436
+ const existing = this.#sessions.get(sessionId);
437
+ if (existing) {
438
+ this.#assertMatchingCwd(existing.session, cwd);
439
+ await this.#configureMcpServers(existing, mcpServers);
440
+ return existing;
441
+ }
442
+
443
+ const storedSession = await this.#findStoredSession(sessionId, cwd);
444
+ if (!storedSession) {
445
+ throw new Error(`ACP session not found: ${sessionId}`);
446
+ }
447
+ return await this.#openStoredSession(storedSession.path, cwd, mcpServers, sessionId);
448
+ }
449
+
450
+ async #forkManagedSession(params: ForkSessionRequest): Promise<ManagedSessionRecord> {
451
+ const sourcePath = await this.#resolveForkSourceSessionPath(params.sessionId);
452
+ const session = await this.#createSession(path.resolve(params.cwd));
453
+ try {
454
+ const success = await session.switchSession(sourcePath);
455
+ if (!success) {
456
+ throw new Error(`ACP session fork was cancelled: ${params.sessionId}`);
457
+ }
458
+ const forked = await session.fork();
459
+ if (!forked) {
460
+ throw new Error(`ACP session fork failed: ${params.sessionId}`);
461
+ }
462
+ } catch (error) {
463
+ await this.#disposeStandaloneSession(session);
464
+ throw error;
465
+ }
466
+ return await this.#registerPreparedSession(session, params.mcpServers ?? []);
467
+ }
468
+
469
+ async #openStoredSession(
470
+ sessionPath: string,
471
+ cwd: string,
472
+ mcpServers: McpServer[],
473
+ sessionId: string,
474
+ ): Promise<ManagedSessionRecord> {
475
+ const session = await this.#createSession(path.resolve(cwd));
476
+ try {
477
+ const success = await session.switchSession(sessionPath);
478
+ if (!success) {
479
+ throw new Error(`ACP session load was cancelled: ${sessionId}`);
480
+ }
481
+ } catch (error) {
482
+ await this.#disposeStandaloneSession(session);
483
+ throw error;
484
+ }
485
+ return await this.#registerPreparedSession(session, mcpServers);
486
+ }
487
+
488
+ async #registerPreparedSession(session: AgentSession, mcpServers: McpServer[]): Promise<ManagedSessionRecord> {
489
+ const record = this.#createManagedSessionRecord(session);
490
+ try {
491
+ await this.#configureExtensions(record);
492
+ await this.#configureMcpServers(record, mcpServers);
493
+ this.#sessions.set(session.sessionId, record);
494
+ return record;
495
+ } catch (error) {
496
+ await this.#disposeSessionRecord(record);
497
+ throw error;
498
+ }
499
+ }
500
+
501
+ #createManagedSessionRecord(session: AgentSession): ManagedSessionRecord {
502
+ return {
503
+ session,
504
+ mcpManager: undefined,
505
+ promptTurn: undefined,
506
+ liveMessageIds: new WeakMap<object, string>(),
507
+ extensionsConfigured: false,
508
+ };
509
+ }
510
+
511
+ #getSessionRecord(sessionId: string): ManagedSessionRecord {
512
+ const record = this.#sessions.get(sessionId);
513
+ if (!record) {
514
+ throw new Error(`Unsupported ACP session: ${sessionId}`);
515
+ }
516
+ return record;
517
+ }
518
+
519
+ #assertMatchingCwd(session: AgentSession, cwd: string): void {
520
+ const expected = path.resolve(cwd);
521
+ const actual = path.resolve(session.sessionManager.getCwd());
522
+ if (actual !== expected) {
523
+ throw new Error(`ACP session ${session.sessionId} is already loaded for ${actual}, not ${expected}`);
524
+ }
525
+ }
526
+
527
+ async #resolveForkSourceSessionPath(sessionId: string): Promise<string> {
528
+ const loaded = this.#sessions.get(sessionId);
529
+ if (loaded) {
530
+ const promptTurn = loaded.promptTurn;
531
+ if (promptTurn && !promptTurn.settled) {
532
+ throw new Error(`ACP session fork is unavailable while a prompt is in progress: ${sessionId}`);
533
+ }
534
+ await loaded.session.sessionManager.flush();
535
+ const sessionPath = loaded.session.sessionManager.getSessionFile();
536
+ if (!sessionPath) {
537
+ throw new Error(`ACP session cannot be forked before it is persisted: ${sessionId}`);
538
+ }
539
+ return sessionPath;
540
+ }
541
+
542
+ const storedSession = await this.#findStoredSessionById(sessionId);
543
+ if (!storedSession) {
544
+ throw new Error(`ACP session not found: ${sessionId}`);
545
+ }
546
+ return storedSession.path;
338
547
  }
339
548
 
340
- async #handlePromptEvent(event: AgentSessionEvent): Promise<void> {
341
- const promptTurn = this.#promptTurn;
549
+ async #handlePromptEvent(record: ManagedSessionRecord, event: AgentSessionEvent): Promise<void> {
550
+ const promptTurn = record.promptTurn;
342
551
  if (!promptTurn || promptTurn.settled) {
343
552
  return;
344
553
  }
345
554
 
346
- for (const notification of mapAgentSessionEventToAcpSessionUpdates(event, this.#sessionId)) {
555
+ for (const notification of mapAgentSessionEventToAcpSessionUpdates(event, record.session.sessionId, {
556
+ getMessageId: message => this.#getLiveMessageId(record, message),
557
+ })) {
347
558
  await this.#connection.sessionUpdate(notification);
348
559
  }
349
560
 
350
561
  if (event.type === "agent_end") {
351
- await this.#emitEndOfTurnUpdates();
352
- this.#finishPrompt({
562
+ await this.#emitEndOfTurnUpdates(record);
563
+ this.#finishPrompt(record, {
353
564
  stopReason: promptTurn.cancelRequested ? "cancelled" : "end_turn",
354
- userMessageId: promptTurn.messageId,
565
+ usage: this.#buildTurnUsage(promptTurn.usageBaseline, record.session.sessionManager.getUsageStatistics()),
566
+ userMessageId: promptTurn.userMessageId,
355
567
  });
356
568
  }
357
569
  }
358
570
 
359
- #finishPrompt(response?: PromptResponse, error?: unknown): void {
360
- const promptTurn = this.#promptTurn;
571
+ #getLiveMessageId(record: ManagedSessionRecord, message: unknown): string | undefined {
572
+ if (typeof message !== "object" || message === null) {
573
+ return undefined;
574
+ }
575
+ const existing = record.liveMessageIds.get(message);
576
+ if (existing) {
577
+ return existing;
578
+ }
579
+ const nextMessageId = crypto.randomUUID();
580
+ record.liveMessageIds.set(message, nextMessageId);
581
+ return nextMessageId;
582
+ }
583
+
584
+ #finishPrompt(record: ManagedSessionRecord, response?: PromptResponse, error?: unknown): void {
585
+ const promptTurn = record.promptTurn;
361
586
  if (!promptTurn || promptTurn.settled) {
362
587
  return;
363
588
  }
364
589
  promptTurn.settled = true;
365
590
  promptTurn.unsubscribe?.();
366
- this.#promptTurn = undefined;
591
+ record.promptTurn = undefined;
367
592
  if (error !== undefined) {
368
593
  promptTurn.reject(error);
369
594
  return;
@@ -371,12 +596,6 @@ export class AcpAgent implements Agent {
371
596
  promptTurn.resolve(response ?? { stopReason: "end_turn" });
372
597
  }
373
598
 
374
- #assertSameSession(sessionId: string): void {
375
- if (sessionId !== this.#sessionId) {
376
- throw new Error(`Unsupported ACP session: ${sessionId}`);
377
- }
378
- }
379
-
380
599
  #assertAbsoluteCwd(cwd: string): void {
381
600
  if (!path.isAbsolute(cwd)) {
382
601
  throw new Error(`ACP cwd must be absolute: ${cwd}`);
@@ -415,7 +634,7 @@ export class AcpAgent implements Agent {
415
634
  };
416
635
  }
417
636
 
418
- #buildConfigOptions(): SessionConfigOption[] {
637
+ #buildConfigOptions(session: AgentSession): SessionConfigOption[] {
419
638
  const configOptions: SessionConfigOption[] = [
420
639
  {
421
640
  id: MODE_CONFIG_ID,
@@ -427,8 +646,8 @@ export class AcpAgent implements Agent {
427
646
  },
428
647
  ];
429
648
 
430
- const models = this.#session.getAvailableModels();
431
- const currentModel = this.#session.model;
649
+ const models = session.getAvailableModels();
650
+ const currentModel = session.model;
432
651
  if (models.length > 0) {
433
652
  configOptions.push({
434
653
  id: MODEL_CONFIG_ID,
@@ -449,16 +668,38 @@ export class AcpAgent implements Agent {
449
668
  name: "Thinking",
450
669
  category: "thought_level",
451
670
  type: "select",
452
- currentValue: this.#toThinkingConfigValue(this.#session.thinkingLevel),
453
- options: this.#buildThinkingOptions(),
671
+ currentValue: this.#toThinkingConfigValue(session.thinkingLevel),
672
+ options: this.#buildThinkingOptions(session),
454
673
  });
455
674
  return configOptions;
456
675
  }
457
676
 
458
- #buildThinkingOptions(): Array<{ value: string; name: string; description?: string }> {
677
+ #buildModelState(session: AgentSession): SessionModelState | undefined {
678
+ const models = session.getAvailableModels();
679
+ if (models.length === 0) {
680
+ return undefined;
681
+ }
682
+
683
+ const availableModels = models.map(model => ({
684
+ modelId: this.#toModelId(model),
685
+ name: model.name,
686
+ description: `${model.provider}/${model.id}`,
687
+ }));
688
+ const currentModelId = session.model ? this.#toModelId(session.model) : availableModels[0]?.modelId;
689
+ if (!currentModelId) {
690
+ return undefined;
691
+ }
692
+
693
+ return {
694
+ availableModels,
695
+ currentModelId,
696
+ };
697
+ }
698
+
699
+ #buildThinkingOptions(session: AgentSession): Array<{ value: string; name: string; description?: string }> {
459
700
  return [
460
701
  { value: THINKING_OFF, name: "Off" },
461
- ...this.#session.getAvailableThinkingLevels().map(level => ({
702
+ ...session.getAvailableThinkingLevels().map(level => ({
462
703
  value: level,
463
704
  name: level,
464
705
  })),
@@ -469,20 +710,20 @@ export class AcpAgent implements Agent {
469
710
  return value && value !== "inherit" ? value : THINKING_OFF;
470
711
  }
471
712
 
472
- async #setModelById(modelId: string): Promise<void> {
473
- const model = this.#session.getAvailableModels().find(candidate => this.#toModelId(candidate) === modelId);
713
+ async #setModelById(session: AgentSession, modelId: string): Promise<void> {
714
+ const model = session.getAvailableModels().find(candidate => this.#toModelId(candidate) === modelId);
474
715
  if (!model) {
475
716
  throw new Error(`Unknown ACP model: ${modelId}`);
476
717
  }
477
- await this.#session.setModel(model);
718
+ await session.setModel(model);
478
719
  }
479
720
 
480
- #setThinkingLevelById(value: string): void {
721
+ #setThinkingLevelById(session: AgentSession, value: string): void {
481
722
  const thinkingLevel = parseThinkingLevel(value);
482
723
  if (!thinkingLevel) {
483
724
  throw new Error(`Unknown ACP thinking level: ${value}`);
484
725
  }
485
- this.#session.setThinkingLevel(thinkingLevel);
726
+ session.setThinkingLevel(thinkingLevel);
486
727
  }
487
728
 
488
729
  #toModelId(model: Model): string {
@@ -503,7 +744,7 @@ export class AcpAgent implements Agent {
503
744
  };
504
745
  }
505
746
 
506
- async #buildAvailableCommands(): Promise<AvailableCommand[]> {
747
+ async #buildAvailableCommands(session: AgentSession): Promise<AvailableCommand[]> {
507
748
  const commands: AvailableCommand[] = [];
508
749
  const seenNames = new Set<string>();
509
750
  const appendCommand = (command: AvailableCommand): void => {
@@ -514,7 +755,7 @@ export class AcpAgent implements Agent {
514
755
  commands.push(command);
515
756
  };
516
757
 
517
- for (const command of this.#session.customCommands) {
758
+ for (const command of session.customCommands) {
518
759
  appendCommand({
519
760
  name: command.command.name,
520
761
  description: command.command.description,
@@ -522,7 +763,7 @@ export class AcpAgent implements Agent {
522
763
  });
523
764
  }
524
765
 
525
- for (const command of await loadSlashCommands({ cwd: this.#session.sessionManager.getCwd() })) {
766
+ for (const command of await loadSlashCommands({ cwd: session.sessionManager.getCwd() })) {
526
767
  appendCommand({
527
768
  name: command.name,
528
769
  description: command.description,
@@ -543,41 +784,44 @@ export class AcpAgent implements Agent {
543
784
 
544
785
  #scheduleBootstrapUpdates(sessionId: string): void {
545
786
  setTimeout(() => {
546
- if (sessionId !== this.#sessionId || this.#connection.signal.aborted) {
787
+ if (this.#connection.signal.aborted) {
547
788
  return;
548
789
  }
549
- void this.#emitBootstrapUpdates(sessionId);
790
+ const record = this.#sessions.get(sessionId);
791
+ if (!record) {
792
+ return;
793
+ }
794
+ void this.#emitBootstrapUpdates(sessionId, record);
550
795
  }, 0);
551
796
  }
552
797
 
553
- async #emitBootstrapUpdates(sessionId: string): Promise<void> {
554
- if (sessionId !== this.#sessionId) {
798
+ async #emitBootstrapUpdates(sessionId: string, record: ManagedSessionRecord): Promise<void> {
799
+ if (this.#sessions.get(sessionId) !== record) {
555
800
  return;
556
801
  }
557
802
  await this.#connection.sessionUpdate({
558
803
  sessionId,
559
804
  update: {
560
805
  sessionUpdate: "available_commands_update",
561
- availableCommands: await this.#buildAvailableCommands(),
806
+ availableCommands: await this.#buildAvailableCommands(record.session),
562
807
  },
563
808
  });
564
809
  await this.#connection.sessionUpdate({
565
810
  sessionId,
566
811
  update: {
567
812
  sessionUpdate: "session_info_update",
568
- title: this.#session.sessionName,
569
- updatedAt: this.#session.sessionManager.getHeader()?.timestamp,
813
+ title: record.session.sessionName,
814
+ updatedAt: record.session.sessionManager.getHeader()?.timestamp,
570
815
  },
571
816
  });
572
817
  }
573
818
 
574
- async #emitEndOfTurnUpdates(): Promise<void> {
575
- const sessionId = this.#sessionId;
819
+ async #emitEndOfTurnUpdates(record: ManagedSessionRecord): Promise<void> {
820
+ const sessionId = record.session.sessionId;
576
821
 
577
- // Emit usage update with context token counts
578
- const contextUsage = this.#session.getContextUsage();
822
+ const contextUsage = record.session.getContextUsage();
579
823
  if (contextUsage) {
580
- const usageStats = this.#session.sessionManager.getUsageStatistics();
824
+ const usageStats = record.session.sessionManager.getUsageStatistics();
581
825
  await this.#connection.sessionUpdate({
582
826
  sessionId,
583
827
  update: {
@@ -589,17 +833,52 @@ export class AcpAgent implements Agent {
589
833
  });
590
834
  }
591
835
 
592
- // Push latest session title
593
836
  await this.#connection.sessionUpdate({
594
837
  sessionId,
595
838
  update: {
596
839
  sessionUpdate: "session_info_update",
597
- title: this.#session.sessionName,
840
+ title: record.session.sessionName,
598
841
  updatedAt: new Date().toISOString(),
599
842
  },
600
843
  });
601
844
  }
602
845
 
846
+ #cloneUsageStatistics(usage: UsageStatistics): UsageStatistics {
847
+ return {
848
+ input: usage.input,
849
+ output: usage.output,
850
+ cacheRead: usage.cacheRead,
851
+ cacheWrite: usage.cacheWrite,
852
+ premiumRequests: usage.premiumRequests,
853
+ cost: usage.cost,
854
+ };
855
+ }
856
+
857
+ #buildTurnUsage(previous: UsageStatistics, current: UsageStatistics): Usage | undefined {
858
+ const inputTokens = Math.max(0, current.input - previous.input);
859
+ const outputTokens = Math.max(0, current.output - previous.output);
860
+ const cachedReadTokens = Math.max(0, current.cacheRead - previous.cacheRead);
861
+ const cachedWriteTokens = Math.max(0, current.cacheWrite - previous.cacheWrite);
862
+ const totalTokens = inputTokens + outputTokens + cachedReadTokens + cachedWriteTokens;
863
+
864
+ if (totalTokens === 0) {
865
+ return undefined;
866
+ }
867
+
868
+ const usage: Usage = {
869
+ inputTokens,
870
+ outputTokens,
871
+ totalTokens,
872
+ };
873
+ if (cachedReadTokens > 0) {
874
+ usage.cachedReadTokens = cachedReadTokens;
875
+ }
876
+ if (cachedWriteTokens > 0) {
877
+ usage.cachedWriteTokens = cachedWriteTokens;
878
+ }
879
+ return usage;
880
+ }
881
+
603
882
  async #listStoredSessions(cwd?: string): Promise<StoredSessionInfo[]> {
604
883
  const sessions = cwd ? await SessionManager.list(cwd) : await SessionManager.listAll();
605
884
  return sessions.sort((left, right) => right.modified.getTime() - left.modified.getTime());
@@ -610,6 +889,11 @@ export class AcpAgent implements Agent {
610
889
  return sessions.find(session => session.id === sessionId);
611
890
  }
612
891
 
892
+ async #findStoredSessionById(sessionId: string): Promise<StoredSessionInfo | undefined> {
893
+ const sessions = await this.#listStoredSessions();
894
+ return sessions.find(session => session.id === sessionId);
895
+ }
896
+
613
897
  #parseCursor(cursor: string | undefined): number {
614
898
  if (!cursor) {
615
899
  return 0;
@@ -621,17 +905,17 @@ export class AcpAgent implements Agent {
621
905
  return parsed;
622
906
  }
623
907
 
624
- async #replaySessionHistory(): Promise<void> {
625
- for (const message of this.#session.sessionManager.buildSessionContext().messages as ReplayableMessage[]) {
626
- for (const notification of this.#messageToReplayNotifications(message)) {
908
+ async #replaySessionHistory(record: ManagedSessionRecord): Promise<void> {
909
+ for (const message of record.session.sessionManager.buildSessionContext().messages as ReplayableMessage[]) {
910
+ for (const notification of this.#messageToReplayNotifications(record.session.sessionId, message)) {
627
911
  await this.#connection.sessionUpdate(notification);
628
912
  }
629
913
  }
630
914
  }
631
915
 
632
- #messageToReplayNotifications(message: ReplayableMessage): SessionNotification[] {
916
+ #messageToReplayNotifications(sessionId: string, message: ReplayableMessage): SessionNotification[] {
633
917
  if (message.role === "assistant") {
634
- return this.#replayAssistantMessage(message);
918
+ return this.#replayAssistantMessage(sessionId, message);
635
919
  }
636
920
  if (
637
921
  message.role === "user" ||
@@ -639,28 +923,42 @@ export class AcpAgent implements Agent {
639
923
  message.role === "custom" ||
640
924
  message.role === "hookMessage"
641
925
  ) {
642
- return this.#wrapReplayContent(this.#extractReplayContent(message.content, undefined), "user_message_chunk");
926
+ return this.#wrapReplayContent(
927
+ sessionId,
928
+ this.#extractReplayContent(message.content, undefined),
929
+ "user_message_chunk",
930
+ crypto.randomUUID(),
931
+ );
643
932
  }
644
933
  if (
645
934
  message.role === "toolResult" &&
646
935
  typeof message.toolCallId === "string" &&
647
936
  typeof message.toolName === "string"
648
937
  ) {
649
- return this.#replayToolResult({ ...message, toolCallId: message.toolCallId, toolName: message.toolName });
938
+ return this.#replayToolResult(sessionId, {
939
+ ...message,
940
+ toolCallId: message.toolCallId,
941
+ toolName: message.toolName,
942
+ });
650
943
  }
651
944
  if (
652
945
  message.role === "bashExecution" ||
653
946
  message.role === "pythonExecution" ||
654
947
  message.role === "compactionSummary"
655
948
  ) {
656
- return this.#wrapReplayContent(this.#extractReplayContent(message.content, undefined), "user_message_chunk");
949
+ return this.#wrapReplayContent(
950
+ sessionId,
951
+ this.#extractReplayContent(message.content, undefined),
952
+ "user_message_chunk",
953
+ crypto.randomUUID(),
954
+ );
657
955
  }
658
956
  return [];
659
957
  }
660
958
 
661
- #replayAssistantMessage(message: ReplayableMessage): SessionNotification[] {
959
+ #replayAssistantMessage(sessionId: string, message: ReplayableMessage): SessionNotification[] {
662
960
  const notifications: SessionNotification[] = [];
663
- const sessionId = this.#sessionId;
961
+ const messageId = crypto.randomUUID();
664
962
  if (Array.isArray(message.content)) {
665
963
  for (const item of message.content) {
666
964
  if (typeof item !== "object" || item === null || !("type" in item)) {
@@ -669,7 +967,11 @@ export class AcpAgent implements Agent {
669
967
  if (item.type === "text" && "text" in item && typeof item.text === "string" && item.text.length > 0) {
670
968
  notifications.push({
671
969
  sessionId,
672
- update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: item.text } },
970
+ update: {
971
+ sessionUpdate: "agent_message_chunk",
972
+ content: { type: "text", text: item.text },
973
+ messageId,
974
+ },
673
975
  });
674
976
  continue;
675
977
  }
@@ -681,7 +983,11 @@ export class AcpAgent implements Agent {
681
983
  ) {
682
984
  notifications.push({
683
985
  sessionId,
684
- update: { sessionUpdate: "agent_thought_chunk", content: { type: "text", text: item.thinking } },
986
+ update: {
987
+ sessionUpdate: "agent_thought_chunk",
988
+ content: { type: "text", text: item.thinking },
989
+ messageId,
990
+ },
685
991
  });
686
992
  continue;
687
993
  }
@@ -709,13 +1015,18 @@ export class AcpAgent implements Agent {
709
1015
  if (notifications.length === 0 && message.errorMessage) {
710
1016
  notifications.push({
711
1017
  sessionId,
712
- update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: message.errorMessage } },
1018
+ update: {
1019
+ sessionUpdate: "agent_message_chunk",
1020
+ content: { type: "text", text: message.errorMessage },
1021
+ messageId,
1022
+ },
713
1023
  });
714
1024
  }
715
1025
  return notifications;
716
1026
  }
717
1027
 
718
1028
  #replayToolResult(
1029
+ sessionId: string,
719
1030
  message: Required<Pick<ReplayableMessage, "toolCallId" | "toolName">> & ReplayableMessage,
720
1031
  ): SessionNotification[] {
721
1032
  const args = this.#buildReplayToolArgs(message.details);
@@ -737,8 +1048,8 @@ export class AcpAgent implements Agent {
737
1048
  },
738
1049
  };
739
1050
  return [
740
- ...mapAgentSessionEventToAcpSessionUpdates(startEvent, this.#sessionId),
741
- ...mapAgentSessionEventToAcpSessionUpdates(endEvent, this.#sessionId),
1051
+ ...mapAgentSessionEventToAcpSessionUpdates(startEvent, sessionId),
1052
+ ...mapAgentSessionEventToAcpSessionUpdates(endEvent, sessionId),
742
1053
  ];
743
1054
  }
744
1055
 
@@ -751,14 +1062,17 @@ export class AcpAgent implements Agent {
751
1062
  }
752
1063
 
753
1064
  #wrapReplayContent(
1065
+ sessionId: string,
754
1066
  content: PromptRequest["prompt"],
755
1067
  kind: "agent_message_chunk" | "user_message_chunk",
1068
+ messageId: string,
756
1069
  ): SessionNotification[] {
757
1070
  return content.map(block => ({
758
- sessionId: this.#sessionId,
1071
+ sessionId,
759
1072
  update: {
760
1073
  sessionUpdate: kind,
761
1074
  content: block,
1075
+ messageId,
762
1076
  },
763
1077
  }));
764
1078
  }
@@ -791,89 +1105,94 @@ export class AcpAgent implements Agent {
791
1105
  return replay;
792
1106
  }
793
1107
 
794
- async #configureExtensions(): Promise<void> {
795
- const extensionRunner = this.#session.extensionRunner;
1108
+ async #configureExtensions(record: ManagedSessionRecord): Promise<void> {
1109
+ if (record.extensionsConfigured) {
1110
+ return;
1111
+ }
1112
+
1113
+ const extensionRunner = record.session.extensionRunner;
796
1114
  if (!extensionRunner) {
1115
+ record.extensionsConfigured = true;
797
1116
  return;
798
1117
  }
799
1118
 
800
1119
  extensionRunner.initialize(
801
1120
  {
802
1121
  sendMessage: (message, options) => {
803
- this.#session.sendCustomMessage(message, options).catch((error: unknown) => {
1122
+ record.session.sendCustomMessage(message, options).catch((error: unknown) => {
804
1123
  logger.warn("ACP extension sendMessage failed", { error });
805
1124
  });
806
1125
  },
807
1126
  sendUserMessage: (content, options) => {
808
- this.#session.sendUserMessage(content, options).catch((error: unknown) => {
1127
+ record.session.sendUserMessage(content, options).catch((error: unknown) => {
809
1128
  logger.warn("ACP extension sendUserMessage failed", { error });
810
1129
  });
811
1130
  },
812
1131
  appendEntry: (customType, data) => {
813
- this.#session.sessionManager.appendCustomEntry(customType, data);
1132
+ record.session.sessionManager.appendCustomEntry(customType, data);
814
1133
  },
815
1134
  setLabel: (targetId, label) => {
816
- this.#session.sessionManager.appendLabelChange(targetId, label);
1135
+ record.session.sessionManager.appendLabelChange(targetId, label);
817
1136
  },
818
- getActiveTools: () => this.#session.getActiveToolNames(),
819
- getAllTools: () => this.#session.getAllToolNames(),
820
- setActiveTools: toolNames => this.#session.setActiveToolsByName(toolNames),
1137
+ getActiveTools: () => record.session.getActiveToolNames(),
1138
+ getAllTools: () => record.session.getAllToolNames(),
1139
+ setActiveTools: toolNames => record.session.setActiveToolsByName(toolNames),
821
1140
  getCommands: () => [],
822
1141
  setModel: async model => {
823
- const apiKey = await this.#session.modelRegistry.getApiKey(model);
1142
+ const apiKey = await record.session.modelRegistry.getApiKey(model);
824
1143
  if (!apiKey) {
825
1144
  return false;
826
1145
  }
827
- await this.#session.setModel(model);
1146
+ await record.session.setModel(model);
828
1147
  return true;
829
1148
  },
830
- getThinkingLevel: () => this.#session.thinkingLevel,
831
- setThinkingLevel: level => this.#session.setThinkingLevel(level),
1149
+ getThinkingLevel: () => record.session.thinkingLevel,
1150
+ setThinkingLevel: level => record.session.setThinkingLevel(level),
832
1151
  },
833
1152
  {
834
- getModel: () => this.#session.model,
835
- getSearchDb: () => this.#session.searchDb,
836
- isIdle: () => !this.#session.isStreaming,
1153
+ getModel: () => record.session.model,
1154
+ getSearchDb: () => record.session.searchDb,
1155
+ isIdle: () => !record.session.isStreaming,
837
1156
  abort: () => {
838
- void this.#session.abort();
1157
+ void record.session.abort();
839
1158
  },
840
- hasPendingMessages: () => this.#session.queuedMessageCount > 0,
1159
+ hasPendingMessages: () => record.session.queuedMessageCount > 0,
841
1160
  shutdown: () => {},
842
- getContextUsage: () => this.#session.getContextUsage(),
843
- getSystemPrompt: () => this.#session.systemPrompt,
1161
+ getContextUsage: () => record.session.getContextUsage(),
1162
+ getSystemPrompt: () => record.session.systemPrompt,
844
1163
  compact: async instructionsOrOptions => {
845
1164
  const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
846
1165
  const options =
847
1166
  instructionsOrOptions && typeof instructionsOrOptions === "object"
848
1167
  ? instructionsOrOptions
849
1168
  : undefined;
850
- await this.#session.compact(instructions, options);
1169
+ await record.session.compact(instructions, options);
851
1170
  },
852
1171
  },
853
1172
  {
854
- getContextUsage: () => this.#session.getContextUsage(),
855
- waitForIdle: () => this.#session.agent.waitForIdle(),
1173
+ getContextUsage: () => record.session.getContextUsage(),
1174
+ waitForIdle: () => record.session.agent.waitForIdle(),
856
1175
  newSession: async options => {
857
- const success = await this.#session.newSession({ parentSession: options?.parentSession });
1176
+ const success = await record.session.newSession({ parentSession: options?.parentSession });
858
1177
  if (success && options?.setup) {
859
- await options.setup(this.#session.sessionManager);
1178
+ await options.setup(record.session.sessionManager);
860
1179
  }
861
1180
  return { cancelled: !success };
862
1181
  },
863
1182
  branch: async entryId => {
864
- const result = await this.#session.branch(entryId);
1183
+ const result = await record.session.branch(entryId);
865
1184
  return { cancelled: result.cancelled };
866
1185
  },
867
1186
  navigateTree: async (targetId, options) => {
868
- const result = await this.#session.navigateTree(targetId, { summarize: options?.summarize });
1187
+ const result = await record.session.navigateTree(targetId, { summarize: options?.summarize });
869
1188
  return { cancelled: result.cancelled };
870
1189
  },
871
1190
  switchSession: async sessionPath => {
872
- const success = await this.#session.switchSession(sessionPath);
1191
+ const success = await record.session.switchSession(sessionPath);
873
1192
  return { cancelled: !success };
874
1193
  },
875
1194
  reload: async () => {
876
- await this.#session.reload();
1195
+ await record.session.reload();
877
1196
  },
878
1197
  compact: async instructionsOrOptions => {
879
1198
  const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
@@ -881,25 +1200,26 @@ export class AcpAgent implements Agent {
881
1200
  instructionsOrOptions && typeof instructionsOrOptions === "object"
882
1201
  ? instructionsOrOptions
883
1202
  : undefined;
884
- await this.#session.compact(instructions, options);
1203
+ await record.session.compact(instructions, options);
885
1204
  },
886
1205
  },
887
1206
  acpExtensionUiContext,
888
1207
  );
889
1208
  await extensionRunner.emit({ type: "session_start" });
1209
+ record.extensionsConfigured = true;
890
1210
  }
891
1211
 
892
- async #configureMcpServers(servers: McpServer[]): Promise<void> {
893
- if (this.#mcpManager) {
894
- await this.#mcpManager.disconnectAll();
1212
+ async #configureMcpServers(record: ManagedSessionRecord, servers: McpServer[]): Promise<void> {
1213
+ if (record.mcpManager) {
1214
+ await record.mcpManager.disconnectAll();
895
1215
  }
896
1216
  if (servers.length === 0) {
897
- this.#mcpManager = undefined;
898
- await this.#session.refreshMCPTools([]);
1217
+ record.mcpManager = undefined;
1218
+ await record.session.refreshMCPTools([]);
899
1219
  return;
900
1220
  }
901
1221
 
902
- const manager = new MCPManager(this.#session.sessionManager.getCwd());
1222
+ const manager = new MCPManager(record.session.sessionManager.getCwd());
903
1223
  const configs: MCPConfigMap = {};
904
1224
  const sources: MCPSourceMap = {};
905
1225
  for (const server of servers) {
@@ -921,8 +1241,8 @@ export class AcpAgent implements Agent {
921
1241
  );
922
1242
  }
923
1243
 
924
- this.#mcpManager = manager;
925
- await this.#session.refreshMCPTools(result.tools);
1244
+ record.mcpManager = manager;
1245
+ await record.session.refreshMCPTools(result.tools);
926
1246
  }
927
1247
 
928
1248
  #toMcpConfig(server: McpServer): MCPServerConfig {
@@ -955,4 +1275,84 @@ export class AcpAgent implements Agent {
955
1275
  }
956
1276
  return mapped;
957
1277
  }
1278
+
1279
+ async #closeManagedSession(sessionId: string, record: ManagedSessionRecord): Promise<void> {
1280
+ this.#sessions.delete(sessionId);
1281
+ await this.#cancelPromptForClose(record);
1282
+ await this.#disposeSessionRecord(record);
1283
+ }
1284
+
1285
+ async #cancelPromptForClose(record: ManagedSessionRecord): Promise<void> {
1286
+ const promptTurn = record.promptTurn;
1287
+ if (!promptTurn || promptTurn.settled) {
1288
+ return;
1289
+ }
1290
+
1291
+ promptTurn.cancelRequested = true;
1292
+ promptTurn.unsubscribe?.();
1293
+ try {
1294
+ await record.session.abort();
1295
+ } catch (error) {
1296
+ logger.warn("Failed to abort ACP prompt during session close", { error });
1297
+ }
1298
+ this.#finishPrompt(record, {
1299
+ stopReason: "cancelled",
1300
+ usage: this.#buildTurnUsage(promptTurn.usageBaseline, record.session.sessionManager.getUsageStatistics()),
1301
+ userMessageId: promptTurn.userMessageId,
1302
+ });
1303
+ }
1304
+
1305
+ async #disposeSessionRecord(record: ManagedSessionRecord): Promise<void> {
1306
+ if (record.mcpManager) {
1307
+ try {
1308
+ await record.mcpManager.disconnectAll();
1309
+ } catch (error) {
1310
+ logger.warn("Failed to disconnect ACP MCP servers", { error });
1311
+ }
1312
+ record.mcpManager = undefined;
1313
+ }
1314
+ try {
1315
+ await record.session.dispose();
1316
+ } catch (error) {
1317
+ logger.warn("Failed to dispose ACP session", { error });
1318
+ }
1319
+ }
1320
+
1321
+ async #disposeStandaloneSession(session: AgentSession): Promise<void> {
1322
+ try {
1323
+ await session.dispose();
1324
+ } catch (error) {
1325
+ logger.warn("Failed to dispose ACP session", { error });
1326
+ }
1327
+ }
1328
+
1329
+ async #disposeAllSessions(): Promise<void> {
1330
+ if (this.#disposePromise) {
1331
+ await this.#disposePromise;
1332
+ return;
1333
+ }
1334
+
1335
+ this.#disposePromise = (async () => {
1336
+ const records = Array.from(this.#sessions.entries());
1337
+ this.#sessions.clear();
1338
+ await Promise.all(
1339
+ records.map(async ([sessionId, record]) => {
1340
+ try {
1341
+ await this.#cancelPromptForClose(record);
1342
+ await this.#disposeSessionRecord(record);
1343
+ } catch (error) {
1344
+ logger.warn("Failed to clean up ACP session", { sessionId, error });
1345
+ }
1346
+ }),
1347
+ );
1348
+
1349
+ const initialSession = this.#initialSession;
1350
+ this.#initialSession = undefined;
1351
+ if (initialSession) {
1352
+ await this.#disposeStandaloneSession(initialSession);
1353
+ }
1354
+ })();
1355
+
1356
+ await this.#disposePromise;
1357
+ }
958
1358
  }