@getpaseo/server 0.1.76 → 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 (68) hide show
  1. package/dist/server/client/daemon-client.d.ts +1 -0
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +1 -0
  4. package/dist/server/client/daemon-client.js.map +1 -1
  5. package/dist/server/server/agent/agent-manager.d.ts +1 -1
  6. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  7. package/dist/server/server/agent/agent-manager.js +8 -2
  8. package/dist/server/server/agent/agent-manager.js.map +1 -1
  9. package/dist/server/server/agent/agent-sdk-types.d.ts +2 -0
  10. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  11. package/dist/server/server/agent/import-sessions.d.ts.map +1 -1
  12. package/dist/server/server/agent/import-sessions.js +6 -2
  13. package/dist/server/server/agent/import-sessions.js.map +1 -1
  14. package/dist/server/server/agent/mcp-server.d.ts.map +1 -1
  15. package/dist/server/server/agent/mcp-server.js +153 -1
  16. package/dist/server/server/agent/mcp-server.js.map +1 -1
  17. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  18. package/dist/server/server/agent/providers/codex-app-server-agent.js +3 -1
  19. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  20. package/dist/server/server/agent/providers/opencode/server-manager.d.ts.map +1 -1
  21. package/dist/server/server/agent/providers/opencode/server-manager.js +2 -1
  22. package/dist/server/server/agent/providers/opencode/server-manager.js.map +1 -1
  23. package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.d.ts +7 -0
  24. package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.d.ts.map +1 -1
  25. package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.js +50 -1
  26. package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.js.map +1 -1
  27. package/dist/server/server/agent/providers/opencode-agent.d.ts +10 -4
  28. package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
  29. package/dist/server/server/agent/providers/opencode-agent.js +249 -286
  30. package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
  31. package/dist/server/server/bootstrap.d.ts +1 -0
  32. package/dist/server/server/bootstrap.d.ts.map +1 -1
  33. package/dist/server/server/bootstrap.js +8 -2
  34. package/dist/server/server/bootstrap.js.map +1 -1
  35. package/dist/server/server/config.d.ts.map +1 -1
  36. package/dist/server/server/config.js +10 -4
  37. package/dist/server/server/config.js.map +1 -1
  38. package/dist/server/server/pairing-offer.d.ts +1 -0
  39. package/dist/server/server/pairing-offer.d.ts.map +1 -1
  40. package/dist/server/server/pairing-offer.js +2 -1
  41. package/dist/server/server/pairing-offer.js.map +1 -1
  42. package/dist/server/server/persisted-config.d.ts +9 -0
  43. package/dist/server/server/persisted-config.d.ts.map +1 -1
  44. package/dist/server/server/persisted-config.js +10 -3
  45. package/dist/server/server/persisted-config.js.map +1 -1
  46. package/dist/server/server/session.d.ts.map +1 -1
  47. package/dist/server/server/session.js +2 -1
  48. package/dist/server/server/session.js.map +1 -1
  49. package/dist/server/server/utils/diff-highlighter.d.ts.map +1 -1
  50. package/dist/server/server/utils/diff-highlighter.js +30 -9
  51. package/dist/server/server/utils/diff-highlighter.js.map +1 -1
  52. package/dist/server/shared/messages.d.ts +16 -0
  53. package/dist/server/shared/messages.d.ts.map +1 -1
  54. package/dist/server/shared/messages.js +1 -0
  55. package/dist/server/shared/messages.js.map +1 -1
  56. package/dist/server/utils/directory-suggestions.d.ts +2 -0
  57. package/dist/server/utils/directory-suggestions.d.ts.map +1 -1
  58. package/dist/server/utils/directory-suggestions.js +39 -3
  59. package/dist/server/utils/directory-suggestions.js.map +1 -1
  60. package/dist/server/utils/path.d.ts +10 -0
  61. package/dist/server/utils/path.d.ts.map +1 -1
  62. package/dist/server/utils/path.js +65 -1
  63. package/dist/server/utils/path.js.map +1 -1
  64. package/dist/src/server/persisted-config.js +10 -3
  65. package/dist/src/server/persisted-config.js.map +1 -1
  66. package/dist/src/shared/messages.js +1 -0
  67. package/dist/src/shared/messages.js.map +1 -1
  68. 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";
@@ -25,7 +24,7 @@ const OPENCODE_CAPABILITIES = {
25
24
  };
