@evident-ai/cli 3.4.1-dev.6e623a8 → 3.4.1-dev.71ade33

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 CHANGED
@@ -1138,6 +1138,37 @@ var TelemetryEventTypes = {
1138
1138
  RUNNER_ACTIVITY: "runner.activity"
1139
1139
  };
1140
1140
 
1141
+ // ../../packages/types/src/tunnel/binary-frame.ts
1142
+ var BINARY_FRAME_REQ_DATA = 1;
1143
+ var BINARY_FRAME_RES_DATA = 2;
1144
+ var textEncoder = new TextEncoder();
1145
+ var textDecoder = new TextDecoder();
1146
+ function encodeBinaryBodyFrame(type, sid, payload) {
1147
+ const sidBytes = textEncoder.encode(sid);
1148
+ if (sidBytes.length === 0 || sidBytes.length > 255) {
1149
+ throw new RangeError("sid must contain between 1 and 255 UTF-8 bytes");
1150
+ }
1151
+ const frame = new Uint8Array(2 + sidBytes.length + payload.length);
1152
+ frame[0] = type;
1153
+ frame[1] = sidBytes.length;
1154
+ frame.set(sidBytes, 2);
1155
+ frame.set(payload, 2 + sidBytes.length);
1156
+ return frame;
1157
+ }
1158
+ function decodeBinaryBodyFrame(bytes) {
1159
+ if (bytes.length < 2) return null;
1160
+ const type = bytes[0];
1161
+ if (type !== BINARY_FRAME_REQ_DATA && type !== BINARY_FRAME_RES_DATA) return null;
1162
+ const sidLen = bytes[1];
1163
+ if (sidLen === 0 || bytes.length < 2 + sidLen) return null;
1164
+ const payloadOffset = 2 + sidLen;
1165
+ return {
1166
+ type,
1167
+ sid: textDecoder.decode(bytes.subarray(2, payloadOffset)),
1168
+ payload: bytes.subarray(payloadOffset)
1169
+ };
1170
+ }
1171
+
1141
1172
  // ../../packages/types/src/tunnel/index.ts
1142
1173
  var MAX_FRAME_BYTES = 256 * 1024;
1143
1174
  var TUNNEL_DRAIN_PING_PATH = "/__evident/drain";
@@ -1182,7 +1213,7 @@ import ora4 from "ora";
1182
1213
  import { select as select4 } from "@inquirer/prompts";
1183
1214
 
1184
1215
  // src/lib/telemetry.ts
1185
- var CLI_VERSION = (true ? "3.4.1-dev.6e623a8" : void 0) ?? process.env.npm_package_version ?? "unknown";
1216
+ var CLI_VERSION = (true ? "3.4.1-dev.71ade33" : void 0) ?? process.env.npm_package_version ?? "unknown";
1186
1217
  function getCliVersion() {
1187
1218
  return CLI_VERSION;
1188
1219
  }
@@ -4720,6 +4751,7 @@ var StreamForwarder = class {
4720
4751
  this.options = options;
4721
4752
  }
4722
4753
  inflight = /* @__PURE__ */ new Map();
4754
+ binaryFramesSupported = false;
4723
4755
  /**
4724
4756
  * Handle an edge→agent frame. Unknown frame types are ignored.
4725
4757
  */
@@ -4739,6 +4771,12 @@ var StreamForwarder = class {
4739
4771
  break;
4740
4772
  }
4741
4773
  }
4774
+ handleBinaryBodyFrame(sid, payload) {
4775
+ this.inflight.get(sid)?.pushBody?.(payload);
4776
+ }
4777
+ setBinaryFramesSupported(supported) {
4778
+ this.binaryFramesSupported = supported;
4779
+ }
4742
4780
  /**
4743
4781
  * Abort every in-flight stream (e.g. on WebSocket close).
4744
4782
  */
