@miraland-labs/conduit-bridge 0.16.29 → 0.16.30

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/cli.js CHANGED
@@ -440,7 +440,10 @@ async function driversCommand() {
440
440
  for (const lane of laneStatuses(config)) {
441
441
  const when = lane.observed_at ? ` observed=${lane.observed_at}` : "";
442
442
  const reset = lane.resets_at ? ` resets=${lane.resets_at}` : lane.allowance === "unavailable" ? " resets=unknown" : "";
443
- console.log(` ${lane.id.padEnd(14)} ${lane.state.padEnd(8)} fuel=${lane.fuel} allowance=${lane.allowance}${when}${reset} (${lane.label})`);
443
+ // A degraded provider never refuses, so allowance stays "unknown" while every turn times out.
444
+ // Printing the count is the only way an operator sees why dispatch keeps avoiding this lane.
445
+ const slow = lane.timeouts ? ` timeouts=${lane.timeouts} (demoted)` : "";
446
+ console.log(` ${lane.id.padEnd(14)} ${lane.state.padEnd(8)} fuel=${lane.fuel} allowance=${lane.allowance}${when}${reset}${slow} (${lane.label})`);
444
447
  }
445
448
  const blocked = laneDispatchBlock(laneStatuses(config));
446
449
  console.log(blocked
package/dist/drivers.js CHANGED
@@ -55,10 +55,21 @@ export function normalizeDrivers(raw) {
55
55
  const state = lane.state === "online" ? "online" : "offline";
56
56
  const fuel = lane.fuel === "local" || lane.fuel === "conduit" ? lane.fuel : undefined;
57
57
  const quota = normalizeQuota(lane.quota);
58
- out[id] = { state, ...(fuel ? { fuel } : {}), ...(quota ? { quota } : {}) };
58
+ const outcome = normalizeOutcome(lane.outcome);
59
+ out[id] = { state, ...(fuel ? { fuel } : {}), ...(quota ? { quota } : {}), ...(outcome ? { outcome } : {}) };
59
60
  }
60
61
  return out;
61
62
  }
63
+ /** Same rule as quota: a malformed outcome is dropped, never allowed to hold a lane back. */
64
+ function normalizeOutcome(raw) {
65
+ if (!raw || typeof raw !== "object")
66
+ return undefined;
67
+ const value = raw;
68
+ if (!Number.isFinite(value.consecutive_timeouts) || typeof value.last_timeout_at !== "string")
69
+ return undefined;
70
+ const count = Math.max(0, Math.floor(value.consecutive_timeouts));
71
+ return { consecutive_timeouts: count, last_timeout_at: value.last_timeout_at };
72
+ }
62
73
  /** Drop a malformed quota rather than fail the whole config: a bad record must not dark a lane. */
63
74
  function normalizeQuota(raw) {
64
75
  if (!raw || typeof raw !== "object")
@@ -159,11 +170,14 @@ export function laneStatuses(config, now = Date.now()) {
159
170
  const allowance = !local || !quota
160
171
  ? "unknown"
161
172
  : quota.exhausted ? "unavailable" : "available";
173
+ const outcome = normalizeDrivers(config.drivers)[lane.id]?.outcome;
162
174
  return {
163
175
  ...lane,
164
176
  allowance,
165
177
  observed_at: quota?.observed_at,
166
178
  resets_at: quota?.resets_at ?? undefined,
179
+ ...(laneTimedOutRecently(config, lane.id, now) ? { timeouts: outcome?.consecutive_timeouts } : {}),
180
+ // Timeouts demote but never make a lane ineligible: see pickDriverForClaim.
167
181
  eligible: lane.state === "online" && allowance !== "unavailable",
168
182
  };
169
183
  });
@@ -178,6 +192,53 @@ export function laneDispatchBlock(statuses) {
178
192
  return "every lane is offline";
179
193
  return "every online lane has an allowance the provider refused";
180
194
  }
195
+ /** Timeouts in a row before a lane stops being preferred. One is noise; two is a pattern. */
196
+ export const TIMEOUT_SUPPRESSION_THRESHOLD = 2;
197
+ /** How long a timeout pattern is believed. Short, because the cause is often the budget, not the lane. */
198
+ export const TIMEOUT_SUPPRESSION_MS = 60 * 60_000;
199
+ /** Record a turn that ended at the execution ceiling. Never touches the quota record. */
200
+ export function recordDriverTimeout(config, driverId, now = Date.now()) {
201
+ if (!isSupportedDriverId(driverId))
202
+ return config;
203
+ const drivers = normalizeDrivers(config.drivers);
204
+ const lane = drivers[driverId] ?? { state: "offline" };
205
+ const previous = lane.outcome?.consecutive_timeouts ?? 0;
206
+ drivers[driverId] = {
207
+ ...lane,
208
+ outcome: { consecutive_timeouts: previous + 1, last_timeout_at: new Date(now).toISOString() },
209
+ };
210
+ return { ...config, drivers };
211
+ }
212
+ /**
213
+ * Clear what the last runs said about this lane.
214
+ *
215
+ * A run that finished is the strongest evidence there is, and it is worth more than any older claim
216
+ * — a lane that just worked is not spent and is not slow.
217
+ */
218
+ export function clearDriverOutcome(config, driverId) {
219
+ if (!isSupportedDriverId(driverId))
220
+ return config;
221
+ const drivers = normalizeDrivers(config.drivers);
222
+ const lane = drivers[driverId];
223
+ if (!lane?.outcome)
224
+ return config;
225
+ const { outcome: _cleared, ...rest } = lane;
226
+ drivers[driverId] = rest;
227
+ return { ...config, drivers };
228
+ }
229
+ /**
230
+ * Whether this lane's recent timeouts should push it behind others.
231
+ *
232
+ * Expires on its own, because a timeout usually means the budget was too small for the work rather
233
+ * than that the lane is bad, and the next package may be smaller.
234
+ */
235
+ export function laneTimedOutRecently(config, driverId, now = Date.now()) {
236
+ const outcome = normalizeDrivers(config.drivers)[driverId]?.outcome;
237
+ if (!outcome || outcome.consecutive_timeouts < TIMEOUT_SUPPRESSION_THRESHOLD)
238
+ return false;
239
+ const seen = Date.parse(outcome.last_timeout_at);
240
+ return !Number.isFinite(seen) || now - seen <= TIMEOUT_SUPPRESSION_MS;
241
+ }
181
242
  export function onlineDriverIds(config) {
182
243
  const drivers = normalizeDrivers(config.drivers);
183
244
  return SUPPORTED_AGENTS.map((agent) => agent.id).filter((id) => drivers[id]?.state === "online");
@@ -290,14 +351,17 @@ export function pickDriverForClaim(config, processOnlineIds, eligible = () => tr
290
351
  load.set(active.driverId, (load.get(active.driverId) ?? 0) + 1);
291
352
  }
292
353
  }
354
+ // Demote, never exclude. A lane held out of selection can never run the work that would prove it
355
+ // well again, and suppressing the last candidate would idle the machine over a claim about the
356
+ // past. Ordering is enough: a healthy lane wins while one exists, and a suppressed lane is still
357
+ // picked when it is all there is.
358
+ const rank = (id) => (laneTimedOutRecently(config, id) ? 1 : 0);
293
359
  let best = online[0];
294
- let bestLoad = load.get(best) ?? 0;
295
360
  for (const id of online.slice(1)) {
296
- const n = load.get(id) ?? 0;
297
- if (n < bestLoad) {
361
+ const better = rank(id) < rank(best)
362
+ || (rank(id) === rank(best) && (load.get(id) ?? 0) < (load.get(best) ?? 0));
363
+ if (better)
298
364
  best = id;
299
- bestLoad = n;
300
- }
301
365
  }
302
366
  return best;
303
367
  }
