@miraland-labs/conduit-bridge 0.6.1 → 0.7.0
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/attempt-worktree.js +25 -2
- package/dist/cli.js +16 -11
- package/dist/config.js +11 -3
- package/dist/execution.js +102 -35
- package/package.json +1 -1
package/dist/attempt-worktree.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* F-07 slice 1: one Git worktree per attempt. The install-service source checkout is never the
|
|
3
3
|
* agent cwd; retries must not inherit dirty files from a failed run.
|
|
4
4
|
*/
|
|
5
|
-
import { appendFile, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
-
import {
|
|
5
|
+
import { access, appendFile, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { constants } from "node:fs";
|
|
7
|
+
import { join, resolve } from "node:path";
|
|
7
8
|
import { execFile } from "node:child_process";
|
|
8
9
|
import { promisify } from "node:util";
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
@@ -54,6 +55,28 @@ export async function removeAttemptWorktree(sourceWorkspace, worktreePath) {
|
|
|
54
55
|
}).catch(() => undefined);
|
|
55
56
|
}
|
|
56
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* F-07 safe resume: prove the interrupted attempt still owns this worktree path.
|
|
60
|
+
* Does not check session identity — caller requires config.sessions[taskId] separately.
|
|
61
|
+
*/
|
|
62
|
+
export async function proveResumeWorktree(sourceWorkspace, attemptId, worktreePath) {
|
|
63
|
+
if (!worktreePath?.trim())
|
|
64
|
+
return false;
|
|
65
|
+
const expected = attemptWorktreePath(sourceWorkspace, attemptId);
|
|
66
|
+
if (resolve(worktreePath) !== resolve(expected))
|
|
67
|
+
return false;
|
|
68
|
+
try {
|
|
69
|
+
await access(worktreePath, constants.F_OK);
|
|
70
|
+
const { stdout } = await execFileAsync("git", ["-C", worktreePath, "rev-parse", "HEAD"], {
|
|
71
|
+
timeout: 15_000,
|
|
72
|
+
maxBuffer: 1_000_000,
|
|
73
|
+
});
|
|
74
|
+
return /^[0-9a-f]{40,64}$/i.test(stdout.trim());
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
57
80
|
/** Hold a crashed attempt tree for local diagnosis; never reused as agent cwd. */
|
|
58
81
|
export async function quarantineAttemptWorktree(sourceWorkspace, worktreePath, attemptId) {
|
|
59
82
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
package/dist/cli.js
CHANGED
|
@@ -6,13 +6,13 @@ import { spawn } from "node:child_process";
|
|
|
6
6
|
import { readFileSync } from "node:fs";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { ConduitClient } from "./client.js";
|
|
9
|
-
import { BRIDGE_LEASE_CAPACITY, clearPendingConnection, loadConfig, loadConfigIfPresent, loadOrCreateInstallationId, loadPendingConnection, redactSecrets, saveConfig, savePendingConnection, suggestMachineName, } from "./config.js";
|
|
9
|
+
import { BRIDGE_LEASE_CAPACITY, BRIDGE_MAX_LEASE_CAPACITY, clampLeaseCapacity, clearPendingConnection, loadConfig, loadConfigIfPresent, loadOrCreateInstallationId, loadPendingConnection, redactSecrets, saveConfig, savePendingConnection, suggestMachineName, } from "./config.js";
|
|
10
10
|
import { runMcp } from "./mcp.js";
|
|
11
11
|
import { detectInstalledClients, localFuelOnlyClients, suggestFuelSource } from "./detect.js";
|
|
12
12
|
import { DRIVERS } from "./driver.js";
|
|
13
13
|
import { buildWorkspaceBrief } from "./brief.js";
|
|
14
14
|
import { ensureCheckout } from "./checkout.js";
|
|
15
|
-
import {
|
|
15
|
+
import { pumpExecutionSlots, renewLeases } from "./execution.js";
|
|
16
16
|
import { installRunnerService, uninstallRunnerService } from "./service.js";
|
|
17
17
|
const [command] = process.argv.slice(2);
|
|
18
18
|
/** Read our own package version so every runner start logs exactly which build is live. */
|
|
@@ -91,10 +91,11 @@ async function join() {
|
|
|
91
91
|
const operatorName = values.operator?.trim() || userInfo().username;
|
|
92
92
|
const machineName = values.machine?.trim() || suggestMachineName(hostname(), installationId);
|
|
93
93
|
const capabilities = values.capability?.map((item) => item.trim()).filter(Boolean) ?? ["implement"];
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
const requestedCapacity = values.capacity ? Number(values.capacity) : BRIDGE_LEASE_CAPACITY;
|
|
95
|
+
if (!Number.isInteger(requestedCapacity) || requestedCapacity < 1 || requestedCapacity > BRIDGE_MAX_LEASE_CAPACITY) {
|
|
96
|
+
throw new Error(`--capacity must be an integer from 1 to ${BRIDGE_MAX_LEASE_CAPACITY} (concurrent attempt slots)`);
|
|
96
97
|
}
|
|
97
|
-
const leaseCapacity =
|
|
98
|
+
const leaseCapacity = clampLeaseCapacity(requestedCapacity);
|
|
98
99
|
console.log("\nRequesting a Conduit connection:");
|
|
99
100
|
console.log(`Operator: ${operatorName}`);
|
|
100
101
|
console.log(`Computer: ${machineName}`);
|
|
@@ -193,7 +194,7 @@ async function finishConnection(baseUrl, data, fuelSource) {
|
|
|
193
194
|
runnerKey: String(data.runner_secret),
|
|
194
195
|
capabilities: data.capabilities,
|
|
195
196
|
grants: data.grants,
|
|
196
|
-
leaseCapacity:
|
|
197
|
+
leaseCapacity: clampLeaseCapacity(data.lease_capacity),
|
|
197
198
|
activeAttempts: {},
|
|
198
199
|
fuelSource: resolvedFuel,
|
|
199
200
|
...(Object.keys(fuel).length ? { fuel } : {}),
|
|
@@ -326,24 +327,28 @@ async function runner() {
|
|
|
326
327
|
if (!driver || !workspace) {
|
|
327
328
|
console.warn(`WARNING: heartbeat only — no work will execute. Use: ${bridgeUsage("runner", "--agent", AGENT_PLACEHOLDER, "--workspace", "<repo>")}`);
|
|
328
329
|
}
|
|
329
|
-
console.log(`Conduit runner (bridge v${bridgeVersion()}) connected to ${config.baseUrl}${driver ? ` — executing via ${driver.name} in ${workspace}` : " — heartbeat only (no --agent)"} (fuel: ${fuelLabel})`);
|
|
330
|
+
console.log(`Conduit runner (bridge v${bridgeVersion()}) connected to ${config.baseUrl}${driver ? ` — executing via ${driver.name} in ${workspace}` : " — heartbeat only (no --agent)"} (fuel: ${fuelLabel}; slots: ${config.leaseCapacity})`);
|
|
331
|
+
const running = new Map();
|
|
330
332
|
for (;;) {
|
|
331
|
-
let
|
|
333
|
+
let progressed = false;
|
|
332
334
|
try {
|
|
333
335
|
await heartbeat(client, config, workspace ? await buildWorkspaceBrief(workspace).catch(() => brief) : null);
|
|
334
336
|
await renewLeases(client, config);
|
|
335
337
|
if (driver && workspace) {
|
|
336
|
-
|
|
338
|
+
progressed = await pumpExecutionSlots(client, config, driver, workspace, brief, timeoutMs, {
|
|
337
339
|
heartbeat: async () => heartbeat(client, config, await buildWorkspaceBrief(workspace).catch(() => brief)),
|
|
338
340
|
heartbeatIntervalMs: intervalMs,
|
|
339
|
-
});
|
|
341
|
+
}, running);
|
|
340
342
|
}
|
|
341
343
|
}
|
|
342
344
|
catch (error) {
|
|
343
345
|
console.error(`Runner cycle failed; retrying: ${redactSecrets(error instanceof Error ? error.message : "unknown error")}`);
|
|
344
346
|
}
|
|
345
|
-
if (values.once &&
|
|
347
|
+
if (values.once && progressed) {
|
|
348
|
+
if (running.size)
|
|
349
|
+
await Promise.allSettled([...running.values()]);
|
|
346
350
|
return;
|
|
351
|
+
}
|
|
347
352
|
await new Promise((resolveSleep) => setTimeout(resolveSleep, intervalMs));
|
|
348
353
|
}
|
|
349
354
|
}
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,16 @@ import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
|
+
/** Default when join omits --capacity. */
|
|
5
6
|
export const BRIDGE_LEASE_CAPACITY = 1;
|
|
7
|
+
/** Hard ceiling for truthful concurrent Bridge slots (one agent + worktree each). */
|
|
8
|
+
export const BRIDGE_MAX_LEASE_CAPACITY = 4;
|
|
9
|
+
export function clampLeaseCapacity(value) {
|
|
10
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
11
|
+
if (!Number.isInteger(n) || n < 1)
|
|
12
|
+
return BRIDGE_LEASE_CAPACITY;
|
|
13
|
+
return Math.min(n, BRIDGE_MAX_LEASE_CAPACITY);
|
|
14
|
+
}
|
|
6
15
|
const directory = process.env.CONDUIT_BRIDGE_CONFIG_DIR?.trim()
|
|
7
16
|
? resolve(process.env.CONDUIT_BRIDGE_CONFIG_DIR)
|
|
8
17
|
: join(homedir(), ".config", "conduit");
|
|
@@ -39,9 +48,8 @@ export async function loadConfigIfPresent() {
|
|
|
39
48
|
active.phase ??= "agent_running";
|
|
40
49
|
if (config.fuelSource !== "local" && config.fuelSource !== "conduit")
|
|
41
50
|
config.fuelSource = "conduit";
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
config.leaseCapacity = BRIDGE_LEASE_CAPACITY;
|
|
51
|
+
// Clamp to what concurrent slot workers can run (1..BRIDGE_MAX_LEASE_CAPACITY).
|
|
52
|
+
config.leaseCapacity = clampLeaseCapacity(config.leaseCapacity ?? BRIDGE_LEASE_CAPACITY);
|
|
45
53
|
return config;
|
|
46
54
|
}
|
|
47
55
|
export async function saveConfig(config) {
|
package/dist/execution.js
CHANGED
|
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { ConduitRequestError } from "./client.js";
|
|
4
4
|
import { redactSecrets } from "./config.js";
|
|
5
5
|
import { agentReportTemplate, buildAssignmentPrompt, evidenceKinds, parseAgentReport, pickModelCandidate, tierForRisk } from "./driver.js";
|
|
6
|
-
import { attemptWorktreePath, createAttemptWorktree, quarantineAttemptWorktree, removeAttemptWorktree, } from "./attempt-worktree.js";
|
|
6
|
+
import { attemptWorktreePath, createAttemptWorktree, proveResumeWorktree, quarantineAttemptWorktree, removeAttemptWorktree, } from "./attempt-worktree.js";
|
|
7
7
|
import { buildWorkspaceBrief, isBaseCommitAncestor, normalizeRepositoryUrl } from "./brief.js";
|
|
8
8
|
const assignmentSchema = z.object({
|
|
9
9
|
id: z.string().uuid(),
|
|
@@ -66,13 +66,22 @@ export async function renewLeases(client, config) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
export async function recoverActiveAttempt(client, config, driver, workspace, brief, timeoutMs, supervision) {
|
|
70
|
-
const active = Object.values(config.activeAttempts)[0];
|
|
69
|
+
export async function recoverActiveAttempt(client, config, driver, workspace, brief, timeoutMs, supervision, taskId) {
|
|
70
|
+
const active = taskId ? config.activeAttempts[taskId] : Object.values(config.activeAttempts)[0];
|
|
71
71
|
if (!active)
|
|
72
72
|
return false;
|
|
73
73
|
if (active.phase === "agent_running") {
|
|
74
|
-
// F-07: never resume a dirty shared checkout. Quarantine/remove the attempt worktree first.
|
|
75
74
|
const worktree = active.worktreePath ?? attemptWorktreePath(workspace, active.attemptId);
|
|
75
|
+
const sessionId = config.sessions?.[active.taskId]?.trim() || "";
|
|
76
|
+
// F-07 safe resume: only when session + worktree identity are both proven. Otherwise interrupt.
|
|
77
|
+
if (sessionId && await proveResumeWorktree(workspace, active.attemptId, worktree)) {
|
|
78
|
+
console.log(`Resuming interrupted attempt ${active.attemptId} with proven worktree and session.`);
|
|
79
|
+
await runClaimedAssignment(client, config, driver, workspace, brief, active.taskId, timeoutMs, supervision, {
|
|
80
|
+
existingWorktree: worktree,
|
|
81
|
+
forceResumeSessionId: sessionId,
|
|
82
|
+
});
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
76
85
|
await quarantineAttemptWorktree(workspace, worktree, active.attemptId).catch(() => removeAttemptWorktree(workspace, worktree).catch(() => undefined));
|
|
77
86
|
await client.updateAttempt(active.taskId, { worktreePath: undefined });
|
|
78
87
|
await queueTerminal(client, active.taskId, {
|
|
@@ -92,17 +101,51 @@ export async function recoverActiveAttempt(client, config, driver, workspace, br
|
|
|
92
101
|
await runClaimedAssignment(client, config, driver, workspace, brief, active.taskId, timeoutMs, supervision);
|
|
93
102
|
return true;
|
|
94
103
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
/**
|
|
105
|
+
* F-07 multi-slot: keep up to leaseCapacity concurrent agent runs (each with its own worktree).
|
|
106
|
+
* `running` is owned by the runner loop and must outlive a single pump call.
|
|
107
|
+
*/
|
|
108
|
+
export async function pumpExecutionSlots(client, config, driver, workspace, brief, timeoutMs, supervision, running) {
|
|
109
|
+
let progressed = running.size > 0;
|
|
110
|
+
for (const id of Object.keys(config.activeAttempts)) {
|
|
111
|
+
if (running.has(id))
|
|
112
|
+
continue;
|
|
113
|
+
progressed = true;
|
|
114
|
+
const slot = recoverActiveAttempt(client, config, driver, workspace, brief, timeoutMs, supervision, id)
|
|
115
|
+
.catch((error) => {
|
|
116
|
+
console.error(`Slot recovery failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`);
|
|
117
|
+
})
|
|
118
|
+
.then(() => undefined)
|
|
119
|
+
.finally(() => { running.delete(id); });
|
|
120
|
+
running.set(id, slot);
|
|
121
|
+
}
|
|
122
|
+
while (running.size < config.leaseCapacity) {
|
|
123
|
+
const taskId = await claimNextAssignment(client, config, workspace, brief);
|
|
124
|
+
if (!taskId)
|
|
125
|
+
break;
|
|
126
|
+
progressed = true;
|
|
127
|
+
const slot = runClaimedAssignment(client, config, driver, workspace, brief, taskId, timeoutMs, supervision)
|
|
128
|
+
.catch((error) => {
|
|
129
|
+
console.error(`Slot execution failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`);
|
|
130
|
+
})
|
|
131
|
+
.then(() => undefined)
|
|
132
|
+
.finally(() => { running.delete(taskId); });
|
|
133
|
+
running.set(taskId, slot);
|
|
134
|
+
}
|
|
135
|
+
return progressed;
|
|
136
|
+
}
|
|
137
|
+
/** Claim one assignment when under capacity; does not start the agent (multi-slot pump does). */
|
|
138
|
+
export async function claimNextAssignment(client, config, workspace, brief) {
|
|
139
|
+
if (Object.keys(config.activeAttempts).length >= config.leaseCapacity)
|
|
140
|
+
return null;
|
|
98
141
|
const data = await client.request("/runner/v1/assignments");
|
|
99
142
|
const assignments = z.array(assignmentSchema).parse(data.assignments ?? []);
|
|
100
143
|
const assignment = assignments[0];
|
|
101
144
|
if (!assignment)
|
|
102
|
-
return
|
|
145
|
+
return null;
|
|
103
146
|
if (assignment.execution_mode === "human") {
|
|
104
147
|
console.log(`Human takeover assignment ${assignment.id} — use Bridge MCP tools to claim and submit (agent runner skips).`);
|
|
105
|
-
return
|
|
148
|
+
return null;
|
|
106
149
|
}
|
|
107
150
|
const liveBrief = await buildWorkspaceBrief(workspace).catch(() => brief);
|
|
108
151
|
const liveRepository = liveBrief?.repository ? normalizeRepositoryUrl(liveBrief.repository) : null;
|
|
@@ -122,14 +165,22 @@ export async function executeNextAssignment(client, config, driver, workspace, b
|
|
|
122
165
|
body: JSON.stringify({ attempt_id: assignment.attempt_id, reason: rejection, idempotency_key: `bridge:preflight-reject:${assignment.attempt_id}:${rejection}` }),
|
|
123
166
|
});
|
|
124
167
|
console.error(`Assignment ${assignment.id} rejected before claim: ${rejection}`);
|
|
125
|
-
return
|
|
168
|
+
return null;
|
|
126
169
|
}
|
|
127
170
|
console.log(`Claiming assignment ${assignment.id} (attempt ${assignment.attempt_id})`);
|
|
128
171
|
await client.claim(assignment.id, assignment.attempt_id);
|
|
129
|
-
|
|
172
|
+
return assignment.id;
|
|
173
|
+
}
|
|
174
|
+
export async function executeNextAssignment(client, config, driver, workspace, brief, timeoutMs, supervision) {
|
|
175
|
+
if (Object.keys(config.activeAttempts).length)
|
|
176
|
+
return recoverActiveAttempt(client, config, driver, workspace, brief, timeoutMs, supervision);
|
|
177
|
+
const taskId = await claimNextAssignment(client, config, workspace, brief);
|
|
178
|
+
if (!taskId)
|
|
179
|
+
return false;
|
|
180
|
+
await runClaimedAssignment(client, config, driver, workspace, brief, taskId, timeoutMs, supervision);
|
|
130
181
|
return true;
|
|
131
182
|
}
|
|
132
|
-
async function runClaimedAssignment(client, config, driver, workspace, brief, taskId, timeoutMs, supervision) {
|
|
183
|
+
async function runClaimedAssignment(client, config, driver, workspace, brief, taskId, timeoutMs, supervision, options = {}) {
|
|
133
184
|
const active = client.attempt(taskId);
|
|
134
185
|
const detail = await client.request(`/runner/v1/tasks/${taskId}`);
|
|
135
186
|
const task = taskDetailSchema.parse(detail.task);
|
|
@@ -157,27 +208,39 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
157
208
|
return;
|
|
158
209
|
}
|
|
159
210
|
let attemptWorkspace;
|
|
160
|
-
|
|
161
|
-
attemptWorkspace =
|
|
162
|
-
sourceWorkspace: workspace,
|
|
163
|
-
attemptId: active.attemptId,
|
|
164
|
-
startCommit,
|
|
165
|
-
});
|
|
211
|
+
if (options.existingWorktree) {
|
|
212
|
+
attemptWorkspace = options.existingWorktree;
|
|
166
213
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
214
|
+
else {
|
|
215
|
+
try {
|
|
216
|
+
attemptWorkspace = await createAttemptWorktree({
|
|
217
|
+
sourceWorkspace: workspace,
|
|
218
|
+
attemptId: active.attemptId,
|
|
219
|
+
startCommit,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
const code = error instanceof Error ? error.message : "worktree_create_failed";
|
|
224
|
+
const message = code === "source_workspace_dirty"
|
|
225
|
+
? "Source workspace has uncommitted changes; refuse to start an isolated attempt"
|
|
226
|
+
: `Unable to create attempt worktree: ${code}`;
|
|
227
|
+
await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable: true, idempotency_key: `bridge:worktree:${active.attemptId}:${code}` } });
|
|
228
|
+
console.error(`Assignment ${taskId} worktree create failed: ${redactSecrets(message)}`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
await client.updateAttempt(taskId, { worktreePath: attemptWorkspace });
|
|
175
232
|
}
|
|
176
|
-
await client.updateAttempt(taskId, { worktreePath: attemptWorkspace });
|
|
177
233
|
// Same list the driver hands to the permission contract, so the prompt states exactly what is
|
|
178
234
|
// executable rather than leaving the agent to guess and hit rejections.
|
|
179
235
|
const prompt = buildAssignmentPrompt({ taskId, objective: task.objective, spec, grants, workspace: attemptWorkspace, currentHead: startCommit, reworkFeedback, workPackage, verificationCommands: liveBrief?.verification ?? [] });
|
|
180
|
-
|
|
236
|
+
const resuming = Boolean(options.forceResumeSessionId);
|
|
237
|
+
await client.attemptRequest(taskId, "progress", {
|
|
238
|
+
phase: "changing",
|
|
239
|
+
message: resuming
|
|
240
|
+
? `Resuming ${driver.name} after Bridge restart with proven session and worktree.`
|
|
241
|
+
: `Starting ${driver.name} for this assignment${reworkFeedback ? " with review feedback" : ""}.`,
|
|
242
|
+
idempotency_key: resuming ? `bridge:progress:${active.attemptId}:resume` : `bridge:progress:${active.attemptId}:start`,
|
|
243
|
+
});
|
|
181
244
|
const fuelSource = config.fuelSource === "local" ? "local" : "conduit";
|
|
182
245
|
let fuel;
|
|
183
246
|
if (fuelSource === "conduit") {
|
|
@@ -193,10 +256,12 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
193
256
|
// that was chosen. updateAttempt alone only moves local runner state, which the database never sees.
|
|
194
257
|
await client.attemptRequest(taskId, "progress", {
|
|
195
258
|
phase: "agent_running",
|
|
196
|
-
message:
|
|
197
|
-
|
|
259
|
+
message: resuming
|
|
260
|
+
? `Resumed ${driver.name}${selection.model ? ` on ${selection.model}` : " on its CLI default"}.`
|
|
261
|
+
: `Started ${driver.name}${selection.model ? ` on ${selection.model}` : " on its CLI default"}.`,
|
|
262
|
+
idempotency_key: resuming ? `bridge:progress:${active.attemptId}:agent-resume` : `bridge:progress:${active.attemptId}:agent-start`,
|
|
198
263
|
});
|
|
199
|
-
await client.updateAttempt(taskId, { phase: "agent_running", fuelMode: fuelSource });
|
|
264
|
+
await client.updateAttempt(taskId, { phase: "agent_running", fuelMode: fuelSource, worktreePath: attemptWorkspace });
|
|
200
265
|
const renewTimer = setInterval(() => { void renewLeases(client, config).catch((error) => console.error(`Lease renewal failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`)); }, 60_000);
|
|
201
266
|
let heartbeatRunning = false;
|
|
202
267
|
const heartbeatTimer = supervision ? setInterval(() => {
|
|
@@ -208,15 +273,17 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
208
273
|
.finally(() => { heartbeatRunning = false; });
|
|
209
274
|
}, supervision.heartbeatIntervalMs) : null;
|
|
210
275
|
try {
|
|
276
|
+
// Resume for (1) Bridge restart with proven session+worktree, or (2) review rework.
|
|
277
|
+
// A fresh attempt after a failed delivery must start clean: resuming a session whose last
|
|
278
|
+
// turn already "finished" makes the agent reply conversationally without the report block.
|
|
279
|
+
const resumeSessionId = options.forceResumeSessionId
|
|
280
|
+
?? (reworkFeedback ? config.sessions?.[taskId] : undefined);
|
|
211
281
|
const result = await driver.run({
|
|
212
282
|
prompt,
|
|
213
283
|
workspace: attemptWorkspace,
|
|
214
284
|
grants,
|
|
215
285
|
verificationCommands: liveBrief?.verification ?? [],
|
|
216
|
-
|
|
217
|
-
// A fresh attempt after a failed delivery must start clean: resuming a session whose last
|
|
218
|
-
// turn already "finished" makes the agent reply conversationally without the report block.
|
|
219
|
-
resumeSessionId: reworkFeedback ? config.sessions?.[taskId] : undefined,
|
|
286
|
+
resumeSessionId,
|
|
220
287
|
timeoutMs,
|
|
221
288
|
model: selection.model,
|
|
222
289
|
fuel,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Conduit Bridge CLI — join, connect, and run Claude Code / Codex / Cursor / OpenCode / Kiro / Antigravity agents for a Conduit organization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|