26
25
  const OPENCODE_BUILD_MODE_ID = "build";
27
26
  const OPENCODE_FULL_ACCESS_MODE_ID = "full-access";
28
- const OPENCODE_STORAGE_SESSION_LIMIT = 200;
27
+ const OPENCODE_PERSISTED_SESSION_LIMIT = 200;
29
28
  const OPENCODE_PENDING_ABORT_START_TIMEOUT_MS = 10000;
30
29
  const DEFAULT_MODES = [
31
30
  {
@@ -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 = [
@@ -505,35 +466,30 @@ function buildOpenCodePromptParts(prompt) {
505
466
  }
506
467
  return output;
507
468
  }
508
- function resolveOpenCodeStorageRoot() {
509
- const xdgDataHome = process.env.XDG_DATA_HOME;
510
- const dataHome = typeof xdgDataHome === "string" && xdgDataHome.trim().length > 0
511
- ? xdgDataHome
512
- : path.join(homedir(), ".local", "share");
513
- return path.join(dataHome, "opencode", "storage");
514
- }
515
- async function collectOpenCodePersistedAgentsFromStorage(storageRoot, options) {
516
- const sessions = await readOpenCodeStoredSessions(path.join(storageRoot, "session"));
517
- 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;
518
482
  const candidates = sessions
519
- .filter((session) => !options?.cwd || session.directory === options.cwd)
483
+ .filter((session) => !matchesCwd || matchesCwd(session.directory))
520
484
  .sort((left, right) => getOpenCodeSessionTimestamp(right) - getOpenCodeSessionTimestamp(left))
521
485
  .slice(0, limit);
522
- return await Promise.all(candidates.map((session) => buildOpenCodePersistedAgentDescriptor(storageRoot, session)));
486
+ return await Promise.all(candidates.map((session) => buildOpenCodePersistedAgentDescriptor(client, session)));
523
487
  }
524
- async function readOpenCodeStoredSessions(sessionRoot) {
525
- const files = await findJsonFiles(sessionRoot);
526
- const sessions = [];
527
- for (const file of files) {
528
- const parsed = await readJsonFile(file, OpenCodeStoredSessionSchema);
529
- if (parsed) {
530
- sessions.push(parsed);
531
- }
532
- }
533
- return sessions;
534
- }
535
- async function buildOpenCodePersistedAgentDescriptor(storageRoot, session) {
536
- 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);
537
493
  return {
538
494
  provider: "opencode",
539
495
  sessionId: session.id,
@@ -548,84 +504,13 @@ async function buildOpenCodePersistedAgentDescriptor(storageRoot, session) {
548
504
  provider: "opencode",
549
505
  cwd: session.directory,
550
506
  title: normalizeOpenCodeSessionTitle(session.title),
507
+ ...(modeId ? { modeId } : {}),
508
+ ...(model ? { model } : {}),
551
509
  },
552
510
  },
553
511
  timeline,
554
512
  };
555
513
  }