package/dist/execution.js CHANGED
@@ -6,7 +6,7 @@ import { ConduitRequestError } from "./client.js";
6
6
  import { redactSecrets, saveDriverQuota } from "./config.js";
7
7
  import { DRIVERS, agentReportTemplate, buildAssignmentPrompt, evidenceKinds, extractAgentReportJsonText, fuelEndpoint, normalizeEvidenceDigest, parseAgentReport, parseJsonObjectCandidate, pickModelCandidate, requireStampedExecutionClass, tierForRisk } from "./driver.js";
8
8
  import { assertClassFloor } from "./execution-class.js";
9
- import { pickDriverForClaim, recordDriverQuota, resolveDriverFuel, resolveDriverFuelProvenance, supportsReadOnlyDiagnosis } from "./drivers.js";
9
+ import { pickDriverForClaim, recordDriverQuota, resolveDriverFuel, resolveDriverFuelProvenance, supportsReadOnlyDiagnosis, clearDriverOutcome, recordDriverTimeout } from "./drivers.js";
10
10
  import { attemptWorktreePath, createAttemptWorktree, proveResumeWorktree, quarantineAttemptWorktree, removeAttemptWorktree, } from "./attempt-worktree.js";
11
11
  import { execFile } from "node:child_process";
12
12
  import { promisify } from "node:util";
