@ordewell/core 0.4.17 → 0.4.19

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.
@@ -458,6 +458,14 @@ interface ITerminalSession {
458
458
  kill(): void;
459
459
  getOutput(): string;
460
460
  write(text: string): void;
461
+ /**
462
+ * True when the session runs the agent as a raw-mode TUI (a real PTY for the
463
+ * VS Code terminal, a tmux window). Such a surface submits an input line on
464
+ * the Enter keystroke (`\r`), so a synchronized resume token terminated with
465
+ * `\n` only types the line and never sends it. A line-oriented piped session
466
+ * (`defaultInteractive = false`) leaves this false and accepts `\n`.
467
+ */
468
+ readonly interactive?: boolean;
461
469
  /**
462
470
  * Optional transport-level control channel: PTY resize requests for a session
463
471
  * whose runner renders a TUI. Absent on transports without a resizable PTY
@@ -458,6 +458,14 @@ interface ITerminalSession {
458
458
  kill(): void;
459
459
  getOutput(): string;
460
460
  write(text: string): void;
461
+ /**
462
+ * True when the session runs the agent as a raw-mode TUI (a real PTY for the
463
+ * VS Code terminal, a tmux window). Such a surface submits an input line on
464
+ * the Enter keystroke (`\r`), so a synchronized resume token terminated with
465
+ * `\n` only types the line and never sends it. A line-oriented piped session
466
+ * (`defaultInteractive = false`) leaves this false and accepts `\n`.
467
+ */
468
+ readonly interactive?: boolean;
461
469
  /**
462
470
  * Optional transport-level control channel: PTY resize requests for a session
463
471
  * whose runner renders a TUI. Absent on transports without a resizable PTY
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { h as RunnerId, c as PlanStatus, L as LegacyPlanState, a as DiscoveredModel, T as Task, d as ResearchProgress, R as ResearchLogEntry, C as ConversationMessage, n as TaskSnapshot, Q as QueuedMessage, A as ActiveTaskSession, g as ResearchToolType, b as PlanState, v as Verdict } from './Task-4rDGr8Su.mjs';
2
2
  export { D as DiscoveredMode, M as Message, P as PlanModificationWarnings, e as ResearchStep, f as ResearchStepOutcome, S as StreamEvent, i as StreamStepEvent, j as StreamThinkingEvent, k as TaskMode, l as TaskModelAssignment, m as TaskOutputSummary, o as TaskStatus, p as TaskType, q as TaskWithParent, r as ThinkingBlock, U as UserPromptEntry, s as UserStep, V as ValidationCheck, t as ValidationContext, u as ValidationResult, w as VerificationCheck, x as addTaskToPlan, y as createEmptyPlan, z as createTask, B as emptyWarnings, E as flattenTasks, F as flattenTasksWithParents, G as migrateLegacyPlan, H as migratePlanState, I as migrateTask, J as removeTaskFromPlan, K as renumberTasks, N as updateTaskInPlan, O as validateModifiedPlan, W as warningsText } from './Task-4rDGr8Su.mjs';
3
- import { f as IFileSystem, R as ReadFileOpts, T as ToolOutcome, d as GrepOptions, c as GlobOptions, b as FindSymbolOptions, I as IConfig, A as AiProvider, r as ProviderModelLists, i as ITerminalSession, h as ITerminalRunner, w as RunnerRegistry, k as OrchestratorOption, u as RunnerInvocation, g as IPluginStore, v as RunnerPluginManifest, t as ResolveContext } from './ITerminalRunner-E3-6QNUS.mjs';
4
- export { a as AllProviderModels, C as CatalogModel, D as DiscoveryCommand, F as FetchAllProviderModelsOptions, G as GREP_DEFAULT_HEAD_LIMIT, e as GrepOutputMode, M as ModelCatalog, j as ModelShortcut, O as ORCHESTRATOR_SHORTCUTS, P as PluginCloneFn, l as PluginEntry, m as PluginFeatures, n as PluginMode, o as PluginModelDiscovery, p as PluginRunnerDef, q as ProviderCredentialSource, s as ProviderModelsResult, S as SEARCH_EXCLUSIONS, x as collectProviderCredentials, y as enabledRunners, z as fetchAllProviderModels, B as isReservedRunnerName, E as knownModelId, H as resolveModelShortcut, J as resolveProvider, K as toOrchestratorOptions } from './ITerminalRunner-E3-6QNUS.mjs';
3
+ import { f as IFileSystem, R as ReadFileOpts, T as ToolOutcome, d as GrepOptions, c as GlobOptions, b as FindSymbolOptions, I as IConfig, A as AiProvider, r as ProviderModelLists, i as ITerminalSession, h as ITerminalRunner, w as RunnerRegistry, k as OrchestratorOption, u as RunnerInvocation, g as IPluginStore, v as RunnerPluginManifest, t as ResolveContext } from './ITerminalRunner-C-HlFxuS.mjs';
4
+ export { a as AllProviderModels, C as CatalogModel, D as DiscoveryCommand, F as FetchAllProviderModelsOptions, G as GREP_DEFAULT_HEAD_LIMIT, e as GrepOutputMode, M as ModelCatalog, j as ModelShortcut, O as ORCHESTRATOR_SHORTCUTS, P as PluginCloneFn, l as PluginEntry, m as PluginFeatures, n as PluginMode, o as PluginModelDiscovery, p as PluginRunnerDef, q as ProviderCredentialSource, s as ProviderModelsResult, S as SEARCH_EXCLUSIONS, x as collectProviderCredentials, y as enabledRunners, z as fetchAllProviderModels, B as isReservedRunnerName, E as knownModelId, H as resolveModelShortcut, J as resolveProvider, K as toOrchestratorOptions } from './ITerminalRunner-C-HlFxuS.mjs';
5
5
  import { I as IApproval, d as ApprovalRequest, a as ApprovalMode } from './ApprovalPolicy-BVhGdECT.mjs';
6
6
  export { A as ApprovalKind, b as ApprovalPolicy, c as ApprovalPolicyOptions, e as ApprovalSource, D as DENY_ALL } from './ApprovalPolicy-BVhGdECT.mjs';
7
7
  import { R as RunnerModeInfo } from './ModeResolver-mtIWrLXn.mjs';
@@ -936,10 +936,11 @@ interface HeadlessRunnerDeps {
936
936
  }
937
937
  declare class HeadlessSession extends AbstractTerminalSession {
938
938
  private spawnImpl;
939
+ readonly interactive: boolean;
939
940
  private process;
940
941
  private outputBuffer;
941
942
  private controlStream;
942
- constructor(id: string, taskId: string, spawnImpl: SpawnFn);
943
+ constructor(id: string, taskId: string, spawnImpl: SpawnFn, interactive?: boolean);
943
944
  get isStarted(): boolean;
944
945
  start(launch: LaunchPlan, cwd: string, resolvedPath: string, env?: Record<string, string>, options?: {
945
946
  controlChannel?: boolean;
@@ -2255,9 +2256,32 @@ declare class OpenCodeAdapter implements AgentAdapter {
2255
2256
  private opts;
2256
2257
  /** Whether this turn has already emitted reply text — see {@link emitPart}. */
2257
2258
  private turnHasText;
2259
+ /** The last assistant message already settled — the baseline {@link recoverReply} measures a new reply against. */
2260
+ private lastAssistantId;
2258
2261
  constructor(deps: AgentProcessDeps);
2259
2262
  start(opts: AgentStartOptions): Promise<void>;
2260
2263
  send(message: string, onEvent: (event: AgentEvent) => void, signal?: AbortSignal, onActivity?: () => void): Promise<void>;
2264
+ /**
2265
+ * Turn one settled assistant message into events. The settled response is
2266
+ * authoritative: it names the assistant message, so its parts are the ones
2267
+ * that make up the reply. Tool parts already seen live are deduplicated by
2268
+ * call id; anything the stream missed (including a stream that never
2269
+ * connected) arrives here.
2270
+ */
2271
+ private settle;
2272
+ /**
2273
+ * Poll the session for this turn's assistant message after the POST's socket
2274
+ * died under it. Node's global `fetch` is undici, which aborts a request
2275
+ * whose response headers have not arrived within 300s — and OpenCode sends
2276
+ * none until the turn is done, so any turn past five minutes fails as
2277
+ * `TypeError: fetch failed` while the server is still working. The message
2278
+ * exists server-side either way, so it is waited for and read back.
2279
+ *
2280
+ * A message that exists but has not completed is progress, not an answer:
2281
+ * it refreshes the watchdog and the poll continues.
2282
+ */
2283
+ private recoverReply;
2284
+ private newestAssistantId;
2261
2285
  /**
2262
2286
  * Emit one message part, once. OpenCode reports a tool part repeatedly as it
2263
2287
  * moves through pending → running → completed, so parts are keyed by id and
@@ -2845,6 +2869,14 @@ declare class VerdictEngine {
2845
2869
  private touchIdle;
2846
2870
  /** Tear down idle tracking for a task; broadcasts the null transition if it was idle. */
2847
2871
  private clearIdle;
2872
+ /**
2873
+ * Submit a synchronized resume token to the paused session. An interactive
2874
+ * TUI only accepts the Enter keystroke (`\r`) — a `\n` types the token into
2875
+ * its composer without sending it, leaving the agent paused until a human
2876
+ * presses Enter. A line-oriented piped session has no composer; it reads a
2877
+ * `\n`-terminated line, and the leading newline flushes a partial line.
2878
+ */
2879
+ private resumeToken;
2848
2880
  approveCheckpoint(taskId: string): void;
2849
2881
  rejectCheckpoint(taskId: string, reason: string): void;
2850
2882
  /**
@@ -3143,6 +3175,8 @@ declare class TmuxSession extends AbstractTerminalSession {
3143
3175
  private pollIntervalMs;
3144
3176
  private logPath;
3145
3177
  private socket;
3178
+ /** A tmux window is a real TTY running the agent's TUI, so it submits on Enter. */
3179
+ readonly interactive = true;
3146
3180
  private outputBuffer;
3147
3181
  private offset;
3148
3182
  private timer;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { h as RunnerId, c as PlanStatus, L as LegacyPlanState, a as DiscoveredModel, T as Task, d as ResearchProgress, R as ResearchLogEntry, C as ConversationMessage, n as TaskSnapshot, Q as QueuedMessage, A as ActiveTaskSession, g as ResearchToolType, b as PlanState, v as Verdict } from './Task-4rDGr8Su.js';
2
2
  export { D as DiscoveredMode, M as Message, P as PlanModificationWarnings, e as ResearchStep, f as ResearchStepOutcome, S as StreamEvent, i as StreamStepEvent, j as StreamThinkingEvent, k as TaskMode, l as TaskModelAssignment, m as TaskOutputSummary, o as TaskStatus, p as TaskType, q as TaskWithParent, r as ThinkingBlock, U as UserPromptEntry, s as UserStep, V as ValidationCheck, t as ValidationContext, u as ValidationResult, w as VerificationCheck, x as addTaskToPlan, y as createEmptyPlan, z as createTask, B as emptyWarnings, E as flattenTasks, F as flattenTasksWithParents, G as migrateLegacyPlan, H as migratePlanState, I as migrateTask, J as removeTaskFromPlan, K as renumberTasks, N as updateTaskInPlan, O as validateModifiedPlan, W as warningsText } from './Task-4rDGr8Su.js';
3
- import { f as IFileSystem, R as ReadFileOpts, T as ToolOutcome, d as GrepOptions, c as GlobOptions, b as FindSymbolOptions, I as IConfig, A as AiProvider, r as ProviderModelLists, i as ITerminalSession, h as ITerminalRunner, w as RunnerRegistry, k as OrchestratorOption, u as RunnerInvocation, g as IPluginStore, v as RunnerPluginManifest, t as ResolveContext } from './ITerminalRunner-CbLHKXT5.js';
4
- export { a as AllProviderModels, C as CatalogModel, D as DiscoveryCommand, F as FetchAllProviderModelsOptions, G as GREP_DEFAULT_HEAD_LIMIT, e as GrepOutputMode, M as ModelCatalog, j as ModelShortcut, O as ORCHESTRATOR_SHORTCUTS, P as PluginCloneFn, l as PluginEntry, m as PluginFeatures, n as PluginMode, o as PluginModelDiscovery, p as PluginRunnerDef, q as ProviderCredentialSource, s as ProviderModelsResult, S as SEARCH_EXCLUSIONS, x as collectProviderCredentials, y as enabledRunners, z as fetchAllProviderModels, B as isReservedRunnerName, E as knownModelId, H as resolveModelShortcut, J as resolveProvider, K as toOrchestratorOptions } from './ITerminalRunner-CbLHKXT5.js';
3
+ import { f as IFileSystem, R as ReadFileOpts, T as ToolOutcome, d as GrepOptions, c as GlobOptions, b as FindSymbolOptions, I as IConfig, A as AiProvider, r as ProviderModelLists, i as ITerminalSession, h as ITerminalRunner, w as RunnerRegistry, k as OrchestratorOption, u as RunnerInvocation, g as IPluginStore, v as RunnerPluginManifest, t as ResolveContext } from './ITerminalRunner-BVU7TfT3.js';
4
+ export { a as AllProviderModels, C as CatalogModel, D as DiscoveryCommand, F as FetchAllProviderModelsOptions, G as GREP_DEFAULT_HEAD_LIMIT, e as GrepOutputMode, M as ModelCatalog, j as ModelShortcut, O as ORCHESTRATOR_SHORTCUTS, P as PluginCloneFn, l as PluginEntry, m as PluginFeatures, n as PluginMode, o as PluginModelDiscovery, p as PluginRunnerDef, q as ProviderCredentialSource, s as ProviderModelsResult, S as SEARCH_EXCLUSIONS, x as collectProviderCredentials, y as enabledRunners, z as fetchAllProviderModels, B as isReservedRunnerName, E as knownModelId, H as resolveModelShortcut, J as resolveProvider, K as toOrchestratorOptions } from './ITerminalRunner-BVU7TfT3.js';
5
5
  import { I as IApproval, d as ApprovalRequest, a as ApprovalMode } from './ApprovalPolicy-BVhGdECT.js';
6
6
  export { A as ApprovalKind, b as ApprovalPolicy, c as ApprovalPolicyOptions, e as ApprovalSource, D as DENY_ALL } from './ApprovalPolicy-BVhGdECT.js';
7
7
  import { R as RunnerModeInfo } from './ModeResolver-Ob-ihQbI.js';
@@ -936,10 +936,11 @@ interface HeadlessRunnerDeps {
936
936
  }
937
937
  declare class HeadlessSession extends AbstractTerminalSession {
938
938
  private spawnImpl;
939
+ readonly interactive: boolean;
939
940
  private process;
940
941
  private outputBuffer;
941
942
  private controlStream;
942
- constructor(id: string, taskId: string, spawnImpl: SpawnFn);
943
+ constructor(id: string, taskId: string, spawnImpl: SpawnFn, interactive?: boolean);
943
944
  get isStarted(): boolean;
944
945
  start(launch: LaunchPlan, cwd: string, resolvedPath: string, env?: Record<string, string>, options?: {
945
946
  controlChannel?: boolean;
@@ -2255,9 +2256,32 @@ declare class OpenCodeAdapter implements AgentAdapter {
2255
2256
  private opts;
2256
2257
  /** Whether this turn has already emitted reply text — see {@link emitPart}. */
2257
2258
  private turnHasText;
2259
+ /** The last assistant message already settled — the baseline {@link recoverReply} measures a new reply against. */
2260
+ private lastAssistantId;
2258
2261
  constructor(deps: AgentProcessDeps);
2259
2262
  start(opts: AgentStartOptions): Promise<void>;
2260
2263
  send(message: string, onEvent: (event: AgentEvent) => void, signal?: AbortSignal, onActivity?: () => void): Promise<void>;
2264
+ /**
2265
+ * Turn one settled assistant message into events. The settled response is
2266
+ * authoritative: it names the assistant message, so its parts are the ones
2267
+ * that make up the reply. Tool parts already seen live are deduplicated by
2268
+ * call id; anything the stream missed (including a stream that never
2269
+ * connected) arrives here.
2270
+ */
2271
+ private settle;
2272
+ /**
2273
+ * Poll the session for this turn's assistant message after the POST's socket
2274
+ * died under it. Node's global `fetch` is undici, which aborts a request
2275
+ * whose response headers have not arrived within 300s — and OpenCode sends
2276
+ * none until the turn is done, so any turn past five minutes fails as
2277
+ * `TypeError: fetch failed` while the server is still working. The message
2278
+ * exists server-side either way, so it is waited for and read back.
2279
+ *
2280
+ * A message that exists but has not completed is progress, not an answer:
2281
+ * it refreshes the watchdog and the poll continues.
2282
+ */
2283
+ private recoverReply;
2284
+ private newestAssistantId;
2261
2285
  /**
2262
2286
  * Emit one message part, once. OpenCode reports a tool part repeatedly as it
2263
2287
  * moves through pending → running → completed, so parts are keyed by id and
@@ -2845,6 +2869,14 @@ declare class VerdictEngine {
2845
2869
  private touchIdle;
2846
2870
  /** Tear down idle tracking for a task; broadcasts the null transition if it was idle. */
2847
2871
  private clearIdle;
2872
+ /**
2873
+ * Submit a synchronized resume token to the paused session. An interactive
2874
+ * TUI only accepts the Enter keystroke (`\r`) — a `\n` types the token into
2875
+ * its composer without sending it, leaving the agent paused until a human
2876
+ * presses Enter. A line-oriented piped session has no composer; it reads a
2877
+ * `\n`-terminated line, and the leading newline flushes a partial line.
2878
+ */
2879
+ private resumeToken;
2848
2880
  approveCheckpoint(taskId: string): void;
2849
2881
  rejectCheckpoint(taskId: string, reason: string): void;
2850
2882
  /**
@@ -3143,6 +3175,8 @@ declare class TmuxSession extends AbstractTerminalSession {
3143
3175
  private pollIntervalMs;
3144
3176
  private logPath;
3145
3177
  private socket;
3178
+ /** A tmux window is a real TTY running the agent's TUI, so it submits on Enter. */
3179
+ readonly interactive = true;
3146
3180
  private outputBuffer;
3147
3181
  private offset;
3148
3182
  private timer;
package/dist/index.js CHANGED
@@ -8114,19 +8114,29 @@ var VerdictEngine = class {
8114
8114
  for (const l of this.idleListeners) l(taskId, null);
8115
8115
  }
8116
8116
  }
8117
+ /**
8118
+ * Submit a synchronized resume token to the paused session. An interactive
8119
+ * TUI only accepts the Enter keystroke (`\r`) — a `\n` types the token into
8120
+ * its composer without sending it, leaving the agent paused until a human
8121
+ * presses Enter. A line-oriented piped session has no composer; it reads a
8122
+ * `\n`-terminated line, and the leading newline flushes a partial line.
8123
+ */
8124
+ resumeToken(session, line) {
8125
+ return session.interactive ? `${line}\r` : `
8126
+ ${line}
8127
+ `;
8128
+ }
8117
8129
  approveCheckpoint(taskId) {
8118
8130
  const session = this.pausedSessions.get(taskId);
8119
8131
  if (session) {
8120
- session.write("\nORDEWELL_CONTINUE\n");
8132
+ session.write(this.resumeToken(session, "ORDEWELL_CONTINUE"));
8121
8133
  this.pausedSessions.delete(taskId);
8122
8134
  }
8123
8135
  }
8124
8136
  rejectCheckpoint(taskId, reason) {
8125
8137
  const session = this.pausedSessions.get(taskId);
8126
8138
  if (session) {
8127
- session.write(`
8128
- ORDEWELL_REJECT: ${reason}
8129
- `);
8139
+ session.write(this.resumeToken(session, `ORDEWELL_REJECT: ${reason}`));
8130
8140
  this.pausedSessions.delete(taskId);
8131
8141
  }
8132
8142
  }
@@ -11528,6 +11538,8 @@ ${item.text}` : item.text });
11528
11538
  var SERVER_READY_TIMEOUT_MS = 3e4;
11529
11539
  var STDERR_TAIL_CHARS2 = 4e3;
11530
11540
  var STREAM_CONNECT_TIMEOUT_MS = 5e3;
11541
+ var RECOVERY_POLL_INTERVAL_MS = 2e3;
11542
+ var RECOVERY_TIMEOUT_MS = 9e5;
11531
11543
  var DISABLED_TOOLS = {
11532
11544
  question: false,
11533
11545
  edit: false,
@@ -11540,6 +11552,33 @@ function splitModelId(id) {
11540
11552
  if (slash <= 0 || slash === id.length - 1) return null;
11541
11553
  return { providerID: id.slice(0, slash), modelID: id.slice(slash + 1) };
11542
11554
  }
11555
+ function describeError(err) {
11556
+ const visited = /* @__PURE__ */ new Set();
11557
+ const lines = [];
11558
+ let current = err;
11559
+ while (current && !visited.has(current)) {
11560
+ visited.add(current);
11561
+ if (!(current instanceof Error)) {
11562
+ lines.push(String(current));
11563
+ break;
11564
+ }
11565
+ const code = current.code;
11566
+ const suffix = typeof code === "string" && !current.message.includes(code) ? ` (${code})` : "";
11567
+ lines.push(`${current.message}${suffix}`);
11568
+ current = current.cause;
11569
+ }
11570
+ return lines.join(": ");
11571
+ }
11572
+ function delay(ms, signal) {
11573
+ return new Promise((resolve4) => {
11574
+ const timer = setTimeout(resolve4, ms);
11575
+ timer.unref?.();
11576
+ signal?.addEventListener("abort", () => {
11577
+ clearTimeout(timer);
11578
+ resolve4();
11579
+ }, { once: true });
11580
+ });
11581
+ }
11543
11582
  var OpenCodeAdapter = class {
11544
11583
  constructor(deps) {
11545
11584
  this.deps = deps;
@@ -11555,6 +11594,8 @@ var OpenCodeAdapter = class {
11555
11594
  opts = null;
11556
11595
  /** Whether this turn has already emitted reply text — see {@link emitPart}. */
11557
11596
  turnHasText = false;
11597
+ /** The last assistant message already settled — the baseline {@link recoverReply} measures a new reply against. */
11598
+ lastAssistantId = null;
11558
11599
  async start(opts) {
11559
11600
  this.opts = opts;
11560
11601
  assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
@@ -11608,6 +11649,8 @@ ${this.stderrTail.trim()}` : ""}`);
11608
11649
  const existing = await this.json("GET", `/session/${opts.resumeSessionId}`).catch(() => null);
11609
11650
  if (existing?.id) {
11610
11651
  this.sessionId = existing.id;
11652
+ const history = await this.json("GET", `/session/${existing.id}/message`).catch(() => null);
11653
+ this.lastAssistantId = this.newestAssistantId(history);
11611
11654
  return;
11612
11655
  }
11613
11656
  }
@@ -11658,29 +11701,76 @@ ${this.stderrTail.trim()}` : ""}`);
11658
11701
  this.dispose();
11659
11702
  return;
11660
11703
  }
11661
- const failure = typeof reply?.error === "string" ? reply.error : reply?.error?.message ?? reply?.info?.error?.data?.message;
11662
- if (failure) {
11663
- onEvent({ type: "error", message: failure });
11664
- return;
11665
- }
11666
- const assistantId = reply?.info?.id;
11667
- for (const part of reply?.parts ?? []) {
11668
- if (part.type !== "tool" && assistantId && part.messageID !== assistantId) continue;
11669
- this.emitPart(part, seen, onEvent);
11670
- }
11671
- onEvent({ type: "turn_end" });
11704
+ this.settle(reply, seen, onEvent);
11672
11705
  } catch (err) {
11673
11706
  if (signal?.aborted) {
11674
11707
  this.dispose();
11675
11708
  return;
11676
11709
  }
11677
- onEvent({ type: "error", message: `The OpenCode planner turn failed: ${err instanceof Error ? err.message : String(err)}` });
11710
+ const recovered = this.exited ? null : await this.recoverReply(signal, onActivity);
11711
+ if (recovered) {
11712
+ this.settle(recovered, seen, onEvent);
11713
+ return;
11714
+ }
11715
+ onEvent({ type: "error", message: `The OpenCode planner turn failed: ${describeError(err)}` });
11678
11716
  } finally {
11679
11717
  streamAbort.abort();
11680
11718
  await live.catch(() => {
11681
11719
  });
11682
11720
  }
11683
11721
  }
11722
+ /**
11723
+ * Turn one settled assistant message into events. The settled response is
11724
+ * authoritative: it names the assistant message, so its parts are the ones
11725
+ * that make up the reply. Tool parts already seen live are deduplicated by
11726
+ * call id; anything the stream missed (including a stream that never
11727
+ * connected) arrives here.
11728
+ */
11729
+ settle(reply, seen, onEvent) {
11730
+ const failure = typeof reply?.error === "string" ? reply.error : reply?.error?.message ?? reply?.info?.error?.data?.message;
11731
+ if (failure) {
11732
+ onEvent({ type: "error", message: failure });
11733
+ return;
11734
+ }
11735
+ const assistantId = reply?.info?.id;
11736
+ for (const part of reply?.parts ?? []) {
11737
+ if (part.type !== "tool" && assistantId && part.messageID !== assistantId) continue;
11738
+ this.emitPart(part, seen, onEvent);
11739
+ }
11740
+ if (assistantId) this.lastAssistantId = assistantId;
11741
+ onEvent({ type: "turn_end" });
11742
+ }
11743
+ /**
11744
+ * Poll the session for this turn's assistant message after the POST's socket
11745
+ * died under it. Node's global `fetch` is undici, which aborts a request
11746
+ * whose response headers have not arrived within 300s — and OpenCode sends
11747
+ * none until the turn is done, so any turn past five minutes fails as
11748
+ * `TypeError: fetch failed` while the server is still working. The message
11749
+ * exists server-side either way, so it is waited for and read back.
11750
+ *
11751
+ * A message that exists but has not completed is progress, not an answer:
11752
+ * it refreshes the watchdog and the poll continues.
11753
+ */
11754
+ async recoverReply(signal, onActivity) {
11755
+ const deadline = Date.now() + RECOVERY_TIMEOUT_MS;
11756
+ for (; ; ) {
11757
+ if (signal?.aborted || this.exited || this.disposed || !this.sessionId) return null;
11758
+ const messages = await this.json("GET", `/session/${this.sessionId}/message`).catch(() => null);
11759
+ const pending = Array.isArray(messages) ? [...messages].reverse().find((m) => m.info?.role === "assistant" && m.info.id && m.info.id !== this.lastAssistantId) : void 0;
11760
+ if (pending?.info?.time?.completed || pending?.info?.error) return pending;
11761
+ if (pending) onActivity?.();
11762
+ if (Date.now() >= deadline) return null;
11763
+ await delay(RECOVERY_POLL_INTERVAL_MS, signal);
11764
+ }
11765
+ }
11766
+ newestAssistantId(messages) {
11767
+ if (!Array.isArray(messages)) return null;
11768
+ for (let i = messages.length - 1; i >= 0; i--) {
11769
+ const info = messages[i]?.info;
11770
+ if (info?.role === "assistant" && info.id) return info.id;
11771
+ }
11772
+ return null;
11773
+ }
11684
11774
  /**
11685
11775
  * Emit one message part, once. OpenCode reports a tool part repeatedly as it
11686
11776
  * moves through pending → running → completed, so parts are keyed by id and
@@ -14553,11 +14643,13 @@ function defaultHasScriptCmd() {
14553
14643
  }
14554
14644
  }
14555
14645
  var HeadlessSession = class extends AbstractTerminalSession {
14556
- constructor(id, taskId, spawnImpl) {
14646
+ constructor(id, taskId, spawnImpl, interactive = false) {
14557
14647
  super(id, taskId);
14558
14648
  this.spawnImpl = spawnImpl;
14649
+ this.interactive = interactive;
14559
14650
  }
14560
14651
  spawnImpl;
14652
+ interactive;
14561
14653
  process = null;
14562
14654
  outputBuffer = "";
14563
14655
  controlStream = null;
@@ -14639,7 +14731,7 @@ var HeadlessRunner = class extends AbstractRunner {
14639
14731
  this.launchDeps = { resolvePath: this.resolvePath, ...deps.launchDeps };
14640
14732
  }
14641
14733
  createSession(id, taskId) {
14642
- return new HeadlessSession(id, taskId, this.spawnImpl);
14734
+ return new HeadlessSession(id, taskId, this.spawnImpl, this.defaultInteractive);
14643
14735
  }
14644
14736
  /** Everything up to, but not including, spawning — so a surface that owns its own child reaches the same decisions. */
14645
14737
  async prepareLaunch(opts, ptyOptions) {
@@ -14756,6 +14848,8 @@ var TmuxSession = class extends AbstractTerminalSession {
14756
14848
  pollIntervalMs;
14757
14849
  logPath;
14758
14850
  socket;
14851
+ /** A tmux window is a real TTY running the agent's TUI, so it submits on Enter. */
14852
+ interactive = true;
14759
14853
  outputBuffer = "";
14760
14854
  offset = 0;
14761
14855
  timer = null;