@getpaseo/server 0.1.72 → 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/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/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.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.js +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
- package/dist/server/server/agent/providers/opencode-agent.d.ts +33 -1
- package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/opencode-agent.js +528 -9
- package/dist/server/server/agent/providers/opencode-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/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/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/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/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/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 +4 -4
|
@@ -24,6 +24,22 @@ 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
|
+
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;
|
|
27
43
|
const DEFAULT_MODES = [
|
|
28
44
|
{
|
|
29
45
|
id: OPENCODE_BUILD_MODE_ID,
|
|
@@ -435,6 +451,16 @@ function mergeOpenCodeStepFinishUsage(usage, part) {
|
|
|
435
451
|
usage.totalCostUsd = (usage.totalCostUsd ?? 0) + cost;
|
|
436
452
|
}
|
|
437
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
|
+
}
|
|
438
464
|
function hasNormalizedOpenCodeUsage(usage) {
|
|
439
465
|
return [
|
|
440
466
|
usage.inputTokens,
|
|
@@ -574,17 +600,24 @@ async function readOpenCodeSessionTimeline(storageRoot, sessionId) {
|
|
|
574
600
|
return timeline;
|
|
575
601
|
}
|
|
576
602
|
async function readOpenCodeMessageText(storageRoot, messageId) {
|
|
603
|
+
const parts = await readOpenCodeStoredParts(storageRoot, messageId);
|
|
604
|
+
return readOpenCodeTextFromParts(parts);
|
|
605
|
+
}
|
|
606
|
+
async function readOpenCodeStoredParts(storageRoot, messageId) {
|
|
577
607
|
const partRoot = path.join(storageRoot, "part", messageId);
|
|
578
608
|
const partFiles = await findJsonFiles(partRoot);
|
|
579
609
|
const parts = [];
|
|
580
610
|
for (const file of partFiles) {
|
|
581
611
|
const parsed = await readJsonFile(file, OpenCodeStoredPartSchema);
|
|
582
|
-
if (parsed
|
|
612
|
+
if (parsed) {
|
|
583
613
|
parts.push(parsed);
|
|
584
614
|
}
|
|
585
615
|
}
|
|
616
|
+
return parts.sort((left, right) => getOpenCodePartTimestamp(left) - getOpenCodePartTimestamp(right));
|
|
617
|
+
}
|
|
618
|
+
function readOpenCodeTextFromParts(parts) {
|
|
586
619
|
return parts
|
|
587
|
-
.
|
|
620
|
+
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
588
621
|
.map((part) => part.text?.trim() ?? "")
|
|
589
622
|
.filter(Boolean)
|
|
590
623
|
.join("\n\n");
|
|
@@ -669,6 +702,11 @@ export class OpenCodeAgentClient {
|
|
|
669
702
|
this.logger = logger.child({ module: "agent", provider: "opencode" });
|
|
670
703
|
this.runtimeSettings = runtimeSettings;
|
|
671
704
|
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
|
+
};
|
|
672
710
|
this.runtime =
|
|
673
711
|
deps.runtime ??
|
|
674
712
|
new ProductionOpenCodeRuntime(OpenCodeServerManager.getInstance(this.logger, runtimeSettings));
|
|
@@ -691,7 +729,7 @@ export class OpenCodeAgentClient {
|
|
|
691
729
|
throw new Error("OpenCode session creation returned no data");
|
|
692
730
|
}
|
|
693
731
|
await this.populateModelContextWindowCache(client, openCodeConfig.cwd);
|
|
694
|
-
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);
|
|
695
733
|
}
|
|
696
734
|
catch (error) {
|
|
697
735
|
acquisition.release();
|
|
@@ -717,7 +755,7 @@ export class OpenCodeAgentClient {
|
|
|
717
755
|
});
|
|
718
756
|
try {
|
|
719
757
|
await this.populateModelContextWindowCache(client, openCodeConfig.cwd);
|
|
720
|
-
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);
|
|
721
759
|
}
|
|
722
760
|
catch (error) {
|
|
723
761
|
acquisition.release();
|
|
@@ -1554,13 +1592,31 @@ function createDeferred() {
|
|
|
1554
1592
|
});
|
|
1555
1593
|
return { promise, resolve, reject };
|
|
1556
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
|
+
}
|
|
1557
1608
|
class OpenCodeAgentSession {
|
|
1558
|
-
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
|
+
}) {
|
|
1559
1614
|
this.provider = "opencode";
|
|
1560
1615
|
this.capabilities = OPENCODE_CAPABILITIES;
|
|
1561
1616
|
this.currentMode = "default";
|
|
1562
1617
|
this.pendingPermissions = new Map();
|
|
1563
1618
|
this.abortController = null;
|
|
1619
|
+
this.pendingAbortPromise = null;
|
|
1564
1620
|
this.accumulatedUsage = {};
|
|
1565
1621
|
this.mcpConfigured = false;
|
|
1566
1622
|
this.mcpSetupPromise = null;
|
|
@@ -1581,14 +1637,25 @@ class OpenCodeAgentSession {
|
|
|
1581
1637
|
this.subAgentCallIdByChildSessionId = new Map();
|
|
1582
1638
|
this.pendingChildToolPartsBySessionId = new Map();
|
|
1583
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;
|
|
1584
1649
|
this.config = config;
|
|
1585
1650
|
this.client = client;
|
|
1586
1651
|
this.sessionId = sessionId;
|
|
1587
1652
|
this.logger = logger;
|
|
1653
|
+
this.storageRoot = storageRoot;
|
|
1588
1654
|
this.modelContextWindowsByModelKey = modelContextWindowsByModelKey;
|
|
1589
1655
|
this.currentMode = normalizeOpenCodeModeId(config.modeId);
|
|
1590
1656
|
this.releaseServer = releaseServer ?? null;
|
|
1591
1657
|
this.persistSession = persistSession;
|
|
1658
|
+
this.recovery = recovery;
|
|
1592
1659
|
this.selectedModelContextWindowMaxTokens = this.resolveConfiguredModelContextWindowMaxTokens(config.model);
|
|
1593
1660
|
}
|
|
1594
1661
|
get id() {
|
|
@@ -1626,22 +1693,65 @@ class OpenCodeAgentSession {
|
|
|
1626
1693
|
const turnId = this.activeForegroundTurnId;
|
|
1627
1694
|
const turnAbortController = this.abortController;
|
|
1628
1695
|
turnAbortController?.abort();
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
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");
|
|
1632
1705
|
});
|
|
1633
1706
|
if (turnId) {
|
|
1634
1707
|
this.finishForegroundTurn({ type: "turn_canceled", provider: "opencode", reason: "interrupted" }, turnId);
|
|
1635
1708
|
}
|
|
1636
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
|
+
}
|
|
1637
1737
|
async startTurn(prompt, options) {
|
|
1638
1738
|
if (this.activeForegroundTurnId) {
|
|
1639
1739
|
throw new Error("A foreground turn is already active");
|
|
1640
1740
|
}
|
|
1741
|
+
await this.awaitPendingAbortBeforeStartingTurn();
|
|
1742
|
+
this.foregroundTurnStartedAt = Date.now();
|
|
1641
1743
|
this.runningToolCalls.clear();
|
|
1642
1744
|
this.subAgentsByCallId.clear();
|
|
1643
1745
|
this.subAgentCallIdByChildSessionId.clear();
|
|
1644
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();
|
|
1645
1755
|
const turnAbortController = new AbortController();
|
|
1646
1756
|
this.abortController = turnAbortController;
|
|
1647
1757
|
await this.ensureMcpServersConfigured();
|
|
@@ -1750,6 +1860,14 @@ class OpenCodeAgentSession {
|
|
|
1750
1860
|
// SDK input validation) is caught alongside async rejections. A plain
|
|
1751
1861
|
// `.then().catch()` chain would let a sync throw escape unhandled.
|
|
1752
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
|
+
});
|
|
1753
1871
|
try {
|
|
1754
1872
|
const promptResponse = await this.client.session.promptAsync({
|
|
1755
1873
|
sessionID: this.sessionId,
|
|
@@ -1768,6 +1886,12 @@ class OpenCodeAgentSession {
|
|
|
1768
1886
|
...(effectiveMode ? { agent: effectiveMode } : {}),
|
|
1769
1887
|
...(effectiveVariant ? { variant: effectiveVariant } : {}),
|
|
1770
1888
|
});
|
|
1889
|
+
traceOpenCode("promptAsync.response", {
|
|
1890
|
+
turnId,
|
|
1891
|
+
hasError: promptResponse.error !== undefined,
|
|
1892
|
+
error: promptResponse.error,
|
|
1893
|
+
data: promptResponse.data,
|
|
1894
|
+
});
|
|
1771
1895
|
if (promptResponse.error) {
|
|
1772
1896
|
this.finishForegroundTurn({
|
|
1773
1897
|
type: "turn_failed",
|
|
@@ -1777,6 +1901,12 @@ class OpenCodeAgentSession {
|
|
|
1777
1901
|
}
|
|
1778
1902
|
}
|
|
1779
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
|
+
});
|
|
1780
1910
|
this.finishForegroundTurn({
|
|
1781
1911
|
type: "turn_failed",
|
|
1782
1912
|
provider: "opencode",
|
|
@@ -1794,16 +1924,39 @@ class OpenCodeAgentSession {
|
|
|
1794
1924
|
};
|
|
1795
1925
|
}
|
|
1796
1926
|
async consumeEventStream(turnId, turnAbortController, subscriptionReady) {
|
|
1927
|
+
traceOpenCode("subscribe.start", { turnId, sessionId: this.sessionId, cwd: this.config.cwd });
|
|
1797
1928
|
try {
|
|
1798
|
-
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 });
|
|
1799
1931
|
subscriptionReady.resolve();
|
|
1932
|
+
let eventCount = 0;
|
|
1800
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
|
+
});
|
|
1801
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
|
+
});
|
|
1802
1948
|
break;
|
|
1803
1949
|
}
|
|
1804
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
|
+
});
|
|
1805
1957
|
for (const e of translated) {
|
|
1806
1958
|
if (this.activeForegroundTurnId !== turnId) {
|
|
1959
|
+
traceOpenCode("event.translated.skip-active", { turnId, type: e.type });
|
|
1807
1960
|
return;
|
|
1808
1961
|
}
|
|
1809
1962
|
if (e.type === "timeline" && e.item.type === "tool_call") {
|
|
@@ -1811,13 +1964,26 @@ class OpenCodeAgentSession {
|
|
|
1811
1964
|
}
|
|
1812
1965
|
const terminalEvent = toTerminalTurnEvent(e);
|
|
1813
1966
|
if (terminalEvent) {
|
|
1967
|
+
traceOpenCode("event.terminal", { turnId, type: terminalEvent.type });
|
|
1814
1968
|
this.finishForegroundTurn(terminalEvent, turnId);
|
|
1815
1969
|
return;
|
|
1816
1970
|
}
|
|
1817
1971
|
this.notifySubscribers(e, turnId);
|
|
1818
1972
|
}
|
|
1819
1973
|
}
|
|
1974
|
+
traceOpenCode("stream.eof", {
|
|
1975
|
+
turnId,
|
|
1976
|
+
eventCount,
|
|
1977
|
+
aborted: turnAbortController.signal.aborted,
|
|
1978
|
+
stillActive: this.activeForegroundTurnId === turnId,
|
|
1979
|
+
});
|
|
1820
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 });
|
|
1821
1987
|
this.finishForegroundTurn({
|
|
1822
1988
|
type: "turn_failed",
|
|
1823
1989
|
provider: "opencode",
|
|
@@ -1826,6 +1992,10 @@ class OpenCodeAgentSession {
|
|
|
1826
1992
|
}
|
|
1827
1993
|
}
|
|
1828
1994
|
catch (error) {
|
|
1995
|
+
traceOpenCode("subscribe.error", {
|
|
1996
|
+
turnId,
|
|
1997
|
+
error: error instanceof Error ? { name: error.name, message: error.message } : String(error),
|
|
1998
|
+
});
|
|
1829
1999
|
subscriptionReady.reject(error);
|
|
1830
2000
|
if (!turnAbortController.signal.aborted && this.activeForegroundTurnId === turnId) {
|
|
1831
2001
|
this.finishForegroundTurn({
|
|
@@ -1848,7 +2018,348 @@ class OpenCodeAgentSession {
|
|
|
1848
2018
|
}
|
|
1849
2019
|
}
|
|
1850
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
|
+
}
|
|
1851
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
|
+
});
|
|
1852
2363
|
if (this.activeForegroundTurnId !== turnId) {
|
|
1853
2364
|
return;
|
|
1854
2365
|
}
|
|
@@ -1858,6 +2369,7 @@ class OpenCodeAgentSession {
|
|
|
1858
2369
|
else {
|
|
1859
2370
|
this.runningToolCalls.clear();
|
|
1860
2371
|
}
|
|
2372
|
+
this.foregroundTurnStartedAt = null;
|
|
1861
2373
|
this.activeForegroundTurnId = null;
|
|
1862
2374
|
// Abort the SSE connection so the SDK tears down the underlying fetch.
|
|
1863
2375
|
this.abortController?.abort();
|
|
@@ -1896,6 +2408,13 @@ class OpenCodeAgentSession {
|
|
|
1896
2408
|
}
|
|
1897
2409
|
notifySubscribers(event, turnIdOverride) {
|
|
1898
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
|
+
}
|
|
1899
2418
|
const tagged = turnId ? { ...event, turnId } : event;
|
|
1900
2419
|
for (const callback of this.subscribers) {
|
|
1901
2420
|
try {
|