@bivy/bivy 0.10.1-staging.236 → 0.10.1-staging.237
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/control-plane-tasks.js +92 -17
- package/dist/server.js +55 -20
- package/package.json +1 -1
|
@@ -79,9 +79,15 @@ export async function claimWork(cfg, id) {
|
|
|
79
79
|
const res = await cp(cfg, "POST", `/node/work/${encodeURIComponent(id)}/claim`);
|
|
80
80
|
return res.ok;
|
|
81
81
|
}
|
|
82
|
+
/** Renew ownership and retain the reason a renewal was rejected. Cancellation is
|
|
83
|
+
* intentionally distinct from a generic lost lease so an active agent can be
|
|
84
|
+
* stopped promptly when the account cancels its Run. */
|
|
82
85
|
export async function renewWorkLease(cfg, id) {
|
|
83
86
|
const res = await cp(cfg, "POST", `/node/work/${encodeURIComponent(id)}/heartbeat`);
|
|
84
|
-
|
|
87
|
+
if (res.ok)
|
|
88
|
+
return "renewed";
|
|
89
|
+
const data = (await res.json().catch(() => ({})));
|
|
90
|
+
return data.reason === "cancelled" ? "cancelled" : "lost";
|
|
85
91
|
}
|
|
86
92
|
export async function completeWork(cfg, id) {
|
|
87
93
|
await transitionWork(cfg, id, "complete");
|
|
@@ -119,9 +125,12 @@ export class ControlPlaneTaskPoller {
|
|
|
119
125
|
runItem;
|
|
120
126
|
maxConcurrent;
|
|
121
127
|
timer;
|
|
122
|
-
|
|
128
|
+
/** Keep the controller alongside the reservation: relay pokes can arrive at
|
|
129
|
+
* any point from claim through policy retries and must address the same Run. */
|
|
130
|
+
inFlight = new Map();
|
|
123
131
|
policy;
|
|
124
132
|
sleep;
|
|
133
|
+
leaseHeartbeatMs;
|
|
125
134
|
constructor(cfg, runItem,
|
|
126
135
|
/** Node's cap on concurrently-running queue sessions (0/undefined = unlimited).
|
|
127
136
|
* Read fresh each tick so the Settings → Nodes value takes effect live. */
|
|
@@ -131,6 +140,7 @@ export class ControlPlaneTaskPoller {
|
|
|
131
140
|
this.maxConcurrent = maxConcurrent;
|
|
132
141
|
this.policy = options.policy;
|
|
133
142
|
this.sleep = options.sleep ?? defaultSleep;
|
|
143
|
+
this.leaseHeartbeatMs = options.leaseHeartbeatMs ?? 30_000;
|
|
134
144
|
}
|
|
135
145
|
start() {
|
|
136
146
|
void this.tick();
|
|
@@ -138,8 +148,16 @@ export class ControlPlaneTaskPoller {
|
|
|
138
148
|
this.timer.unref?.();
|
|
139
149
|
console.log(`[control-plane-tasks] watching hosted queue for labels [${this.cfg.labels.join(", ")}] (relay push + ${Math.round(this.cfg.pollMs / 1000)}s fallback poll)`);
|
|
140
150
|
}
|
|
141
|
-
/**
|
|
142
|
-
|
|
151
|
+
/** React to a relay work notification. If it names a Run already executing,
|
|
152
|
+
* check its lease immediately (the notification may be its cancellation);
|
|
153
|
+
* otherwise fetch the queue as before. The optional id keeps old callers
|
|
154
|
+
* that only use poke() fully compatible. */
|
|
155
|
+
poke(id) {
|
|
156
|
+
const run = id ? this.inFlight.get(id) : undefined;
|
|
157
|
+
if (id && run) {
|
|
158
|
+
void this.checkLease(id, run);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
143
161
|
void this.tick();
|
|
144
162
|
}
|
|
145
163
|
/** Number of queue items currently running on this node. Lets an ephemeral
|
|
@@ -193,37 +211,76 @@ export class ControlPlaneTaskPoller {
|
|
|
193
211
|
// within a single tick: items ran one at a time regardless of `max`, and
|
|
194
212
|
// only overlapping `setInterval` ticks happened to run more than one
|
|
195
213
|
// concurrently.
|
|
196
|
-
|
|
197
|
-
|
|
214
|
+
const run = { controller: new AbortController(), state: "active" };
|
|
215
|
+
this.inFlight.set(item.id, run);
|
|
216
|
+
running.push(this.runOne(item, run));
|
|
198
217
|
}
|
|
199
218
|
await Promise.all(running);
|
|
200
219
|
}
|
|
201
|
-
async runOne(item) {
|
|
202
|
-
|
|
220
|
+
async runOne(item, reserved) {
|
|
221
|
+
// runOne is exercised directly by a few callers/tests, so create a control
|
|
222
|
+
// when there was no tick reservation. Never replace an existing control.
|
|
223
|
+
const run = reserved ?? this.inFlight.get(item.id) ?? { controller: new AbortController(), state: "active" };
|
|
224
|
+
if (!this.inFlight.has(item.id))
|
|
225
|
+
this.inFlight.set(item.id, run);
|
|
203
226
|
try {
|
|
204
227
|
// Claim first so only one node runs it; skip if another node won (no
|
|
205
228
|
// claim → not ours → don't run or complete it). A heartbeat keeps the
|
|
206
229
|
// finite lease alive; process death stops it and makes the item reclaimable.
|
|
207
|
-
if (!(await claimWork(this.cfg, item.id)))
|
|
230
|
+
if (!(await claimWork(this.cfg, item.id)) || run.state !== "active")
|
|
208
231
|
return;
|
|
209
|
-
|
|
210
|
-
|
|
232
|
+
run.heartbeat = setInterval(() => void this.checkLease(item.id, run), this.leaseHeartbeatMs);
|
|
233
|
+
run.heartbeat.unref?.();
|
|
211
234
|
const report = (patch) => reportEvidence(this.cfg, item.id, patch);
|
|
212
235
|
await transitionWork(this.cfg, item.id, "running");
|
|
236
|
+
if (run.state !== "active")
|
|
237
|
+
return;
|
|
213
238
|
console.log(`[control-plane-tasks] running ${item.source} item ${item.id}: ${item.title}`);
|
|
214
239
|
// routingReason is a coarse baseline — a manual "Run…" override picked
|
|
215
240
|
// this agent/model explicitly; otherwise it's whatever the queue label
|
|
216
241
|
// routed to. runWorkItem/runIssueTask may layer a more specific reason
|
|
217
242
|
// (e.g. a fallback after an error) on top via the same `report` hook.
|
|
218
243
|
await report({ routingReason: item.runtimeId || item.model ? "manual override" : "queue label" });
|
|
219
|
-
|
|
244
|
+
if (run.state !== "active")
|
|
245
|
+
return;
|
|
246
|
+
await this.runWithPolicy(item, report, run);
|
|
220
247
|
}
|
|
221
248
|
finally {
|
|
222
|
-
if (
|
|
223
|
-
clearInterval(
|
|
224
|
-
|
|
249
|
+
if (run.heartbeat)
|
|
250
|
+
clearInterval(run.heartbeat);
|
|
251
|
+
// A stale completion must not remove a newer reservation for the same id.
|
|
252
|
+
if (this.inFlight.get(item.id) === run)
|
|
253
|
+
this.inFlight.delete(item.id);
|
|
225
254
|
}
|
|
226
255
|
}
|
|
256
|
+
checkLease(id, run) {
|
|
257
|
+
if (run.state !== "active")
|
|
258
|
+
return Promise.resolve();
|
|
259
|
+
if (run.leaseCheck)
|
|
260
|
+
return run.leaseCheck;
|
|
261
|
+
run.leaseCheck = (async () => {
|
|
262
|
+
try {
|
|
263
|
+
const renewal = await renewWorkLease(this.cfg, id);
|
|
264
|
+
if (renewal === "renewed" || this.inFlight.get(id) !== run || run.state !== "active")
|
|
265
|
+
return;
|
|
266
|
+
run.state = renewal;
|
|
267
|
+
if (run.heartbeat) {
|
|
268
|
+
clearInterval(run.heartbeat);
|
|
269
|
+
run.heartbeat = undefined;
|
|
270
|
+
}
|
|
271
|
+
run.controller.abort(new Error(renewal === "cancelled" ? "Run cancelled" : "Run lease lost"));
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
// A transient network error does not prove ownership was lost. Keep the
|
|
275
|
+
// Run alive and let the next heartbeat retry.
|
|
276
|
+
console.warn(`[control-plane-tasks] work ${id} heartbeat failed:`, error instanceof Error ? error.message : error);
|
|
277
|
+
}
|
|
278
|
+
finally {
|
|
279
|
+
run.leaseCheck = undefined;
|
|
280
|
+
}
|
|
281
|
+
})();
|
|
282
|
+
return run.leaseCheck;
|
|
283
|
+
}
|
|
227
284
|
/**
|
|
228
285
|
* Run one item under the run policy: on failure, classify → decide → retry /
|
|
229
286
|
* reroute (rewrite routing for the next attempt) / park (needs_attention) /
|
|
@@ -233,17 +290,27 @@ export class ControlPlaneTaskPoller {
|
|
|
233
290
|
* safe evidence event. With no policy injected this is the historical path:
|
|
234
291
|
* one attempt, any throw fails the run.
|
|
235
292
|
*/
|
|
236
|
-
async runWithPolicy(item, report) {
|
|
293
|
+
async runWithPolicy(item, report, run) {
|
|
237
294
|
let current = item;
|
|
238
295
|
let attempt = 1;
|
|
239
296
|
let rerouteCount = 0;
|
|
240
297
|
for (;;) {
|
|
298
|
+
if (run.state !== "active")
|
|
299
|
+
return;
|
|
241
300
|
try {
|
|
242
|
-
|
|
301
|
+
// Functions declared with the historical two arguments remain valid in
|
|
302
|
+
// TypeScript/JavaScript; cancellation-aware runners can use the third.
|
|
303
|
+
await this.runItem(current, report, run.controller.signal);
|
|
304
|
+
if (run.state !== "active")
|
|
305
|
+
return;
|
|
243
306
|
await completeWork(this.cfg, item.id);
|
|
244
307
|
return;
|
|
245
308
|
}
|
|
246
309
|
catch (error) {
|
|
310
|
+
// Abort errors are ordinary throws to the policy layer unless guarded.
|
|
311
|
+
// A cancelled/lost Run has no node-side terminal transition or retry.
|
|
312
|
+
if (run.state !== "active")
|
|
313
|
+
return;
|
|
247
314
|
const policy = typeof this.policy === "function" ? this.policy(current) : this.policy;
|
|
248
315
|
const decision = policy?.decide({
|
|
249
316
|
routing: { runtimeId: current.runtimeId, model: current.model },
|
|
@@ -258,6 +325,8 @@ export class ControlPlaneTaskPoller {
|
|
|
258
325
|
const summary = `Attempt limit reached (${maxAttempts}); automation parked for review.`;
|
|
259
326
|
console.warn(`[control-plane-tasks] item ${item.id} needs attention: ${summary}`);
|
|
260
327
|
await report({ events: [{ at: new Date().toISOString(), kind: "needs_attention", summary, attempt }] });
|
|
328
|
+
if (run.state !== "active")
|
|
329
|
+
return;
|
|
261
330
|
await needsAttentionWork(this.cfg, item.id);
|
|
262
331
|
return;
|
|
263
332
|
}
|
|
@@ -283,15 +352,21 @@ export class ControlPlaneTaskPoller {
|
|
|
283
352
|
}
|
|
284
353
|
if (decision.delayMs > 0)
|
|
285
354
|
await this.sleep(decision.delayMs);
|
|
355
|
+
if (run.state !== "active")
|
|
356
|
+
return;
|
|
286
357
|
continue;
|
|
287
358
|
}
|
|
288
359
|
if (decision.action === "park") {
|
|
289
360
|
console.warn(`[control-plane-tasks] item ${item.id} needs attention (${decision.condition}): ${decision.summary}`);
|
|
290
361
|
await report({ events: [{ at: new Date().toISOString(), kind: "needs_attention", summary: decision.summary }] });
|
|
362
|
+
if (run.state !== "active")
|
|
363
|
+
return;
|
|
291
364
|
await needsAttentionWork(this.cfg, item.id);
|
|
292
365
|
return;
|
|
293
366
|
}
|
|
294
367
|
console.warn(`[control-plane-tasks] item ${item.id} failed:`, error);
|
|
368
|
+
if (run.state !== "active")
|
|
369
|
+
return;
|
|
295
370
|
await failWork(this.cfg, item.id);
|
|
296
371
|
return;
|
|
297
372
|
}
|
package/dist/server.js
CHANGED
|
@@ -3008,8 +3008,8 @@ function startRelayIfConfigured() {
|
|
|
3008
3008
|
terminals.dropClient(RELAY_CLIENT_ID);
|
|
3009
3009
|
relay = new RelayConnector(config, (msg) => void handleRelayMessage(msg), {
|
|
3010
3010
|
pairing: pairingStore,
|
|
3011
|
-
onWorkAvailable: () => {
|
|
3012
|
-
controlPlanePoller?.poke();
|
|
3011
|
+
onWorkAvailable: (hint) => {
|
|
3012
|
+
controlPlanePoller?.poke(hint.id);
|
|
3013
3013
|
// A relay wake also means "something changed for this account" — kick a
|
|
3014
3014
|
// (debounced) model-auth sync so a peer node answers any pending vault-key
|
|
3015
3015
|
// request from a freshly-launched ephemeral runner without waiting for its
|
|
@@ -4073,12 +4073,18 @@ async function runIssueTaskInner(cfg, issue, source, overrides = {}) {
|
|
|
4073
4073
|
throw new Error("worktree was not created for the issue session");
|
|
4074
4074
|
try {
|
|
4075
4075
|
emit(record, "started", `Started work on ${cfg.owner}/${cfg.repo}#${issue.number}.`);
|
|
4076
|
-
await runSessionTurn(record, buildTaskPrompt(issue, nodeGithubIssuePrompt()));
|
|
4076
|
+
await runSessionTurn(record, buildTaskPrompt(issue, nodeGithubIssuePrompt()), overrides.signal);
|
|
4077
|
+
if (overrides.signal?.aborted)
|
|
4078
|
+
throw overrides.signal.reason ?? new Error("Run cancelled");
|
|
4077
4079
|
emit(record, "agent_done", `Agent finished issue #${issue.number}; running deterministic checks.`);
|
|
4078
4080
|
await reportIssueOutcome(cfg, issue, record, emit, { followUp: false, onEvidence: overrides.onEvidence });
|
|
4079
4081
|
}
|
|
4080
4082
|
catch (error) {
|
|
4081
|
-
|
|
4083
|
+
// Cancellation has its own durable control-plane outcome. Do not append a
|
|
4084
|
+
// misleading execution-failed event after the operator stopped the Run.
|
|
4085
|
+
if (!overrides.signal?.aborted) {
|
|
4086
|
+
emit(record, "failed", `GitHub issue #${issue.number} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4087
|
+
}
|
|
4082
4088
|
throw error;
|
|
4083
4089
|
}
|
|
4084
4090
|
}
|
|
@@ -4103,7 +4109,9 @@ async function runIssueFollowUp(cfg, issue, record, emit, overrides = {}) {
|
|
|
4103
4109
|
const merge = await mergeBaseIntoBranch(cfg, wt.path, base);
|
|
4104
4110
|
if (merge.status === "conflicts") {
|
|
4105
4111
|
emit(record, "resolving_conflicts", `Merge conflicts with ${base} in ${merge.conflicts.length} file(s); asking the agent to resolve them.`, { files: merge.conflicts });
|
|
4106
|
-
await runSessionTurn(record, buildConflictPrompt(base, merge.conflicts));
|
|
4112
|
+
await runSessionTurn(record, buildConflictPrompt(base, merge.conflicts), overrides.signal);
|
|
4113
|
+
if (overrides.signal?.aborted)
|
|
4114
|
+
throw overrides.signal.reason ?? new Error("Run cancelled");
|
|
4107
4115
|
const resolved = await completeMerge(wt.path, merge.conflicts);
|
|
4108
4116
|
if (resolved) {
|
|
4109
4117
|
emit(record, "conflicts_resolved", `Resolved merge conflicts with ${base}.`);
|
|
@@ -4116,12 +4124,16 @@ async function runIssueFollowUp(cfg, issue, record, emit, overrides = {}) {
|
|
|
4116
4124
|
emit(record, "conflicts_unresolved", `Could not automatically resolve conflicts with ${base}; continuing without merging it in.`);
|
|
4117
4125
|
}
|
|
4118
4126
|
}
|
|
4119
|
-
await runSessionTurn(record, buildFollowUpPrompt(issue));
|
|
4127
|
+
await runSessionTurn(record, buildFollowUpPrompt(issue), overrides.signal);
|
|
4128
|
+
if (overrides.signal?.aborted)
|
|
4129
|
+
throw overrides.signal.reason ?? new Error("Run cancelled");
|
|
4120
4130
|
emit(record, "agent_done", `Agent handled the follow-up on issue #${issue.number}; running deterministic checks.`);
|
|
4121
4131
|
await reportIssueOutcome(cfg, issue, record, emit, { followUp: true, onEvidence: overrides.onEvidence });
|
|
4122
4132
|
}
|
|
4123
4133
|
catch (error) {
|
|
4124
|
-
|
|
4134
|
+
if (!overrides.signal?.aborted) {
|
|
4135
|
+
emit(record, "failed", `GitHub issue #${issue.number} follow-up failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4136
|
+
}
|
|
4125
4137
|
throw error;
|
|
4126
4138
|
}
|
|
4127
4139
|
}
|
|
@@ -4240,17 +4252,31 @@ function buildConflictPrompt(base, conflicts) {
|
|
|
4240
4252
|
* Mirrors how the issue pickup awaited a turn inline; factored out so both the
|
|
4241
4253
|
* implementation turn and the conflict-resolution turn share it.
|
|
4242
4254
|
*/
|
|
4243
|
-
async function runSessionTurn(record, prompt) {
|
|
4255
|
+
async function runSessionTurn(record, prompt, signal) {
|
|
4256
|
+
if (signal?.aborted)
|
|
4257
|
+
throw signal.reason ?? new Error("Run cancelled");
|
|
4258
|
+
let unsubscribe = () => { };
|
|
4244
4259
|
const finished = new Promise((resolve) => {
|
|
4245
|
-
|
|
4246
|
-
if (event.type === "agent_end")
|
|
4247
|
-
off();
|
|
4260
|
+
unsubscribe = record.session.subscribe((event) => {
|
|
4261
|
+
if (event.type === "agent_end")
|
|
4248
4262
|
resolve();
|
|
4249
|
-
}
|
|
4250
4263
|
});
|
|
4251
4264
|
});
|
|
4252
|
-
|
|
4253
|
-
|
|
4265
|
+
let rejectCancellation = () => { };
|
|
4266
|
+
const cancelled = new Promise((_resolve, reject) => { rejectCancellation = reject; });
|
|
4267
|
+
const onAbort = () => {
|
|
4268
|
+
abortSessionRecord(record);
|
|
4269
|
+
rejectCancellation(signal?.reason ?? new Error("Run cancelled"));
|
|
4270
|
+
};
|
|
4271
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4272
|
+
try {
|
|
4273
|
+
await turnWatchdog.promptWithWatchdog(record, prompt);
|
|
4274
|
+
await (signal ? Promise.race([finished, cancelled]) : finished);
|
|
4275
|
+
}
|
|
4276
|
+
finally {
|
|
4277
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4278
|
+
unsubscribe();
|
|
4279
|
+
}
|
|
4254
4280
|
}
|
|
4255
4281
|
async function anthropicHeadersFromNodeCredential() {
|
|
4256
4282
|
const cred = await createCredentialStore(credsDir).getCredential("anthropic");
|
|
@@ -4621,7 +4647,9 @@ async function continueCorrelatedSession(item, prompt, report, opts) {
|
|
|
4621
4647
|
throw new Error(`Repository policy requires ${safety.sandbox}; refusing to continue a session opened as ${currentSandbox}`);
|
|
4622
4648
|
}
|
|
4623
4649
|
record.approvalMode = safety.approval;
|
|
4624
|
-
await runSessionTurn(record, prompt);
|
|
4650
|
+
await runSessionTurn(record, prompt, opts?.signal);
|
|
4651
|
+
if (opts?.signal?.aborted)
|
|
4652
|
+
throw opts.signal.reason ?? new Error("Run cancelled");
|
|
4625
4653
|
if (record.worktree && !opts?.isMessage && !opts?.resumeOnMissing) {
|
|
4626
4654
|
await branchPublish.maybePushWorktreeBranch(record);
|
|
4627
4655
|
await prDetection.maybeDetectPullRequest(record);
|
|
@@ -4655,7 +4683,9 @@ function lastUserMessageText(record) {
|
|
|
4655
4683
|
}
|
|
4656
4684
|
return "";
|
|
4657
4685
|
}
|
|
4658
|
-
async function runWorkItem(item, report) {
|
|
4686
|
+
async function runWorkItem(item, report, signal) {
|
|
4687
|
+
if (signal.aborted)
|
|
4688
|
+
throw signal.reason ?? new Error("Run cancelled");
|
|
4659
4689
|
// Scheduled, manual, and webhook-triggered automations carry the operator's
|
|
4660
4690
|
// instructions as an E2E template (`bivy-room-v1:<node>:<ciphertext>`) only the
|
|
4661
4691
|
// assigned node can read. The envelope prefix is Bivy's own and never appears
|
|
@@ -4739,6 +4769,7 @@ async function runWorkItem(item, report) {
|
|
|
4739
4769
|
sandbox: normalizeSandboxTier(item.sandbox),
|
|
4740
4770
|
approvalMode: approvalModeFrom(item.approvalMode),
|
|
4741
4771
|
onEvidence: report,
|
|
4772
|
+
signal,
|
|
4742
4773
|
});
|
|
4743
4774
|
return;
|
|
4744
4775
|
}
|
|
@@ -4757,7 +4788,7 @@ async function runWorkItem(item, report) {
|
|
|
4757
4788
|
throw new Error(`Linear work item has an invalid repo "${repoSlug}"`);
|
|
4758
4789
|
// Case B: a re-dispatch the control plane correlated to an existing session
|
|
4759
4790
|
// continues it as a normal chat instead of starting cold (mirrors GitHub).
|
|
4760
|
-
if (await continueCorrelatedSession(item, buildLinearTaskPrompt(issue), report))
|
|
4791
|
+
if (await continueCorrelatedSession(item, buildLinearTaskPrompt(issue), report, { signal }))
|
|
4761
4792
|
return;
|
|
4762
4793
|
const githubToken = await resolveGitHubToken();
|
|
4763
4794
|
if (!githubToken)
|
|
@@ -4784,7 +4815,9 @@ async function runWorkItem(item, report) {
|
|
|
4784
4815
|
catch { }
|
|
4785
4816
|
}
|
|
4786
4817
|
await report({ output: { sessionId: record.id, branch }, events: [{ at: new Date().toISOString(), kind: "branch", summary: "Linear issue working branch and session created.", ref: branch, url: issue.url }] });
|
|
4787
|
-
await runSessionTurn(record, buildLinearTaskPrompt(issue));
|
|
4818
|
+
await runSessionTurn(record, buildLinearTaskPrompt(issue), signal);
|
|
4819
|
+
if (signal.aborted)
|
|
4820
|
+
throw signal.reason ?? new Error("Run cancelled");
|
|
4788
4821
|
await branchPublish.maybePushWorktreeBranch(record);
|
|
4789
4822
|
await prDetection.maybeDetectPullRequest(record);
|
|
4790
4823
|
await report({ output: { sessionId: record.id, branch, prUrl: record.prUrl }, events: record.prUrl ? [{ at: new Date().toISOString(), kind: "pull_request", summary: "Pull request opened.", ref: branch, url: record.prUrl }] : undefined });
|
|
@@ -4815,7 +4848,7 @@ async function runWorkItem(item, report) {
|
|
|
4815
4848
|
// Scheduled runs targeting an existing session are STRICT: the message must
|
|
4816
4849
|
// land in that session (resumed from disk if needed), never silently in a new
|
|
4817
4850
|
// one — so a session that can't be resumed fails the run instead.
|
|
4818
|
-
if (await continueCorrelatedSession(item, request, report, { resumeOnMissing: item.source === "schedule", isMessage }))
|
|
4851
|
+
if (await continueCorrelatedSession(item, request, report, { resumeOnMissing: item.source === "schedule", isMessage, signal }))
|
|
4819
4852
|
return;
|
|
4820
4853
|
const requestedSandbox = normalizeSandboxTier(item.sandbox);
|
|
4821
4854
|
// Prepare an explicit repository before resolving its policy. Otherwise a
|
|
@@ -4858,7 +4891,9 @@ async function runWorkItem(item, report) {
|
|
|
4858
4891
|
"When finished, commit and push your changes, then open a pull request with a clear title and description. If you cannot open it, leave the changes committed on this branch.",
|
|
4859
4892
|
].join("\n")
|
|
4860
4893
|
: request;
|
|
4861
|
-
await runSessionTurn(record, prompt);
|
|
4894
|
+
await runSessionTurn(record, prompt, signal);
|
|
4895
|
+
if (signal.aborted)
|
|
4896
|
+
throw signal.reason ?? new Error("Run cancelled");
|
|
4862
4897
|
if (!isMessage && record.worktree) {
|
|
4863
4898
|
await branchPublish.maybePushWorktreeBranch(record);
|
|
4864
4899
|
await prDetection.maybeDetectPullRequest(record);
|
package/package.json
CHANGED