@getpaseo/server 0.1.75 → 0.1.77

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 (200) hide show
  1. package/dist/server/client/daemon-client.d.ts +14 -2
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +37 -0
  4. package/dist/server/client/daemon-client.js.map +1 -1
  5. package/dist/server/server/agent/agent-manager.d.ts +2 -1
  6. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  7. package/dist/server/server/agent/agent-manager.js +21 -10
  8. package/dist/server/server/agent/agent-manager.js.map +1 -1
  9. package/dist/server/server/agent/agent-sdk-types.d.ts +5 -0
  10. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  11. package/dist/server/server/agent/agent-sdk-types.js +7 -0
  12. package/dist/server/server/agent/agent-sdk-types.js.map +1 -1
  13. package/dist/server/server/agent/import-sessions.d.ts.map +1 -1
  14. package/dist/server/server/agent/import-sessions.js +6 -2
  15. package/dist/server/server/agent/import-sessions.js.map +1 -1
  16. package/dist/server/server/agent/mcp-server.d.ts.map +1 -1
  17. package/dist/server/server/agent/mcp-server.js +227 -4
  18. package/dist/server/server/agent/mcp-server.js.map +1 -1
  19. package/dist/server/server/agent/provider-history-timestamps.d.ts +2 -0
  20. package/dist/server/server/agent/provider-history-timestamps.d.ts.map +1 -0
  21. package/dist/server/server/agent/provider-history-timestamps.js +16 -0
  22. package/dist/server/server/agent/provider-history-timestamps.js.map +1 -0
  23. package/dist/server/server/agent/provider-manifest.d.ts +1 -1
  24. package/dist/server/server/agent/provider-manifest.d.ts.map +1 -1
  25. package/dist/server/server/agent/provider-manifest.js +14 -0
  26. package/dist/server/server/agent/provider-manifest.js.map +1 -1
  27. package/dist/server/server/agent/provider-registry.d.ts.map +1 -1
  28. package/dist/server/server/agent/provider-registry.js +53 -27
  29. package/dist/server/server/agent/provider-registry.js.map +1 -1
  30. package/dist/server/server/agent/provider-snapshot-manager.d.ts +2 -6
  31. package/dist/server/server/agent/provider-snapshot-manager.d.ts.map +1 -1
  32. package/dist/server/server/agent/provider-snapshot-manager.js +43 -32
  33. package/dist/server/server/agent/provider-snapshot-manager.js.map +1 -1
  34. package/dist/server/server/agent/providers/acp-agent.d.ts +3 -1
  35. package/dist/server/server/agent/providers/acp-agent.d.ts.map +1 -1
  36. package/dist/server/server/agent/providers/acp-agent.js +61 -14
  37. package/dist/server/server/agent/providers/acp-agent.js.map +1 -1
  38. package/dist/server/server/agent/providers/claude/agent.d.ts.map +1 -1
  39. package/dist/server/server/agent/providers/claude/agent.js +111 -38
  40. package/dist/server/server/agent/providers/claude/agent.js.map +1 -1
  41. package/dist/server/server/agent/providers/claude/models.d.ts +2 -0
  42. package/dist/server/server/agent/providers/claude/models.d.ts.map +1 -1
  43. package/dist/server/server/agent/providers/claude/models.js +78 -0
  44. package/dist/server/server/agent/providers/claude/models.js.map +1 -1
  45. package/dist/server/server/agent/providers/codex/app-server-transport.d.ts +1 -0
  46. package/dist/server/server/agent/providers/codex/app-server-transport.d.ts.map +1 -1
  47. package/dist/server/server/agent/providers/codex/app-server-transport.js +14 -10
  48. package/dist/server/server/agent/providers/codex/app-server-transport.js.map +1 -1
  49. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +15 -1
  50. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  51. package/dist/server/server/agent/providers/codex-app-server-agent.js +372 -69
  52. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  53. package/dist/server/server/agent/providers/cursor-acp-agent.d.ts +21 -0
  54. package/dist/server/server/agent/providers/cursor-acp-agent.d.ts.map +1 -0
  55. package/dist/server/server/agent/providers/cursor-acp-agent.js +85 -0
  56. package/dist/server/server/agent/providers/cursor-acp-agent.js.map +1 -0
  57. package/dist/server/server/agent/providers/generic-acp-agent.d.ts +13 -0
  58. package/dist/server/server/agent/providers/generic-acp-agent.d.ts.map +1 -1
  59. package/dist/server/server/agent/providers/generic-acp-agent.js +209 -2
  60. package/dist/server/server/agent/providers/generic-acp-agent.js.map +1 -1
  61. package/dist/server/server/agent/providers/opencode/server-manager.d.ts.map +1 -1
  62. package/dist/server/server/agent/providers/opencode/server-manager.js +2 -1
  63. package/dist/server/server/agent/providers/opencode/server-manager.js.map +1 -1
  64. package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.d.ts +7 -0
  65. package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.d.ts.map +1 -1
  66. package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.js +50 -1
  67. package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.js.map +1 -1
  68. package/dist/server/server/agent/providers/opencode-agent.d.ts +11 -7
  69. package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
  70. package/dist/server/server/agent/providers/opencode-agent.js +307 -346
  71. package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
  72. package/dist/server/server/agent/providers/pi-direct-agent.d.ts +1 -1
  73. package/dist/server/server/agent/providers/pi-direct-agent.d.ts.map +1 -1
  74. package/dist/server/server/agent/providers/pi-direct-agent.js +14 -0
  75. package/dist/server/server/agent/providers/pi-direct-agent.js.map +1 -1
  76. package/dist/server/server/agent/providers/pi-session-recovery-policy.d.ts +22 -0
  77. package/dist/server/server/agent/providers/pi-session-recovery-policy.d.ts.map +1 -0
  78. package/dist/server/server/agent/providers/pi-session-recovery-policy.js +51 -0
  79. package/dist/server/server/agent/providers/pi-session-recovery-policy.js.map +1 -0
  80. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  81. package/dist/server/server/agent/timeline-projection.d.ts.map +1 -1
  82. package/dist/server/server/agent/timeline-projection.js +9 -0
  83. package/dist/server/server/agent/timeline-projection.js.map +1 -1
  84. package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +40 -0
  85. package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts.map +1 -0
  86. package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +80 -0
  87. package/dist/server/server/auto-archive-on-merge/archive-if-safe.js.map +1 -0
  88. package/dist/server/server/auto-archive-on-merge/index.d.ts +8 -0
  89. package/dist/server/server/auto-archive-on-merge/index.d.ts.map +1 -0
  90. package/dist/server/server/auto-archive-on-merge/index.js +15 -0
  91. package/dist/server/server/auto-archive-on-merge/index.js.map +1 -0
  92. package/dist/server/server/bootstrap.d.ts +2 -0
  93. package/dist/server/server/bootstrap.d.ts.map +1 -1
  94. package/dist/server/server/bootstrap.js +82 -40
  95. package/dist/server/server/bootstrap.js.map +1 -1
  96. package/dist/server/server/checkout/status-projection.d.ts.map +1 -1
  97. package/dist/server/server/checkout/status-projection.js +5 -1
  98. package/dist/server/server/checkout/status-projection.js.map +1 -1
  99. package/dist/server/server/config.d.ts.map +1 -1
  100. package/dist/server/server/config.js +13 -5
  101. package/dist/server/server/config.js.map +1 -1
  102. package/dist/server/server/daemon-config-store.js +1 -0
  103. package/dist/server/server/daemon-config-store.js.map +1 -1
  104. package/dist/server/server/loop/rpc-schemas.d.ts +96 -96
  105. package/dist/server/server/loop-service.d.ts +18 -18
  106. package/dist/server/server/pairing-offer.d.ts +1 -0
  107. package/dist/server/server/pairing-offer.d.ts.map +1 -1
  108. package/dist/server/server/pairing-offer.js +2 -1
  109. package/dist/server/server/pairing-offer.js.map +1 -1
  110. package/dist/server/server/paseo-worktree-service.d.ts +3 -1
  111. package/dist/server/server/paseo-worktree-service.d.ts.map +1 -1
  112. package/dist/server/server/paseo-worktree-service.js +55 -18
  113. package/dist/server/server/paseo-worktree-service.js.map +1 -1
  114. package/dist/server/server/persisted-config.d.ts +16 -0
  115. package/dist/server/server/persisted-config.d.ts.map +1 -1
  116. package/dist/server/server/persisted-config.js +11 -3
  117. package/dist/server/server/persisted-config.js.map +1 -1
  118. package/dist/server/server/relay-transport.d.ts +2 -1
  119. package/dist/server/server/relay-transport.d.ts.map +1 -1
  120. package/dist/server/server/relay-transport.js +26 -4
  121. package/dist/server/server/relay-transport.js.map +1 -1
  122. package/dist/server/server/session.d.ts +6 -0
  123. package/dist/server/server/session.d.ts.map +1 -1
  124. package/dist/server/server/session.js +195 -34
  125. package/dist/server/server/session.js.map +1 -1
  126. package/dist/server/server/utils/diff-highlighter.d.ts.map +1 -1
  127. package/dist/server/server/utils/diff-highlighter.js +30 -9
  128. package/dist/server/server/utils/diff-highlighter.js.map +1 -1
  129. package/dist/server/server/websocket-server.d.ts.map +1 -1
  130. package/dist/server/server/websocket-server.js +5 -0
  131. package/dist/server/server/websocket-server.js.map +1 -1
  132. package/dist/server/server/workspace-git-service.d.ts +6 -1
  133. package/dist/server/server/workspace-git-service.d.ts.map +1 -1
  134. package/dist/server/server/workspace-git-service.js +27 -4
  135. package/dist/server/server/workspace-git-service.js.map +1 -1
  136. package/dist/server/server/workspace-reconciliation-service.d.ts +4 -2
  137. package/dist/server/server/workspace-reconciliation-service.d.ts.map +1 -1
  138. package/dist/server/server/workspace-reconciliation-service.js +112 -14
  139. package/dist/server/server/workspace-reconciliation-service.js.map +1 -1
  140. package/dist/server/server/workspace-registry.d.ts +6 -1
  141. package/dist/server/server/workspace-registry.d.ts.map +1 -1
  142. package/dist/server/server/workspace-registry.js +11 -0
  143. package/dist/server/server/workspace-registry.js.map +1 -1
  144. package/dist/server/server/worktree-session.d.ts.map +1 -1
  145. package/dist/server/server/worktree-session.js +1 -0
  146. package/dist/server/server/worktree-session.js.map +1 -1
  147. package/dist/server/services/github-service.d.ts +46 -0
  148. package/dist/server/services/github-service.d.ts.map +1 -1
  149. package/dist/server/services/github-service.js +274 -5
  150. package/dist/server/services/github-service.js.map +1 -1
  151. package/dist/server/shared/messages.d.ts +3443 -290
  152. package/dist/server/shared/messages.d.ts.map +1 -1
  153. package/dist/server/shared/messages.js +94 -3
  154. package/dist/server/shared/messages.js.map +1 -1
  155. package/dist/server/shared/terminal-input-mode.d.ts +26 -0
  156. package/dist/server/shared/terminal-input-mode.d.ts.map +1 -0
  157. package/dist/server/shared/terminal-input-mode.js +151 -0
  158. package/dist/server/shared/terminal-input-mode.js.map +1 -0
  159. package/dist/server/terminal/terminal-session-controller.d.ts.map +1 -1
  160. package/dist/server/terminal/terminal-session-controller.js +12 -2
  161. package/dist/server/terminal/terminal-session-controller.js.map +1 -1
  162. package/dist/server/terminal/terminal.d.ts +1 -0
  163. package/dist/server/terminal/terminal.d.ts.map +1 -1
  164. package/dist/server/terminal/terminal.js +16 -3
  165. package/dist/server/terminal/terminal.js.map +1 -1
  166. package/dist/server/terminal/worker-terminal-manager.d.ts.map +1 -1
  167. package/dist/server/terminal/worker-terminal-manager.js +8 -0
  168. package/dist/server/terminal/worker-terminal-manager.js.map +1 -1
  169. package/dist/server/utils/checkout-git.d.ts +4 -1
  170. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  171. package/dist/server/utils/checkout-git.js +85 -29
  172. package/dist/server/utils/checkout-git.js.map +1 -1
  173. package/dist/server/utils/directory-suggestions.d.ts +2 -0
  174. package/dist/server/utils/directory-suggestions.d.ts.map +1 -1
  175. package/dist/server/utils/directory-suggestions.js +87 -14
  176. package/dist/server/utils/directory-suggestions.js.map +1 -1
  177. package/dist/server/utils/executable.d.ts.map +1 -1
  178. package/dist/server/utils/executable.js +6 -3
  179. package/dist/server/utils/executable.js.map +1 -1
  180. package/dist/server/utils/path.d.ts +10 -0
  181. package/dist/server/utils/path.d.ts.map +1 -1
  182. package/dist/server/utils/path.js +65 -1
  183. package/dist/server/utils/path.js.map +1 -1
  184. package/dist/server/utils/run-git-command.d.ts +2 -0
  185. package/dist/server/utils/run-git-command.d.ts.map +1 -1
  186. package/dist/server/utils/run-git-command.js +41 -1
  187. package/dist/server/utils/run-git-command.js.map +1 -1
  188. package/dist/server/utils/worktree.js +1 -1
  189. package/dist/server/utils/worktree.js.map +1 -1
  190. package/dist/src/server/agent/agent-sdk-types.js +7 -0
  191. package/dist/src/server/agent/agent-sdk-types.js.map +1 -1
  192. package/dist/src/server/agent/provider-manifest.js +14 -0
  193. package/dist/src/server/agent/provider-manifest.js.map +1 -1
  194. package/dist/src/server/persisted-config.js +11 -3
  195. package/dist/src/server/persisted-config.js.map +1 -1
  196. package/dist/src/shared/messages.js +94 -3
  197. package/dist/src/shared/messages.js.map +1 -1
  198. package/dist/src/utils/executable.js +6 -3
  199. package/dist/src/utils/executable.js.map +1 -1
  200. package/package.json +3 -3
