@evident-ai/cli 3.1.1-dev.1e270a5 → 3.1.1-dev.201318e

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
@@ -797,6 +797,16 @@ async function checkStatus(jsonMode) {
797
797
  exitCode: 1
798
798
  };
799
799
  }
800
+ if (response.status === 404) {
801
+ return {
802
+ ok: false,
803
+ endpoint: apiUrl,
804
+ authLabel: authLabelFor(credentials2),
805
+ reason: "endpoint_not_found",
806
+ error: `${apiUrl}/me returned HTTP 404 \u2014 that endpoint has no /me route, so it is probably missing the /v1 prefix. The credentials were NOT validated.`,
807
+ exitCode: 75
808
+ };
809
+ }
800
810
  if (response.status >= 500) {
801
811
  const serverMessage = await readErrorMessage(response);
802
812
  return {
@@ -3318,6 +3328,23 @@ var ChannelDriver = class _ChannelDriver {
3318
3328
  * `clearRedriveUnresolved` the instant either PATCH succeeds.
3319
3329
  */
3320
3330
  redriveOutcomeUnreportedSignalled = /* @__PURE__ */ new Map();
3331
+ /**
3332
+ * First `now()` a Class B outcome PATCH (reattach/settle/fail_permanent) was
3333
+ * observed to fail for this message (#1366's failure-window trip arm,
3334
+ * `boundRedriveOutcome`). Duration, not a tick count — bounded by the
3335
+ * existing `pausedMaxWaitMs` window (reusing the knob, not a new constant).
3336
+ * Cleared by `clearRedriveUnresolved` the instant the original PATCH
3337
+ * succeeds.
3338
+ */
3339
+ redriveOutcomeFailingSince = /* @__PURE__ */ new Map();
3340
+ /**
3341
+ * "Already posted `redrive_outcome_abandoned` with `reported: false` for this
3342
+ * row" (#1366) — the bound tripped but the terminal `markFailed` fallback ALSO
3343
+ * failed (the route-level fault of G2), so every following tick re-attempts
3344
+ * the same terminal PATCH. Guards that quiet retry from re-signalling on
3345
+ * every tick. Cleared by `clearRedriveUnresolved`.
3346
+ */
3347
+ redriveOutcomeAbandonedSignalled = /* @__PURE__ */ new Set();
3321
3348
  /**
3322
3349
  * Consecutive-UNCONFIRMED-dispatch streak for a `pending` row with NO stored
3323
3350
  * `opencode_message_id` yet — i.e. one that has never even reached the
@@ -3674,6 +3701,9 @@ var ChannelDriver = class _ChannelDriver {
3674
3701
  }
3675
3702
  if (message.opencode_message_id) {
3676
3703
  const outcome = await this.resolveRedrive(conv, sessionId, message, sessionCreated);
3704
+ if (outcome === "abandoned") {
3705
+ continue;
3706
+ }
3677
3707
  if (outcome !== "dispatch") {
3678
3708
  break;
3679
3709
  }
@@ -3943,8 +3973,8 @@ var ChannelDriver = class _ChannelDriver {
3943
3973
  conversation_id: conv.id,
3944
3974
  message_id: message.id
3945
3975
  });
3946
- this.signalRedriveOutcomeUnreported(conv, message, "reattach");
3947
- return "unresolved";
3976
+ const bound = await this.boundRedriveOutcome(conv, message, "reattach");
3977
+ return bound === "abandoned" ? "abandoned" : "unresolved";
3948
3978
  }
3949
3979
  this.clearRedriveUnresolved(message.id);
3950
3980
  this.registerReadopted(conv, sessionId, message, ocId ?? "", anchorMs);
@@ -4004,8 +4034,8 @@ var ChannelDriver = class _ChannelDriver {
4004
4034
  conversation_id: conv.id,
4005
4035
  message_id: message.id
4006
4036
  });
4007
- this.signalRedriveOutcomeUnreported(conv, message, "settle");
4008
- return "unresolved";
4037
+ const bound = await this.boundRedriveOutcome(conv, message, "settle");
4038
+ return bound === "abandoned" ? "abandoned" : "unresolved";
4009
4039
  }
4010
4040
  this.clearRedriveUnresolved(message.id);
4011
4041
  void this.postSignal(conv.id, message.id, "redrive_settled");
@@ -4044,8 +4074,8 @@ var ChannelDriver = class _ChannelDriver {
4044
4074
  conversation_id: conv.id,
4045
4075
  message_id: message.id
4046
4076
  });
4047
- this.signalRedriveOutcomeUnreported(conv, message, "fail_permanent");
4048
- return "unresolved";
4077
+ const bound = await this.boundRedriveOutcome(conv, message, "fail_permanent");
4078
+ return bound === "abandoned" ? "abandoned" : "unresolved";
4049
4079
  }
4050
4080
  this.clearRedriveUnresolved(message.id);
4051
4081
  void this.postSignal(conv.id, message.id, "redrive_poll_failed");
@@ -4054,10 +4084,12 @@ var ChannelDriver = class _ChannelDriver {
4054
4084
  /**
4055
4085
  * The bounded `unresolved` outcome (Task 3.4): opencode's state could not be
4056
4086
  * observed (snapshot unreadable/empty, or `isSessionOngoing` returned `null`).
4057
- * A `pending` row is invisible to every cron arm (all require `status =
4058
- * 'processing'`), so an indefinitely-`unresolved` row would be stranded with
4059
- * nothing driving it bound it to the existing `pausedMaxWaitMs` window
4060
- * (reusing the knob, not a new constant) and take `dispatch` once elapsed.
4087
+ * A `pending` row is swept by the server's own `PENDING_MAX_AGE_MS` (24h,
4088
+ * #1368) cron arm, but that is a day-scale backstop this local bound acts
4089
+ * in minutes so the row (and the conversation it starves, per the ordering
4090
+ * invariant below) isn't left stranded for that long. Bound to the existing
4091
+ * `pausedMaxWaitMs` window (reusing the knob, not a new constant); takes
4092
+ * `dispatch` once elapsed.
4061
4093
  */
4062
4094
  resolveRedriveUnresolved(conv, message) {
4063
4095
  const now = this.now();
@@ -4082,16 +4114,15 @@ var ChannelDriver = class _ChannelDriver {
4082
4114
  this.redriveUnresolvedSignalled.delete(messageId);
4083
4115
  this.redrivePollFailures.delete(messageId);
4084
4116
  this.redriveOutcomeUnreportedSignalled.delete(messageId);
4117
+ this.redriveOutcomeFailingSince.delete(messageId);
4118
+ this.redriveOutcomeAbandonedSignalled.delete(messageId);
4085
4119
  }
4086
4120
  /**
4087
4121
  * Class B (#1340): the runner DECIDED an outcome (reattach/settle/fail_permanent)
4088
- * but its own PATCH to record it failed. Deliberately NOT bounded like
4089
- * `resolveRedriveUnresolved`, for two load-bearing reasons: (1) the only
4090
- * remedy is a PATCH to the server the very call that is failing, so a bound
4091
- * would act by retrying it; (2) this class is self-clearing by construction
4092
- * (the next successful PATCH resolves it via `clearRedriveUnresolved`),
4093
- * unlike #1340's deterministic, permanent shape. Fires at most once per
4094
- * (message, outcome) streak.
4122
+ * but its own PATCH to record it failed. Fires at most once per (message,
4123
+ * outcome) streak, and only while `boundRedriveOutcome` has not yet tripped —
4124
+ * once it trips, `redrive_outcome_abandoned` takes over reporting for the row
4125
+ * (#1366).
4095
4126
  */
4096
4127
  signalRedriveOutcomeUnreported(conv, message, outcome) {
4097
4128
  if (this.redriveOutcomeUnreportedSignalled.get(message.id) === outcome) return;
@@ -4100,6 +4131,81 @@ var ChannelDriver = class _ChannelDriver {
4100
4131
  attempted_outcome: outcome
4101
4132
  });
4102
4133
  }
4134
+ /**
4135
+ * The runner-authored, honest error text for the terminal fallback a tripped
4136
+ * `boundRedriveOutcome` sends. Distinguishable per outcome and truthful about
4137
+ * what actually happened — the `settle`/done case must say the turn finished
4138
+ * but its result could not be recorded, never that the runner stopped
4139
+ * responding (that would be a lie for this shape, see #1366's "why this ships").
4140
+ */
4141
+ static REDRIVE_ABANDON_ERROR = {
4142
+ reattach: "your runner could not record that this message had started, so it was given up on",
4143
+ settle: "your runner finished this message but could not record the result, so the reply could not be delivered",
4144
+ fail_permanent: "the runner could not read this conversation's state from OpenCode, and could not record that failure either, so the message was given up on"
4145
+ };
4146
+ /**
4147
+ * Bound for Class B (#1340, #1366): the runner DECIDED an outcome but its own
4148
+ * PATCH to record it failed. Two independent trip arms (either sufficient):
4149
+ * (1) this failure streak has lasted `pausedMaxWaitMs` — DURATION, not a tick
4150
+ * count, reusing the knob `resolveRedriveUnresolved` already established; (2)
4151
+ * the turn's `processing_started_at` age has crossed
4152
+ * `ABSOLUTE_MAX_PROCESSING_MS` — durable and restart-surviving, since arm (1)'s
4153
+ * in-memory streak resets on a scale-to-zero restart.
4154
+ *
4155
+ * INVARIANT — a tripped bound never suppresses the original outcome attempt;
4156
+ * it only adds a fallback after that attempt has failed again. This is only
4157
+ * ever reached from inside the catch of the ORIGINAL outcome PATCH, which is
4158
+ * attempted first on every tick whether or not this bound tripped before —
4159
+ * there is no give-up latch that would short-circuit it. That is what lets a
4160
+ * route-level fault that heals later still deliver the turn's real
4161
+ * `done`/`failed` payload: once the original PATCH succeeds again, this
4162
+ * helper is never entered and the row settles with its real result.
4163
+ */
4164
+ async boundRedriveOutcome(conv, message, outcome) {
4165
+ const now = this.now();
4166
+ const since = this.redriveOutcomeFailingSince.get(message.id);
4167
+ if (since === void 0) this.redriveOutcomeFailingSince.set(message.id, now);
4168
+ const durationTripped = now - (since ?? now) >= this.pausedMaxWaitMs;
4169
+ const parsed = message.processing_started_at ? Date.parse(message.processing_started_at) : NaN;
4170
+ const absoluteAgeTripped = !Number.isNaN(parsed) && now - parsed >= ABSOLUTE_MAX_PROCESSING_MS;
4171
+ if (!durationTripped && !absoluteAgeTripped) {
4172
+ this.signalRedriveOutcomeUnreported(conv, message, outcome);
4173
+ return "retry";
4174
+ }
4175
+ const arm = durationTripped ? "failure_window" : "absolute_age";
4176
+ try {
4177
+ await this.markFailed(
4178
+ conv.id,
4179
+ message.id,
4180
+ void 0,
4181
+ _ChannelDriver.REDRIVE_ABANDON_ERROR[outcome]
4182
+ );
4183
+ } catch (err) {
4184
+ if (err instanceof ChannelAuthError) throw err;
4185
+ this.log({
4186
+ level: "warn",
4187
+ message: `Re-drive bound: fallback markFailed for message ${message.id.slice(0, 8)} also failed (arm ${arm}, will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
4188
+ conversation_id: conv.id,
4189
+ message_id: message.id
4190
+ });
4191
+ if (!this.redriveOutcomeAbandonedSignalled.has(message.id)) {
4192
+ this.redriveOutcomeAbandonedSignalled.add(message.id);
4193
+ void this.postSignal(conv.id, message.id, "redrive_outcome_abandoned", {
4194
+ attempted_outcome: outcome,
4195
+ reported: false,
4196
+ arm
4197
+ });
4198
+ }
4199
+ return "retry";
4200
+ }
4201
+ this.clearRedriveUnresolved(message.id);
4202
+ void this.postSignal(conv.id, message.id, "redrive_outcome_abandoned", {
4203
+ attempted_outcome: outcome,
4204
+ reported: true,
4205
+ arm
4206
+ });
4207
+ return "abandoned";
4208
+ }
4103
4209
  /**
4104
4210
  * Record one poll outcome toward the re-drive fence's consecutive-identical-
4105
4211
  * failure streak (#1348) and return the resulting count. `signature === null`