@getpaseo/server 0.1.73 → 0.1.74

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.
@@ -24,22 +24,8 @@ const OPENCODE_CAPABILITIES = {
24
24
  const OPENCODE_BUILD_MODE_ID = "build";
25
25
  const OPENCODE_FULL_ACCESS_MODE_ID = "full-access";
26
26
  const OPENCODE_STORAGE_SESSION_LIMIT = 200;
27
- // COMPAT(opencodeEofRecovery): added in v0.1.73 to compensate for OpenCode 1.14.42+
28
- // closing the /event SSE stream cleanly after `server.connected`. Drop this whole
29
- // recovery path once OpenCode upstream restores live event delivery and the floor
30
- // version reflects that.
31
- // Upstream: anomalyco/opencode#26697 (SSE /event closes immediately after
32
- // server.connected) and anomalyco/opencode#26635 (prompt_async silently discards
33
- // requests; SSE path broken).
34
- const OPENCODE_EOF_RECOVERY_TIMEOUT_MS = 5 * 60 * 1000;
35
- const OPENCODE_EOF_RECOVERY_POLL_INTERVAL_MS = 1000;
36
- const OPENCODE_RECOVERY_ABORT_TIMEOUT_MS = 2000;
37
27
  const OPENCODE_PENDING_ABORT_START_TIMEOUT_MS = 10000;
38
- // If OpenCode silently rejects the prompt (invalid model/mode/auth), no assistant
39
- // message is ever persisted. Bound the wait so the turn fails in seconds instead
40
- // of hanging until the completion cap. Valid models normally persist their first
41
- // message within a second of LLM start, so 10s leaves comfortable headroom.
42
- const OPENCODE_EOF_RECOVERY_LIVENESS_MS = 10000;
28
+ const OPENCODE_RETRY_STATUS_FAILURE_MS = 10000;
43
29
  const DEFAULT_MODES = [
44
30
  {
45
31
  id: OPENCODE_BUILD_MODE_ID,
@@ -451,16 +437,6 @@ function mergeOpenCodeStepFinishUsage(usage, part) {
451
437
  usage.totalCostUsd = (usage.totalCostUsd ?? 0) + cost;
452
438
  }
453
439
  }
454
- function formatOpenCodeAssistantErrorMessage(error) {
455
- const data = error.data;
456
- if (data && typeof data === "object" && "message" in data) {
457
- const message = data.message;
458
- if (typeof message === "string" && message.trim().length > 0) {
459
- return message.trim();
460
- }
461
- }
462
- return error.name;
463
- }
464
440
  function hasNormalizedOpenCodeUsage(usage) {
465
441
  return [
466
442
  usage.inputTokens,
@@ -702,11 +678,6 @@ export class OpenCodeAgentClient {
702
678
  this.logger = logger.child({ module: "agent", provider: "opencode" });
703
679
  this.runtimeSettings = runtimeSettings;
704
680
  this.storageRoot = storageRoot ?? resolveOpenCodeStorageRoot();
705
- this.recovery = deps.recovery ?? {
706
- timeoutMs: OPENCODE_EOF_RECOVERY_TIMEOUT_MS,
707
- pollIntervalMs: OPENCODE_EOF_RECOVERY_POLL_INTERVAL_MS,
708
- livenessMs: OPENCODE_EOF_RECOVERY_LIVENESS_MS,
709
- };
710
681
  this.runtime =
711
682
  deps.runtime ??
712
683
  new ProductionOpenCodeRuntime(OpenCodeServerManager.getInstance(this.logger, runtimeSettings));
@@ -729,7 +700,7 @@ export class OpenCodeAgentClient {
729
700
  throw new Error("OpenCode session creation returned no data");
730
701
  }
731
702
  await this.populateModelContextWindowCache(client, openCodeConfig.cwd);
732
- return new OpenCodeAgentSession(openCodeConfig, client, session.id, this.logger, this.storageRoot, new Map(this.modelContextWindows), acquisition.release, options?.persistSession, this.recovery);
703
+ return new OpenCodeAgentSession(openCodeConfig, client, session.id, this.logger, this.storageRoot, new Map(this.modelContextWindows), acquisition.release, options?.persistSession);
733
704
  }
734
705
  catch (error) {
735
706
  acquisition.release();
@@ -755,7 +726,7 @@ export class OpenCodeAgentClient {
755
726
  });
756
727
  try {
757
728
  await this.populateModelContextWindowCache(client, openCodeConfig.cwd);
758
- return new OpenCodeAgentSession(openCodeConfig, client, handle.sessionId, this.logger, this.storageRoot, new Map(this.modelContextWindows), acquisition.release, undefined, this.recovery);
729
+ return new OpenCodeAgentSession(openCodeConfig, client, handle.sessionId, this.logger, this.storageRoot, new Map(this.modelContextWindows), acquisition.release, undefined);
759
730
  }
760
731
  catch (error) {
761
732
  acquisition.release();
@@ -1605,12 +1576,22 @@ function traceOpenCode(tag, data = {}) {
1605
1576
  });
1606
1577
  process.stderr.write(`[opencode-trace] ${line}\n`);
1607
1578
  }
1579
+ function unwrapOpenCodeGlobalEvent(event) {
1580
+ const record = readOpenCodeRecord(event);
1581
+ if (!record) {
1582
+ return null;
1583
+ }
1584
+ const payload = readOpenCodeRecord(record.payload);
1585
+ if (typeof payload?.type === "string") {
1586
+ return payload;
1587
+ }
1588
+ if (typeof record.type === "string") {
1589
+ return record;
1590
+ }
1591
+ return null;
1592
+ }
1608
1593
  class OpenCodeAgentSession {
1609
- constructor(config, client, sessionId, logger, storageRoot, modelContextWindowsByModelKey = new Map(), releaseServer, persistSession = true, recovery = {
1610
- timeoutMs: OPENCODE_EOF_RECOVERY_TIMEOUT_MS,
1611
- pollIntervalMs: OPENCODE_EOF_RECOVERY_POLL_INTERVAL_MS,
1612
- livenessMs: OPENCODE_EOF_RECOVERY_LIVENESS_MS,
1613
- }) {
1594
+ constructor(config, client, sessionId, logger, _storageRoot, modelContextWindowsByModelKey = new Map(), releaseServer, persistSession = true) {
1614
1595
  this.provider = "opencode";
1615
1596
  this.capabilities = OPENCODE_CAPABILITIES;
1616
1597
  this.currentMode = "default";
@@ -1637,25 +1618,15 @@ class OpenCodeAgentSession {
1637
1618
  this.subAgentCallIdByChildSessionId = new Map();
1638
1619
  this.pendingChildToolPartsBySessionId = new Map();
1639
1620
  this.deletedFromProvider = false;
1640
- this.foregroundAssistantMessageEmitted = false;
1641
- this.foregroundAssistantText = "";
1642
- this.foregroundUsageUpdated = false;
1643
- this.foregroundKnownMessageIds = new Set();
1644
- this.foregroundEmittedQuestionIds = new Set();
1645
- this.foregroundEmittedPermissionIds = new Set();
1646
- this.foregroundEmittedReasoningTextLengthByPartId = new Map();
1647
- this.foregroundEmittedToolCallSignatureByCallId = new Map();
1648
- this.foregroundTurnStartedAt = null;
1621
+ this.retryFailureTimer = null;
1649
1622
  this.config = config;
1650
1623
  this.client = client;
1651
1624
  this.sessionId = sessionId;
1652
1625
  this.logger = logger;
1653
- this.storageRoot = storageRoot;
1654
1626
  this.modelContextWindowsByModelKey = modelContextWindowsByModelKey;
1655
1627
  this.currentMode = normalizeOpenCodeModeId(config.modeId);
1656
1628
  this.releaseServer = releaseServer ?? null;
1657
1629
  this.persistSession = persistSession;
1658
- this.recovery = recovery;
1659
1630
  this.selectedModelContextWindowMaxTokens = this.resolveConfiguredModelContextWindowMaxTokens(config.model);
1660
1631
  }
1661
1632
  get id() {
@@ -1739,19 +1710,11 @@ class OpenCodeAgentSession {
1739
1710
  throw new Error("A foreground turn is already active");
1740
1711
  }
1741
1712
  await this.awaitPendingAbortBeforeStartingTurn();
1742
- this.foregroundTurnStartedAt = Date.now();
1743
1713
  this.runningToolCalls.clear();
1744
1714
  this.subAgentsByCallId.clear();
1745
1715
  this.subAgentCallIdByChildSessionId.clear();
1746
1716
  this.pendingChildToolPartsBySessionId.clear();
1747
- this.foregroundAssistantMessageEmitted = false;
1748
- this.foregroundAssistantText = "";
1749
- this.foregroundUsageUpdated = false;
1750
- this.foregroundEmittedQuestionIds.clear();
1751
- this.foregroundEmittedPermissionIds.clear();
1752
- this.foregroundEmittedReasoningTextLengthByPartId.clear();
1753
- this.foregroundEmittedToolCallSignatureByCallId.clear();
1754
- this.foregroundKnownMessageIds = await this.readPersistedSessionMessageIds();
1717
+ this.clearRetryFailureTimer();
1755
1718
  const turnAbortController = new AbortController();
1756
1719
  this.abortController = turnAbortController;
1757
1720
  await this.ensureMcpServersConfigured();
@@ -1926,49 +1889,27 @@ class OpenCodeAgentSession {
1926
1889
  async consumeEventStream(turnId, turnAbortController, subscriptionReady) {
1927
1890
  traceOpenCode("subscribe.start", { turnId, sessionId: this.sessionId, cwd: this.config.cwd });
1928
1891
  try {
1929
- const result = await this.client.event.subscribe({ directory: this.config.cwd }, { signal: turnAbortController.signal });
1930
- traceOpenCode("subscribe.ready", { turnId, sessionId: this.sessionId });
1931
- subscriptionReady.resolve();
1892
+ const result = await this.client.global.event({
1893
+ signal: turnAbortController.signal,
1894
+ sseMaxRetryAttempts: 0,
1895
+ });
1932
1896
  let eventCount = 0;
1933
- for await (const event of result.stream) {
1897
+ let subscriptionReadyResolved = false;
1898
+ for await (const rawEvent of result.stream) {
1934
1899
  eventCount += 1;
1935
- traceOpenCode("event.raw", {
1936
- turnId,
1937
- n: eventCount,
1938
- type: event.type,
1939
- properties: event.properties,
1940
- });
1941
- if (turnAbortController.signal.aborted || this.activeForegroundTurnId !== turnId) {
1942
- traceOpenCode("event.skip", {
1943
- turnId,
1944
- n: eventCount,
1945
- aborted: turnAbortController.signal.aborted,
1946
- activeTurnId: this.activeForegroundTurnId,
1947
- });
1948
- break;
1900
+ if (!subscriptionReadyResolved) {
1901
+ subscriptionReadyResolved = true;
1902
+ traceOpenCode("subscribe.ready", { turnId, sessionId: this.sessionId });
1903
+ subscriptionReady.resolve();
1949
1904
  }
1950
- const translated = await this.translateEvent(event);
1951
- traceOpenCode("event.translated", {
1905
+ const shouldContinue = await this.consumeOpenCodeStreamEvent({
1906
+ rawEvent,
1907
+ eventCount,
1952
1908
  turnId,
1953
- n: eventCount,
1954
- count: translated.length,
1955
- types: translated.map((t) => t.type),
1909
+ turnAbortController,
1956
1910
  });
1957
- for (const e of translated) {
1958
- if (this.activeForegroundTurnId !== turnId) {
1959
- traceOpenCode("event.translated.skip-active", { turnId, type: e.type });
1960
- return;
1961
- }
1962
- if (e.type === "timeline" && e.item.type === "tool_call") {
1963
- this.trackToolCall(e.item);
1964
- }
1965
- const terminalEvent = toTerminalTurnEvent(e);
1966
- if (terminalEvent) {
1967
- traceOpenCode("event.terminal", { turnId, type: terminalEvent.type });
1968
- this.finishForegroundTurn(terminalEvent, turnId);
1969
- return;
1970
- }
1971
- this.notifySubscribers(e, turnId);
1911
+ if (!shouldContinue) {
1912
+ return;
1972
1913
  }
1973
1914
  }
1974
1915
  traceOpenCode("stream.eof", {
@@ -1978,12 +1919,10 @@ class OpenCodeAgentSession {
1978
1919
  stillActive: this.activeForegroundTurnId === turnId,
1979
1920
  });
1980
1921
  if (!turnAbortController.signal.aborted && this.activeForegroundTurnId === turnId) {
1981
- const recovered = await this.recoverTurnFromPersistedCompletion(turnId);
1982
- traceOpenCode("recovery.result", { turnId, recovered });
1983
- if (recovered) {
1984
- return;
1985
- }
1986
1922
  traceOpenCode("turn.fail.eof", { turnId, eventCount });
1923
+ if (!subscriptionReadyResolved) {
1924
+ subscriptionReady.reject(new Error("OpenCode event stream ended before it became ready"));
1925
+ }
1987
1926
  this.finishForegroundTurn({
1988
1927
  type: "turn_failed",
1989
1928
  provider: "opencode",
@@ -2018,340 +1957,55 @@ class OpenCodeAgentSession {
2018
1957
  }
2019
1958
  }
2020
1959
  }
2021
- async recoverTurnFromPersistedCompletion(turnId) {
2022
- traceOpenCode("recovery.start", {
1960
+ async consumeOpenCodeStreamEvent(params) {
1961
+ const { rawEvent, eventCount, turnId, turnAbortController } = params;
1962
+ const event = unwrapOpenCodeGlobalEvent(rawEvent);
1963
+ traceOpenCode("event.raw", {
2023
1964
  turnId,
2024
- foregroundTurnStartedAt: this.foregroundTurnStartedAt,
2025
- knownMessageIds: Array.from(this.foregroundKnownMessageIds),
2026
- sessionId: this.sessionId,
2027
- timeoutMs: this.recovery.timeoutMs,
2028
- pollIntervalMs: this.recovery.pollIntervalMs,
1965
+ n: eventCount,
1966
+ type: event?.type,
1967
+ rawType: readOpenCodeRecord(rawEvent)?.type,
1968
+ directory: readOpenCodeRecord(rawEvent)?.directory,
1969
+ properties: event ? event.properties : undefined,
2029
1970
  });
2030
- const startedAt = this.foregroundTurnStartedAt;
2031
- if (startedAt === null) {
2032
- traceOpenCode("recovery.no-start-time", { turnId });
2033
- return false;
2034
- }
2035
- const completionDeadline = Date.now() + this.recovery.timeoutMs;
2036
- const livenessDeadline = Date.now() + this.recovery.livenessMs;
2037
- let attempt = 0;
2038
- let observedActivity = false;
2039
- while (true) {
2040
- if (this.activeForegroundTurnId !== turnId) {
2041
- traceOpenCode("recovery.cancelled", { turnId, attempt });
2042
- return true;
2043
- }
2044
- attempt += 1;
2045
- const emittedPromptIds = await this.pollPendingQuestionsAndPermissions(turnId);
2046
- if (emittedPromptIds > 0) {
2047
- observedActivity = true;
2048
- }
2049
- const outcome = await this.fetchAssistantOutcomeFromMessagesApi(startedAt);
2050
- traceOpenCode("recovery.poll", {
2051
- turnId,
2052
- attempt,
2053
- kind: outcome?.kind ?? "none",
2054
- messageId: outcome?.messageId,
2055
- emittedPromptIds,
2056
- });
2057
- if (outcome?.kind === "failure") {
2058
- this.foregroundKnownMessageIds.add(outcome.messageId);
2059
- this.finishForegroundTurn({
2060
- type: "turn_failed",
2061
- provider: "opencode",
2062
- error: outcome.error,
2063
- }, turnId);
2064
- return true;
2065
- }
2066
- if (outcome?.kind === "completion") {
2067
- this.emitIncrementalAssistantParts(outcome.parts, turnId);
2068
- return this.applyRecoveredAssistantCompletion(outcome, turnId);
2069
- }
2070
- if (outcome?.kind === "in-progress") {
2071
- observedActivity = true;
2072
- this.emitIncrementalAssistantParts(outcome.parts, turnId);
2073
- }
2074
- const now = Date.now();
2075
- if (!observedActivity && now >= livenessDeadline) {
2076
- const deferred = await this.deferForPendingPermissionOrFailRecoveredTurnAfterCap(turnId, attempt, "liveness");
2077
- if (deferred) {
2078
- continue;
2079
- }
2080
- return true;
2081
- }
2082
- if (now >= completionDeadline) {
2083
- const deferred = await this.deferForPendingPermissionOrFailRecoveredTurnAfterCap(turnId, attempt, "completion");
2084
- if (deferred) {
2085
- continue;
2086
- }
2087
- return true;
2088
- }
2089
- const waitMs = Math.min(this.recovery.pollIntervalMs, completionDeadline - now);
2090
- await new Promise((resolve) => setTimeout(resolve, waitMs));
1971
+ if (!event) {
1972
+ return true;
2091
1973
  }
2092
- }
2093
- async deferForPendingPermissionOrFailRecoveredTurnAfterCap(turnId, attempt, cap) {
2094
- if (this.pendingPermissions.size > 0) {
2095
- // A pending OpenCode question/permission means the turn is blocked on
2096
- // user input, not dead. Keep polling until the user response lets the
2097
- // assistant finish or the turn is canceled.
2098
- traceOpenCode(`recovery.${cap}-deferred-for-permission`, {
1974
+ if (turnAbortController.signal.aborted || this.activeForegroundTurnId !== turnId) {
1975
+ traceOpenCode("event.skip", {
2099
1976
  turnId,
2100
- attempt,
2101
- pendingPermissionIds: Array.from(this.pendingPermissions.keys()),
1977
+ n: eventCount,
1978
+ aborted: turnAbortController.signal.aborted,
1979
+ activeTurnId: this.activeForegroundTurnId,
2102
1980
  });
2103
- await new Promise((resolve) => setTimeout(resolve, this.recovery.pollIntervalMs));
2104
- return true;
1981
+ return false;
2105
1982
  }
2106
- traceOpenCode(cap === "liveness" ? "recovery.liveness-exhausted" : "recovery.exhausted", {
1983
+ this.armRetryFailureTimerForStatus(event, turnId);
1984
+ const translated = await this.translateEvent(event);
1985
+ traceOpenCode("event.translated", {
2107
1986
  turnId,
2108
- attempt,
2109
- });
2110
- await this.failRecoveredTurnAfterCap(turnId, cap);
2111
- return false;
2112
- }
2113
- async failRecoveredTurnAfterCap(turnId, cap) {
2114
- await this.abortOpenCodeSessionAfterRecoveryCap(turnId, cap);
2115
- this.finishForegroundTurn({
2116
- type: "turn_failed",
2117
- provider: "opencode",
2118
- error: "OpenCode event stream ended before the turn reached a terminal state",
2119
- }, turnId);
2120
- }
2121
- async abortOpenCodeSessionAfterRecoveryCap(turnId, cap) {
2122
- const abortPromise = this.beginSessionAbort(turnId, `recovery-${cap}`);
2123
- await withTimeout(abortPromise, OPENCODE_RECOVERY_ABORT_TIMEOUT_MS, "OpenCode session.abort").catch((error) => {
2124
- this.logger.warn({ err: error, sessionId: this.sessionId, turnId, cap }, "OpenCode session.abort exceeded the EOF recovery cap");
2125
- });
2126
- }
2127
- async pollPendingQuestionsAndPermissions(turnId) {
2128
- const [questionsResponse, permissionsResponse] = await Promise.all([
2129
- Promise.resolve()
2130
- .then(() => this.client.question.list({ directory: this.config.cwd }))
2131
- .catch((error) => {
2132
- traceOpenCode("recovery.question-list.throw", {
2133
- turnId,
2134
- error: error instanceof Error
2135
- ? { name: error.name, message: error.message, stack: error.stack }
2136
- : String(error),
2137
- });
2138
- return null;
2139
- }),
2140
- Promise.resolve()
2141
- .then(() => this.client.permission.list({ directory: this.config.cwd }))
2142
- .catch((error) => {
2143
- traceOpenCode("recovery.permission-list.throw", {
2144
- turnId,
2145
- error: error instanceof Error
2146
- ? { name: error.name, message: error.message, stack: error.stack }
2147
- : String(error),
2148
- });
2149
- return null;
2150
- }),
2151
- ]);
2152
- if (this.activeForegroundTurnId !== turnId)
2153
- return 0;
2154
- let emitted = 0;
2155
- for (const question of questionsResponse?.data ?? []) {
2156
- if (question.sessionID !== this.sessionId)
2157
- continue;
2158
- if (this.foregroundEmittedQuestionIds.has(question.id))
2159
- continue;
2160
- this.foregroundEmittedQuestionIds.add(question.id);
2161
- emitted += 1;
2162
- const synthetic = {
2163
- id: question.id,
2164
- type: "question.asked",
2165
- properties: question,
2166
- };
2167
- const events = await this.translateEvent(synthetic);
2168
- for (const event of events) {
2169
- this.notifySubscribers(event, turnId);
2170
- }
2171
- }
2172
- for (const permission of permissionsResponse?.data ?? []) {
2173
- if (permission.sessionID !== this.sessionId)
2174
- continue;
2175
- if (this.foregroundEmittedPermissionIds.has(permission.id))
2176
- continue;
2177
- this.foregroundEmittedPermissionIds.add(permission.id);
2178
- emitted += 1;
2179
- const synthetic = {
2180
- id: permission.id,
2181
- type: "permission.asked",
2182
- properties: permission,
2183
- };
2184
- const events = await this.translateEvent(synthetic);
2185
- for (const event of events) {
2186
- this.notifySubscribers(event, turnId);
2187
- }
2188
- }
2189
- return emitted;
2190
- }
2191
- async fetchAssistantOutcomeFromMessagesApi(startedAt) {
2192
- const response = await Promise.resolve()
2193
- .then(() => this.client.session.messages({
2194
- sessionID: this.sessionId,
2195
- directory: this.config.cwd,
2196
- }))
2197
- .catch((error) => {
2198
- traceOpenCode("recovery.messages.throw", {
2199
- error: error instanceof Error
2200
- ? { name: error.name, message: error.message, stack: error.stack }
2201
- : String(error),
2202
- });
2203
- return null;
1987
+ n: eventCount,
1988
+ count: translated.length,
1989
+ types: translated.map((t) => t.type),
2204
1990
  });
2205
- if (response === null) {
2206
- return null;
2207
- }
2208
- if (response.error || !response.data) {
2209
- return null;
2210
- }
2211
- for (let index = response.data.length - 1; index >= 0; index -= 1) {
2212
- const item = response.data[index];
2213
- if (!item)
2214
- continue;
2215
- const info = item.info;
2216
- if (info.role !== "assistant")
2217
- continue;
2218
- if (this.foregroundKnownMessageIds.has(info.id))
2219
- continue;
2220
- if (typeof info.time?.created === "number" && info.time.created < startedAt)
2221
- continue;
2222
- if (info.error) {
2223
- return {
2224
- kind: "failure",
2225
- messageId: info.id,
2226
- error: formatOpenCodeAssistantErrorMessage(info.error),
2227
- };
1991
+ for (const e of translated) {
1992
+ if (this.activeForegroundTurnId !== turnId) {
1993
+ traceOpenCode("event.translated.skip-active", { turnId, type: e.type });
1994
+ return false;
2228
1995
  }
2229
- if (typeof info.time?.completed !== "number") {
2230
- return { kind: "in-progress", messageId: info.id, parts: item.parts };
1996
+ if (e.type === "timeline" && e.item.type === "tool_call") {
1997
+ this.trackToolCall(e.item);
2231
1998
  }
2232
- let text = item.parts
2233
- .filter((part) => part.type === "text")
2234
- .map((part) => (part.text ?? "").trim())
2235
- .filter((part) => part.length > 0)
2236
- .join("\n\n");
2237
- if (!text) {
2238
- text = stringifyStructuredAssistantMessage(info.structured) ?? "";
1999
+ const terminalEvent = toTerminalTurnEvent(e);
2000
+ if (terminalEvent) {
2001
+ traceOpenCode("event.terminal", { turnId, type: terminalEvent.type });
2002
+ this.finishForegroundTurn(terminalEvent, turnId);
2003
+ return false;
2239
2004
  }
2240
- if (!text)
2241
- continue;
2242
- const usage = {};
2243
- mergeOpenCodeStepFinishUsage(usage, { cost: info.cost, tokens: info.tokens });
2244
- return { kind: "completion", messageId: info.id, text, parts: item.parts, usage };
2245
- }
2246
- return null;
2247
- }
2248
- emitIncrementalAssistantParts(parts, turnId) {
2249
- for (const part of parts) {
2250
- if (part.type === "reasoning" && part.text) {
2251
- const emittedTextLength = this.foregroundEmittedReasoningTextLengthByPartId.get(part.id) ?? 0;
2252
- if (part.text.length <= emittedTextLength)
2253
- continue;
2254
- const text = part.text.slice(emittedTextLength);
2255
- this.foregroundEmittedReasoningTextLengthByPartId.set(part.id, part.text.length);
2256
- this.notifySubscribers({
2257
- type: "timeline",
2258
- provider: "opencode",
2259
- item: { type: "reasoning", text },
2260
- }, turnId);
2261
- continue;
2262
- }
2263
- if (part.type !== "tool")
2264
- continue;
2265
- const parsedToolPart = OpencodeToolPartToTimelineItemSchema.safeParse(part);
2266
- if (!parsedToolPart.success || !parsedToolPart.data)
2267
- continue;
2268
- const callId = parsedToolPart.data.callId;
2269
- const signature = this.createRecoveredToolCallSignature(part, parsedToolPart.data);
2270
- const lastSignature = this.foregroundEmittedToolCallSignatureByCallId.get(callId);
2271
- if (lastSignature === signature)
2272
- continue;
2273
- this.foregroundEmittedToolCallSignatureByCallId.set(callId, signature);
2274
- this.trackToolCall(parsedToolPart.data);
2275
- this.notifySubscribers({
2276
- type: "timeline",
2277
- provider: "opencode",
2278
- item: parsedToolPart.data,
2279
- }, turnId);
2280
- }
2281
- }
2282
- createRecoveredToolCallSignature(part, item) {
2283
- const state = part
2284
- .state;
2285
- return JSON.stringify([
2286
- item.callId,
2287
- item.status,
2288
- state?.input ?? null,
2289
- state?.output ?? null,
2290
- state?.error ?? null,
2291
- ]);
2292
- }
2293
- applyRecoveredAssistantCompletion(completion, turnId) {
2294
- if (this.activeForegroundTurnId !== turnId) {
2295
- return false;
2296
- }
2297
- this.foregroundKnownMessageIds.add(completion.messageId);
2298
- this.logger.warn({ sessionId: this.sessionId, turnId }, "Recovered OpenCode turn completion via messages API after SSE EOF");
2299
- const recoveryText = this.resolvePersistedAssistantRecoveryText(completion.text);
2300
- if (recoveryText === null) {
2301
- return false;
2302
- }
2303
- if (recoveryText.length > 0) {
2304
- this.notifySubscribers({
2305
- type: "timeline",
2306
- provider: "opencode",
2307
- item: { type: "assistant_message", text: recoveryText },
2308
- }, turnId);
2309
- this.foregroundAssistantMessageEmitted = true;
2005
+ this.notifySubscribers(e, turnId);
2310
2006
  }
2311
- if (hasNormalizedOpenCodeUsage(completion.usage) && !this.foregroundUsageUpdated) {
2312
- this.accumulatedUsage = {
2313
- ...this.accumulatedUsage,
2314
- ...completion.usage,
2315
- };
2316
- this.notifySubscribers({
2317
- type: "usage_updated",
2318
- provider: "opencode",
2319
- usage: { ...this.accumulatedUsage },
2320
- }, turnId);
2321
- this.foregroundUsageUpdated = true;
2322
- }
2323
- this.finishForegroundTurn({
2324
- type: "turn_completed",
2325
- provider: "opencode",
2326
- usage: hasNormalizedOpenCodeUsage(this.accumulatedUsage)
2327
- ? { ...this.accumulatedUsage }
2328
- : undefined,
2329
- }, turnId);
2330
2007
  return true;
2331
2008
  }
2332
- resolvePersistedAssistantRecoveryText(completedText) {
2333
- if (!this.foregroundAssistantMessageEmitted) {
2334
- return completedText;
2335
- }
2336
- if (completedText === this.foregroundAssistantText) {
2337
- return "";
2338
- }
2339
- return completedText.startsWith(this.foregroundAssistantText)
2340
- ? completedText.slice(this.foregroundAssistantText.length)
2341
- : null;
2342
- }
2343
- async readPersistedSessionMessageIds() {
2344
- const messageRoot = path.join(this.storageRoot, "message", this.sessionId);
2345
- const messageFiles = await findJsonFiles(messageRoot);
2346
- const messageIds = new Set();
2347
- for (const file of messageFiles) {
2348
- const parsed = await readJsonFile(file, OpenCodeStoredMessageSchema);
2349
- if (parsed?.sessionID === this.sessionId) {
2350
- messageIds.add(parsed.id);
2351
- }
2352
- }
2353
- return messageIds;
2354
- }
2355
2009
  finishForegroundTurn(event, turnId) {
2356
2010
  traceOpenCode("finishForegroundTurn", {
2357
2011
  turnId,
@@ -2369,7 +2023,7 @@ class OpenCodeAgentSession {
2369
2023
  else {
2370
2024
  this.runningToolCalls.clear();
2371
2025
  }
2372
- this.foregroundTurnStartedAt = null;
2026
+ this.clearRetryFailureTimer();
2373
2027
  this.activeForegroundTurnId = null;
2374
2028
  // Abort the SSE connection so the SDK tears down the underlying fetch.
2375
2029
  this.abortController?.abort();
@@ -2383,6 +2037,37 @@ class OpenCodeAgentSession {
2383
2037
  }
2384
2038
  this.runningToolCalls.delete(item.callId);
2385
2039
  }
2040
+ armRetryFailureTimerForStatus(event, turnId) {
2041
+ if (this.retryFailureTimer || event.type !== "session.status") {
2042
+ return;
2043
+ }
2044
+ if (event.properties.sessionID !== this.sessionId || event.properties.status.type !== "retry") {
2045
+ return;
2046
+ }
2047
+ const retry = event.properties.status;
2048
+ const message = typeof retry.message === "string" ? retry.message.trim() : "";
2049
+ const error = message
2050
+ ? `OpenCode provider retry did not recover: ${message}`
2051
+ : "OpenCode provider retry did not recover";
2052
+ this.retryFailureTimer = setTimeout(() => {
2053
+ this.retryFailureTimer = null;
2054
+ if (this.activeForegroundTurnId !== turnId) {
2055
+ return;
2056
+ }
2057
+ this.finishForegroundTurn({
2058
+ type: "turn_failed",
2059
+ provider: "opencode",
2060
+ error,
2061
+ }, turnId);
2062
+ }, OPENCODE_RETRY_STATUS_FAILURE_MS);
2063
+ }
2064
+ clearRetryFailureTimer() {
2065
+ if (!this.retryFailureTimer) {
2066
+ return;
2067
+ }
2068
+ clearTimeout(this.retryFailureTimer);
2069
+ this.retryFailureTimer = null;
2070
+ }
2386
2071
  synthesizeInterruptedToolCalls(turnId) {
2387
2072
  for (const item of this.runningToolCalls.values()) {
2388
2073
  const error = { message: "Tool execution aborted" };
@@ -2408,13 +2093,6 @@ class OpenCodeAgentSession {
2408
2093
  }
2409
2094
  notifySubscribers(event, turnIdOverride) {
2410
2095
  const turnId = turnIdOverride ?? this.activeForegroundTurnId;
2411
- if (event.type === "timeline" && event.item.type === "assistant_message") {
2412
- this.foregroundAssistantMessageEmitted = true;
2413
- this.foregroundAssistantText += event.item.text;
2414
- }
2415
- if (event.type === "usage_updated") {
2416
- this.foregroundUsageUpdated = true;
2417
- }
2418
2096
  const tagged = turnId ? { ...event, turnId } : event;
2419
2097
  for (const callback of this.subscribers) {
2420
2098
  try {