@@ -231,7 +231,11 @@ export function classifyFinalizeFailure(message) {
231
231
  * leave the lane eligible.
232
232
  */
233
233
  export async function learnDriverFuel(config, driverId, fuelSource, result) {
234
- const drivers = observeDriverFuel(config, driverId, fuelSource, result).drivers;
234
+ // Fuel is what the vendor said; outcome is what the run did. Both are learned here so every path
235
+ // that already reports a result learns both, and no new call site can forget one of them.
236
+ let next = observeDriverFuel(config, driverId, fuelSource, result);
237
+ next = observeDriverOutcome(next, driverId, result);
238
+ const drivers = next.drivers;
235
239
  if (JSON.stringify(drivers ?? {}) === JSON.stringify(config.drivers ?? {}))
236
240
  return;
237
241
  config.drivers = drivers;
@@ -240,6 +244,21 @@ export async function learnDriverFuel(config, driverId, fuelSource, result) {
240
244
  // A fuel record is worth less than the run it came from: never let a disk fault fail the attempt.
241
245
  await saveDriverQuota(driverId, drivers?.[driverId]?.quota).catch(() => undefined);
242
246
  }
247
+ /**
248
+ * Learn from what the run did, not from what a vendor said about it.
249
+ *
250
+ * A provider that degrades instead of refusing — a spent Cursor subscription still answering from a
251
+ * slow free tier — produces no refusal to learn from, so the quota record stays empty and the lane
252
+ * looks healthy while every turn runs to the ceiling. The timeout itself is the evidence.
253
+ *
254
+ * Recorded separately from quota on purpose. A timeout means the budget was too small or the lane is
255
+ * degraded; a refusal means the allowance is spent. They call for different operator actions.
256
+ */
257
+ export function observeDriverOutcome(config, driverId, result) {
258
+ if (result.status !== "failed")
259
+ return clearDriverOutcome(config, driverId);
260
+ return agentTurnTimedOut(result.error ?? "") ? recordDriverTimeout(config, driverId) : config;
261
+ }
243
262
  export function observeDriverFuel(config, driverId, fuelSource, result, now = Date.now()) {
244
263
  if (fuelSource !== "local")
245
264
  return config;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.16.29",
3
+ "version": "0.16.30",
4
4
  "description": "Conduit Bridge CLI \u2014 join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Pi / Kiro / Antigravity / Grok Build agents for a Conduit organization",
5
5
  "type": "module",
6
6
  "bin": {