@getpaseo/server 0.1.71 → 0.1.73
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/agent-archive.d.ts +11 -0
- package/dist/server/server/agent/agent-archive.d.ts.map +1 -0
- package/dist/server/server/agent/agent-archive.js +16 -0
- package/dist/server/server/agent/agent-archive.js.map +1 -0
- package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
- package/dist/server/server/agent/agent-manager.js +3 -23
- package/dist/server/server/agent/agent-manager.js.map +1 -1
- package/dist/server/server/agent/agent-response-loop.d.ts.map +1 -1
- package/dist/server/server/agent/agent-response-loop.js +2 -1
- package/dist/server/server/agent/agent-response-loop.js.map +1 -1
- package/dist/server/server/agent/create-agent-title.d.ts +8 -0
- package/dist/server/server/agent/create-agent-title.d.ts.map +1 -0
- package/dist/server/server/agent/create-agent-title.js +29 -0
- package/dist/server/server/agent/create-agent-title.js.map +1 -0
- package/dist/server/server/agent/import-sessions.d.ts +20 -1
- package/dist/server/server/agent/import-sessions.d.ts.map +1 -1
- package/dist/server/server/agent/import-sessions.js +101 -0
- package/dist/server/server/agent/import-sessions.js.map +1 -1
- package/dist/server/server/agent/provider-launch-config.d.ts.map +1 -1
- package/dist/server/server/agent/provider-launch-config.js +1 -0
- package/dist/server/server/agent/provider-launch-config.js.map +1 -1
- package/dist/server/server/agent/provider-registry.d.ts.map +1 -1
- package/dist/server/server/agent/provider-registry.js +2 -1
- package/dist/server/server/agent/provider-registry.js.map +1 -1
- package/dist/server/server/agent/providers/codex/app-server-transport.d.ts +25 -0
- package/dist/server/server/agent/providers/codex/app-server-transport.d.ts.map +1 -0
- package/dist/server/server/agent/providers/codex/app-server-transport.js +183 -0
- package/dist/server/server/agent/providers/codex/app-server-transport.js.map +1 -0
- package/dist/server/server/agent/providers/codex/test-utils/fake-app-server.d.ts +31 -0
- package/dist/server/server/agent/providers/codex/test-utils/fake-app-server.d.ts.map +1 -0
- package/dist/server/server/agent/providers/codex/test-utils/fake-app-server.js +172 -0
- package/dist/server/server/agent/providers/codex/test-utils/fake-app-server.js.map +1 -0
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +1 -21
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.js +2 -171
- package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
- package/dist/server/server/agent/providers/opencode/runtime.d.ts +27 -0
- package/dist/server/server/agent/providers/opencode/runtime.d.ts.map +1 -0
- package/dist/server/server/agent/providers/opencode/runtime.js +5 -0
- package/dist/server/server/agent/providers/opencode/runtime.js.map +1 -0
- package/dist/server/server/agent/providers/opencode/server-manager.d.ts +55 -0
- package/dist/server/server/agent/providers/opencode/server-manager.d.ts.map +1 -0
- package/dist/server/server/agent/providers/opencode/server-manager.js +255 -0
- package/dist/server/server/agent/providers/opencode/server-manager.js.map +1 -0
- package/dist/server/server/agent/providers/opencode/test-server-manager.d.ts +22 -0
- package/dist/server/server/agent/providers/opencode/test-server-manager.d.ts.map +1 -0
- package/dist/server/server/agent/providers/opencode/test-server-manager.js +28 -0
- package/dist/server/server/agent/providers/opencode/test-server-manager.js.map +1 -0
- package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.d.ts +75 -0
- package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.d.ts.map +1 -0
- package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.js +169 -0
- package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.js.map +1 -0
- package/dist/server/server/agent/providers/opencode-agent.d.ts +38 -36
- package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/opencode-agent.js +556 -267
- package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
- package/dist/server/server/agent/providers/pi-direct-agent.d.ts +8 -3
- package/dist/server/server/agent/providers/pi-direct-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/pi-direct-agent.js +44 -34
- package/dist/server/server/agent/providers/pi-direct-agent.js.map +1 -1
- package/dist/server/server/bootstrap.d.ts +2 -0
- package/dist/server/server/bootstrap.d.ts.map +1 -1
- package/dist/server/server/bootstrap.js +45 -8
- package/dist/server/server/bootstrap.js.map +1 -1
- package/dist/server/server/checkout/status-projection.d.ts +19 -0
- package/dist/server/server/checkout/status-projection.d.ts.map +1 -0
- package/dist/server/server/checkout/status-projection.js +98 -0
- package/dist/server/server/checkout/status-projection.js.map +1 -0
- package/dist/server/server/file-explorer/service.d.ts.map +1 -1
- package/dist/server/server/file-explorer/service.js +84 -67
- package/dist/server/server/file-explorer/service.js.map +1 -1
- package/dist/server/server/paseo-worktree-service.d.ts +2 -1
- package/dist/server/server/paseo-worktree-service.d.ts.map +1 -1
- package/dist/server/server/paseo-worktree-service.js +30 -3
- package/dist/server/server/paseo-worktree-service.js.map +1 -1
- package/dist/server/server/push/notifications.d.ts +9 -0
- package/dist/server/server/push/notifications.d.ts.map +1 -0
- package/dist/server/server/push/notifications.js +15 -0
- package/dist/server/server/push/notifications.js.map +1 -0
- package/dist/server/server/push/push-service.d.ts +1 -2
- package/dist/server/server/push/push-service.d.ts.map +1 -1
- package/dist/server/server/relay-transport.d.ts +7 -1
- package/dist/server/server/relay-transport.d.ts.map +1 -1
- package/dist/server/server/relay-transport.js +10 -5
- package/dist/server/server/relay-transport.js.map +1 -1
- package/dist/server/server/session.d.ts +1 -17
- package/dist/server/server/session.d.ts.map +1 -1
- package/dist/server/server/session.js +24 -233
- package/dist/server/server/session.js.map +1 -1
- package/dist/server/server/websocket/runtime-metrics.d.ts +71 -0
- package/dist/server/server/websocket/runtime-metrics.d.ts.map +1 -0
- package/dist/server/server/websocket/runtime-metrics.js +148 -0
- package/dist/server/server/websocket/runtime-metrics.js.map +1 -0
- package/dist/server/server/websocket-server.d.ts +4 -21
- package/dist/server/server/websocket-server.d.ts.map +1 -1
- package/dist/server/server/websocket-server.js +28 -137
- package/dist/server/server/websocket-server.js.map +1 -1
- package/dist/server/server/workspace-directory.d.ts +0 -2
- package/dist/server/server/workspace-directory.d.ts.map +1 -1
- package/dist/server/server/workspace-directory.js +9 -26
- package/dist/server/server/workspace-directory.js.map +1 -1
- package/dist/server/shared/agent-state-bucket.d.ts +13 -0
- package/dist/server/shared/agent-state-bucket.d.ts.map +1 -0
- package/dist/server/shared/agent-state-bucket.js +41 -0
- package/dist/server/shared/agent-state-bucket.js.map +1 -0
- package/dist/server/shared/connection-offer.d.ts +6 -6
- package/dist/server/shared/connection-offer.js +1 -1
- package/dist/server/shared/connection-offer.js.map +1 -1
- package/dist/server/shared/git-remote.d.ts +16 -0
- package/dist/server/shared/git-remote.d.ts.map +1 -0
- package/dist/server/shared/git-remote.js +72 -0
- package/dist/server/shared/git-remote.js.map +1 -0
- package/dist/server/utils/checkout-git.d.ts +1 -0
- package/dist/server/utils/checkout-git.d.ts.map +1 -1
- package/dist/server/utils/checkout-git.js +3 -0
- package/dist/server/utils/checkout-git.js.map +1 -1
- package/dist/server/utils/github-remote.d.ts +3 -7
- package/dist/server/utils/github-remote.d.ts.map +1 -1
- package/dist/server/utils/github-remote.js +4 -70
- package/dist/server/utils/github-remote.js.map +1 -1
- package/dist/server/utils/run-git-command.d.ts.map +1 -1
- package/dist/server/utils/run-git-command.js +1 -0
- package/dist/server/utils/run-git-command.js.map +1 -1
- package/dist/src/server/agent/provider-launch-config.js +1 -0
- package/dist/src/server/agent/provider-launch-config.js.map +1 -1
- package/package.json +5 -5
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { createOpencodeClient, } from "@opencode-ai/sdk/v2/client";
|
|
5
|
-
import net from "node:net";
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
import { createProviderEnvSpec, resolveProviderCommandPrefix, } from "../provider-launch-config.js";
|
|
8
4
|
import { findExecutable, isCommandAvailable } from "../../../utils/executable.js";
|
|
9
|
-
import {
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { createProviderEnvSpec } from "../provider-launch-config.js";
|
|
10
7
|
import { withTimeout } from "../../../utils/promise-timeout.js";
|
|
11
|
-
import { execCommand
|
|
8
|
+
import { execCommand } from "../../../utils/spawn.js";
|
|
12
9
|
import { buildToolCallDisplayModel } from "../../../shared/tool-call-display.js";
|
|
13
10
|
import { mapOpencodeToolCall } from "./opencode/tool-call-mapper.js";
|
|
11
|
+
import { OpenCodeServerManager } from "./opencode/server-manager.js";
|
|
14
12
|
import { formatDiagnosticStatus, formatProviderDiagnostic, formatProviderDiagnosticError, resolveBinaryVersion, toDiagnosticErrorMessage, } from "./diagnostic-utils.js";
|
|
15
13
|
import { runProviderTurn } from "./provider-runner.js";
|
|
16
14
|
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
|
|
15
|
+
import { createSdkOpenCodeClient, } from "./opencode/runtime.js";
|
|
17
16
|
const OPENCODE_CAPABILITIES = {
|
|
18
17
|
supportsStreaming: true,
|
|
19
18
|
supportsSessionPersistence: true,
|
|
@@ -25,6 +24,22 @@ const OPENCODE_CAPABILITIES = {
|
|
|
25
24
|
const OPENCODE_BUILD_MODE_ID = "build";
|
|
26
25
|
const OPENCODE_FULL_ACCESS_MODE_ID = "full-access";
|
|
27
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
|
+
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
43
|
const DEFAULT_MODES = [
|
|
29
44
|
{
|
|
30
45
|
id: OPENCODE_BUILD_MODE_ID,
|
|
@@ -82,8 +97,6 @@ const OpenCodeStoredPartSchema = z
|
|
|
82
97
|
.passthrough();
|
|
83
98
|
const MCP_ALREADY_PRESENT_ERROR_TOKENS = ["already", "exists", "connected"];
|
|
84
99
|
const OPENCODE_PROVIDER_LIST_TIMEOUT_MS = 30000;
|
|
85
|
-
const OPENCODE_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT_MS = 5000;
|
|
86
|
-
const OPENCODE_SERVER_FORCE_SHUTDOWN_TIMEOUT_MS = 1000;
|
|
87
100
|
const OPENCODE_HANDLED_BUILTIN_SLASH_COMMANDS = [
|
|
88
101
|
{ name: "compact", description: "Compact the current session", argumentHint: "" },
|
|
89
102
|
{ name: "summarize", description: "Compact the current session", argumentHint: "" },
|
|
@@ -162,13 +175,6 @@ const OpencodeToolPartToTimelineItemSchema = OpencodeToolPartTimelineEnvelopeSch
|
|
|
162
175
|
output: part.output,
|
|
163
176
|
error: part.error,
|
|
164
177
|
}));
|
|
165
|
-
async function resolveOpenCodeBinary() {
|
|
166
|
-
const found = await findExecutable("opencode");
|
|
167
|
-
if (found) {
|
|
168
|
-
return found;
|
|
169
|
-
}
|
|
170
|
-
throw new Error("OpenCode binary not found. Install OpenCode (https://github.com/opencode-ai/opencode) and ensure it is available in your shell PATH.");
|
|
171
|
-
}
|
|
172
178
|
function toOpenCodeMcpConfig(config) {
|
|
173
179
|
if (config.type === "stdio") {
|
|
174
180
|
return {
|
|
@@ -278,22 +284,6 @@ function isAlreadyPresentMcpError(error) {
|
|
|
278
284
|
const normalized = toDiagnosticErrorMessage(error).toLowerCase();
|
|
279
285
|
return MCP_ALREADY_PRESENT_ERROR_TOKENS.some((token) => normalized.includes(token));
|
|
280
286
|
}
|
|
281
|
-
async function findAvailablePort() {
|
|
282
|
-
return new Promise((resolve, reject) => {
|
|
283
|
-
const server = net.createServer();
|
|
284
|
-
server.listen(0, () => {
|
|
285
|
-
const address = server.address();
|
|
286
|
-
if (address && typeof address === "object") {
|
|
287
|
-
const port = address.port;
|
|
288
|
-
server.close(() => resolve(port));
|
|
289
|
-
}
|
|
290
|
-
else {
|
|
291
|
-
server.close(() => reject(new Error("Failed to get port")));
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
server.on("error", reject);
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
287
|
function resolvePartDedupeKey(part, partType) {
|
|
298
288
|
if (part.id.trim().length > 0) {
|
|
299
289
|
return `${partType}:${part.id}`;
|
|
@@ -461,6 +451,16 @@ function mergeOpenCodeStepFinishUsage(usage, part) {
|
|
|
461
451
|
usage.totalCostUsd = (usage.totalCostUsd ?? 0) + cost;
|
|
462
452
|
}
|
|
463
453
|
}
|
|
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
464
|
function hasNormalizedOpenCodeUsage(usage) {
|
|
465
465
|
return [
|
|
466
466
|
usage.inputTokens,
|
|
@@ -600,17 +600,24 @@ async function readOpenCodeSessionTimeline(storageRoot, sessionId) {
|
|
|
600
600
|
return timeline;
|
|
601
601
|
}
|
|
602
602
|
async function readOpenCodeMessageText(storageRoot, messageId) {
|
|
603
|
+
const parts = await readOpenCodeStoredParts(storageRoot, messageId);
|
|
604
|
+
return readOpenCodeTextFromParts(parts);
|
|
605
|
+
}
|
|
606
|
+
async function readOpenCodeStoredParts(storageRoot, messageId) {
|
|
603
607
|
const partRoot = path.join(storageRoot, "part", messageId);
|
|
604
608
|
const partFiles = await findJsonFiles(partRoot);
|
|
605
609
|
const parts = [];
|
|
606
610
|
for (const file of partFiles) {
|
|
607
611
|
const parsed = await readJsonFile(file, OpenCodeStoredPartSchema);
|
|
608
|
-
if (parsed
|
|
612
|
+
if (parsed) {
|
|
609
613
|
parts.push(parsed);
|
|
610
614
|
}
|
|
611
615
|
}
|
|
616
|
+
return parts.sort((left, right) => getOpenCodePartTimestamp(left) - getOpenCodePartTimestamp(right));
|
|
617
|
+
}
|
|
618
|
+
function readOpenCodeTextFromParts(parts) {
|
|
612
619
|
return parts
|
|
613
|
-
.
|
|
620
|
+
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
614
621
|
.map((part) => part.text?.trim() ?? "")
|
|
615
622
|
.filter(Boolean)
|
|
616
623
|
.join("\n\n");
|
|
@@ -670,244 +677,45 @@ export const __openCodeInternals = {
|
|
|
670
677
|
return OpenCodeAgentSession;
|
|
671
678
|
},
|
|
672
679
|
};
|
|
673
|
-
|
|
674
|
-
constructor(
|
|
675
|
-
this.
|
|
676
|
-
this.retiredServers = new Set();
|
|
677
|
-
this.startPromise = null;
|
|
678
|
-
this.forcedRefreshPromise = null;
|
|
679
|
-
this.logger = logger;
|
|
680
|
-
this.runtimeSettings = runtimeSettings;
|
|
681
|
-
this.runtimeSettingsKey = JSON.stringify(runtimeSettings ?? {});
|
|
680
|
+
class ProductionOpenCodeRuntime {
|
|
681
|
+
constructor(serverManager) {
|
|
682
|
+
this.serverManager = serverManager;
|
|
682
683
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
if (!OpenCodeServerManager.instance) {
|
|
686
|
-
OpenCodeServerManager.instance = new OpenCodeServerManager(logger, runtimeSettings);
|
|
687
|
-
OpenCodeServerManager.registerExitHandler();
|
|
688
|
-
}
|
|
689
|
-
else if (OpenCodeServerManager.instance.runtimeSettingsKey !== nextSettingsKey) {
|
|
690
|
-
logger.warn({
|
|
691
|
-
existingRuntimeSettings: OpenCodeServerManager.instance.runtimeSettingsKey,
|
|
692
|
-
requestedRuntimeSettings: nextSettingsKey,
|
|
693
|
-
}, "OpenCode server manager already initialized with different runtime settings");
|
|
694
|
-
}
|
|
695
|
-
return OpenCodeServerManager.instance;
|
|
684
|
+
async acquireServer(options) {
|
|
685
|
+
return this.serverManager.acquire(options);
|
|
696
686
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
OpenCodeServerManager.exitHandlerRegistered = true;
|
|
702
|
-
const cleanup = () => {
|
|
703
|
-
const instance = OpenCodeServerManager.instance;
|
|
704
|
-
void instance?.shutdown();
|
|
705
|
-
};
|
|
706
|
-
process.on("exit", cleanup);
|
|
707
|
-
process.on("SIGTERM", cleanup);
|
|
708
|
-
process.on("SIGINT", cleanup);
|
|
709
|
-
}
|
|
710
|
-
async ensureRunning() {
|
|
711
|
-
const acquisition = await this.acquire({ force: false });
|
|
712
|
-
acquisition.release();
|
|
713
|
-
return acquisition.server;
|
|
714
|
-
}
|
|
715
|
-
async acquire(options) {
|
|
716
|
-
const server = options.force
|
|
717
|
-
? await this.getForcedRefreshServer()
|
|
718
|
-
: await this.getCurrentServer();
|
|
719
|
-
server.refCount += 1;
|
|
720
|
-
let released = false;
|
|
721
|
-
return {
|
|
722
|
-
server: { port: server.port, url: server.url },
|
|
723
|
-
release: () => {
|
|
724
|
-
if (released) {
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
released = true;
|
|
728
|
-
server.refCount -= 1;
|
|
729
|
-
this.cleanupRetiredServers();
|
|
730
|
-
},
|
|
731
|
-
};
|
|
687
|
+
async ensureServerRunning() {
|
|
688
|
+
return this.serverManager.ensureRunning();
|
|
732
689
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
return this.forcedRefreshPromise;
|
|
736
|
-
}
|
|
737
|
-
this.forcedRefreshPromise = Promise.resolve()
|
|
738
|
-
.then(async () => {
|
|
739
|
-
await this.rotateCurrentServer();
|
|
740
|
-
return this.getCurrentServer();
|
|
741
|
-
})
|
|
742
|
-
.finally(() => {
|
|
743
|
-
this.forcedRefreshPromise = null;
|
|
744
|
-
});
|
|
745
|
-
return this.forcedRefreshPromise;
|
|
746
|
-
}
|
|
747
|
-
async getCurrentServer() {
|
|
748
|
-
if (this.startPromise) {
|
|
749
|
-
return this.startPromise;
|
|
750
|
-
}
|
|
751
|
-
if (this.currentServer && !this.currentServer.process.killed) {
|
|
752
|
-
return this.currentServer;
|
|
753
|
-
}
|
|
754
|
-
this.startPromise = this.startServer();
|
|
755
|
-
try {
|
|
756
|
-
const result = await this.startPromise;
|
|
757
|
-
if (!result.retired) {
|
|
758
|
-
this.currentServer = result;
|
|
759
|
-
}
|
|
760
|
-
return result;
|
|
761
|
-
}
|
|
762
|
-
finally {
|
|
763
|
-
this.startPromise = null;
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
async rotateCurrentServer() {
|
|
767
|
-
const existing = this.currentServer;
|
|
768
|
-
if (existing) {
|
|
769
|
-
existing.retired = true;
|
|
770
|
-
this.retiredServers.add(existing);
|
|
771
|
-
this.currentServer = null;
|
|
772
|
-
this.cleanupRetiredServers();
|
|
773
|
-
}
|
|
774
|
-
if (this.startPromise) {
|
|
775
|
-
const pending = await this.startPromise;
|
|
776
|
-
pending.retired = true;
|
|
777
|
-
this.retiredServers.add(pending);
|
|
778
|
-
this.currentServer = null;
|
|
779
|
-
this.cleanupRetiredServers();
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
async startServer() {
|
|
783
|
-
const port = await findAvailablePort();
|
|
784
|
-
const url = `http://127.0.0.1:${port}`;
|
|
785
|
-
const launchPrefix = await resolveProviderCommandPrefix(this.runtimeSettings?.command, resolveOpenCodeBinary);
|
|
786
|
-
return new Promise((resolve, reject) => {
|
|
787
|
-
const serverProcess = spawnProcess(launchPrefix.command, [...launchPrefix.args, "serve", "--port", String(port)], {
|
|
788
|
-
detached: process.platform !== "win32",
|
|
789
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
790
|
-
...createProviderEnvSpec({ runtimeSettings: this.runtimeSettings }),
|
|
791
|
-
});
|
|
792
|
-
let started = false;
|
|
793
|
-
let stderrBuffer = "";
|
|
794
|
-
let stdoutBuffer = "";
|
|
795
|
-
const STARTUP_BUFFER_CAP = 8192;
|
|
796
|
-
const appendCapped = (current, chunk) => {
|
|
797
|
-
if (current.length >= STARTUP_BUFFER_CAP) {
|
|
798
|
-
return current;
|
|
799
|
-
}
|
|
800
|
-
const remaining = STARTUP_BUFFER_CAP - current.length;
|
|
801
|
-
return current + chunk.slice(0, remaining);
|
|
802
|
-
};
|
|
803
|
-
const buildStartupErrorMessage = (headline) => {
|
|
804
|
-
const sections = [headline];
|
|
805
|
-
const stderrTrimmed = stderrBuffer.trim();
|
|
806
|
-
if (stderrTrimmed.length > 0) {
|
|
807
|
-
sections.push(`stderr: ${stderrTrimmed}`);
|
|
808
|
-
}
|
|
809
|
-
const stdoutTrimmed = stdoutBuffer.trim();
|
|
810
|
-
if (stdoutTrimmed.length > 0) {
|
|
811
|
-
sections.push(`stdout: ${stdoutTrimmed}`);
|
|
812
|
-
}
|
|
813
|
-
return sections.join("\n");
|
|
814
|
-
};
|
|
815
|
-
const timeout = setTimeout(() => {
|
|
816
|
-
if (!started) {
|
|
817
|
-
reject(new Error(buildStartupErrorMessage("OpenCode server startup timeout")));
|
|
818
|
-
}
|
|
819
|
-
}, 30000);
|
|
820
|
-
serverProcess.stdout?.on("data", (data) => {
|
|
821
|
-
const output = data.toString();
|
|
822
|
-
stdoutBuffer = appendCapped(stdoutBuffer, output);
|
|
823
|
-
if (output.includes("listening on") && !started) {
|
|
824
|
-
started = true;
|
|
825
|
-
clearTimeout(timeout);
|
|
826
|
-
resolve({
|
|
827
|
-
process: serverProcess,
|
|
828
|
-
port,
|
|
829
|
-
url,
|
|
830
|
-
refCount: 0,
|
|
831
|
-
retired: false,
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
});
|
|
835
|
-
serverProcess.stderr?.on("data", (data) => {
|
|
836
|
-
const output = data.toString();
|
|
837
|
-
stderrBuffer = appendCapped(stderrBuffer, output);
|
|
838
|
-
this.logger.error({ stderr: output.trim() }, "OpenCode server stderr");
|
|
839
|
-
});
|
|
840
|
-
serverProcess.on("error", (error) => {
|
|
841
|
-
clearTimeout(timeout);
|
|
842
|
-
const headline = error instanceof Error ? error.message : String(error);
|
|
843
|
-
reject(new Error(buildStartupErrorMessage(headline)));
|
|
844
|
-
});
|
|
845
|
-
serverProcess.on("exit", (code) => {
|
|
846
|
-
if (!started) {
|
|
847
|
-
clearTimeout(timeout);
|
|
848
|
-
reject(new Error(buildStartupErrorMessage(`OpenCode server exited with code ${code}`)));
|
|
849
|
-
}
|
|
850
|
-
if (this.currentServer?.process === serverProcess) {
|
|
851
|
-
this.currentServer = null;
|
|
852
|
-
}
|
|
853
|
-
for (const retired of Array.from(this.retiredServers)) {
|
|
854
|
-
if (retired.process === serverProcess) {
|
|
855
|
-
this.retiredServers.delete(retired);
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
});
|
|
859
|
-
});
|
|
690
|
+
createClient(options) {
|
|
691
|
+
return createSdkOpenCodeClient(options);
|
|
860
692
|
}
|
|
861
693
|
async shutdown() {
|
|
862
|
-
|
|
863
|
-
...(this.currentServer ? [this.currentServer] : []),
|
|
864
|
-
...Array.from(this.retiredServers),
|
|
865
|
-
];
|
|
866
|
-
await Promise.all(servers.map((server) => this.killServer(server)));
|
|
867
|
-
this.currentServer = null;
|
|
868
|
-
this.retiredServers.clear();
|
|
869
|
-
}
|
|
870
|
-
cleanupRetiredServers() {
|
|
871
|
-
for (const server of Array.from(this.retiredServers)) {
|
|
872
|
-
if (server.refCount === 0) {
|
|
873
|
-
this.retiredServers.delete(server);
|
|
874
|
-
void this.killServer(server);
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
async killServer(server) {
|
|
879
|
-
if (server.process.killed) {
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
const result = await terminateWithTreeKill(server.process, {
|
|
883
|
-
gracefulTimeoutMs: OPENCODE_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT_MS,
|
|
884
|
-
forceTimeoutMs: OPENCODE_SERVER_FORCE_SHUTDOWN_TIMEOUT_MS,
|
|
885
|
-
onForceSignal: () => {
|
|
886
|
-
this.logger.warn({ timeoutMs: OPENCODE_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT_MS }, "OpenCode server did not exit after SIGTERM; sending SIGKILL");
|
|
887
|
-
},
|
|
888
|
-
});
|
|
889
|
-
if (result === "kill-timeout") {
|
|
890
|
-
this.logger.warn({ timeoutMs: OPENCODE_SERVER_FORCE_SHUTDOWN_TIMEOUT_MS }, "OpenCode server did not report exit after SIGKILL");
|
|
891
|
-
}
|
|
694
|
+
await this.serverManager.shutdown();
|
|
892
695
|
}
|
|
893
696
|
}
|
|
894
|
-
OpenCodeServerManager.instance = null;
|
|
895
|
-
OpenCodeServerManager.exitHandlerRegistered = false;
|
|
896
697
|
export class OpenCodeAgentClient {
|
|
897
|
-
constructor(logger, runtimeSettings, storageRoot) {
|
|
698
|
+
constructor(logger, runtimeSettings, storageRoot, deps = {}) {
|
|
898
699
|
this.provider = "opencode";
|
|
899
700
|
this.capabilities = OPENCODE_CAPABILITIES;
|
|
900
701
|
this.modelContextWindows = new Map();
|
|
901
702
|
this.logger = logger.child({ module: "agent", provider: "opencode" });
|
|
902
703
|
this.runtimeSettings = runtimeSettings;
|
|
903
704
|
this.storageRoot = storageRoot ?? resolveOpenCodeStorageRoot();
|
|
904
|
-
this.
|
|
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
|
+
this.runtime =
|
|
711
|
+
deps.runtime ??
|
|
712
|
+
new ProductionOpenCodeRuntime(OpenCodeServerManager.getInstance(this.logger, runtimeSettings));
|
|
905
713
|
}
|
|
906
714
|
async createSession(config, _launchContext, options) {
|
|
907
715
|
const openCodeConfig = this.assertConfig(config);
|
|
908
|
-
const acquisition = await this.
|
|
716
|
+
const acquisition = await this.runtime.acquireServer({ force: false });
|
|
909
717
|
const { url } = acquisition.server;
|
|
910
|
-
const client =
|
|
718
|
+
const client = this.runtime.createClient({
|
|
911
719
|
baseUrl: url,
|
|
912
720
|
directory: openCodeConfig.cwd,
|
|
913
721
|
});
|
|
@@ -921,7 +729,7 @@ export class OpenCodeAgentClient {
|
|
|
921
729
|
throw new Error("OpenCode session creation returned no data");
|
|
922
730
|
}
|
|
923
731
|
await this.populateModelContextWindowCache(client, openCodeConfig.cwd);
|
|
924
|
-
return new OpenCodeAgentSession(openCodeConfig, client, session.id, this.logger, new Map(this.modelContextWindows), acquisition.release, options?.persistSession);
|
|
732
|
+
return new OpenCodeAgentSession(openCodeConfig, client, session.id, this.logger, this.storageRoot, new Map(this.modelContextWindows), acquisition.release, options?.persistSession, this.recovery);
|
|
925
733
|
}
|
|
926
734
|
catch (error) {
|
|
927
735
|
acquisition.release();
|
|
@@ -939,15 +747,15 @@ export class OpenCodeAgentClient {
|
|
|
939
747
|
...overrides,
|
|
940
748
|
};
|
|
941
749
|
const openCodeConfig = this.assertConfig(config);
|
|
942
|
-
const acquisition = await this.
|
|
750
|
+
const acquisition = await this.runtime.acquireServer({ force: false });
|
|
943
751
|
const { url } = acquisition.server;
|
|
944
|
-
const client =
|
|
752
|
+
const client = this.runtime.createClient({
|
|
945
753
|
baseUrl: url,
|
|
946
754
|
directory: openCodeConfig.cwd,
|
|
947
755
|
});
|
|
948
756
|
try {
|
|
949
757
|
await this.populateModelContextWindowCache(client, openCodeConfig.cwd);
|
|
950
|
-
return new OpenCodeAgentSession(openCodeConfig, client, handle.sessionId, this.logger, new Map(this.modelContextWindows), acquisition.release);
|
|
758
|
+
return new OpenCodeAgentSession(openCodeConfig, client, handle.sessionId, this.logger, this.storageRoot, new Map(this.modelContextWindows), acquisition.release, undefined, this.recovery);
|
|
951
759
|
}
|
|
952
760
|
catch (error) {
|
|
953
761
|
acquisition.release();
|
|
@@ -955,9 +763,9 @@ export class OpenCodeAgentClient {
|
|
|
955
763
|
}
|
|
956
764
|
}
|
|
957
765
|
async listModels(options) {
|
|
958
|
-
const acquisition = await this.
|
|
766
|
+
const acquisition = await this.runtime.acquireServer({ force: options.force });
|
|
959
767
|
const { url } = acquisition.server;
|
|
960
|
-
const client =
|
|
768
|
+
const client = this.runtime.createClient({
|
|
961
769
|
baseUrl: url,
|
|
962
770
|
directory: options.cwd,
|
|
963
771
|
});
|
|
@@ -1001,10 +809,10 @@ export class OpenCodeAgentClient {
|
|
|
1001
809
|
}
|
|
1002
810
|
}
|
|
1003
811
|
async listModes(options) {
|
|
1004
|
-
const acquisition = await this.
|
|
812
|
+
const acquisition = await this.runtime.acquireServer({ force: options.force });
|
|
1005
813
|
const { url } = acquisition.server;
|
|
1006
814
|
const directory = options.cwd;
|
|
1007
|
-
const client =
|
|
815
|
+
const client = this.runtime.createClient({ baseUrl: url, directory });
|
|
1008
816
|
try {
|
|
1009
817
|
const response = await withTimeout(client.app.agents({ directory }), 10000, "OpenCode app.agents timed out after 10s");
|
|
1010
818
|
if (response.error || !response.data) {
|
|
@@ -1041,7 +849,7 @@ export class OpenCodeAgentClient {
|
|
|
1041
849
|
let modelsValue = "Not checked";
|
|
1042
850
|
let status = formatDiagnosticStatus(available);
|
|
1043
851
|
try {
|
|
1044
|
-
const { url } = await this.
|
|
852
|
+
const { url } = await this.runtime.ensureServerRunning();
|
|
1045
853
|
serverStatus = `Running (${url})`;
|
|
1046
854
|
}
|
|
1047
855
|
catch (error) {
|
|
@@ -1784,13 +1592,31 @@ function createDeferred() {
|
|
|
1784
1592
|
});
|
|
1785
1593
|
return { promise, resolve, reject };
|
|
1786
1594
|
}
|
|
1595
|
+
const OPENCODE_TRACE_ENABLED = process.env.PASEO_OPENCODE_TRACE === "1";
|
|
1596
|
+
function traceOpenCode(tag, data = {}) {
|
|
1597
|
+
if (!OPENCODE_TRACE_ENABLED)
|
|
1598
|
+
return;
|
|
1599
|
+
const line = JSON.stringify({ ts: new Date().toISOString(), tag, ...data }, (_k, v) => {
|
|
1600
|
+
if (v instanceof Error)
|
|
1601
|
+
return { name: v.name, message: v.message, stack: v.stack };
|
|
1602
|
+
if (typeof v === "bigint")
|
|
1603
|
+
return v.toString();
|
|
1604
|
+
return v;
|
|
1605
|
+
});
|
|
1606
|
+
process.stderr.write(`[opencode-trace] ${line}\n`);
|
|
1607
|
+
}
|
|
1787
1608
|
class OpenCodeAgentSession {
|
|
1788
|
-
constructor(config, client, sessionId, logger, modelContextWindowsByModelKey = new Map(), releaseServer, persistSession = true
|
|
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
|
+
}) {
|
|
1789
1614
|
this.provider = "opencode";
|
|
1790
1615
|
this.capabilities = OPENCODE_CAPABILITIES;
|
|
1791
1616
|
this.currentMode = "default";
|
|
1792
1617
|
this.pendingPermissions = new Map();
|
|
1793
1618
|
this.abortController = null;
|
|
1619
|
+
this.pendingAbortPromise = null;
|
|
1794
1620
|
this.accumulatedUsage = {};
|
|
1795
1621
|
this.mcpConfigured = false;
|
|
1796
1622
|
this.mcpSetupPromise = null;
|
|
@@ -1811,14 +1637,25 @@ class OpenCodeAgentSession {
|
|
|
1811
1637
|
this.subAgentCallIdByChildSessionId = new Map();
|
|
1812
1638
|
this.pendingChildToolPartsBySessionId = new Map();
|
|
1813
1639
|
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;
|
|
1814
1649
|
this.config = config;
|
|
1815
1650
|
this.client = client;
|
|
1816
1651
|
this.sessionId = sessionId;
|
|
1817
1652
|
this.logger = logger;
|
|
1653
|
+
this.storageRoot = storageRoot;
|
|
1818
1654
|
this.modelContextWindowsByModelKey = modelContextWindowsByModelKey;
|
|
1819
1655
|
this.currentMode = normalizeOpenCodeModeId(config.modeId);
|
|
1820
1656
|
this.releaseServer = releaseServer ?? null;
|
|
1821
1657
|
this.persistSession = persistSession;
|
|
1658
|
+
this.recovery = recovery;
|
|
1822
1659
|
this.selectedModelContextWindowMaxTokens = this.resolveConfiguredModelContextWindowMaxTokens(config.model);
|
|
1823
1660
|
}
|
|
1824
1661
|
get id() {
|
|
@@ -1856,22 +1693,65 @@ class OpenCodeAgentSession {
|
|
|
1856
1693
|
const turnId = this.activeForegroundTurnId;
|
|
1857
1694
|
const turnAbortController = this.abortController;
|
|
1858
1695
|
turnAbortController?.abort();
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1696
|
+
// COMPAT(opencodeSlowAbort): OpenCode 1.14.42+ blocks session.abort until
|
|
1697
|
+
// the running tool actually stops, which can be tens of seconds for
|
|
1698
|
+
// long-running tools. Cap the wait so the user-visible cancel lands
|
|
1699
|
+
// quickly while still giving OpenCode a chance to confirm the abort
|
|
1700
|
+
// cleanly. Drop the timeout once upstream returns abort acknowledgement
|
|
1701
|
+
// before tool teardown.
|
|
1702
|
+
const abortPromise = this.beginSessionAbort(turnId, "interrupt");
|
|
1703
|
+
await withTimeout(abortPromise, 2000, "OpenCode session.abort").catch((error) => {
|
|
1704
|
+
this.logger.warn({ err: error, sessionId: this.sessionId, turnId }, "OpenCode session.abort exceeded the cancel cap; proceeding with local cancel");
|
|
1862
1705
|
});
|
|
1863
1706
|
if (turnId) {
|
|
1864
1707
|
this.finishForegroundTurn({ type: "turn_canceled", provider: "opencode", reason: "interrupted" }, turnId);
|
|
1865
1708
|
}
|
|
1866
1709
|
}
|
|
1710
|
+
beginSessionAbort(turnId, reason) {
|
|
1711
|
+
const abortPromise = this.client.session
|
|
1712
|
+
.abort({
|
|
1713
|
+
sessionID: this.sessionId,
|
|
1714
|
+
directory: this.config.cwd,
|
|
1715
|
+
})
|
|
1716
|
+
.then(() => undefined)
|
|
1717
|
+
.catch((error) => {
|
|
1718
|
+
this.logger.warn({ err: error, sessionId: this.sessionId, turnId, reason }, "OpenCode session.abort rejected");
|
|
1719
|
+
});
|
|
1720
|
+
const trackedAbortPromise = abortPromise.finally(() => {
|
|
1721
|
+
if (this.pendingAbortPromise === trackedAbortPromise) {
|
|
1722
|
+
this.pendingAbortPromise = null;
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1725
|
+
this.pendingAbortPromise = trackedAbortPromise;
|
|
1726
|
+
return trackedAbortPromise;
|
|
1727
|
+
}
|
|
1728
|
+
async awaitPendingAbortBeforeStartingTurn() {
|
|
1729
|
+
const pendingAbortPromise = this.pendingAbortPromise;
|
|
1730
|
+
if (!pendingAbortPromise) {
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
await withTimeout(pendingAbortPromise, OPENCODE_PENDING_ABORT_START_TIMEOUT_MS, "OpenCode pending session.abort").catch((error) => {
|
|
1734
|
+
this.logger.warn({ err: error, sessionId: this.sessionId }, "OpenCode session.abort was still pending before starting the next turn");
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1867
1737
|
async startTurn(prompt, options) {
|
|
1868
1738
|
if (this.activeForegroundTurnId) {
|
|
1869
1739
|
throw new Error("A foreground turn is already active");
|
|
1870
1740
|
}
|
|
1741
|
+
await this.awaitPendingAbortBeforeStartingTurn();
|
|
1742
|
+
this.foregroundTurnStartedAt = Date.now();
|
|
1871
1743
|
this.runningToolCalls.clear();
|
|
1872
1744
|
this.subAgentsByCallId.clear();
|
|
1873
1745
|
this.subAgentCallIdByChildSessionId.clear();
|
|
1874
1746
|
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();
|
|
1875
1755
|
const turnAbortController = new AbortController();
|
|
1876
1756
|
this.abortController = turnAbortController;
|
|
1877
1757
|
await this.ensureMcpServersConfigured();
|
|
@@ -1980,6 +1860,14 @@ class OpenCodeAgentSession {
|
|
|
1980
1860
|
// SDK input validation) is caught alongside async rejections. A plain
|
|
1981
1861
|
// `.then().catch()` chain would let a sync throw escape unhandled.
|
|
1982
1862
|
void (async () => {
|
|
1863
|
+
traceOpenCode("promptAsync.start", {
|
|
1864
|
+
turnId,
|
|
1865
|
+
sessionId: this.sessionId,
|
|
1866
|
+
model,
|
|
1867
|
+
effectiveMode,
|
|
1868
|
+
effectiveVariant,
|
|
1869
|
+
partTypes: parts.map((p) => p.type),
|
|
1870
|
+
});
|
|
1983
1871
|
try {
|
|
1984
1872
|
const promptResponse = await this.client.session.promptAsync({
|
|
1985
1873
|
sessionID: this.sessionId,
|
|
@@ -1998,6 +1886,12 @@ class OpenCodeAgentSession {
|
|
|
1998
1886
|
...(effectiveMode ? { agent: effectiveMode } : {}),
|
|
1999
1887
|
...(effectiveVariant ? { variant: effectiveVariant } : {}),
|
|
2000
1888
|
});
|
|
1889
|
+
traceOpenCode("promptAsync.response", {
|
|
1890
|
+
turnId,
|
|
1891
|
+
hasError: promptResponse.error !== undefined,
|
|
1892
|
+
error: promptResponse.error,
|
|
1893
|
+
data: promptResponse.data,
|
|
1894
|
+
});
|
|
2001
1895
|
if (promptResponse.error) {
|
|
2002
1896
|
this.finishForegroundTurn({
|
|
2003
1897
|
type: "turn_failed",
|
|
@@ -2007,6 +1901,12 @@ class OpenCodeAgentSession {
|
|
|
2007
1901
|
}
|
|
2008
1902
|
}
|
|
2009
1903
|
catch (error) {
|
|
1904
|
+
traceOpenCode("promptAsync.throw", {
|
|
1905
|
+
turnId,
|
|
1906
|
+
error: error instanceof Error
|
|
1907
|
+
? { name: error.name, message: error.message, stack: error.stack }
|
|
1908
|
+
: String(error),
|
|
1909
|
+
});
|
|
2010
1910
|
this.finishForegroundTurn({
|
|
2011
1911
|
type: "turn_failed",
|
|
2012
1912
|
provider: "opencode",
|
|
@@ -2024,16 +1924,39 @@ class OpenCodeAgentSession {
|
|
|
2024
1924
|
};
|
|
2025
1925
|
}
|
|
2026
1926
|
async consumeEventStream(turnId, turnAbortController, subscriptionReady) {
|
|
1927
|
+
traceOpenCode("subscribe.start", { turnId, sessionId: this.sessionId, cwd: this.config.cwd });
|
|
2027
1928
|
try {
|
|
2028
|
-
const result = await this.client.event.subscribe({ directory: this.config.cwd }, { signal: turnAbortController.signal
|
|
1929
|
+
const result = await this.client.event.subscribe({ directory: this.config.cwd }, { signal: turnAbortController.signal });
|
|
1930
|
+
traceOpenCode("subscribe.ready", { turnId, sessionId: this.sessionId });
|
|
2029
1931
|
subscriptionReady.resolve();
|
|
1932
|
+
let eventCount = 0;
|
|
2030
1933
|
for await (const event of result.stream) {
|
|
1934
|
+
eventCount += 1;
|
|
1935
|
+
traceOpenCode("event.raw", {
|
|
1936
|
+
turnId,
|
|
1937
|
+
n: eventCount,
|
|
1938
|
+
type: event.type,
|
|
1939
|
+
properties: event.properties,
|
|
1940
|
+
});
|
|
2031
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
|
+
});
|
|
2032
1948
|
break;
|
|
2033
1949
|
}
|
|
2034
1950
|
const translated = await this.translateEvent(event);
|
|
1951
|
+
traceOpenCode("event.translated", {
|
|
1952
|
+
turnId,
|
|
1953
|
+
n: eventCount,
|
|
1954
|
+
count: translated.length,
|
|
1955
|
+
types: translated.map((t) => t.type),
|
|
1956
|
+
});
|
|
2035
1957
|
for (const e of translated) {
|
|
2036
1958
|
if (this.activeForegroundTurnId !== turnId) {
|
|
1959
|
+
traceOpenCode("event.translated.skip-active", { turnId, type: e.type });
|
|
2037
1960
|
return;
|
|
2038
1961
|
}
|
|
2039
1962
|
if (e.type === "timeline" && e.item.type === "tool_call") {
|
|
@@ -2041,13 +1964,26 @@ class OpenCodeAgentSession {
|
|
|
2041
1964
|
}
|
|
2042
1965
|
const terminalEvent = toTerminalTurnEvent(e);
|
|
2043
1966
|
if (terminalEvent) {
|
|
1967
|
+
traceOpenCode("event.terminal", { turnId, type: terminalEvent.type });
|
|
2044
1968
|
this.finishForegroundTurn(terminalEvent, turnId);
|
|
2045
1969
|
return;
|
|
2046
1970
|
}
|
|
2047
1971
|
this.notifySubscribers(e, turnId);
|
|
2048
1972
|
}
|
|
2049
1973
|
}
|
|
1974
|
+
traceOpenCode("stream.eof", {
|
|
1975
|
+
turnId,
|
|
1976
|
+
eventCount,
|
|
1977
|
+
aborted: turnAbortController.signal.aborted,
|
|
1978
|
+
stillActive: this.activeForegroundTurnId === turnId,
|
|
1979
|
+
});
|
|
2050
1980
|
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
|
+
traceOpenCode("turn.fail.eof", { turnId, eventCount });
|
|
2051
1987
|
this.finishForegroundTurn({
|
|
2052
1988
|
type: "turn_failed",
|
|
2053
1989
|
provider: "opencode",
|
|
@@ -2056,6 +1992,10 @@ class OpenCodeAgentSession {
|
|
|
2056
1992
|
}
|
|
2057
1993
|
}
|
|
2058
1994
|
catch (error) {
|
|
1995
|
+
traceOpenCode("subscribe.error", {
|
|
1996
|
+
turnId,
|
|
1997
|
+
error: error instanceof Error ? { name: error.name, message: error.message } : String(error),
|
|
1998
|
+
});
|
|
2059
1999
|
subscriptionReady.reject(error);
|
|
2060
2000
|
if (!turnAbortController.signal.aborted && this.activeForegroundTurnId === turnId) {
|
|
2061
2001
|
this.finishForegroundTurn({
|
|
@@ -2078,7 +2018,348 @@ class OpenCodeAgentSession {
|
|
|
2078
2018
|
}
|
|
2079
2019
|
}
|
|
2080
2020
|
}
|
|
2021
|
+
async recoverTurnFromPersistedCompletion(turnId) {
|
|
2022
|
+
traceOpenCode("recovery.start", {
|
|
2023
|
+
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,
|
|
2029
|
+
});
|
|
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));
|
|
2091
|
+
}
|
|
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`, {
|
|
2099
|
+
turnId,
|
|
2100
|
+
attempt,
|
|
2101
|
+
pendingPermissionIds: Array.from(this.pendingPermissions.keys()),
|
|
2102
|
+
});
|
|
2103
|
+
await new Promise((resolve) => setTimeout(resolve, this.recovery.pollIntervalMs));
|
|
2104
|
+
return true;
|
|
2105
|
+
}
|
|
2106
|
+
traceOpenCode(cap === "liveness" ? "recovery.liveness-exhausted" : "recovery.exhausted", {
|
|
2107
|
+
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;
|
|
2204
|
+
});
|
|
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
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
if (typeof info.time?.completed !== "number") {
|
|
2230
|
+
return { kind: "in-progress", messageId: info.id, parts: item.parts };
|
|
2231
|
+
}
|
|
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) ?? "";
|
|
2239
|
+
}
|
|
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;
|
|
2310
|
+
}
|
|
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
|
+
return true;
|
|
2331
|
+
}
|
|
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
|
+
}
|
|
2081
2355
|
finishForegroundTurn(event, turnId) {
|
|
2356
|
+
traceOpenCode("finishForegroundTurn", {
|
|
2357
|
+
turnId,
|
|
2358
|
+
activeTurnId: this.activeForegroundTurnId,
|
|
2359
|
+
type: event.type,
|
|
2360
|
+
error: event.type === "turn_failed" ? event.error : undefined,
|
|
2361
|
+
reason: event.type === "turn_canceled" ? event.reason : undefined,
|
|
2362
|
+
});
|
|
2082
2363
|
if (this.activeForegroundTurnId !== turnId) {
|
|
2083
2364
|
return;
|
|
2084
2365
|
}
|
|
@@ -2088,6 +2369,7 @@ class OpenCodeAgentSession {
|
|
|
2088
2369
|
else {
|
|
2089
2370
|
this.runningToolCalls.clear();
|
|
2090
2371
|
}
|
|
2372
|
+
this.foregroundTurnStartedAt = null;
|
|
2091
2373
|
this.activeForegroundTurnId = null;
|
|
2092
2374
|
// Abort the SSE connection so the SDK tears down the underlying fetch.
|
|
2093
2375
|
this.abortController?.abort();
|
|
@@ -2126,6 +2408,13 @@ class OpenCodeAgentSession {
|
|
|
2126
2408
|
}
|
|
2127
2409
|
notifySubscribers(event, turnIdOverride) {
|
|
2128
2410
|
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
|
+
}
|
|
2129
2418
|
const tagged = turnId ? { ...event, turnId } : event;
|
|
2130
2419
|
for (const callback of this.subscribers) {
|
|
2131
2420
|
try {
|