556
- async function readOpenCodeSessionTimeline(storageRoot, sessionId) {
557
- const messageRoot = path.join(storageRoot, "message", sessionId);
558
- const messageFiles = await findJsonFiles(messageRoot);
559
- const messages = [];
560
- for (const file of messageFiles) {
561
- const parsed = await readJsonFile(file, OpenCodeStoredMessageSchema);
562
- if (parsed?.sessionID === sessionId) {
563
- messages.push(parsed);
564
- }
565
- }
566
- const timeline = [];
567
- for (const message of messages.sort((left, right) => getOpenCodeMessageTimestamp(left) - getOpenCodeMessageTimestamp(right))) {
568
- const text = await readOpenCodeMessageText(storageRoot, message.id);
569
- if (!text) {
570
- continue;
571
- }
572
- if (message.role === "user") {
573
- timeline.push({ type: "user_message", text, messageId: message.id });
574
- }
575
- else if (message.role === "assistant") {
576
- timeline.push({ type: "assistant_message", text });
577
- }
578
- }
579
- return timeline;
580
- }
581
- async function readOpenCodeMessageText(storageRoot, messageId) {
582
- const parts = await readOpenCodeStoredParts(storageRoot, messageId);
583
- return readOpenCodeTextFromParts(parts);
584
- }
585
- async function readOpenCodeStoredParts(storageRoot, messageId) {
586
- const partRoot = path.join(storageRoot, "part", messageId);
587
- const partFiles = await findJsonFiles(partRoot);
588
- const parts = [];
589
- for (const file of partFiles) {
590
- const parsed = await readJsonFile(file, OpenCodeStoredPartSchema);
591
- if (parsed) {
592
- parts.push(parsed);
593
- }
594
- }
595
- return parts.sort((left, right) => getOpenCodePartTimestamp(left) - getOpenCodePartTimestamp(right));
596
- }
597
- function readOpenCodeTextFromParts(parts) {
598
- return parts
599
- .filter((part) => part.type === "text" && typeof part.text === "string")
600
- .map((part) => part.text?.trim() ?? "")
601
- .filter(Boolean)
602
- .join("\n\n");
603
- }
604
- async function findJsonFiles(root) {
605
- let entries;
606
- try {
607
- entries = await readdir(root, { withFileTypes: true });
608
- }
609
- catch {
610
- return [];
611
- }
612
- const files = await Promise.all(entries.map(async (entry) => {
613
- const entryPath = path.join(root, entry.name);
614
- if (entry.isDirectory()) {
615
- return findJsonFiles(entryPath);
616
- }
617
- return entry.isFile() && entry.name.endsWith(".json") ? [entryPath] : [];
618
- }));
619
- return files.flat();
620
- }
621
- async function readJsonFile(file, schema) {
622
- try {
623
- return schema.parse(JSON.parse(await readFile(file, "utf8")));
624
- }
625
- catch {
626
- return null;
627
- }
628
- }
629
514
  function normalizeOpenCodeSessionTitle(title) {
630
515
  const normalized = title?.trim();
631
516
  return normalized ? normalized : null;
@@ -633,12 +518,6 @@ function normalizeOpenCodeSessionTitle(title) {
633
518
  function getOpenCodeSessionTimestamp(session) {
634
519
  return session.time?.updated ?? session.time?.created ?? 0;
635
520
  }
636
- function getOpenCodeMessageTimestamp(message) {
637
- return message.time?.created ?? message.time?.completed ?? 0;
638
- }
639
- function getOpenCodePartTimestamp(part) {
640
- return part.time?.start ?? part.time?.end ?? 0;
641
- }
642
521
  function resolveOpenCodeReplayTimestamp(params) {
643
522
  const timedPart = params.part;
644
523
  const partTimestamp = timedPart?.time?.start ??
@@ -688,6 +567,82 @@ function buildOpenCodeReplayPartTimelineEvent(params) {
688
567
  part,
689
568
  });
690
569
  }
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;
579
+ }
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;
586
+ }
587
+ function readOpenCodeMessageAgent(message) {
588
+ const agent = message.info.agent;
589
+ return typeof agent === "string" && agent.trim() ? agent : undefined;
590
+ }
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;
645
+ }
691
646
  export const __openCodeInternals = {
692
647
  buildOpenCodePromptParts,
693
648
  buildOpenCodeModelContextWindowLookup,
@@ -723,13 +678,12 @@ class ProductionOpenCodeRuntime {
723
678
  }
724
679
  }
725
680
  export class OpenCodeAgentClient {
726
- constructor(logger, runtimeSettings, storageRoot, deps = {}) {
681
+ constructor(logger, runtimeSettings, deps = {}) {
727
682
  this.provider = "opencode";
728
683
  this.capabilities = OPENCODE_CAPABILITIES;
729
684
  this.modelContextWindows = new Map();
730
685
  this.logger = logger.child({ module: "agent", provider: "opencode" });
731
686
  this.runtimeSettings = runtimeSettings;
732
- this.storageRoot = storageRoot ?? resolveOpenCodeStorageRoot();
733
687
  this.runtime =
734
688
  deps.runtime ??
735
689
  new ProductionOpenCodeRuntime(OpenCodeServerManager.getInstance(this.logger, runtimeSettings));
@@ -752,7 +706,7 @@ export class OpenCodeAgentClient {
752
706
  throw new Error("OpenCode session creation returned no data");
753
707
  }
754
708
  await this.populateModelContextWindowCache(client, openCodeConfig.cwd);
755
- 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);
756
710
  }
757
711
  catch (error) {
758
712
  acquisition.release();
@@ -760,14 +714,16 @@ export class OpenCodeAgentClient {
760
714
  }
761
715
  }
762
716
  async resumeSession(handle, overrides, launchContext) {
763
- const cwd = overrides?.cwd ?? handle.metadata?.cwd;
717
+ const metadata = (handle.metadata ?? {});
718
+ const cwd = overrides?.cwd ?? metadata.cwd;
764
719
  if (!cwd) {
765
720
  throw new Error("OpenCode resume requires the original working directory");
766
721
  }
767
722
  const config = {
723
+ ...metadata,
724
+ ...overrides,
768
725
  provider: "opencode",
769
726
  cwd,
770
- ...overrides,
771
727
  };
772
728
  const openCodeConfig = this.assertConfig(config);
773
729
  const acquisition = await this.runtime.acquireServer({ force: false });
@@ -778,7 +734,7 @@ export class OpenCodeAgentClient {
778
734
  });
779
735
  try {
780
736
  await this.populateModelContextWindowCache(client, openCodeConfig.cwd);
781
- 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);
782
738
  }