@@ -1,7 +1,6 @@
1
- import { readdir, readFile } from "node:fs/promises";
2
1
  import { homedir } from "node:os";
3
- import path from "node:path";
4
2
  import { findExecutable, isCommandAvailable } from "../../../utils/executable.js";
3
+ import { createPathEquivalenceMatcher } from "../../../utils/path.js";
5
4
  import { z } from "zod";
6
5
  import { getAgentStreamEventTurnId, } from "../agent-sdk-types.js";
7
6
  import { createProviderEnvSpec } from "../provider-launch-config.js";
@@ -14,6 +13,7 @@ import { formatDiagnosticStatus, formatProviderDiagnostic, formatProviderDiagnos
14
13
  import { runProviderTurn } from "./provider-runner.js";
15
14
  import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
16
15
  import { createSdkOpenCodeClient, } from "./opencode/runtime.js";
16
+ import { normalizeProviderReplayTimestamp } from "../provider-history-timestamps.js";
17
17
  const OPENCODE_CAPABILITIES = {
18
18
  supportsStreaming: true,
19
19
  supportsSessionPersistence: true,
@@ -24,9 +24,8 @@ const OPENCODE_CAPABILITIES = {
24
24
  };
25
25
  const OPENCODE_BUILD_MODE_ID = "build";
26
26
  const OPENCODE_FULL_ACCESS_MODE_ID = "full-access";
27
- const OPENCODE_STORAGE_SESSION_LIMIT = 200;
27
+ const OPENCODE_PERSISTED_SESSION_LIMIT = 200;
28
28
  const OPENCODE_PENDING_ABORT_START_TIMEOUT_MS = 10000;
29
- const OPENCODE_RETRY_STATUS_FAILURE_MS = 10000;
30
29
  const DEFAULT_MODES = [
31
30
  {
32
31
  id: OPENCODE_BUILD_MODE_ID,
@@ -44,44 +43,6 @@ const DEFAULT_MODES = [
44
43
  description: "Automatically approves all tool permission prompts for the session",
45
44
  },
46
45
  ];
47
- const OpenCodeStoredSessionSchema = z
48
- .object({
49
- id: z.string().min(1),
50
- directory: z.string().min(1),
51
- title: z.string().nullable().optional(),
52
- time: z
53
- .object({
54
- created: z.number().optional(),
55
- updated: z.number().optional(),
56
- })
57
- .optional(),
58
- })
59
- .passthrough();
60
- const OpenCodeStoredMessageSchema = z
61
- .object({
62
- id: z.string().min(1),
63
- sessionID: z.string().min(1),
64
- role: z.string().optional(),
65
- time: z
66
- .object({
67
- created: z.number().optional(),
68
- completed: z.number().optional(),
69
- })
70
- .optional(),
71
- })
72
- .passthrough();
73
- const OpenCodeStoredPartSchema = z
74
- .object({
75
- type: z.string().optional(),
76
- text: z.string().optional(),
77
- time: z
78
- .object({
79
- start: z.number().optional(),
80
- end: z.number().optional(),
81
- })
82
- .optional(),
83
- })
84
- .passthrough();
85
46
  const MCP_ALREADY_PRESENT_ERROR_TOKENS = ["already", "exists", "connected"];
86
47
  const OPENCODE_PROVIDER_LIST_TIMEOUT_MS = 30000;
87
48
  const OPENCODE_HANDLED_BUILTIN_SLASH_COMMANDS = [
@@ -389,7 +350,9 @@ function buildOpenCodeModelContextWindowLookup(providers) {
389
350
  }
390
351
  const connectedProviderIds = new Set(providers.connected ?? []);
391
352
  for (const provider of providers.all ?? []) {
392
- if (!connectedProviderIds.has(provider.id)) {
353
+ // Providers with source "api" are managed by the OpenCode console/subscription and are
354
+ // usable even though they don't appear in `connected` (which only lists env/config providers).
355
+ if (!connectedProviderIds.has(provider.id) && provider.source !== "api") {
393
356
  continue;
394
357
  }
395
358
  for (const [modelId, modelDefinition] of Object.entries(provider.models ?? {})) {
@@ -503,35 +466,30 @@ function buildOpenCodePromptParts(prompt) {
503
466
  }
504
467
  return output;
505
468
  }
506
- function resolveOpenCodeStorageRoot() {
507
- const xdgDataHome = process.env.XDG_DATA_HOME;
508
- const dataHome = typeof xdgDataHome === "string" && xdgDataHome.trim().length > 0
509
- ? xdgDataHome
510
- : path.join(homedir(), ".local", "share");
511
- return path.join(dataHome, "opencode", "storage");
512
- }
513
- async function collectOpenCodePersistedAgentsFromStorage(storageRoot, options) {
514
- const sessions = await readOpenCodeStoredSessions(path.join(storageRoot, "session"));
515
- const limit = options?.limit ?? OPENCODE_STORAGE_SESSION_LIMIT;
469
+ async function collectOpenCodePersistedAgentsFromSdk(client, options) {
470
+ const limit = options?.limit ?? OPENCODE_PERSISTED_SESSION_LIMIT;
471
+ const sessionListLimit = options?.cwd ? Math.max(limit, OPENCODE_PERSISTED_SESSION_LIMIT) : limit;
472
+ const response = await client.experimental.session.list({
473
+ archived: true,
474
+ roots: true,
475
+ limit: sessionListLimit,
476
+ });
477
+ if (response.error) {
478
+ throw new Error(`Failed to list OpenCode sessions: ${JSON.stringify(response.error)}`);
479
+ }
480
+ const sessions = response.data ?? [];
481
+ const matchesCwd = options?.cwd ? createPathEquivalenceMatcher(options.cwd) : null;
516
482
  const candidates = sessions
517
- .filter((session) => !options?.cwd || session.directory === options.cwd)
483
+ .filter((session) => !matchesCwd || matchesCwd(session.directory))
518
484
  .sort((left, right) => getOpenCodeSessionTimestamp(right) - getOpenCodeSessionTimestamp(left))
519
485
  .slice(0, limit);
520
- return await Promise.all(candidates.map((session) => buildOpenCodePersistedAgentDescriptor(storageRoot, session)));
486
+ return await Promise.all(candidates.map((session) => buildOpenCodePersistedAgentDescriptor(client, session)));
521
487
  }
522
- async function readOpenCodeStoredSessions(sessionRoot) {
523
- const files = await findJsonFiles(sessionRoot);
524
- const sessions = [];
525
- for (const file of files) {
526
- const parsed = await readJsonFile(file, OpenCodeStoredSessionSchema);
527
- if (parsed) {
528
- sessions.push(parsed);
529
- }
530
- }
531
- return sessions;
532
- }
533
- async function buildOpenCodePersistedAgentDescriptor(storageRoot, session) {
534
- const timeline = await readOpenCodeSessionTimeline(storageRoot, session.id);
488
+ async function buildOpenCodePersistedAgentDescriptor(client, session) {
489
+ const messages = await readOpenCodeSessionMessagesFromSdk(client, session);
490
+ const timeline = buildOpenCodeSessionTimeline(messages);
491
+ const modeId = resolveOpenCodePersistedSessionModeId(session, messages);
492
+ const model = resolveOpenCodePersistedSessionModel(session, messages);
535
493
  return {
536
494
  provider: "opencode",
537
495
  sessionId: session.id,
@@ -546,96 +504,144 @@ async function buildOpenCodePersistedAgentDescriptor(storageRoot, session) {
546
504
  provider: "opencode",
547
505
  cwd: session.directory,
548
506
  title: normalizeOpenCodeSessionTitle(session.title),
507
+ ...(modeId ? { modeId } : {}),
508
+ ...(model ? { model } : {}),
549
509
  },
550
510
  },
551
511
  timeline,
552
512
  };
553
513
  }
554
- async function readOpenCodeSessionTimeline(storageRoot, sessionId) {
555
- const messageRoot = path.join(storageRoot, "message", sessionId);
556
- const messageFiles = await findJsonFiles(messageRoot);
557
- const messages = [];
558
- for (const file of messageFiles) {
559
- const parsed = await readJsonFile(file, OpenCodeStoredMessageSchema);
560
- if (parsed?.sessionID === sessionId) {
561
- messages.push(parsed);
562
- }
563
- }
564
- const timeline = [];
565
- for (const message of messages.sort((left, right) => getOpenCodeMessageTimestamp(left) - getOpenCodeMessageTimestamp(right))) {
566
- const text = await readOpenCodeMessageText(storageRoot, message.id);
567
- if (!text) {
568
- continue;
569
- }
570
- if (message.role === "user") {
571
- timeline.push({ type: "user_message", text, messageId: message.id });
572
- }
573
- else if (message.role === "assistant") {
574
- timeline.push({ type: "assistant_message", text });
575
- }
576
- }
577
- return timeline;
578
- }
579
- async function readOpenCodeMessageText(storageRoot, messageId) {
580
- const parts = await readOpenCodeStoredParts(storageRoot, messageId);
581
- return readOpenCodeTextFromParts(parts);
514
+ function normalizeOpenCodeSessionTitle(title) {
515
+ const normalized = title?.trim();
516
+ return normalized ? normalized : null;
582
517
  }
583
- async function readOpenCodeStoredParts(storageRoot, messageId) {
584
- const partRoot = path.join(storageRoot, "part", messageId);
585
- const partFiles = await findJsonFiles(partRoot);
586
- const parts = [];
587
- for (const file of partFiles) {
588
- const parsed = await readJsonFile(file, OpenCodeStoredPartSchema);
589
- if (parsed) {
590
- parts.push(parsed);
591
- }
592
- }
593
- return parts.sort((left, right) => getOpenCodePartTimestamp(left) - getOpenCodePartTimestamp(right));
518
+ function getOpenCodeSessionTimestamp(session) {
519
+ return session.time?.updated ?? session.time?.created ?? 0;
594
520
  }
595
- function readOpenCodeTextFromParts(parts) {
596
- return parts
597
- .filter((part) => part.type === "text" && typeof part.text === "string")
598
- .map((part) => part.text?.trim() ?? "")
599
- .filter(Boolean)
600
- .join("\n\n");
521
+ function resolveOpenCodeReplayTimestamp(params) {
522
+ const timedPart = params.part;
523
+ const partTimestamp = timedPart?.time?.start ??
524
+ timedPart?.time?.end ??
525
+ params.message.time?.created ??
526
+ params.message.time?.completed;
527
+ return normalizeProviderReplayTimestamp(partTimestamp);
528
+ }
529
+ function buildOpenCodeReplayTimelineEvent(params) {
530
+ const timestamp = resolveOpenCodeReplayTimestamp({
531
+ message: params.message,
532
+ part: params.part,
533
+ });
534
+ return {
535
+ type: "timeline",
536
+ provider: "opencode",
537
+ item: params.item,
538
+ ...(timestamp ? { timestamp } : {}),
539
+ };
601
540
  }
602
- async function findJsonFiles(root) {
603
- let entries;
604
- try {
605
- entries = await readdir(root, { withFileTypes: true });
541
+ function buildOpenCodeReplayPartTimelineEvent(params) {
542
+ const { part, message } = params;
543
+ if (part.type === "text" && part.text) {
544
+ return buildOpenCodeReplayTimelineEvent({
545
+ item: { type: "assistant_message", text: part.text },
546
+ message,
547
+ part,
548
+ });
606
549
  }
607
- catch {
608
- return [];
550
+ if (part.type === "reasoning" && part.text) {
551
+ return buildOpenCodeReplayTimelineEvent({
552
+ item: { type: "reasoning", text: part.text },
553
+ message,
554
+ part,
555
+ });
609
556
  }
610
- const files = await Promise.all(entries.map(async (entry) => {
611
- const entryPath = path.join(root, entry.name);
612
- if (entry.isDirectory()) {
613
- return findJsonFiles(entryPath);
614
- }
615
- return entry.isFile() && entry.name.endsWith(".json") ? [entryPath] : [];
616
- }));
617
- return files.flat();
618
- }
619
- async function readJsonFile(file, schema) {
620
- try {
621
- return schema.parse(JSON.parse(await readFile(file, "utf8")));
557
+ if (part.type !== "tool") {
558
+ return null;
622
559
  }
623
- catch {
560
+ const parsedToolPart = OpencodeToolPartToTimelineItemSchema.safeParse(part);
561
+ if (!parsedToolPart.success || !parsedToolPart.data) {
624
562
  return null;
625
563
  }
564
+ return buildOpenCodeReplayTimelineEvent({
565
+ item: parsedToolPart.data,
566
+ message,
567
+ part,
568
+ });
626
569
  }
627
- function normalizeOpenCodeSessionTitle(title) {
628
- const normalized = title?.trim();
629
- return normalized ? normalized : null;
570
+ async function readOpenCodeSessionMessagesFromSdk(client, session) {
571
+ const response = await client.session.messages({
572
+ sessionID: session.id,
573
+ directory: session.directory,
574
+ });
575
+ if (response.error || !response.data) {
576
+ return [];
577
+ }
578
+ return response.data;
630
579
  }
631
- function getOpenCodeSessionTimestamp(session) {
632
- return session.time?.updated ?? session.time?.created ?? 0;
580
+ function buildOpenCodeSessionTimeline(messages) {
581
+ return messages.flatMap((message) => buildOpenCodeReplayTimelineEvents(message).map((event) => event.item));
582
+ }
583
+ function resolveOpenCodePersistedSessionModeId(session, messages) {
584
+ const agent = session.agent ?? messages.map(readOpenCodeMessageAgent).find(Boolean);
585
+ return agent ? normalizeOpenCodeModeId(agent) : undefined;
633
586
  }
634
- function getOpenCodeMessageTimestamp(message) {
635
- return message.time?.created ?? message.time?.completed ?? 0;
587
+ function readOpenCodeMessageAgent(message) {
588
+ const agent = message.info.agent;
589
+ return typeof agent === "string" && agent.trim() ? agent : undefined;
636
590
  }
637
- function getOpenCodePartTimestamp(part) {
638
- return part.time?.start ?? part.time?.end ?? 0;
591
+ function resolveOpenCodePersistedSessionModel(session, messages) {
592
+ if (session.model) {
593
+ return buildOpenCodeModelLookupKey(session.model.providerID, session.model.id);
594
+ }
595
+ const model = messages.map(readOpenCodeMessageModel).find(Boolean);
596
+ return model ? buildOpenCodeModelLookupKey(model.providerID, model.modelID) : undefined;
597
+ }
598
+ function readOpenCodeMessageModel(message) {
599
+ const { info } = message;
600
+ if (info.role === "user") {
601
+ return info.model;
602
+ }
603
+ return {
604
+ providerID: info.providerID,
605
+ modelID: info.modelID,
606
+ };
607
+ }
608
+ function buildOpenCodeReplayTimelineEvents(message) {
609
+ const { info, parts } = message;
610
+ if (info.role === "user") {
611
+ const text = parts
612
+ .filter((part) => part.type === "text")
613
+ .map((part) => part.text)
614
+ .join("");
615
+ return text
616
+ ? [
617
+ buildOpenCodeReplayTimelineEvent({
618
+ item: { type: "user_message", text, messageId: info.id },
619
+ message: info,
620
+ }),
621
+ ]
622
+ : [];
623
+ }
624
+ const events = [];
625
+ let emittedAssistantText = false;
626
+ for (const part of parts) {
627
+ if (part.type === "text" && part.text) {
628
+ emittedAssistantText = true;
629
+ }
630
+ const event = buildOpenCodeReplayPartTimelineEvent({ part, message: info });
631
+ if (event) {
632
+ events.push(event);
633
+ }
634
+ }
635
+ if (!emittedAssistantText) {
636
+ const text = stringifyStructuredAssistantMessage(info.structured);
637
+ if (text) {
638
+ events.push(buildOpenCodeReplayTimelineEvent({
639
+ item: { type: "assistant_message", text },
640
+ message: info,
641
+ }));
642
+ }
643
+ }
644
+ return events;
639
645
  }
640
646
  export const __openCodeInternals = {
641
647
  buildOpenCodePromptParts,
@@ -672,13 +678,12 @@ class ProductionOpenCodeRuntime {
672
678
  }
673
679
  }
674
680
  export class OpenCodeAgentClient {
675
- constructor(logger, runtimeSettings, storageRoot, deps = {}) {
681
+ constructor(logger, runtimeSettings, deps = {}) {
676
682
  this.provider = "opencode";
677
683
  this.capabilities = OPENCODE_CAPABILITIES;
678
684
  this.modelContextWindows = new Map();
679
685
  this.logger = logger.child({ module: "agent", provider: "opencode" });
680
686
  this.runtimeSettings = runtimeSettings;
681
- this.storageRoot = storageRoot ?? resolveOpenCodeStorageRoot();
682
687
  this.runtime =
683
688
  deps.runtime ??
684
689
  new ProductionOpenCodeRuntime(OpenCodeServerManager.getInstance(this.logger, runtimeSettings));
@@ -701,7 +706,7 @@ export class OpenCodeAgentClient {
701
706
  throw new Error("OpenCode session creation returned no data");
702
707
  }
703
708
  await this.populateModelContextWindowCache(client, openCodeConfig.cwd);
704
- return new OpenCodeAgentSession(openCodeConfig, client, session.id, this.logger, this.storageRoot, new Map(this.modelContextWindows), acquisition.release, options?.persistSession, launchContext?.agentId);
709
+ return new OpenCodeAgentSession(openCodeConfig, client, session.id, this.logger, new Map(this.modelContextWindows), acquisition.release, options?.persistSession, launchContext?.agentId);
705
710
  }
706
711
  catch (error) {
707
712
  acquisition.release();
@@ -709,14 +714,16 @@ export class OpenCodeAgentClient {
709
714
  }
710
715
  }
711
716
  async resumeSession(handle, overrides, launchContext) {
712
- const cwd = overrides?.cwd ?? handle.metadata?.cwd;
717
+ const metadata = (handle.metadata ?? {});
718
+ const cwd = overrides?.cwd ?? metadata.cwd;
713
719
  if (!cwd) {
714
720
  throw new Error("OpenCode resume requires the original working directory");
715
721
  }
716
722
  const config = {
723
+ ...metadata,
724
+ ...overrides,
717
725
  provider: "opencode",
718
726
  cwd,
719
- ...overrides,
720
727
  };
721
728
  const openCodeConfig = this.assertConfig(config);
722
729
  const acquisition = await this.runtime.acquireServer({ force: false });
@@ -727,7 +734,7 @@ export class OpenCodeAgentClient {
727
734
  });
728
735
  try {
729
736
  await this.populateModelContextWindowCache(client, openCodeConfig.cwd);
730
- return new OpenCodeAgentSession(openCodeConfig, client, handle.sessionId, this.logger, this.storageRoot, new Map(this.modelContextWindows), acquisition.release, undefined, launchContext?.agentId);
737
+ return new OpenCodeAgentSession(openCodeConfig, client, handle.sessionId, this.logger, new Map(this.modelContextWindows), acquisition.release, undefined, launchContext?.agentId);
731
738
  }
732
739
  catch (error) {
733
740
  acquisition.release();
@@ -752,17 +759,21 @@ export class OpenCodeAgentClient {
752
759
  if (!providers) {
753
760
  return [];
754
761
  }
755
- // Only include models from connected providers (ones that are actually available)
756
762
  const connectedProviderIds = new Set(providers.connected);
757
- // Fail fast if no providers are connected
758
- if (connectedProviderIds.size === 0) {
759
- throw new Error("OpenCode has no connected providers. Please authenticate with at least one provider (e.g., openai, anthropic) or set appropriate environment variables (e.g., OPENAI_API_KEY).");
763
+ // Providers with source "api" are managed by the OpenCode console/subscription (e.g. Pi
764
+ // coding agent). They do not appear in `connected` (which only lists env/config providers)
765
+ // but are fully usable OpenCode authenticates them internally via the console session.
766
+ const isAccessible = (provider) => connectedProviderIds.has(provider.id) || provider.source === "api";
767
+ // Fail fast if no providers are accessible at all
768
+ if (!providers.all.some(isAccessible)) {
769
+ throw new Error("OpenCode has no connected providers. Please authenticate with at least one provider " +
770
+ "(e.g., openai, anthropic), set appropriate environment variables (e.g., OPENAI_API_KEY), " +
771
+ "or log in to OpenCode Go via the console.");
760
772
  }
761
773
  const models = [];
762
774
  this.modelContextWindows.clear();
763
775
  for (const provider of providers.all) {
764
- // Skip providers that aren't connected/configured
765
- if (!connectedProviderIds.has(provider.id)) {
776
+ if (!isAccessible(provider)) {
766
777
  continue;
767
778
  }
768
779
  for (const [modelId, model] of Object.entries(provider.models)) {
@@ -803,8 +814,37 @@ export class OpenCodeAgentClient {
803
814
  acquisition.release();
804
815
  }
805
816
  }
817
+ async listCommands(config) {
818
+ const openCodeConfig = this.assertConfig(config);
819
+ const acquisition = await this.runtime.acquireServer({ force: false });
820
+ const { url } = acquisition.server;
821
+ const client = this.runtime.createClient({
822
+ baseUrl: url,
823
+ directory: openCodeConfig.cwd,
824
+ });
825
+ try {
826
+ return await listOpenCodeCommandsFromSdk(client, openCodeConfig.cwd);
827
+ }
828
+ finally {
829
+ acquisition.release();
830
+ }
831
+ }
832
+ async listFeatures(_config) {
833
+ return [];
834
+ }
806
835
  async listPersistedAgents(options) {
807
- return collectOpenCodePersistedAgentsFromStorage(this.storageRoot, options);
836
+ const acquisition = await this.runtime.acquireServer({ force: false });
837
+ const { url } = acquisition.server;
838
+ const client = this.runtime.createClient({
839
+ baseUrl: url,
840
+ directory: options?.cwd ?? "",
841
+ });
842
+ try {
843
+ return await collectOpenCodePersistedAgentsFromSdk(client, options);
844
+ }
845
+ finally {
846
+ acquisition.release();
847
+ }
808
848
  }
809
849
  async isAvailable() {
810
850
  const command = this.runtimeSettings?.command;
@@ -923,6 +963,21 @@ function stringifyStructuredAssistantMessage(value) {
923
963
  return null;
924
964
  }
925
965
  }
966
+ async function listOpenCodeCommandsFromSdk(client, directory) {
967
+ const result = await client.command.list({ directory });
968
+ const commandsByName = new Map(OPENCODE_HANDLED_BUILTIN_SLASH_COMMANDS.map((command) => [command.name, command]));
969
+ if (result.error || !result.data) {
970
+ return Array.from(commandsByName.values());
971
+ }
972
+ for (const cmd of result.data) {
973
+ commandsByName.set(cmd.name, {
974
+ name: cmd.name,
975
+ description: cmd.description ?? "",
976
+ argumentHint: cmd.hints?.length ? cmd.hints.join(" ") : "",
977
+ });
978
+ }
979
+ return Array.from(commandsByName.values());
980
+ }
926
981
  function readOpenCodeRecord(value) {
927
982
  return typeof value === "object" && value !== null && !Array.isArray(value)
928
983
  ? value
@@ -1579,7 +1634,7 @@ function unwrapOpenCodeGlobalEvent(event) {
1579
1634
  return null;
1580
1635
  }
1581
1636
  class OpenCodeAgentSession {
1582
- constructor(config, client, sessionId, logger, _storageRoot, modelContextWindowsByModelKey = new Map(), releaseServer, persistSession = true, agentId) {
1637
+ constructor(config, client, sessionId, logger, modelContextWindowsByModelKey = new Map(), releaseServer, persistSession = true, agentId) {
1583
1638
  this.agentId = agentId;
1584
1639
  this.provider = "opencode";
1585
1640
  this.capabilities = OPENCODE_CAPABILITIES;
@@ -1606,8 +1661,10 @@ class OpenCodeAgentSession {
1606
1661
  this.subAgentsByCallId = new Map();
1607
1662
  this.subAgentCallIdByChildSessionId = new Map();
1608
1663
  this.pendingChildToolPartsBySessionId = new Map();
1664
+ this.eventStreamAbortController = null;
1665
+ this.eventStreamReady = null;
1666
+ this.closed = false;
1609
1667
  this.deletedFromProvider = false;
1610
- this.retryFailureTimer = null;
1611
1668
  this.config = config;
1612
1669
  this.client = client;
1613
1670
  this.sessionId = sessionId;
@@ -1617,6 +1674,7 @@ class OpenCodeAgentSession {
1617
1674
  this.releaseServer = releaseServer ?? null;
1618
1675
  this.persistSession = persistSession;
1619
1676
  this.selectedModelContextWindowMaxTokens = this.resolveConfiguredModelContextWindowMaxTokens(config.model);
1677
+ this.startEventStream();
1620
1678
  }
1621
1679
  get id() {
1622
1680
  return this.sessionId;
@@ -1703,7 +1761,6 @@ class OpenCodeAgentSession {
1703
1761
  this.subAgentsByCallId.clear();
1704
1762
  this.subAgentCallIdByChildSessionId.clear();
1705
1763
  this.pendingChildToolPartsBySessionId.clear();
1706
- this.clearRetryFailureTimer();
1707
1764
  const turnAbortController = new AbortController();
1708
1765
  this.abortController = turnAbortController;
1709
1766
  await this.ensureMcpServersConfigured();
@@ -1714,22 +1771,18 @@ class OpenCodeAgentSession {
1714
1771
  const thinkingOptionId = this.config.thinkingOptionId;
1715
1772
  const effectiveVariant = thinkingOptionId ?? undefined;
1716
1773
  const effectiveMode = resolveOpenCodeRuntimeAgentId(this.currentMode);
1717
- const turnId = this.createTurnId();
1718
- this.activeForegroundTurnId = turnId;
1719
- // OpenCode's /event SSE endpoint does NOT replay past events. If we send
1720
- // the prompt before our reader is connected, terminal events fired early
1721
- // by the server (e.g. session.error / session.idle for invalid model or
1722
- // mode) are missed and the turn hangs forever. Wait for the subscription
1723
- // to be established before sending anything.
1724
- const subscriptionReady = createDeferred();
1725
- void this.consumeEventStream(turnId, turnAbortController, subscriptionReady);
1726
1774
  try {
1727
- await subscriptionReady.promise;
1775
+ await this.ensureEventStreamReady();
1728
1776
  }
1729
- catch {
1730
- // consumeEventStream already finished the turn with the subscription error.
1731
- return { turnId };
1777
+ catch (error) {
1778
+ if (this.abortController === turnAbortController) {
1779
+ this.abortController = null;
1780
+ }
1781
+ throw error;
1732
1782
  }
1783
+ const turnId = this.createTurnId();
1784
+ this.activeForegroundTurnId = turnId;
1785
+ this.notifySubscribers({ type: "turn_started", provider: "opencode" }, turnId);
1733
1786
  const slashCommand = await this.resolveSlashCommandInvocation(prompt);
1734
1787
  if (slashCommand) {
1735
1788
  if (slashCommand.commandName === "compact" || slashCommand.commandName === "summarize") {
@@ -1761,12 +1814,8 @@ class OpenCodeAgentSession {
1761
1814
  });
1762
1815
  return { turnId };
1763
1816
  }
1764
- // command() blocks until the server finishes processing. OpenCode's SSE
1765
- // endpoint does NOT replay past events, so if the command completes before
1766
- // our SSE reader connects, we miss `session.idle` and the turn hangs.
1767
- // Handle both success and error in the response handler as a fallback —
1768
- // finishForegroundTurn's guard prevents duplicate terminal events if the
1769
- // SSE stream already delivered the event.
1817
+ // command() is only dispatch acknowledgement. OpenCode session events are
1818
+ // the source of truth for when the command turn becomes idle or fails.
1770
1819
  void this.client.session
1771
1820
  .command({
1772
1821
  sessionID: this.sessionId,
@@ -1790,9 +1839,6 @@ class OpenCodeAgentSession {
1790
1839
  const errorMsg = toDiagnosticErrorMessage(response.error);
1791
1840
  this.finishForegroundTurn({ type: "turn_failed", provider: "opencode", error: errorMsg }, turnId);
1792
1841
  }
1793
- else {
1794
- this.finishForegroundTurn({ type: "turn_completed", provider: "opencode", usage: undefined }, turnId);
1795
- }
1796
1842
  return;
1797
1843
  })
1798
1844
  .catch((err) => {
@@ -1875,89 +1921,95 @@ class OpenCodeAgentSession {
1875
1921
  this.subscribers.delete(callback);
1876
1922
  };
1877
1923
  }
1878
- async consumeEventStream(turnId, turnAbortController, subscriptionReady) {
1924
+ startEventStream() {
1925
+ void this.ensureEventStreamReady().catch((error) => {
1926
+ this.logger.warn({ err: error, sessionId: this.sessionId }, "OpenCode event stream failed");
1927
+ });
1928
+ }
1929
+ ensureEventStreamReady() {
1930
+ if (this.eventStreamReady) {
1931
+ return this.eventStreamReady.promise;
1932
+ }
1933
+ const eventStreamAbortController = new AbortController();
1934
+ const eventStreamReady = createDeferred();
1935
+ this.eventStreamAbortController = eventStreamAbortController;
1936
+ this.eventStreamReady = eventStreamReady;
1937
+ void this.consumeEventStream(eventStreamAbortController, eventStreamReady).finally(() => {
1938
+ if (this.eventStreamAbortController === eventStreamAbortController) {
1939
+ this.eventStreamAbortController = null;
1940
+ this.eventStreamReady = null;
1941
+ }
1942
+ });
1943
+ return eventStreamReady.promise;
1944
+ }
1945
+ async consumeEventStream(eventStreamAbortController, eventStreamReady) {
1879
1946
  this.traceOpenCode("provider.opencode.subscribe.start", {
1880
- turnId,
1881
1947
  sessionId: this.sessionId,
1882
1948
  cwd: this.config.cwd,
1883
1949
  });
1950
+ let eventStreamReadyResolved = false;
1884
1951
  try {
1885
1952
  const result = await this.client.global.event({
1886
- signal: turnAbortController.signal,
1953
+ signal: eventStreamAbortController.signal,
1887
1954
  sseMaxRetryAttempts: 0,
1888
1955
  });
1956
+ eventStreamReadyResolved = true;
1957
+ this.traceOpenCode("provider.opencode.subscribe.ready", {
1958
+ sessionId: this.sessionId,
1959
+ });
1960
+ eventStreamReady.resolve();
1889
1961
  let eventCount = 0;
1890
- let subscriptionReadyResolved = false;
1891
1962
  for await (const rawEvent of result.stream) {
1892
1963
  eventCount += 1;
1893
- if (!subscriptionReadyResolved) {
1894
- subscriptionReadyResolved = true;
1895
- this.traceOpenCode("provider.opencode.subscribe.ready", {
1896
- turnId,
1897
- sessionId: this.sessionId,
1898
- });
1899
- subscriptionReady.resolve();
1900
- }
1901
- const shouldContinue = await this.consumeOpenCodeStreamEvent({
1902
- rawEvent,
1903
- eventCount,
1904
- turnId,
1905
- turnAbortController,
1906
- });
1907
- if (!shouldContinue) {
1908
- return;
1909
- }
1964
+ await this.consumeOpenCodeStreamEvent({ rawEvent, eventCount });
1910
1965
  }
1911
1966
  this.traceOpenCode("provider.opencode.stream.eof", {
1912
- turnId,
1913
1967
  eventCount,
1914
- aborted: turnAbortController.signal.aborted,
1915
- stillActive: this.activeForegroundTurnId === turnId,
1968
+ aborted: eventStreamAbortController.signal.aborted,
1969
+ activeTurnId: this.activeForegroundTurnId,
1916
1970
  });
1917
- if (!turnAbortController.signal.aborted && this.activeForegroundTurnId === turnId) {
1918
- this.traceOpenCode("provider.opencode.turn.fail_eof", { turnId, eventCount });
1919
- if (!subscriptionReadyResolved) {
1920
- subscriptionReady.reject(new Error("OpenCode event stream ended before it became ready"));
1971
+ if (!eventStreamAbortController.signal.aborted) {
1972
+ if (!eventStreamReadyResolved) {
1973
+ eventStreamReady.reject(new Error("OpenCode event stream ended before it became ready"));
1974
+ }
1975
+ const activeTurnId = this.activeForegroundTurnId;
1976
+ if (activeTurnId) {
1977
+ this.traceOpenCode("provider.opencode.turn.fail_eof", {
1978
+ turnId: activeTurnId,
1979
+ eventCount,
1980
+ });
1981
+ this.finishForegroundTurn({
1982
+ type: "turn_failed",
1983
+ provider: "opencode",
1984
+ error: "OpenCode event stream ended before the turn reached a terminal state",
1985
+ }, activeTurnId);
1921
1986
  }
1922
- this.finishForegroundTurn({
1923
- type: "turn_failed",
1924
- provider: "opencode",
1925
- error: "OpenCode event stream ended before the turn reached a terminal state",
1926
- }, turnId);
1927
1987
  }
1928
1988
  }
1929
1989
  catch (error) {
1930
1990
  this.traceOpenCode("provider.opencode.subscribe.error", {
1931
- turnId,
1991
+ turnId: this.activeForegroundTurnId ?? undefined,
1932
1992
  error: error instanceof Error ? { name: error.name, message: error.message } : String(error),
1933
1993
  });
1934
- subscriptionReady.reject(error);
1935
- if (!turnAbortController.signal.aborted && this.activeForegroundTurnId === turnId) {
1994
+ if (!eventStreamReadyResolved) {
1995
+ eventStreamReady.reject(error);
1996
+ }
1997
+ const activeTurnId = this.activeForegroundTurnId;
1998
+ if (!eventStreamAbortController.signal.aborted && activeTurnId) {
1936
1999
  this.finishForegroundTurn({
1937
2000
  type: "turn_failed",
1938
2001
  provider: "opencode",
1939
2002
  error: toDiagnosticErrorMessage(error),
1940
- }, turnId);
1941
- }
1942
- }
1943
- finally {
1944
- if (turnAbortController.signal.aborted) {
1945
- this.finishForegroundTurn({
1946
- type: "turn_canceled",
1947
- provider: "opencode",
1948
- reason: "interrupted",
1949
- }, turnId);
1950
- }
1951
- if (this.abortController === turnAbortController && this.activeForegroundTurnId !== turnId) {
1952
- this.abortController = null;
2003
+ }, activeTurnId);
1953
2004
  }
1954
2005
  }
1955
2006
  }
1956
2007
  async consumeOpenCodeStreamEvent(params) {
1957
- const { rawEvent, eventCount, turnId, turnAbortController } = params;
2008
+ const { rawEvent, eventCount } = params;
2009
+ const turnId = this.activeForegroundTurnId;
1958
2010
  const event = unwrapOpenCodeGlobalEvent(rawEvent);
1959
2011
  this.traceOpenCode("provider.opencode.raw_event", {
1960
- turnId,
2012
+ turnId: turnId ?? undefined,
1961
2013
  n: eventCount,
1962
2014
  type: event?.type,
1963
2015
  rawType: readOpenCodeRecord(rawEvent)?.type,
@@ -1966,18 +2018,16 @@ class OpenCodeAgentSession {
1966
2018
  properties: event?.properties,
1967
2019
  });
1968
2020
  if (!event) {
1969
- return true;
2021
+ return;
1970
2022
  }
1971
- if (turnAbortController.signal.aborted || this.activeForegroundTurnId !== turnId) {
2023
+ if (!turnId) {
1972
2024
  this.traceOpenCode("provider.opencode.event.skip", {
1973
- turnId,
1974
2025
  n: eventCount,
1975
- aborted: turnAbortController.signal.aborted,
1976
- activeTurnId: this.activeForegroundTurnId,
2026
+ reason: "no_active_turn",
2027
+ type: event.type,
1977
2028
  });
1978
- return false;
2029
+ return;
1979
2030
  }
1980
- this.armRetryFailureTimerForStatus(event, turnId);
1981
2031
  const translated = await this.translateEvent(event);
1982
2032
  this.traceOpenCode("provider.opencode.parsed_event", {
1983
2033
  turnId,
@@ -1989,7 +2039,7 @@ class OpenCodeAgentSession {
1989
2039
  for (const e of translated) {
1990
2040
  if (this.activeForegroundTurnId !== turnId) {
1991
2041
  this.traceOpenCode("provider.opencode.parsed_event.skip_active", { turnId, type: e.type });
1992
- return false;
2042
+ return;
1993
2043
  }
1994
2044
  if (e.type === "timeline" && e.item.type === "tool_call") {
1995
2045
  this.trackToolCall(e.item);
@@ -2001,11 +2051,10 @@ class OpenCodeAgentSession {
2001
2051
  type: terminalEvent.type,
2002
2052
  });
2003
2053
  this.finishForegroundTurn(terminalEvent, turnId);
2004
- return false;
2054
+ return;
2005
2055
  }
2006
2056
  this.notifySubscribers(e, turnId);
2007
2057
  }
2008
- return true;
2009
2058
  }
2010
2059
  finishForegroundTurn(event, turnId) {
2011
2060
  this.traceOpenCode("provider.opencode.finish_foreground_turn", {
@@ -2024,10 +2073,7 @@ class OpenCodeAgentSession {
2024
2073
  else {
2025
2074
  this.runningToolCalls.clear();
2026
2075
  }
2027
- this.clearRetryFailureTimer();
2028
2076
  this.activeForegroundTurnId = null;
2029
- // Abort the SSE connection so the SDK tears down the underlying fetch.
2030
- this.abortController?.abort();
2031
2077
  this.abortController = null;
2032
2078
  this.notifySubscribers(event, turnId);
2033
2079
  }
@@ -2038,37 +2084,6 @@ class OpenCodeAgentSession {
2038
2084
  }
2039
2085
  this.runningToolCalls.delete(item.callId);
2040
2086
  }
2041
- armRetryFailureTimerForStatus(event, turnId) {
2042
- if (this.retryFailureTimer || event.type !== "session.status") {
2043
- return;
2044
- }
2045
- if (event.properties.sessionID !== this.sessionId || event.properties.status.type !== "retry") {
2046
- return;
2047
- }
2048
- const retry = event.properties.status;
2049
- const message = typeof retry.message === "string" ? retry.message.trim() : "";
2050
- const error = message
2051
- ? `OpenCode provider retry did not recover: ${message}`
2052
- : "OpenCode provider retry did not recover";
2053
- this.retryFailureTimer = setTimeout(() => {
2054
- this.retryFailureTimer = null;
2055
- if (this.activeForegroundTurnId !== turnId) {
2056
- return;
2057
- }
2058
- this.finishForegroundTurn({
2059
- type: "turn_failed",
2060
- provider: "opencode",
2061
- error,
2062
- }, turnId);
2063
- }, OPENCODE_RETRY_STATUS_FAILURE_MS);
2064
- }
2065
- clearRetryFailureTimer() {
2066
- if (!this.retryFailureTimer) {
2067
- return;
2068
- }
2069
- clearTimeout(this.retryFailureTimer);
2070
- this.retryFailureTimer = null;
2071
- }
2072
2087
  synthesizeInterruptedToolCalls(turnId) {
2073
2088
  for (const item of this.runningToolCalls.values()) {
2074
2089
  const error = { message: "Tool execution aborted" };
@@ -2093,6 +2108,9 @@ class OpenCodeAgentSession {
2093
2108
  this.runningToolCalls.clear();
2094
2109
  }
2095
2110
  notifySubscribers(event, turnIdOverride) {
2111
+ if (this.closed) {
2112
+ return;
2113
+ }
2096
2114
  const turnId = turnIdOverride ?? this.activeForegroundTurnId;
2097
2115
  const tagged = turnId ? { ...event, turnId } : event;
2098
2116
  this.traceOpenCode("provider.opencode.event_emit", {
@@ -2128,62 +2146,9 @@ class OpenCodeAgentSession {
2128
2146
  if (response.error || !response.data) {
2129
2147
  return;
2130
2148
  }
2131
- for (const { info, parts } of response.data) {
2132
- if (info.role === "user") {
2133
- const text = parts
2134
- .filter((p) => p.type === "text")
2135
- .map((p) => p.text)
2136
- .join("");
2137
- if (text) {
2138
- yield {
2139
- type: "timeline",
2140
- provider: "opencode",
2141
- item: { type: "user_message", text },
2142
- };
2143
- }
2144
- }
2145
- else {
2146
- let emittedAssistantText = false;
2147
- for (const part of parts) {
2148
- if (part.type === "text" && part.text) {
2149
- emittedAssistantText = true;
2150
- yield {
2151
- type: "timeline",
2152
- provider: "opencode",
2153
- item: { type: "assistant_message", text: part.text },
2154
- };
2155
- continue;
2156
- }
2157
- if (part.type === "reasoning" && part.text) {
2158
- yield {
2159
- type: "timeline",
2160
- provider: "opencode",
2161
- item: { type: "reasoning", text: part.text },
2162
- };
2163
- continue;
2164
- }
2165
- if (part.type !== "tool") {
2166
- continue;
2167
- }
2168
- const parsedToolPart = OpencodeToolPartToTimelineItemSchema.safeParse(part);
2169
- if (parsedToolPart.success && parsedToolPart.data) {
2170
- yield {
2171
- type: "timeline",
2172
- provider: "opencode",
2173
- item: parsedToolPart.data,
2174
- };
2175
- }
2176
- }
2177
- if (!emittedAssistantText) {
2178
- const text = stringifyStructuredAssistantMessage(info.structured);
2179
- if (text) {
2180
- yield {
2181
- type: "timeline",
2182
- provider: "opencode",
2183
- item: { type: "assistant_message", text },
2184
- };
2185
- }
2186
- }
2149
+ for (const message of response.data) {
2150
+ for (const event of buildOpenCodeReplayTimelineEvents(message)) {
2151
+ yield event;
2187
2152
  }
2188
2153
  }
2189
2154
  }
@@ -2209,21 +2174,7 @@ class OpenCodeAgentSession {
2209
2174
  return this.currentMode;
2210
2175
  }
2211
2176
  async listCommands() {
2212
- const result = await this.client.command.list({
2213
- directory: this.config.cwd,
2214
- });
2215
- const commandsByName = new Map(OPENCODE_HANDLED_BUILTIN_SLASH_COMMANDS.map((command) => [command.name, command]));
2216
- if (result.error || !result.data) {
2217
- return Array.from(commandsByName.values());
2218
- }
2219
- for (const cmd of result.data) {
2220
- commandsByName.set(cmd.name, {
2221
- name: cmd.name,
2222
- description: cmd.description ?? "",
2223
- argumentHint: cmd.hints?.length ? cmd.hints.join(" ") : "",
2224
- });
2225
- }
2226
- return Array.from(commandsByName.values());
2177
+ return await listOpenCodeCommandsFromSdk(this.client, this.config.cwd);
2227
2178
  }
2228
2179
  async setMode(modeId) {
2229
2180
  this.currentMode = normalizeOpenCodeModeId(modeId);
@@ -2282,12 +2233,23 @@ class OpenCodeAgentSession {
2282
2233
  nativeHandle: this.sessionId,
2283
2234
  metadata: {
2284
2235
  cwd: this.config.cwd,
2236
+ ...(this.config.modeId ? { modeId: this.config.modeId } : {}),
2237
+ ...(this.config.model ? { model: this.config.model } : {}),
2285
2238
  },
2286
2239
  };
2287
2240
  }
2288
2241
  async close() {
2289
2242
  try {
2243
+ // Flip closed before clearing subscribers so any event the SDK delivers
2244
+ // after the abort (between here and subscribers.clear) is swallowed by
2245
+ // notifySubscribers instead of bubbling through provider-runner as an
2246
+ // unhandled rejection in whichever test the daemon hops to next.
2247
+ this.closed = true;
2290
2248
  this.abortController?.abort();
2249
+ this.eventStreamAbortController?.abort();
2250
+ this.eventStreamAbortController = null;
2251
+ this.eventStreamReady = null;
2252
+ this.subscribers.clear();
2291
2253
  await reconcileOpenCodeSessionClose({
2292
2254
  client: this.client,
2293
2255
  sessionId: this.sessionId,
@@ -2295,7 +2257,6 @@ class OpenCodeAgentSession {
2295
2257
  logger: this.logger,
2296
2258
  });
2297
2259
  await this.deleteProviderSessionIfEphemeral();
2298
- this.subscribers.clear();
2299
2260
  this.activeForegroundTurnId = null;
2300
2261
  }
2301
2262
  finally {