@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 +4 -1
- package/dist/drivers.js +70 -6
- package/dist/execution.js +21 -2
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
297
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|