783
739
  catch (error) {
784
740
  acquisition.release();
@@ -858,8 +814,37 @@ export class OpenCodeAgentClient {
858
814
  acquisition.release();
859
815
  }
860
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
+ }
861
835
  async listPersistedAgents(options) {
862
- 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
+ }
863
848
  }
864
849
  async isAvailable() {
865
850
  const command = this.runtimeSettings?.command;
@@ -978,6 +963,21 @@ function stringifyStructuredAssistantMessage(value) {
978
963
  return null;
979
964
  }
980
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
+ }
981
981
  function readOpenCodeRecord(value) {
982
982
  return typeof value === "object" && value !== null && !Array.isArray(value)
983
983
  ? value
@@ -1634,7 +1634,7 @@ function unwrapOpenCodeGlobalEvent(event) {
1634
1634
  return null;
1635
1635
  }
1636
1636
  class OpenCodeAgentSession {
1637
- 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) {
1638
1638
  this.agentId = agentId;
1639
1639
  this.provider = "opencode";
1640
1640
  this.capabilities = OPENCODE_CAPABILITIES;
@@ -1661,6 +1661,9 @@ class OpenCodeAgentSession {
1661
1661
  this.subAgentsByCallId = new Map();
1662
1662
  this.subAgentCallIdByChildSessionId = new Map();
1663
1663
  this.pendingChildToolPartsBySessionId = new Map();
1664
+ this.eventStreamAbortController = null;
1665
+ this.eventStreamReady = null;
1666
+ this.closed = false;
1664
1667
  this.deletedFromProvider = false;
1665
1668
  this.config = config;
1666
1669
  this.client = client;
@@ -1671,6 +1674,7 @@ class OpenCodeAgentSession {
1671
1674
  this.releaseServer = releaseServer ?? null;
1672
1675
  this.persistSession = persistSession;
1673
1676
  this.selectedModelContextWindowMaxTokens = this.resolveConfiguredModelContextWindowMaxTokens(config.model);
1677
+ this.startEventStream();
1674
1678
  }
1675
1679
  get id() {
1676
1680
  return this.sessionId;
@@ -1767,22 +1771,17 @@ class OpenCodeAgentSession {
1767
1771
  const thinkingOptionId = this.config.thinkingOptionId;
1768
1772
  const effectiveVariant = thinkingOptionId ?? undefined;
1769
1773
  const effectiveMode = resolveOpenCodeRuntimeAgentId(this.currentMode);
1770
- const turnId = this.createTurnId();
1771
- this.activeForegroundTurnId = turnId;
1772
- // OpenCode's /event SSE endpoint does NOT replay past events. If we send
1773
- // the prompt before our reader is connected, terminal events fired early
1774
- // by the server (e.g. session.error / session.idle for invalid model or
1775
- // mode) are missed and the turn hangs forever. Wait for the subscription
1776
- // to be established before sending anything.
1777
- const subscriptionReady = createDeferred();
1778
- void this.consumeEventStream(turnId, turnAbortController, subscriptionReady);
1779
1774
  try {
1780
- await subscriptionReady.promise;
1775
+ await this.ensureEventStreamReady();
1781
1776
  }
1782
- catch {
1783
- // consumeEventStream already finished the turn with the subscription error.
1784
- return { turnId };
1777
+ catch (error) {
1778
+ if (this.abortController === turnAbortController) {
1779
+ this.abortController = null;
1780
+ }
1781
+ throw error;
1785
1782
  }
1783
+ const turnId = this.createTurnId();
1784
+ this.activeForegroundTurnId = turnId;
1786
1785
  this.notifySubscribers({ type: "turn_started", provider: "opencode" }, turnId);
1787
1786
  const slashCommand = await this.resolveSlashCommandInvocation(prompt);
1788
1787
  if (slashCommand) {
@@ -1815,12 +1814,8 @@ class OpenCodeAgentSession {
1815
1814
  });
1816
1815
  return { turnId };
1817
1816
  }
1818
- // command() blocks until the server finishes processing. OpenCode's SSE
1819
- // endpoint does NOT replay past events, so if the command completes before
1820
- // our SSE reader connects, we miss `session.idle` and the turn hangs.
1821
- // Handle both success and error in the response handler as a fallback —
1822
- // finishForegroundTurn's guard prevents duplicate terminal events if the
1823
- // 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.
1824
1819
  void this.client.session
1825
1820
  .command({
1826
1821
  sessionID: this.sessionId,
@@ -1844,9 +1839,6 @@ class OpenCodeAgentSession {
1844
1839
  const errorMsg = toDiagnosticErrorMessage(response.error);
1845
1840
  this.finishForegroundTurn({ type: "turn_failed", provider: "opencode", error: errorMsg }, turnId);
1846
1841
  }
1847
- else {
1848
- this.finishForegroundTurn({ type: "turn_completed", provider: "opencode", usage: undefined }, turnId);
1849
- }
1850
1842
  return;
1851
1843
  })
1852
1844
  .catch((err) => {
@@ -1929,89 +1921,95 @@ class OpenCodeAgentSession {
1929
1921
  this.subscribers.delete(callback);
1930
1922
  };
1931
1923
  }
1932
- 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) {
1933
1946
  this.traceOpenCode("provider.opencode.subscribe.start", {
1934
- turnId,
1935
1947
  sessionId: this.sessionId,
1936
1948
  cwd: this.config.cwd,
1937
1949
  });
1950
+ let eventStreamReadyResolved = false;
1938
1951
  try {
1939
1952
  const result = await this.client.global.event({
1940
- signal: turnAbortController.signal,
1953
+ signal: eventStreamAbortController.signal,
1941
1954
  sseMaxRetryAttempts: 0,
1942
1955
  });
1956
+ eventStreamReadyResolved = true;
1957
+ this.traceOpenCode("provider.opencode.subscribe.ready", {
1958
+ sessionId: this.sessionId,
1959
+ });
1960
+ eventStreamReady.resolve();
1943
1961
  let eventCount = 0;
1944
- let subscriptionReadyResolved = false;
1945
1962
  for await (const rawEvent of result.stream) {
1946
1963
  eventCount += 1;
1947
- if (!subscriptionReadyResolved) {
1948
- subscriptionReadyResolved = true;
1949
- this.traceOpenCode("provider.opencode.subscribe.ready", {
1950
- turnId,
1951
- sessionId: this.sessionId,
1952
- });
1953
- subscriptionReady.resolve();
1954
- }
1955
- const shouldContinue = await this.consumeOpenCodeStreamEvent({
1956
- rawEvent,
1957
- eventCount,
1958
- turnId,
1959
- turnAbortController,
1960
- });
1961
- if (!shouldContinue) {
1962
- return;
1963
- }
1964
+ await this.consumeOpenCodeStreamEvent({ rawEvent, eventCount });
1964
1965
  }
1965
1966
  this.traceOpenCode("provider.opencode.stream.eof", {
1966
- turnId,
1967
1967
  eventCount,
1968
- aborted: turnAbortController.signal.aborted,
1969
- stillActive: this.activeForegroundTurnId === turnId,
1968
+ aborted: eventStreamAbortController.signal.aborted,
1969
+ activeTurnId: this.activeForegroundTurnId,
1970
1970
  });
1971
- if (!turnAbortController.signal.aborted && this.activeForegroundTurnId === turnId) {
1972
- this.traceOpenCode("provider.opencode.turn.fail_eof", { turnId, eventCount });
1973
- if (!subscriptionReadyResolved) {
1974
- 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);
1975
1986
  }
1976
- this.finishForegroundTurn({
1977
- type: "turn_failed",
1978
- provider: "opencode",
1979
- error: "OpenCode event stream ended before the turn reached a terminal state",
1980
- }, turnId);
1981
1987
  }
1982
1988
  }
1983
1989
  catch (error) {
1984
1990
  this.traceOpenCode("provider.opencode.subscribe.error", {
1985
- turnId,
1991
+ turnId: this.activeForegroundTurnId ?? undefined,
1986
1992
  error: error instanceof Error ? { name: error.name, message: error.message } : String(error),
1987
1993
  });
1988
- subscriptionReady.reject(error);
1989
- 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) {
1990
1999
  this.finishForegroundTurn({
1991
2000
  type: "turn_failed",
1992
2001
  provider: "opencode",
1993
2002
  error: toDiagnosticErrorMessage(error),
1994
- }, turnId);
1995
- }
1996
- }
1997
- finally {
1998
- if (turnAbortController.signal.aborted) {
1999
- this.finishForegroundTurn({
2000
- type: "turn_canceled",
2001
- provider: "opencode",
2002
- reason: "interrupted",
2003
- }, turnId);
2004
- }
2005
- if (this.abortController === turnAbortController && this.activeForegroundTurnId !== turnId) {
2006
- this.abortController = null;
2003
+ }, activeTurnId);
2007
2004
  }
2008
2005
  }
2009
2006
  }
2010
2007
  async consumeOpenCodeStreamEvent(params) {
2011
- const { rawEvent, eventCount, turnId, turnAbortController } = params;
2008
+ const { rawEvent, eventCount } = params;
2009
+ const turnId = this.activeForegroundTurnId;
2012
2010
  const event = unwrapOpenCodeGlobalEvent(rawEvent);
2013
2011
  this.traceOpenCode("provider.opencode.raw_event", {
2014
- turnId,
2012
+ turnId: turnId ?? undefined,
2015
2013
  n: eventCount,
2016
2014
  type: event?.type,
2017
2015
  rawType: readOpenCodeRecord(rawEvent)?.type,
@@ -2020,16 +2018,15 @@ class OpenCodeAgentSession {
2020
2018
  properties: event?.properties,
2021
2019
  });
2022
2020
  if (!event) {
2023
- return true;
2021
+ return;
2024
2022
  }
2025
- if (turnAbortController.signal.aborted || this.activeForegroundTurnId !== turnId) {
2023
+ if (!turnId) {
2026
2024
  this.traceOpenCode("provider.opencode.event.skip", {
2027
- turnId,
2028
2025
  n: eventCount,
2029
- aborted: turnAbortController.signal.aborted,
2030
- activeTurnId: this.activeForegroundTurnId,
2026
+ reason: "no_active_turn",
2027
+ type: event.type,
2031
2028
  });
2032
- return false;
2029
+ return;
2033
2030
  }
2034
2031
  const translated = await this.translateEvent(event);
2035
2032
  this.traceOpenCode("provider.opencode.parsed_event", {
@@ -2042,7 +2039,7 @@ class OpenCodeAgentSession {
2042
2039
  for (const e of translated) {
2043
2040
  if (this.activeForegroundTurnId !== turnId) {
2044
2041
  this.traceOpenCode("provider.opencode.parsed_event.skip_active", { turnId, type: e.type });
2045
- return false;
2042
+ return;
2046
2043
  }
2047
2044
  if (e.type === "timeline" && e.item.type === "tool_call") {
2048
2045
  this.trackToolCall(e.item);
@@ -2054,11 +2051,10 @@ class OpenCodeAgentSession {
2054
2051
  type: terminalEvent.type,
2055
2052
  });
2056
2053
  this.finishForegroundTurn(terminalEvent, turnId);
2057
- return false;
2054
+ return;
2058
2055
  }
2059
2056
  this.notifySubscribers(e, turnId);
2060
2057
  }
2061
- return true;
2062
2058
  }
2063
2059
  finishForegroundTurn(event, turnId) {
2064
2060
  this.traceOpenCode("provider.opencode.finish_foreground_turn", {
@@ -2078,8 +2074,6 @@ class OpenCodeAgentSession {
2078
2074
  this.runningToolCalls.clear();
2079
2075
  }
2080
2076
  this.activeForegroundTurnId = null;
2081
- // Abort the SSE connection so the SDK tears down the underlying fetch.
2082
- this.abortController?.abort();
2083
2077
  this.abortController = null;
2084
2078
  this.notifySubscribers(event, turnId);
2085
2079
  }
@@ -2114,6 +2108,9 @@ class OpenCodeAgentSession {
2114
2108
  this.runningToolCalls.clear();
2115
2109
  }
2116
2110
  notifySubscribers(event, turnIdOverride) {
2111
+ if (this.closed) {
2112
+ return;
2113
+ }
2117
2114
  const turnId = turnIdOverride ?? this.activeForegroundTurnId;
2118
2115
  const tagged = turnId ? { ...event, turnId } : event;
2119
2116
  this.traceOpenCode("provider.opencode.event_emit", {
@@ -2149,39 +2146,9 @@ class OpenCodeAgentSession {
2149
2146
  if (response.error || !response.data) {
2150
2147
  return;
2151
2148
  }
2152
- for (const { info, parts } of response.data) {
2153
- if (info.role === "user") {
2154
- const text = parts
2155
- .filter((p) => p.type === "text")
2156
- .map((p) => p.text)
2157
- .join("");
2158
- if (text) {
2159
- yield buildOpenCodeReplayTimelineEvent({
2160
- item: { type: "user_message", text },
2161
- message: info,
2162
- });
2163
- }
2164
- }
2165
- else {
2166
- let emittedAssistantText = false;
2167
- for (const part of parts) {
2168
- if (part.type === "text" && part.text) {
2169
- emittedAssistantText = true;
2170
- }
2171
- const event = buildOpenCodeReplayPartTimelineEvent({ part, message: info });
2172
- if (event) {
2173
- yield event;
2174
- }
2175
- }
2176
- if (!emittedAssistantText) {
2177
- const text = stringifyStructuredAssistantMessage(info.structured);
2178
- if (text) {
2179
- yield buildOpenCodeReplayTimelineEvent({
2180
- item: { type: "assistant_message", text },
2181
- message: info,
2182
- });
2183
- }
2184
- }
2149
+ for (const message of response.data) {
2150
+ for (const event of buildOpenCodeReplayTimelineEvents(message)) {
2151
+ yield event;
2185
2152
  }
2186
2153
  }
2187
2154
  }
@@ -2207,21 +2174,7 @@ class OpenCodeAgentSession {
2207
2174
  return this.currentMode;
2208
2175
  }
2209
2176
  async listCommands() {
2210
- const result = await this.client.command.list({
2211
- directory: this.config.cwd,
2212
- });
2213
- const commandsByName = new Map(OPENCODE_HANDLED_BUILTIN_SLASH_COMMANDS.map((command) => [command.name, command]));
2214
- if (result.error || !result.data) {
2215
- return Array.from(commandsByName.values());
2216
- }
2217
- for (const cmd of result.data) {
2218
- commandsByName.set(cmd.name, {
2219
- name: cmd.name,
2220
- description: cmd.description ?? "",
2221
- argumentHint: cmd.hints?.length ? cmd.hints.join(" ") : "",
2222
- });
2223
- }
2224
- return Array.from(commandsByName.values());
2177
+ return await listOpenCodeCommandsFromSdk(this.client, this.config.cwd);
2225
2178
  }
2226
2179
  async setMode(modeId) {
2227
2180
  this.currentMode = normalizeOpenCodeModeId(modeId);
@@ -2280,12 +2233,23 @@ class OpenCodeAgentSession {
2280
2233
  nativeHandle: this.sessionId,
2281
2234
  metadata: {
2282
2235
  cwd: this.config.cwd,
2236
+ ...(this.config.modeId ? { modeId: this.config.modeId } : {}),
2237
+ ...(this.config.model ? { model: this.config.model } : {}),
2283
2238
  },
2284
2239
  };
2285
2240
  }
2286
2241
  async close() {
2287
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;
2288
2248
  this.abortController?.abort();
2249
+ this.eventStreamAbortController?.abort();
2250
+ this.eventStreamAbortController = null;
2251
+ this.eventStreamReady = null;
2252
+ this.subscribers.clear();
2289
2253
  await reconcileOpenCodeSessionClose({
2290
2254
  client: this.client,
2291
2255
  sessionId: this.sessionId,
@@ -2293,7 +2257,6 @@ class OpenCodeAgentSession {
2293
2257
  logger: this.logger,
2294
2258
  });
2295
2259
  await this.deleteProviderSessionIfEphemeral();
2296
- this.subscribers.clear();
2297
2260
  this.activeForegroundTurnId = null;
2298
2261
  }
2299
2262
  finally {