@evident-ai/cli 3.0.1-dev.d06ae3c → 3.0.1-dev.d0ec1bd
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/index.js +331 -96
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -471,12 +471,6 @@ import chalk6 from "chalk";
|
|
|
471
471
|
import ora3 from "ora";
|
|
472
472
|
import { select as select3 } from "@inquirer/prompts";
|
|
473
473
|
|
|
474
|
-
// ../../packages/types/src/opencode/index.ts
|
|
475
|
-
function opencodeMessageIdFor(queuedMessageId) {
|
|
476
|
-
const sanitized = queuedMessageId.replace(/[^a-zA-Z0-9]/g, "_");
|
|
477
|
-
return `msg_${sanitized}`;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
474
|
// ../../packages/types/src/telemetry/index.ts
|
|
481
475
|
var TelemetryEventTypes = {
|
|
482
476
|
// Agent activity events (shown in web UI activity log)
|
|
@@ -515,7 +509,10 @@ function stripQuery(url) {
|
|
|
515
509
|
}
|
|
516
510
|
|
|
517
511
|
// src/lib/telemetry.ts
|
|
518
|
-
var CLI_VERSION = process.env.npm_package_version
|
|
512
|
+
var CLI_VERSION = (true ? "3.0.0" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
513
|
+
function getCliVersion() {
|
|
514
|
+
return CLI_VERSION;
|
|
515
|
+
}
|
|
519
516
|
var eventBuffer = [];
|
|
520
517
|
var flushTimeout = null;
|
|
521
518
|
var isShuttingDown = false;
|
|
@@ -715,7 +712,7 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
|
715
712
|
}
|
|
716
713
|
|
|
717
714
|
// src/lib/opencode/opencode-version-gate.ts
|
|
718
|
-
var QUEUE_VALIDATED_OPENCODE_VERSIONS = ["1.17.11"];
|
|
715
|
+
var QUEUE_VALIDATED_OPENCODE_VERSIONS = ["1.17.11", "1.18.3"];
|
|
719
716
|
function isQueueValidatedVersion(version2) {
|
|
720
717
|
if (!version2) return false;
|
|
721
718
|
return QUEUE_VALIDATED_OPENCODE_VERSIONS.includes(version2);
|
|
@@ -1039,7 +1036,11 @@ function roleOf(m) {
|
|
|
1039
1036
|
}
|
|
1040
1037
|
function completedOf(m) {
|
|
1041
1038
|
if (!m || typeof m !== "object") return void 0;
|
|
1042
|
-
return m.info?.time?.completed;
|
|
1039
|
+
return m.info?.time?.completed ?? m.time?.completed;
|
|
1040
|
+
}
|
|
1041
|
+
function createdOf(m) {
|
|
1042
|
+
if (!m || typeof m !== "object") return void 0;
|
|
1043
|
+
return m.info?.time?.created ?? m.time?.created;
|
|
1043
1044
|
}
|
|
1044
1045
|
function idOf(m) {
|
|
1045
1046
|
if (!m || typeof m !== "object") return void 0;
|
|
@@ -1059,10 +1060,24 @@ function finishOf(m) {
|
|
|
1059
1060
|
const infoFinish = m.info?.finish;
|
|
1060
1061
|
return typeof infoFinish === "string" ? infoFinish : void 0;
|
|
1061
1062
|
}
|
|
1063
|
+
function errorOf(m) {
|
|
1064
|
+
if (!m || typeof m !== "object") return void 0;
|
|
1065
|
+
return m.info?.error ?? m.error;
|
|
1066
|
+
}
|
|
1062
1067
|
function isAssistantInFlight(m) {
|
|
1063
1068
|
if (completedOf(m) == null) return true;
|
|
1064
1069
|
return finishOf(m) === "tool-calls";
|
|
1065
1070
|
}
|
|
1071
|
+
async function getSessionMessages(port, sessionId) {
|
|
1072
|
+
try {
|
|
1073
|
+
const res = await fetch(`${opencodeBase(port)}/session/${sessionId}/message`);
|
|
1074
|
+
if (!res.ok) return null;
|
|
1075
|
+
const body = await res.json();
|
|
1076
|
+
return Array.isArray(body) ? body : null;
|
|
1077
|
+
} catch {
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1066
1081
|
async function createOpenCodeSession(port, directory) {
|
|
1067
1082
|
const url = new URL(`${opencodeBase(port)}/session`);
|
|
1068
1083
|
if (directory && directory.trim()) {
|
|
@@ -1080,9 +1095,16 @@ async function createOpenCodeSession(port, directory) {
|
|
|
1080
1095
|
const data = await response.json();
|
|
1081
1096
|
return data.id;
|
|
1082
1097
|
}
|
|
1083
|
-
|
|
1098
|
+
function messageText(m) {
|
|
1099
|
+
if (!m || !Array.isArray(m.parts)) return "";
|
|
1100
|
+
return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
1101
|
+
}
|
|
1102
|
+
async function sendPromptAsync(port, sessionId, content, options) {
|
|
1103
|
+
const before = await getSessionMessages(port, sessionId);
|
|
1104
|
+
const knownUserIds = new Set(
|
|
1105
|
+
(before ?? []).filter((m) => roleOf(m) === "user").map((m) => idOf(m)).filter((id) => typeof id === "string")
|
|
1106
|
+
);
|
|
1084
1107
|
const body = {
|
|
1085
|
-
messageID: messageId,
|
|
1086
1108
|
parts: [{ type: "text", text: content }]
|
|
1087
1109
|
};
|
|
1088
1110
|
if (options?.agent) {
|
|
@@ -1106,6 +1128,29 @@ async function sendPromptAsync(port, sessionId, content, options, messageId) {
|
|
|
1106
1128
|
const text = await res.text().catch(() => "");
|
|
1107
1129
|
throw new Error(`OpenCode prompt_async failed: HTTP ${res.status}${text ? `: ${text}` : ""}`);
|
|
1108
1130
|
}
|
|
1131
|
+
const READ_BACK_ATTEMPTS = 5;
|
|
1132
|
+
const READ_BACK_DELAY_MS = 150;
|
|
1133
|
+
for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
|
|
1134
|
+
const after = await getSessionMessages(port, sessionId);
|
|
1135
|
+
if (after) {
|
|
1136
|
+
let best = null;
|
|
1137
|
+
for (const m of after) {
|
|
1138
|
+
if (roleOf(m) !== "user") continue;
|
|
1139
|
+
const id = idOf(m);
|
|
1140
|
+
if (typeof id !== "string" || knownUserIds.has(id)) continue;
|
|
1141
|
+
if (messageText(m) !== content) continue;
|
|
1142
|
+
const created = createdOf(m) ?? 0;
|
|
1143
|
+
if (best === null || created > best.created) {
|
|
1144
|
+
best = { id, created };
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (best) return best.id;
|
|
1148
|
+
}
|
|
1149
|
+
if (attempt < READ_BACK_ATTEMPTS - 1) {
|
|
1150
|
+
await new Promise((resolve) => setTimeout(resolve, READ_BACK_DELAY_MS));
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return null;
|
|
1109
1154
|
}
|
|
1110
1155
|
function findAssistantReplyAfter(messages, userMessageId) {
|
|
1111
1156
|
if (!messages || messages.length === 0) return null;
|
|
@@ -1122,19 +1167,31 @@ function findAssistantReplyAfter(messages, userMessageId) {
|
|
|
1122
1167
|
}
|
|
1123
1168
|
function findLastAssistantReplyFor(messages, userMessageId) {
|
|
1124
1169
|
if (!messages || messages.length === 0) return null;
|
|
1170
|
+
let lastCorrelated = null;
|
|
1171
|
+
let lastNonErrored = null;
|
|
1125
1172
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1126
1173
|
const m = messages[i];
|
|
1127
|
-
if (roleOf(m)
|
|
1174
|
+
if (roleOf(m) !== "assistant" || parentIdOf(m) !== userMessageId) continue;
|
|
1175
|
+
if (lastCorrelated === null) lastCorrelated = m;
|
|
1176
|
+
if (errorOf(m) == null) {
|
|
1177
|
+
lastNonErrored = m;
|
|
1178
|
+
break;
|
|
1179
|
+
}
|
|
1128
1180
|
}
|
|
1181
|
+
if (lastCorrelated) return lastNonErrored ?? lastCorrelated;
|
|
1129
1182
|
const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);
|
|
1130
1183
|
if (userIndex === -1) return null;
|
|
1131
1184
|
let last = null;
|
|
1185
|
+
let lastOk = null;
|
|
1132
1186
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
1133
1187
|
const role = roleOf(messages[i]);
|
|
1134
1188
|
if (role === "user") break;
|
|
1135
|
-
if (role === "assistant")
|
|
1189
|
+
if (role === "assistant") {
|
|
1190
|
+
last = messages[i];
|
|
1191
|
+
if (errorOf(messages[i]) == null) lastOk = messages[i];
|
|
1192
|
+
}
|
|
1136
1193
|
}
|
|
1137
|
-
return last;
|
|
1194
|
+
return lastOk ?? last;
|
|
1138
1195
|
}
|
|
1139
1196
|
function messageRunState(messages, userMessageId) {
|
|
1140
1197
|
if (!messages || messages.length === 0) return "unknown";
|
|
@@ -1144,7 +1201,21 @@ function messageRunState(messages, userMessageId) {
|
|
|
1144
1201
|
if (!reply) return "unknown";
|
|
1145
1202
|
}
|
|
1146
1203
|
if (!reply) return "queued";
|
|
1147
|
-
|
|
1204
|
+
if (isAssistantInFlight(reply)) return "running";
|
|
1205
|
+
return errorOf(reply) != null ? "failed" : "done";
|
|
1206
|
+
}
|
|
1207
|
+
function messageError(messages, userMessageId) {
|
|
1208
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
1209
|
+
const error2 = errorOf(reply);
|
|
1210
|
+
if (error2 == null) return null;
|
|
1211
|
+
if (typeof error2 === "string") return error2;
|
|
1212
|
+
if (typeof error2 === "object") {
|
|
1213
|
+
const e = error2;
|
|
1214
|
+
const dataMessage = e.data?.message;
|
|
1215
|
+
if (typeof dataMessage === "string") return dataMessage;
|
|
1216
|
+
if (typeof e.message === "string") return e.message;
|
|
1217
|
+
}
|
|
1218
|
+
return "The agent run failed.";
|
|
1148
1219
|
}
|
|
1149
1220
|
function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
1150
1221
|
if (!messages || messages.length === 0) return false;
|
|
@@ -1152,9 +1223,6 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
|
1152
1223
|
(m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
1153
1224
|
);
|
|
1154
1225
|
}
|
|
1155
|
-
function opencodeMessageIdFor2(queuedMessageId) {
|
|
1156
|
-
return opencodeMessageIdFor(queuedMessageId);
|
|
1157
|
-
}
|
|
1158
1226
|
|
|
1159
1227
|
// src/lib/tunnel/connection.ts
|
|
1160
1228
|
import WebSocket2 from "ws";
|
|
@@ -1559,7 +1627,6 @@ var DEFAULT_RETRY_POLICY = {
|
|
|
1559
1627
|
};
|
|
1560
1628
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
1561
1629
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
1562
|
-
var DEFAULT_DISPATCH_CONFIRM_MS = 6e3;
|
|
1563
1630
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
1564
1631
|
var ChannelAuthError = class extends Error {
|
|
1565
1632
|
constructor(message) {
|
|
@@ -1595,11 +1662,18 @@ var ChannelDriver = class {
|
|
|
1595
1662
|
sleep;
|
|
1596
1663
|
pausedPollIntervalMs;
|
|
1597
1664
|
pausedMaxWaitMs;
|
|
1598
|
-
dispatchConfirmMs;
|
|
1599
1665
|
stuckQueuedMs;
|
|
1600
1666
|
now;
|
|
1601
1667
|
/** Cache of conversationId → opencode sessionId. */
|
|
1602
1668
|
sessions = /* @__PURE__ */ new Map();
|
|
1669
|
+
/**
|
|
1670
|
+
* Per-opencode-session dispatch lock (Task 2.1a). `sendPromptAsync` is no
|
|
1671
|
+
* longer idempotent (no caller-supplied `messageID`), and its read-back picks
|
|
1672
|
+
* "the one new user row" — which is only unambiguous if no OTHER dispatch into
|
|
1673
|
+
* the SAME session interleaves its snapshot→POST→read-back. This map chains each
|
|
1674
|
+
* session's dispatches so they run serially; distinct sessions stay concurrent.
|
|
1675
|
+
*/
|
|
1676
|
+
sessionDispatchLocks = /* @__PURE__ */ new Map();
|
|
1603
1677
|
/**
|
|
1604
1678
|
* Per-SESSION watchers (WI-3), keyed by opencode sessionId. Single-flight per
|
|
1605
1679
|
* session: one polling loop services all of that session's in-flight messages.
|
|
@@ -1650,6 +1724,20 @@ var ChannelDriver = class {
|
|
|
1650
1724
|
* the row leaves the processing list, exactly like `dontRedispatch`.
|
|
1651
1725
|
*/
|
|
1652
1726
|
doneUndeliverable = /* @__PURE__ */ new Set();
|
|
1727
|
+
/**
|
|
1728
|
+
* "A null-id re-adopt re-dispatch is in flight, awaiting its read-back" (WI-5
|
|
1729
|
+
* Task 5.4, High-2). Since we no longer send a caller-supplied id, a re-dispatch
|
|
1730
|
+
* is NOT idempotent: if `forceReadoptRun` dispatches on tick N but the read-back +
|
|
1731
|
+
* persist hasn't landed before tick N+1 re-reads the still-null
|
|
1732
|
+
* `row.opencode_message_id`, tick N+1 would dispatch AGAIN → duplicate user turns.
|
|
1733
|
+
* A row is added here right before its `sendPromptAsync` and `forceReadoptRun`
|
|
1734
|
+
* short-circuits while it is present, so a null-id row is re-dispatched AT MOST
|
|
1735
|
+
* ONCE per outstanding read-back. Cleared on a SUCCESSFUL dispatch+read-back (the
|
|
1736
|
+
* row is then tracked in `dispatched`, so `readoptOne`'s early skip prevents
|
|
1737
|
+
* re-entry) OR on a failed/unresolved dispatch (the message is genuinely un-sent,
|
|
1738
|
+
* so the NEXT tick may retry exactly once more).
|
|
1739
|
+
*/
|
|
1740
|
+
awaitingReadopt = /* @__PURE__ */ new Set();
|
|
1653
1741
|
/**
|
|
1654
1742
|
* Cache of the opencode root directory (from `GET /path`). Resolved lazily on
|
|
1655
1743
|
* first session creation so drain-created sessions are rooted at the project
|
|
@@ -1672,7 +1760,6 @@ var ChannelDriver = class {
|
|
|
1672
1760
|
this.sleep = config2.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
1673
1761
|
this.pausedPollIntervalMs = config2.pausedPollIntervalMs ?? DEFAULT_PAUSED_POLL_INTERVAL_MS;
|
|
1674
1762
|
this.pausedMaxWaitMs = config2.pausedMaxWaitMs ?? DEFAULT_PAUSED_MAX_WAIT_MS;
|
|
1675
|
-
this.dispatchConfirmMs = config2.dispatchConfirmMs ?? DEFAULT_DISPATCH_CONFIRM_MS;
|
|
1676
1763
|
this.stuckQueuedMs = config2.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
1677
1764
|
this.now = config2.now ?? (() => Date.now());
|
|
1678
1765
|
}
|
|
@@ -1764,11 +1851,11 @@ var ChannelDriver = class {
|
|
|
1764
1851
|
skippedAlreadyDispatched += 1;
|
|
1765
1852
|
continue;
|
|
1766
1853
|
}
|
|
1767
|
-
const opencodeMessageId = opencodeMessageIdFor2(message.id);
|
|
1768
1854
|
const options = {
|
|
1769
1855
|
agent: message.opencode_agent ?? void 0,
|
|
1770
1856
|
model: message.opencode_model ?? void 0
|
|
1771
1857
|
};
|
|
1858
|
+
let opencodeMessageId;
|
|
1772
1859
|
try {
|
|
1773
1860
|
this.log({
|
|
1774
1861
|
level: "info",
|
|
@@ -1776,7 +1863,10 @@ var ChannelDriver = class {
|
|
|
1776
1863
|
conversation_id: conv.id,
|
|
1777
1864
|
message_id: message.id
|
|
1778
1865
|
});
|
|
1779
|
-
await
|
|
1866
|
+
opencodeMessageId = await this.dispatchLocked(
|
|
1867
|
+
sessionId,
|
|
1868
|
+
() => sendPromptAsync(this.port, sessionId, message.content, options)
|
|
1869
|
+
);
|
|
1780
1870
|
} catch (err) {
|
|
1781
1871
|
if (err instanceof ChannelAuthError) throw err;
|
|
1782
1872
|
this.dispatched.delete(message.id);
|
|
@@ -1790,6 +1880,15 @@ var ChannelDriver = class {
|
|
|
1790
1880
|
});
|
|
1791
1881
|
continue;
|
|
1792
1882
|
}
|
|
1883
|
+
if (opencodeMessageId === null) {
|
|
1884
|
+
this.log({
|
|
1885
|
+
level: "error",
|
|
1886
|
+
message: `Message ${message.id.slice(0, 8)} dispatched but its opencode id could not be read back \u2014 leaving un-tracked to retry next tick`,
|
|
1887
|
+
conversation_id: conv.id,
|
|
1888
|
+
message_id: message.id
|
|
1889
|
+
});
|
|
1890
|
+
continue;
|
|
1891
|
+
}
|
|
1793
1892
|
this.dispatched.add(message.id);
|
|
1794
1893
|
this.registerInFlight(conv, sessionId, message, opencodeMessageId);
|
|
1795
1894
|
dispatched += 1;
|
|
@@ -1838,6 +1937,25 @@ var ChannelDriver = class {
|
|
|
1838
1937
|
// -------------------------------------------------------------------------
|
|
1839
1938
|
// Per-session watcher (WI-3)
|
|
1840
1939
|
// -------------------------------------------------------------------------
|
|
1940
|
+
/**
|
|
1941
|
+
* Run one dispatch (`sendPromptAsync` snapshot→POST→read-back) serialized per
|
|
1942
|
+
* opencode session (Task 2.1a), so two dispatches into the SAME session can
|
|
1943
|
+
* never interleave and mis-correlate their read-backs. Distinct sessions run
|
|
1944
|
+
* concurrently. The chained tail intentionally ignores the prior result/error
|
|
1945
|
+
* (each dispatch reports its own outcome to its caller).
|
|
1946
|
+
*/
|
|
1947
|
+
dispatchLocked(sessionId, fn) {
|
|
1948
|
+
const prior = this.sessionDispatchLocks.get(sessionId) ?? Promise.resolve();
|
|
1949
|
+
const run2 = prior.then(fn, fn);
|
|
1950
|
+
this.sessionDispatchLocks.set(
|
|
1951
|
+
sessionId,
|
|
1952
|
+
run2.then(
|
|
1953
|
+
() => void 0,
|
|
1954
|
+
() => void 0
|
|
1955
|
+
)
|
|
1956
|
+
);
|
|
1957
|
+
return run2;
|
|
1958
|
+
}
|
|
1841
1959
|
/** Register a freshly-dispatched message with its session's watcher state. */
|
|
1842
1960
|
registerInFlight(conv, sessionId, message, opencodeMessageId) {
|
|
1843
1961
|
let watcher = this.watchers.get(sessionId);
|
|
@@ -1902,10 +2020,10 @@ var ChannelDriver = class {
|
|
|
1902
2020
|
started: true,
|
|
1903
2021
|
done: false,
|
|
1904
2022
|
// Not yet reported stuck-queued. The once-guard (`stuckReported`) applies,
|
|
1905
|
-
// AND the stuck-queued observer
|
|
1906
|
-
//
|
|
1907
|
-
//
|
|
1908
|
-
// (
|
|
2023
|
+
// AND the stuck-queued observer INCLUDES re-adopted queued wedges: it gates
|
|
2024
|
+
// on `state === 'queued'` (turn produced no reply), not on `started`, so a
|
|
2025
|
+
// re-adopted row left wedged in `queued` still emits the signal once
|
|
2026
|
+
// (#210/#220 observability).
|
|
1909
2027
|
stuckReported: false
|
|
1910
2028
|
});
|
|
1911
2029
|
}
|
|
@@ -1993,10 +2111,15 @@ var ChannelDriver = class {
|
|
|
1993
2111
|
async serviceInFlightMessage(sessionId, watcher, inFlight, messages) {
|
|
1994
2112
|
const conv = watcher.conv;
|
|
1995
2113
|
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
1996
|
-
if ((state === "running" || state === "done") && !inFlight.started) {
|
|
2114
|
+
if ((state === "running" || state === "done" || state === "failed") && !inFlight.started) {
|
|
1997
2115
|
let claimed;
|
|
1998
2116
|
try {
|
|
1999
|
-
claimed = await this.markProcessing(
|
|
2117
|
+
claimed = await this.markProcessing(
|
|
2118
|
+
conv.id,
|
|
2119
|
+
inFlight.evidentMessageId,
|
|
2120
|
+
sessionId,
|
|
2121
|
+
inFlight.opencodeMessageId
|
|
2122
|
+
);
|
|
2000
2123
|
} catch (err) {
|
|
2001
2124
|
if (err instanceof ChannelAuthError) throw err;
|
|
2002
2125
|
this.log({
|
|
@@ -2026,7 +2149,12 @@ var ChannelDriver = class {
|
|
|
2026
2149
|
message_id: inFlight.evidentMessageId
|
|
2027
2150
|
});
|
|
2028
2151
|
try {
|
|
2029
|
-
await this.markDone(
|
|
2152
|
+
await this.markDone(
|
|
2153
|
+
conv.id,
|
|
2154
|
+
inFlight.evidentMessageId,
|
|
2155
|
+
sessionId,
|
|
2156
|
+
inFlight.opencodeMessageId
|
|
2157
|
+
);
|
|
2030
2158
|
} catch (err) {
|
|
2031
2159
|
if (err instanceof ChannelAuthError) throw err;
|
|
2032
2160
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -2062,12 +2190,55 @@ var ChannelDriver = class {
|
|
|
2062
2190
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
2063
2191
|
return;
|
|
2064
2192
|
}
|
|
2065
|
-
if (state === "
|
|
2066
|
-
if (
|
|
2067
|
-
|
|
2193
|
+
if (state === "failed") {
|
|
2194
|
+
if (!inFlight.done) {
|
|
2195
|
+
const error2 = messageError(messages, inFlight.opencodeMessageId) ?? void 0;
|
|
2196
|
+
this.log({
|
|
2197
|
+
level: "error",
|
|
2198
|
+
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} errored \u2014 marking failed: ${error2 ?? "(no error text)"}`,
|
|
2199
|
+
conversation_id: conv.id,
|
|
2200
|
+
message_id: inFlight.evidentMessageId
|
|
2201
|
+
});
|
|
2202
|
+
try {
|
|
2203
|
+
await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error2);
|
|
2204
|
+
} catch (err) {
|
|
2205
|
+
if (err instanceof ChannelAuthError) throw err;
|
|
2206
|
+
if (err instanceof ChannelTerminalError) {
|
|
2207
|
+
this.log({
|
|
2208
|
+
level: "error",
|
|
2209
|
+
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed (terminal HTTP ${err.status}) \u2014 leaving for the cron safety net: ${err.message}`,
|
|
2210
|
+
conversation_id: conv.id,
|
|
2211
|
+
message_id: inFlight.evidentMessageId
|
|
2212
|
+
});
|
|
2213
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
if (this.now() >= inFlight.deadline) {
|
|
2217
|
+
this.log({
|
|
2218
|
+
level: "error",
|
|
2219
|
+
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed within the watch window \u2014 leaving for the cron safety net: ${err instanceof Error ? err.message : String(err)}`,
|
|
2220
|
+
conversation_id: conv.id,
|
|
2221
|
+
message_id: inFlight.evidentMessageId
|
|
2222
|
+
});
|
|
2223
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
this.log({
|
|
2227
|
+
level: "error",
|
|
2228
|
+
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
2229
|
+
conversation_id: conv.id,
|
|
2230
|
+
message_id: inFlight.evidentMessageId
|
|
2231
|
+
});
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
inFlight.done = true;
|
|
2068
2235
|
}
|
|
2236
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
2237
|
+
return;
|
|
2069
2238
|
}
|
|
2070
|
-
|
|
2239
|
+
const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
|
|
2240
|
+
const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
|
|
2241
|
+
if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
|
|
2071
2242
|
inFlight.stuckReported = true;
|
|
2072
2243
|
void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
|
|
2073
2244
|
stuck_for_ms: this.now() - inFlight.dispatchedAt
|
|
@@ -2080,41 +2251,11 @@ var ChannelDriver = class {
|
|
|
2080
2251
|
conversation_id: conv.id,
|
|
2081
2252
|
message_id: inFlight.evidentMessageId
|
|
2082
2253
|
});
|
|
2083
|
-
this.
|
|
2084
|
-
|
|
2085
|
-
}
|
|
2086
|
-
/**
|
|
2087
|
-
* Re-dispatch a message whose user row never appeared (idle-path guard). Safe:
|
|
2088
|
-
* opencode treats a duplicate caller-supplied `messageID` as idempotent (PoC
|
|
2089
|
-
* fact 9) — one user message + one reply even if the original DID land. Resets
|
|
2090
|
-
* the dispatch timestamp so the guard doesn't immediately fire again.
|
|
2091
|
-
*/
|
|
2092
|
-
async redispatchInFlight(sessionId, inFlight) {
|
|
2093
|
-
const options = {
|
|
2094
|
-
agent: inFlight.message.opencode_agent ?? void 0,
|
|
2095
|
-
model: inFlight.message.opencode_model ?? void 0
|
|
2096
|
-
};
|
|
2097
|
-
this.log({
|
|
2098
|
-
level: "info",
|
|
2099
|
-
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} not observed after dispatch \u2014 re-dispatching (idle-path guard)`,
|
|
2100
|
-
message_id: inFlight.evidentMessageId
|
|
2101
|
-
});
|
|
2102
|
-
try {
|
|
2103
|
-
await sendPromptAsync(
|
|
2104
|
-
this.port,
|
|
2105
|
-
sessionId,
|
|
2106
|
-
inFlight.message.content,
|
|
2107
|
-
options,
|
|
2108
|
-
inFlight.opencodeMessageId
|
|
2109
|
-
);
|
|
2110
|
-
} catch (err) {
|
|
2111
|
-
this.log({
|
|
2112
|
-
level: "error",
|
|
2113
|
-
message: `Re-dispatch failed for message ${inFlight.evidentMessageId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`,
|
|
2114
|
-
message_id: inFlight.evidentMessageId
|
|
2254
|
+
void this.postSignal(conv.id, inFlight.evidentMessageId, "gave_up", {
|
|
2255
|
+
watched_for_ms: this.now() - inFlight.dispatchedAt
|
|
2115
2256
|
});
|
|
2257
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
2116
2258
|
}
|
|
2117
|
-
inFlight.dispatchedAt = this.now();
|
|
2118
2259
|
}
|
|
2119
2260
|
// -------------------------------------------------------------------------
|
|
2120
2261
|
// Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)
|
|
@@ -2203,10 +2344,17 @@ var ChannelDriver = class {
|
|
|
2203
2344
|
* Re-adopt ONE `processing` row against the tick's session message snapshot
|
|
2204
2345
|
* (ADR-0046 Decision §1/§2). Idempotent: skips a row already being driven.
|
|
2205
2346
|
*
|
|
2206
|
-
* Branches on `messageRunState(messages,
|
|
2347
|
+
* Branches on `messageRunState(messages, row.opencode_message_id)` — the
|
|
2348
|
+
* opencode-assigned user-message id persisted on the first `processing` PATCH
|
|
2349
|
+
* (#218). A row with a NULL stored id (dispatched but the read-back never landed
|
|
2350
|
+
* before the restart) has no id to correlate → treated as an orphan and
|
|
2351
|
+
* re-dispatched (at most once, see `forceReadoptRun`):
|
|
2207
2352
|
* - `done` → `markDone` now (guarded like the watcher's done branch);
|
|
2208
|
-
* - `
|
|
2209
|
-
*
|
|
2353
|
+
* - `failed` → `markFailed` with the surfaced error (issue #182), so an
|
|
2354
|
+
* errored turn is reported failed on restart, NOT re-dispatched;
|
|
2355
|
+
* - `running`/`queued` → re-attach a watcher via `registerReadopted` (no re-dispatch),
|
|
2356
|
+
* tracking the stored id so the reply correlates by it;
|
|
2357
|
+
* - `unknown`/null id → re-dispatch (opencode assigns a fresh id) + attach a watcher.
|
|
2210
2358
|
*
|
|
2211
2359
|
* Only `ChannelAuthError` propagates.
|
|
2212
2360
|
*/
|
|
@@ -2220,8 +2368,8 @@ var ChannelDriver = class {
|
|
|
2220
2368
|
});
|
|
2221
2369
|
return;
|
|
2222
2370
|
}
|
|
2223
|
-
const ocId =
|
|
2224
|
-
const state = messageRunState(messages, ocId);
|
|
2371
|
+
const ocId = row.opencode_message_id;
|
|
2372
|
+
const state = messageRunState(messages, ocId ?? "");
|
|
2225
2373
|
if (state === "done") {
|
|
2226
2374
|
if (this.doneUndeliverable.has(row.id)) {
|
|
2227
2375
|
this.log({
|
|
@@ -2239,7 +2387,7 @@ var ChannelDriver = class {
|
|
|
2239
2387
|
message_id: row.id
|
|
2240
2388
|
});
|
|
2241
2389
|
try {
|
|
2242
|
-
await this.markDone(row.conversation_id, row.id, sessionId);
|
|
2390
|
+
await this.markDone(row.conversation_id, row.id, sessionId, ocId);
|
|
2243
2391
|
} catch (err) {
|
|
2244
2392
|
if (err instanceof ChannelAuthError) throw err;
|
|
2245
2393
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -2263,6 +2411,39 @@ var ChannelDriver = class {
|
|
|
2263
2411
|
this.dontRedispatch.delete(row.id);
|
|
2264
2412
|
return;
|
|
2265
2413
|
}
|
|
2414
|
+
if (state === "failed") {
|
|
2415
|
+
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
2416
|
+
this.log({
|
|
2417
|
+
level: "error",
|
|
2418
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} errored while unwatched \u2014 marking failed: ${error2 ?? "(no error text)"}`,
|
|
2419
|
+
conversation_id: row.conversation_id,
|
|
2420
|
+
message_id: row.id
|
|
2421
|
+
});
|
|
2422
|
+
try {
|
|
2423
|
+
await this.markFailed(row.conversation_id, row.id, sessionId, error2);
|
|
2424
|
+
} catch (err) {
|
|
2425
|
+
if (err instanceof ChannelAuthError) throw err;
|
|
2426
|
+
if (err instanceof ChannelTerminalError) {
|
|
2427
|
+
this.doneUndeliverable.add(row.id);
|
|
2428
|
+
this.log({
|
|
2429
|
+
level: "error",
|
|
2430
|
+
message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} failed (terminal HTTP ${err.status}) \u2014 parking until it leaves processing; leaving for the cron safety net: ${err.message}`,
|
|
2431
|
+
conversation_id: row.conversation_id,
|
|
2432
|
+
message_id: row.id
|
|
2433
|
+
});
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
this.log({
|
|
2437
|
+
level: "error",
|
|
2438
|
+
message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} failed (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
2439
|
+
conversation_id: row.conversation_id,
|
|
2440
|
+
message_id: row.id
|
|
2441
|
+
});
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
this.dontRedispatch.delete(row.id);
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2266
2447
|
if (this.dontRedispatch.has(row.id)) {
|
|
2267
2448
|
this.log({
|
|
2268
2449
|
level: "info",
|
|
@@ -2272,7 +2453,7 @@ var ChannelDriver = class {
|
|
|
2272
2453
|
});
|
|
2273
2454
|
return;
|
|
2274
2455
|
}
|
|
2275
|
-
if (state === "running" || state === "queued") {
|
|
2456
|
+
if ((state === "running" || state === "queued") && ocId) {
|
|
2276
2457
|
const conv = this.convForRow(sessionId, row);
|
|
2277
2458
|
const message = this.queuedMessageForRow(row);
|
|
2278
2459
|
this.registerReadopted(conv, sessionId, message, ocId, this.processedAtMs(row));
|
|
@@ -2281,7 +2462,7 @@ var ChannelDriver = class {
|
|
|
2281
2462
|
this.ensureWatcherRunning(sessionId);
|
|
2282
2463
|
this.log({
|
|
2283
2464
|
level: "info",
|
|
2284
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} ${state} \u2014 re-attached watcher (
|
|
2465
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} ${state} \u2014 re-attached watcher (stored id, no re-dispatch)`,
|
|
2285
2466
|
conversation_id: row.conversation_id,
|
|
2286
2467
|
message_id: row.id
|
|
2287
2468
|
});
|
|
@@ -2290,23 +2471,36 @@ var ChannelDriver = class {
|
|
|
2290
2471
|
await this.forceReadoptRun(sessionId, row);
|
|
2291
2472
|
}
|
|
2292
2473
|
/**
|
|
2293
|
-
* Re-dispatch an orphaned (`unknown
|
|
2474
|
+
* Re-dispatch an orphaned (`unknown`/null-id) `processing` row (ADR-0046 §2).
|
|
2294
2475
|
*
|
|
2295
|
-
*
|
|
2296
|
-
*
|
|
2297
|
-
*
|
|
2298
|
-
*
|
|
2299
|
-
* (which hangs off the stable id) is found and delivered. The residual
|
|
2300
|
-
* duplicate-incomplete-turn semantics (ADR §2, `.harness/restart-recovery-orphan-finding.md`)
|
|
2301
|
-
* are unchanged and, for an ABSENT id, cannot bite — there is no existing turn
|
|
2302
|
-
* to swallow the duplicate.
|
|
2476
|
+
* #218/WI-5: the row's user message is absent (never kept, or a null stored id),
|
|
2477
|
+
* so we re-`prompt_async` WITHOUT a caller id (opencode assigns a monotonic one),
|
|
2478
|
+
* read it back, and register the watcher under the assigned id so the reply
|
|
2479
|
+
* correlates server-side.
|
|
2303
2480
|
*
|
|
2304
|
-
*
|
|
2305
|
-
*
|
|
2481
|
+
* ⚠️ AT-MOST-ONCE (High-2): dispatch is no longer idempotent (no caller-supplied
|
|
2482
|
+
* id). Without a guard, if this dispatches on tick N but the read-back+persist
|
|
2483
|
+
* hasn't landed before tick N+1 re-reads the still-null `opencode_message_id`,
|
|
2484
|
+
* tick N+1 would dispatch AGAIN → duplicate user turns. The `awaitingReadopt`
|
|
2485
|
+
* latch makes a null-id row re-dispatched AT MOST ONCE per outstanding read-back:
|
|
2486
|
+
* short-circuit while the row is latched; clear it on a successful dispatch (the
|
|
2487
|
+
* row is then tracked in `dispatched`, so `readoptOne`'s early skip prevents
|
|
2488
|
+
* re-entry) OR on a failed/unresolved dispatch (genuinely un-sent → the next tick
|
|
2489
|
+
* may retry exactly once more).
|
|
2490
|
+
*
|
|
2491
|
+
* `evidentMessageId = row.id` addresses the SERVER row. Deadline anchored to
|
|
2306
2492
|
* `processed_at` (Invariant 1).
|
|
2307
2493
|
*/
|
|
2308
2494
|
async forceReadoptRun(sessionId, row) {
|
|
2309
|
-
|
|
2495
|
+
if (this.awaitingReadopt.has(row.id)) {
|
|
2496
|
+
this.log({
|
|
2497
|
+
level: "info",
|
|
2498
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} already has a re-dispatch awaiting read-back \u2014 skipping (at most once)`,
|
|
2499
|
+
conversation_id: row.conversation_id,
|
|
2500
|
+
message_id: row.id
|
|
2501
|
+
});
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2310
2504
|
if (this.processedAtMs(row) + this.pausedMaxWaitMs <= this.now()) {
|
|
2311
2505
|
this.dontRedispatch.add(row.id);
|
|
2312
2506
|
this.log({
|
|
@@ -2323,13 +2517,19 @@ var ChannelDriver = class {
|
|
|
2323
2517
|
};
|
|
2324
2518
|
this.log({
|
|
2325
2519
|
level: "info",
|
|
2326
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} orphaned (user message absent) \u2014 re-dispatching
|
|
2520
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} orphaned (user message absent) \u2014 re-dispatching (opencode assigns a fresh id)`,
|
|
2327
2521
|
conversation_id: row.conversation_id,
|
|
2328
2522
|
message_id: row.id
|
|
2329
2523
|
});
|
|
2524
|
+
this.awaitingReadopt.add(row.id);
|
|
2525
|
+
let ocId;
|
|
2330
2526
|
try {
|
|
2331
|
-
await
|
|
2527
|
+
ocId = await this.dispatchLocked(
|
|
2528
|
+
sessionId,
|
|
2529
|
+
() => sendPromptAsync(this.port, sessionId, row.content, options)
|
|
2530
|
+
);
|
|
2332
2531
|
} catch (err) {
|
|
2532
|
+
this.awaitingReadopt.delete(row.id);
|
|
2333
2533
|
if (err instanceof ChannelAuthError) throw err;
|
|
2334
2534
|
this.log({
|
|
2335
2535
|
level: "error",
|
|
@@ -2339,11 +2539,22 @@ var ChannelDriver = class {
|
|
|
2339
2539
|
});
|
|
2340
2540
|
return;
|
|
2341
2541
|
}
|
|
2542
|
+
if (ocId === null) {
|
|
2543
|
+
this.awaitingReadopt.delete(row.id);
|
|
2544
|
+
this.log({
|
|
2545
|
+
level: "error",
|
|
2546
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} re-dispatched but its opencode id could not be read back \u2014 leaving un-tracked to retry next drain`,
|
|
2547
|
+
conversation_id: row.conversation_id,
|
|
2548
|
+
message_id: row.id
|
|
2549
|
+
});
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2342
2552
|
const conv = this.convForRow(sessionId, row);
|
|
2343
2553
|
const message = this.queuedMessageForRow(row);
|
|
2344
2554
|
this.registerReadopted(conv, sessionId, message, ocId, this.processedAtMs(row));
|
|
2345
2555
|
this.dispatched.add(row.id);
|
|
2346
2556
|
this.readopted.add(row.id);
|
|
2557
|
+
this.awaitingReadopt.delete(row.id);
|
|
2347
2558
|
this.ensureWatcherRunning(sessionId);
|
|
2348
2559
|
}
|
|
2349
2560
|
/**
|
|
@@ -2613,13 +2824,17 @@ var ChannelDriver = class {
|
|
|
2613
2824
|
* A single attempt (no internal retry): the watcher's per-tick loop is the
|
|
2614
2825
|
* retry vehicle for the swap-to-running.
|
|
2615
2826
|
*/
|
|
2616
|
-
async markProcessing(conversationId, messageId, sessionId) {
|
|
2827
|
+
async markProcessing(conversationId, messageId, sessionId, opencodeMessageId) {
|
|
2617
2828
|
const res = await this.fetchImpl(
|
|
2618
2829
|
`${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
2619
2830
|
{
|
|
2620
2831
|
method: "PATCH",
|
|
2621
2832
|
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
2622
|
-
body: JSON.stringify({
|
|
2833
|
+
body: JSON.stringify({
|
|
2834
|
+
status: "processing",
|
|
2835
|
+
opencode_session_id: sessionId,
|
|
2836
|
+
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {}
|
|
2837
|
+
})
|
|
2623
2838
|
}
|
|
2624
2839
|
);
|
|
2625
2840
|
this.assertAuth(res, "marking message as processing");
|
|
@@ -2657,13 +2872,17 @@ var ChannelDriver = class {
|
|
|
2657
2872
|
* watcher retries next tick within the
|
|
2658
2873
|
* deadline, Finding 4).
|
|
2659
2874
|
*/
|
|
2660
|
-
async markDone(conversationId, messageId, sessionId) {
|
|
2875
|
+
async markDone(conversationId, messageId, sessionId, opencodeMessageId) {
|
|
2661
2876
|
const res = await this.fetchImpl(
|
|
2662
2877
|
`${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
2663
2878
|
{
|
|
2664
2879
|
method: "PATCH",
|
|
2665
2880
|
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
2666
|
-
body: JSON.stringify({
|
|
2881
|
+
body: JSON.stringify({
|
|
2882
|
+
status: "done",
|
|
2883
|
+
opencode_session_id: sessionId,
|
|
2884
|
+
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {}
|
|
2885
|
+
})
|
|
2667
2886
|
}
|
|
2668
2887
|
);
|
|
2669
2888
|
this.assertAuth(res, "marking message as done");
|
|
@@ -2673,7 +2892,17 @@ var ChannelDriver = class {
|
|
|
2673
2892
|
}
|
|
2674
2893
|
throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
|
|
2675
2894
|
}
|
|
2676
|
-
|
|
2895
|
+
/**
|
|
2896
|
+
* Mark a message `failed`. `sessionId` / `error` are threaded to the API ONLY
|
|
2897
|
+
* when provided (issue #182): a bare `markFailed(conv, msg)` sends
|
|
2898
|
+
* `{status:'failed'}` unchanged (the dispatch-failure path), while an errored
|
|
2899
|
+
* OpenCode turn sends `{status:'failed', opencode_session_id, error}` so the
|
|
2900
|
+
* failure reason reaches the channel.
|
|
2901
|
+
*/
|
|
2902
|
+
async markFailed(conversationId, messageId, sessionId, error2) {
|
|
2903
|
+
const body = { status: "failed" };
|
|
2904
|
+
if (sessionId !== void 0) body.opencode_session_id = sessionId;
|
|
2905
|
+
if (error2 !== void 0) body.error = error2;
|
|
2677
2906
|
await this.callWithRetry(
|
|
2678
2907
|
"marking message as failed",
|
|
2679
2908
|
() => this.fetchImpl(
|
|
@@ -2681,7 +2910,7 @@ var ChannelDriver = class {
|
|
|
2681
2910
|
{
|
|
2682
2911
|
method: "PATCH",
|
|
2683
2912
|
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
2684
|
-
body: JSON.stringify(
|
|
2913
|
+
body: JSON.stringify(body)
|
|
2685
2914
|
}
|
|
2686
2915
|
)
|
|
2687
2916
|
);
|
|
@@ -3037,6 +3266,7 @@ async function getAgentInfo(agentId, authHeader) {
|
|
|
3037
3266
|
// src/commands/run.ts
|
|
3038
3267
|
var MAX_ACTIVITY_LOG_ENTRIES = 10;
|
|
3039
3268
|
var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
|
|
3269
|
+
var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
|
|
3040
3270
|
function log2(state, message, isError = false) {
|
|
3041
3271
|
if (state.json) {
|
|
3042
3272
|
console.log(
|
|
@@ -3374,6 +3604,7 @@ async function run(options) {
|
|
|
3374
3604
|
apiUrl: getApiUrlConfig(),
|
|
3375
3605
|
getAuthHeader: () => state.authHeader,
|
|
3376
3606
|
conversationFilter: state.conversationFilter,
|
|
3607
|
+
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
3377
3608
|
log: (entry) => logActivity(state, {
|
|
3378
3609
|
type: entry.level === "error" ? "error" : "info",
|
|
3379
3610
|
message: entry.message,
|
|
@@ -3393,7 +3624,11 @@ async function run(options) {
|
|
|
3393
3624
|
type: "info",
|
|
3394
3625
|
message: `Tunnel ${isReconnect ? "reconnected" : "connected"} (agent: ${agentId})`
|
|
3395
3626
|
});
|
|
3396
|
-
emitAgentConnected(state.agentId, {
|
|
3627
|
+
emitAgentConnected(state.agentId, {
|
|
3628
|
+
port: state.port,
|
|
3629
|
+
cli_version: getCliVersion(),
|
|
3630
|
+
opencode_version: state.opencodeVersion
|
|
3631
|
+
});
|
|
3397
3632
|
if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
|
|
3398
3633
|
if (state.interactive) displayStatus(state);
|
|
3399
3634
|
channelDriver.drainPending().then((processed) => {
|