@@ -4856,7 +4894,13 @@ var StreamForwarder = class {
4856
4894
  const chunk = Buffer.from(value);
4857
4895
  for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
4858
4896
  const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
4859
- this.send({ type: "res_data", sid, b64: slice.toString("base64") });
4897
+ if (this.binaryFramesSupported) {
4898
+ if (this.ws.readyState === WebSocket.OPEN) {
4899
+ this.ws.send(encodeBinaryBodyFrame(BINARY_FRAME_RES_DATA, sid, slice));
4900
+ }
4901
+ } else {
4902
+ this.send({ type: "res_data", sid, b64: slice.toString("base64") });
4903
+ }
4860
4904
  }
4861
4905
  }
4862
4906
  }
@@ -4873,6 +4917,11 @@ var StreamForwarder = class {
4873
4917
 
4874
4918
  // src/lib/tunnel/connection.ts
4875
4919
  var FAILURE_REASON_HEADER_LC = FORWARD_FAILURE_REASON_HEADER.toLowerCase();
4920
+ function toUint8Array(data) {
4921
+ if (Array.isArray(data)) return Buffer.concat(data);
4922
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
4923
+ return data;
4924
+ }
4876
4925
  var TunnelUpgradeRejectedError = class extends Error {
4877
4926
  constructor(message, reason) {
4878
4927
  super(message);
@@ -4937,7 +4986,8 @@ function connectTunnel(options) {
4937
4986
  return new Promise((resolve4, reject) => {
4938
4987
  const ws = new WebSocket2(url, {
4939
4988
  headers: {
4940
- Authorization: authHeader
4989
+ Authorization: authHeader,
4990
+ "X-Evident-Tunnel-Binary-Frames": "1"
4941
4991
  }
4942
4992
  });
4943
4993
  const forwarder = new StreamForwarder(
@@ -4986,7 +5036,25 @@ function connectTunnel(options) {
4986
5036
  ws.on("open", () => {
4987
5037
  onInfo?.("WebSocket connection established");
4988
5038
  });
4989
- ws.on("message", (data) => {
5039
+ ws.on("message", (data, isBinary) => {
5040
+ if (isBinary) {
5041
+ try {
5042
+ const frame = decodeBinaryBodyFrame(toUint8Array(data));
5043
+ if (frame === null) {
5044
+ onError?.("Failed to handle binary message: invalid frame");
5045
+ return;
5046
+ }
5047
+ if (frame.type !== BINARY_FRAME_REQ_DATA) {
5048
+ onError?.("Failed to handle binary message: unexpected frame type");
5049
+ return;
5050
+ }
5051
+ forwarder.handleBinaryBodyFrame(frame.sid, frame.payload);
5052
+ } catch (error2) {
5053
+ const errorMessage3 = error2 instanceof Error ? error2.message : "Unknown error";
5054
+ onError?.(`Failed to handle binary message: ${errorMessage3}`);
5055
+ }
5056
+ return;
5057
+ }
4990
5058
  let message;
4991
5059
  try {
4992
5060
  message = JSON.parse(data.toString());
@@ -5002,6 +5070,7 @@ function connectTunnel(options) {
5002
5070
  switch (message.type) {
5003
5071
  case "connected": {
5004
5072
  clearTimeout(connectionTimeout);
5073
+ forwarder.setBinaryFramesSupported(message.binary_frames === true);
5005
5074
  const connectedAgentId = message.agent_id ?? agentId;
5006
5075
  onConnected?.(connectedAgentId);
5007
5076
  resolve4({
@@ -6299,23 +6368,21 @@ var ChannelDriver = class _ChannelDriver {
6299
6368
  */
6300
6369
  readopted = /* @__PURE__ */ new Set();
6301
6370
  /**
6302
- * "Don't re-DISPATCH / re-attach this orphan again" (Bug 2/5). Set when a
6303
- * re-adopted running/orphan row's watcher hit its `processed_at`-anchored
6304
- * deadline (or an orphan whose window already elapsed): the still-`processing`
6305
- * server row would otherwise be re-adopted (and re-dispatched) on EVERY ~2s
6306
- * drain until the 15-min cron resets it spamming new turns.
6307
- *
6308
- * CRITICAL (Bugbot #202): this suppresses ONLY the dispatch/re-attach paths, it
6309
- * does NOT suppress DONE delivery. A row parked here whose reply later COMPLETES
6310
- * in opencode must still be delivered via `markDone` on the next drain — so
6311
- * `readoptOne` computes `state` FIRST and this set is checked only on the
6312
- * non-done path. It is cleared once the row leaves both processing and pending
6313
- * lists, so it can never leak. A V2 prompt
6314
- * acknowledgement with no usable id also uses this fence: OpenCode accepted the
6315
- * turn, but there is no safe id to watch, so a failed `markFailed` report must not
6316
- * allow the pending row to post the prompt again.
6371
+ * Readopt give-up fence. Set when recovery declines to start or continue a turn
6372
+ * for a row that is still `processing`, so the next drain does not re-dispatch or
6373
+ * re-attach it before the cron safety net acts. It suppresses only non-done
6374
+ * recovery paths; DONE delivery still runs. Clear it when
6375
+ * `!stillProcessing.has(id)`, because leaving `processing` hands the row back to
6376
+ * normal processing.
6317
6377
  */
6318
6378
  dontRedispatch = /* @__PURE__ */ new Set();
6379
+ /**
6380
+ * Untrackable-ack fence. Set after OpenCode accepts a prompt without returning a
6381
+ * usable message id, because another POST could create a duplicate turn. Keep it
6382
+ * fenced while the row is `processing` or `pending`; clear it only when the row
6383
+ * is absent from both lists.
6384
+ */
6385
+ untrackableAck = /* @__PURE__ */ new Set();
6319
6386
  /** Pending rows seen in the current drain, used to retain terminal dispatch fences. */
6320
6387
  pendingMessageIds = /* @__PURE__ */ new Set();
6321
6388
  /**
@@ -6900,7 +6967,7 @@ var ChannelDriver = class _ChannelDriver {
6900
6967
  skippedAlreadyDispatched += 1;
6901
6968
  continue;
6902
6969
  }
6903
- if (this.dontRedispatch.has(message.id)) {
6970
+ if (this.untrackableAck.has(message.id)) {
6904
6971
  this.log({
6905
6972
  level: "warn",
6906
6973
  message: `Message ${message.id.slice(0, 8)} is fenced after an untrackable OpenCode turn \u2014 skipping re-dispatch`,
@@ -6966,7 +7033,7 @@ var ChannelDriver = class _ChannelDriver {
6966
7033
  if (err instanceof ChannelAuthError) throw err;
6967
7034
  if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
6968
7035
  const errorMessage4 = err instanceof Error ? err.message : String(err);
6969
- this.dontRedispatch.add(message.id);
7036
+ this.untrackableAck.add(message.id);
6970
7037
  this.log({
6971
7038
  level: "error",
6972
7039
  message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
@@ -8887,14 +8954,14 @@ var ChannelDriver = class _ChannelDriver {
8887
8954
  */
8888
8955
  async readoptProcessing() {
8889
8956
  const rows = await this.getProcessingMessages();
8890
- if (this.dontRedispatch.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
8957
+ if (this.dontRedispatch.size > 0 || this.untrackableAck.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
8891
8958
  const stillProcessing = new Set(rows.map((r) => r.id));
8892
8959
  for (const id of [
8893
8960
  ...this.dontRedispatch,
8894
8961
  ...this.doneUndeliverable,
8895
8962
  ...this.readoptPollUnresolvedSignalled
8896
8963
  ]) {
8897
- if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
8964
+ if (!stillProcessing.has(id)) {
8898
8965
  const cleared = this.dontRedispatch.delete(id);
8899
8966
  const clearedUndeliverable = this.doneUndeliverable.delete(id);
8900
8967
  this.readoptPollUnresolvedSignalled.delete(id);
@@ -8907,6 +8974,16 @@ var ChannelDriver = class _ChannelDriver {
8907
8974
  }
8908
8975
  }
8909
8976
  }
8977
+ for (const id of [...this.untrackableAck]) {
8978
+ if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
8979
+ this.untrackableAck.delete(id);
8980
+ this.log({
8981
+ level: "debug",
8982
+ message: `Re-adopt: message ${id.slice(0, 8)} left processing and pending \u2014 cleared untrackable-ack fence`,
8983
+ message_id: id
8984
+ });
8985
+ }
8986
+ }
8910
8987
  }
8911
8988
  if (rows.length === 0) return;
8912
8989
  const bySession = /* @__PURE__ */ new Map();
@@ -9063,10 +9140,11 @@ var ChannelDriver = class _ChannelDriver {
9063
9140
  }
9064
9141
  await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
9065
9142
  this.dontRedispatch.delete(row.id);
9143
+ this.untrackableAck.delete(row.id);
9066
9144
  void this.postSignal(row.conversation_id, row.id, "readopt_failed");
9067
9145
  return;
9068
9146
  }
9069
- if (this.dontRedispatch.has(row.id)) {
9147
+ if (this.dontRedispatch.has(row.id) || this.untrackableAck.has(row.id)) {
9070
9148
  this.log({
9071
9149
  level: "debug",
9072
9150
  message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
@@ -9175,7 +9253,7 @@ var ChannelDriver = class _ChannelDriver {
9175
9253
  * extracted (#1493 Task 2.4) so the ambiguous-finish guard above can call the
9176
9254
  * SAME delivery instead of duplicating it.
9177
9255
  *
9178
- * EVEN IF the row was previously parked in `dontRedispatch` (a give-up stops
9256
+ * EVEN IF the row was previously parked by either recovery fence (a give-up stops
9179
9257
  * re-dispatch, not delivery — Bugbot #202). Guarded EXACTLY like the watcher's
9180
9258
  * `settleMessageDone`: auth re-throws; terminal → park in `doneUndeliverable` +
9181
9259
  * leave for cron; transient → log + leave for the next drain (the still-
@@ -9242,6 +9320,7 @@ var ChannelDriver = class _ChannelDriver {
9242
9320
  await this.reportSubagentAuthFailures(row.conversation_id, ocId, row.id, messages);
9243
9321
  }
9244
9322
  this.dontRedispatch.delete(row.id);
9323
+ this.untrackableAck.delete(row.id);
9245
9324
  void this.postSignal(row.conversation_id, row.id, "readopt_done");
9246
9325
  }
9247
9326
  /**
@@ -9364,7 +9443,7 @@ var ChannelDriver = class _ChannelDriver {
9364
9443
  return;
9365
9444
  }
9366
9445
  } else {
9367
- this.dontRedispatch.add(row.id);
9446
+ this.untrackableAck.add(row.id);
9368
9447
  }
9369
9448
  this.log({
9370
9449
  level: "error",
@@ -9399,17 +9478,6 @@ var ChannelDriver = class _ChannelDriver {
9399
9478
  return;
9400
9479
  }
9401
9480
  if (ocId === null) {
9402
- if (this.isV2) {
9403
- this.dontRedispatch.add(row.id);
9404
- this.log({
9405
- level: "error",
9406
- message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} completed without an acknowledged message id`,
9407
- conversation_id: row.conversation_id,
9408
- message_id: row.id
9409
- });
9410
- void this.postSignal(row.conversation_id, row.id, "ack_untrackable");
9411
- return;
9412
- }
9413
9481
  this.awaitingReadopt.delete(row.id);
9414
9482
  const streak = this.recordUnconfirmedDispatch(row.id, sessionId);
9415
9483
  if (streak >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {