@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.
- package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.d.ts +1 -0
- package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.d.ts.map +1 -1
- package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.js +7 -0
- package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.js.map +1 -1
- package/dist/server/server/agent/providers/opencode-agent.d.ts +5 -30
- package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/opencode-agent.js +106 -428
- package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
- package/package.json +3 -3
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
|
1930
|
-
|
|
1931
|
-
|
|
1892
|
+
const result = await this.client.global.event({
|
|
1893
|
+
signal: turnAbortController.signal,
|
|
1894
|
+
sseMaxRetryAttempts: 0,
|
|
1895
|
+
});
|
|
1932
1896
|
let eventCount = 0;
|
|
1933
|
-
|
|
1897
|
+
let subscriptionReadyResolved = false;
|
|
1898
|
+
for await (const rawEvent of result.stream) {
|
|
1934
1899
|
eventCount += 1;
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
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
|
|
1951
|
-
|
|
1905
|
+
const shouldContinue = await this.consumeOpenCodeStreamEvent({
|
|
1906
|
+
rawEvent,
|
|
1907
|
+
eventCount,
|
|
1952
1908
|
turnId,
|
|
1953
|
-
|
|
1954
|
-
count: translated.length,
|
|
1955
|
-
types: translated.map((t) => t.type),
|
|
1909
|
+
turnAbortController,
|
|
1956
1910
|
});
|
|
1957
|
-
|
|
1958
|
-
|
|
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
|
|
2022
|
-
|
|
1960
|
+
async consumeOpenCodeStreamEvent(params) {
|
|
1961
|
+
const { rawEvent, eventCount, turnId, turnAbortController } = params;
|
|
1962
|
+
const event = unwrapOpenCodeGlobalEvent(rawEvent);
|
|
1963
|
+
traceOpenCode("event.raw", {
|
|
2023
1964
|
turnId,
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
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
|
-
|
|
2031
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2101
|
-
|
|
1977
|
+
n: eventCount,
|
|
1978
|
+
aborted: turnAbortController.signal.aborted,
|
|
1979
|
+
activeTurnId: this.activeForegroundTurnId,
|
|
2102
1980
|
});
|
|
2103
|
-
|
|
2104
|
-
return true;
|
|
1981
|
+
return false;
|
|
2105
1982
|
}
|
|
2106
|
-
|
|
1983
|
+
this.armRetryFailureTimerForStatus(event, turnId);
|
|
1984
|
+
const translated = await this.translateEvent(event);
|
|
1985
|
+
traceOpenCode("event.translated", {
|
|
2107
1986
|
turnId,
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
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
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
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 (
|
|
2230
|
-
|
|
1996
|
+
if (e.type === "timeline" && e.item.type === "tool_call") {
|
|
1997
|
+
this.trackToolCall(e.item);
|
|
2231
1998
|
}
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
.
|
|
2235
|
-
.
|
|
2236
|
-
|
|
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
|
-
|
|
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.
|
|
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 {
|