@aron98/load-lock 1.3.0 → 1.4.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/adapters/opencode/plugin.d.ts +2 -1
- package/dist/adapters/opencode/plugin.js +85 -5
- package/dist/cli/main.js +34 -9
- package/dist/core/daemon.d.ts +24 -0
- package/dist/core/daemon.js +221 -0
- package/dist/core/state.d.ts +5 -0
- package/dist/core/state.js +16 -0
- package/dist/mcp/server.js +107 -7
- package/dist/policy/text.d.ts +1 -1
- package/dist/policy/text.js +6 -0
- package/dist/shared/protocol.d.ts +32 -0
- package/dist/shared/wait-completion.d.ts +67 -0
- package/dist/shared/wait-completion.js +505 -0
- package/dist/test/mcp.test.js +115 -1
- package/dist/test/wait-completion.test.d.ts +1 -0
- package/dist/test/wait-completion.test.js +418 -0
- package/docs/ADAPTERS.md +3 -2
- package/docs/AGENT_GUIDE.md +49 -3
- package/docs/ARCHITECTURE.md +15 -0
- package/docs/OPERATIONS.md +2 -1
- package/package.json +1 -1
package/dist/mcp/server.js
CHANGED
|
@@ -21,6 +21,7 @@ import { spawn } from "node:child_process";
|
|
|
21
21
|
import { LLClient } from "../shared/client.js";
|
|
22
22
|
import { POLICY_BRIEF } from "../policy/text.js";
|
|
23
23
|
import { waitForTurn, WaitAbortedError } from "../shared/wait-turn.js";
|
|
24
|
+
import { waitForLocalJob, waitForTrackedJob } from "../shared/wait-completion.js";
|
|
24
25
|
import { killProcessGroup } from "../shared/transport.js";
|
|
25
26
|
const ACQUIRE_DESC = `Acquire a machine-wide permit before starting resource-intensive local work (full builds, test suites, big compiles/containers).
|
|
26
27
|
|
|
@@ -263,13 +264,15 @@ Input: request_id (default: your oldest offered, then front-most queued, then ol
|
|
|
263
264
|
const jobs = new Map();
|
|
264
265
|
server.registerTool("run_with_permit", {
|
|
265
266
|
title: "Run heavy command under permit",
|
|
266
|
-
description: "Run the heavy command you hold a permit for, with full lifecycle ownership: spawns the command in its own process group, atomically associates it with your claimed permit, returns a job_id immediately (background execution — do NOT release the permit merely because this returned), and auto-releases the permit when the job exits (success, failure, or kill). Survives this tool call and your session: if this server dies, the daemon still watches the job's process group.
|
|
267
|
+
description: "Run the heavy command you hold a permit for, with full lifecycle ownership: spawns the command in its own process group, atomically associates it with your claimed permit, returns a job_id immediately (background execution — do NOT release the permit merely because this returned), and auto-releases the permit when the job exits (success, failure, or kill). Survives this tool call and your session: if this server dies, the daemon still watches the job's process group. Set wait:true to sleep through execution in this one call (bounded by wait_timeout_ms; returns still_running on timeout — re-call), otherwise sleep via wait_for_completion(job_id) and quick-check via job_status.",
|
|
267
268
|
inputSchema: {
|
|
268
269
|
permit_token: z.string().describe("Permit token from acquire/claim"),
|
|
269
270
|
command: z.string().describe("Shell command to run (the heavy work you acquired the permit for — do not pass unrelated light commands)"),
|
|
270
271
|
cwd: z.string().optional().describe("Working directory (default: this server's cwd)"),
|
|
272
|
+
wait: z.boolean().optional().describe("true = sleep through execution in this one call: wake promptly when the job finishes (bounded by wait_timeout_ms), returning exit code + output tail"),
|
|
273
|
+
wait_timeout_ms: z.number().int().optional().describe("Bounded sleep for wait:true (ms); default 600000. Returns still_running (a checkpoint) on timeout — call wait_for_completion again."),
|
|
271
274
|
},
|
|
272
|
-
}, async (input) => {
|
|
275
|
+
}, async (input, extra) => {
|
|
273
276
|
const child = spawn(input.command, {
|
|
274
277
|
shell: true,
|
|
275
278
|
detached: true, // own process group => pgid === child.pid on Linux
|
|
@@ -277,6 +280,7 @@ Input: request_id (default: your oldest offered, then front-most queued, then ol
|
|
|
277
280
|
env: process.env,
|
|
278
281
|
stdio: ["ignore", "pipe", "pipe"],
|
|
279
282
|
});
|
|
283
|
+
let resolveExit;
|
|
280
284
|
const job = {
|
|
281
285
|
id: "job_" + child.pid + "_" + Math.random().toString(36).slice(2, 8),
|
|
282
286
|
child,
|
|
@@ -286,6 +290,9 @@ Input: request_id (default: your oldest offered, then front-most queued, then ol
|
|
|
286
290
|
exited: false,
|
|
287
291
|
exitCode: null,
|
|
288
292
|
output: [],
|
|
293
|
+
exitPromise: new Promise((resolve) => {
|
|
294
|
+
resolveExit = resolve;
|
|
295
|
+
}),
|
|
289
296
|
};
|
|
290
297
|
// Watch for exit BEFORE associating (no awaits in between): a job that
|
|
291
298
|
// exits instantly must not lose its exit event while we round-trip to
|
|
@@ -299,22 +306,29 @@ Input: request_id (default: your oldest offered, then front-most queued, then ol
|
|
|
299
306
|
child.stderr?.on("data", push);
|
|
300
307
|
child.on("exit", (code) => {
|
|
301
308
|
job.exited = true;
|
|
309
|
+
job.exitedAt = Date.now();
|
|
302
310
|
job.exitCode = code;
|
|
303
311
|
// Auto-release on completion: success, failure, or termination.
|
|
304
|
-
|
|
312
|
+
// releasePromise is assigned BEFORE resolveExit so a woken waiter
|
|
313
|
+
// sees the release round-trip in flight (see waitForLocalJob).
|
|
314
|
+
job.releasePromise = client
|
|
305
315
|
.request("release", { permitToken: job.permitToken, outcome: code === 0 ? "success" : "failure" })
|
|
306
|
-
.
|
|
316
|
+
.then(() => {
|
|
317
|
+
job.permitReleased = true;
|
|
318
|
+
}, () => {
|
|
307
319
|
/* already released/reclaimed — idempotent path */
|
|
308
320
|
});
|
|
321
|
+
resolveExit(code);
|
|
309
322
|
});
|
|
310
323
|
// Atomic association; if the permit was reclaimed already, kill the job
|
|
311
324
|
// before it can do meaningful work and report the failure.
|
|
312
325
|
try {
|
|
313
|
-
await client.request("start_job", {
|
|
326
|
+
const res = await client.request("start_job", {
|
|
314
327
|
permitToken: input.permit_token,
|
|
315
328
|
pid: child.pid,
|
|
316
329
|
pgid: child.pid,
|
|
317
330
|
});
|
|
331
|
+
job.requestId = res.entry?.requestId;
|
|
318
332
|
}
|
|
319
333
|
catch (err) {
|
|
320
334
|
killProcessGroup(child.pid, "SIGKILL");
|
|
@@ -323,12 +337,31 @@ Input: request_id (default: your oldest offered, then front-most queued, then ol
|
|
|
323
337
|
});
|
|
324
338
|
}
|
|
325
339
|
jobs.set(job.id, job);
|
|
340
|
+
const note = `Background job started under permit. Do NOT release the permit manually — it auto-releases when the job exits. Sleep through the execution with wait_for_completion(job_id); job_status(job_id) for a quick check; job_stop(job_id) to terminate.`;
|
|
341
|
+
if (!input.wait) {
|
|
342
|
+
return json({
|
|
343
|
+
job_id: job.id,
|
|
344
|
+
pid: child.pid,
|
|
345
|
+
pgid: child.pid,
|
|
346
|
+
state: "running",
|
|
347
|
+
note,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
// Fire-and-wait: sleep honestly through execution in this one call.
|
|
351
|
+
const r = await waitForLocalJob(job, { timeoutMs: input.wait_timeout_ms, signal: extra?.signal });
|
|
326
352
|
return json({
|
|
327
353
|
job_id: job.id,
|
|
328
354
|
pid: child.pid,
|
|
329
355
|
pgid: child.pid,
|
|
330
|
-
|
|
331
|
-
|
|
356
|
+
command: job.command,
|
|
357
|
+
state: r.state,
|
|
358
|
+
exit_code: r.exitCode,
|
|
359
|
+
output_tail: r.outputTail,
|
|
360
|
+
duration_ms: r.durationMs,
|
|
361
|
+
permit_released: r.permitReleased,
|
|
362
|
+
waited_ms: r.waitedMs,
|
|
363
|
+
next_action: r.nextAction,
|
|
364
|
+
detail: r.detail,
|
|
332
365
|
});
|
|
333
366
|
});
|
|
334
367
|
server.registerTool("job_status", {
|
|
@@ -356,6 +389,7 @@ Input: request_id (default: your oldest offered, then front-most queued, then ol
|
|
|
356
389
|
if (!job)
|
|
357
390
|
return json({ error: "unknown job_id" });
|
|
358
391
|
if (!job.exited) {
|
|
392
|
+
job.stopRequested = true;
|
|
359
393
|
killProcessGroup(job.child.pid, "SIGTERM");
|
|
360
394
|
setTimeout(() => {
|
|
361
395
|
if (!job.exited) {
|
|
@@ -365,6 +399,72 @@ Input: request_id (default: your oldest offered, then front-most queued, then ol
|
|
|
365
399
|
}
|
|
366
400
|
return json({ job_id: job.id, stopping: true });
|
|
367
401
|
});
|
|
402
|
+
server.registerTool("wait_for_completion", {
|
|
403
|
+
title: "Sleep until the job finishes",
|
|
404
|
+
description: `Bounded blocking sleep for the EXECUTION phase — "sleep honestly until my heavy job finishes", replacing sleep-and-poll cycles. Two mechanisms, selected by identifier:
|
|
405
|
+
- job_id of a run_with_permit job (adapter-parented): resolves on the child's exact exit event — no polling, prompt wake, exact exit code + output tail;
|
|
406
|
+
- request_id of any daemon-tracked permit (attach_pid etc.): the daemon holds the wait and wakes it when its liveness sweep observes the process gone (worst-case promptness: one sweep interval, default 5s; the permit is released BEFORE you are woken).
|
|
407
|
+
Returns a STRUCTURED result on every path — never an error for a normal resolution:
|
|
408
|
+
- finished: exit_code, output_tail, duration_ms, permit_released — the work is done, the permit is (being) freed;
|
|
409
|
+
- still_running: timed out mid-execution — a checkpoint; call wait_for_completion again (long jobs = a handful of long sleeps, not a poll loop);
|
|
410
|
+
- timeout: this wait call was aborted/cancelled (harness tool timeout, AbortSignal) — the job is UNTOUCHED; call again;
|
|
411
|
+
- job_unknown: no such job/request, or the permit was reclaimed/expired meanwhile;
|
|
412
|
+
- stopped_by_request: the job/permit was stopped by an explicit admin action.
|
|
413
|
+
Input: job_id OR request_id (one of them), timeout_ms (default 600000, clamped to the machine's maxTurnWaitMs cap for daemon-tracked waits; 0 = check-only). The wait itself holds NO permit and NO capacity. A daemon restart mid-wait is handled transparently (tracked permits survive restarts via the journal); worst case it resolves with job_unknown + re-check instructions.`,
|
|
414
|
+
inputSchema: {
|
|
415
|
+
job_id: z.string().optional().describe("A run_with_permit job_id (adapter-parented; exact exit wake)"),
|
|
416
|
+
request_id: z.string().optional().describe("A daemon-tracked permit's request_id (attach_pid etc.; liveness-sweep wake)"),
|
|
417
|
+
timeout_ms: z.number().int().optional().describe("Bounded sleep (ms); default 600000. 0 = check-only. still_running on timeout — re-call."),
|
|
418
|
+
agent_id: z.string().optional().describe("Override agent identity (rarely needed)"),
|
|
419
|
+
},
|
|
420
|
+
}, async (input, extra) => {
|
|
421
|
+
if (!input.job_id && !input.request_id) {
|
|
422
|
+
return json({
|
|
423
|
+
state: "job_unknown",
|
|
424
|
+
waited_ms: 0,
|
|
425
|
+
next_action: "Pass job_id (a run_with_permit job) or request_id (a daemon-tracked permit).",
|
|
426
|
+
detail: "no identifier given",
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
if (input.job_id && jobs.has(input.job_id)) {
|
|
430
|
+
const job = jobs.get(input.job_id);
|
|
431
|
+
const r = await waitForLocalJob(job, { timeoutMs: input.timeout_ms, signal: extra?.signal });
|
|
432
|
+
return json({
|
|
433
|
+
state: r.state,
|
|
434
|
+
job_id: job.id,
|
|
435
|
+
request_id: job.requestId,
|
|
436
|
+
exit_code: r.exitCode,
|
|
437
|
+
output_tail: r.outputTail,
|
|
438
|
+
duration_ms: r.durationMs,
|
|
439
|
+
permit_released: r.permitReleased,
|
|
440
|
+
waited_ms: r.waitedMs,
|
|
441
|
+
started_at: r.startedAt,
|
|
442
|
+
next_action: r.nextAction,
|
|
443
|
+
detail: r.detail,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
// Daemon-tracked permit (request_id), or a job_id from another/previous
|
|
447
|
+
// server process (unknown here -> falls through to job_unknown).
|
|
448
|
+
const requestId = input.request_id ?? input.job_id;
|
|
449
|
+
const r = await waitForTrackedJob({
|
|
450
|
+
client,
|
|
451
|
+
requestId,
|
|
452
|
+
timeoutMs: input.timeout_ms,
|
|
453
|
+
signal: extra?.signal,
|
|
454
|
+
});
|
|
455
|
+
return json({
|
|
456
|
+
state: r.state,
|
|
457
|
+
request_id: r.requestId ?? requestId,
|
|
458
|
+
exit_code: r.exitCode,
|
|
459
|
+
duration_ms: r.durationMs,
|
|
460
|
+
permit_released: r.permitReleased,
|
|
461
|
+
waited_ms: r.waitedMs,
|
|
462
|
+
effective_wait_ms: r.effectiveWaitMs,
|
|
463
|
+
started_at: r.startedAt,
|
|
464
|
+
next_action: r.nextAction,
|
|
465
|
+
detail: r.detail,
|
|
466
|
+
});
|
|
467
|
+
});
|
|
368
468
|
// Automatic adapter heartbeat: while this server process lives, untracked
|
|
369
469
|
// permits of this agent keep their lease; if the harness session dies, the
|
|
370
470
|
// lease lapses and the daemon reclaims. The model never has to remember.
|
package/dist/policy/text.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Deliberately SHORT (spec: no noisy reminders). The detailed playbook lives
|
|
12
12
|
* in the skill / docs/AGENT_GUIDE.md.
|
|
13
13
|
*/
|
|
14
|
-
export declare const POLICY_BRIEF = "# load-lock: machine-wide heavy-work budget\n\nMultiple coding agents share this machine. Before resource-intensive local work\n(builds, test suites, bulky containers, big compiles), get a permit from the\nshared, machine-wide counting semaphore. While queued, keep doing useful light\nwork (reading, editing, searching, planning); you will be offered capacity.\n\nRules:\n1. Heavy vs light: judge by expected CPU/memory/IO cost for the whole machine.\n A quick lint or single-file check is light. Full builds/tests are heavy.\n When unsure, call the status tool \u2014 it shows current machine pressure.\n2. Immediately before starting heavy work: acquire a permit\n (tool: load-lock acquire, CLI: load-lock acquire). Pass a short reason and\n a stable op_key for the pending operation.\n3. If you are queued: keep working on light tasks. When you receive an offer\n (in any load-lock tool result or a wakeup message), claim it promptly \u2014\n offers expire (default 30s) and capacity passes to the next waiter.\n Out of light work? Sleep honestly with wait_for_turn (MCP/plugin) or\n 'load-lock wait' (CLI) \u2014 a bounded sleep until your turn that\n auto-claims and holds no capacity.\n4. Start heavy work ONLY after acquire/claim succeeds. Never start heavy work\n on a \"queued\" result; never treat a notification as authorization.\n5. Hold the permit until the heavy process actually finishes \u2014 including when\n it runs in the background beyond this tool call or session. Optionally\n attach its pid (load-lock attach_pid) so a dead process auto-releases.\n6. Release immediately when done (success or failure), and cancel queued\n requests you no longer need.\n7. Capacity is a cooperative budget, not an enforced one. If the budget seems\n wrong for this machine, check load-lock inspect_machine (real cores/memory,\n including container limits) and adjust with configure (explain why). Don't\n assume N cores = N parallel heavy jobs.\n\nDetailed guide: the \"load-lock\" skill or `load-lock agent-guide`.";
|
|
14
|
+
export declare const POLICY_BRIEF = "# load-lock: machine-wide heavy-work budget\n\nMultiple coding agents share this machine. Before resource-intensive local work\n(builds, test suites, bulky containers, big compiles), get a permit from the\nshared, machine-wide counting semaphore. While queued, keep doing useful light\nwork (reading, editing, searching, planning); you will be offered capacity.\n\nRules:\n1. Heavy vs light: judge by expected CPU/memory/IO cost for the whole machine.\n A quick lint or single-file check is light. Full builds/tests are heavy.\n When unsure, call the status tool \u2014 it shows current machine pressure.\n2. Immediately before starting heavy work: acquire a permit\n (tool: load-lock acquire, CLI: load-lock acquire). Pass a short reason and\n a stable op_key for the pending operation.\n3. If you are queued: keep working on light tasks. When you receive an offer\n (in any load-lock tool result or a wakeup message), claim it promptly \u2014\n offers expire (default 30s) and capacity passes to the next waiter.\n Out of light work? Sleep honestly with wait_for_turn (MCP/plugin) or\n 'load-lock wait' (CLI) \u2014 a bounded sleep until your turn that\n auto-claims and holds no capacity.\n4. Start heavy work ONLY after acquire/claim succeeds. Never start heavy work\n on a \"queued\" result; never treat a notification as authorization.\n5. Hold the permit until the heavy process actually finishes \u2014 including when\n it runs in the background beyond this tool call or session. Optionally\n attach its pid (load-lock attach_pid) so a dead process auto-releases.\n Out of light work while it runs? Sleep honestly with wait_for_completion\n (MCP/plugin: run_with_permit job_id or tracked request_id; or run_with_permit\n with wait:true) or 'load-lock wait --job <request-id>' (CLI) \u2014 a bounded\n sleep that wakes promptly when the job finishes (exit code + output tail;\n the permit is freed before you are woken). On still_running it is a\n checkpoint: call again. Do NOT guess sleeps or poll job_status in a loop.\n6. Release immediately when done (success or failure), and cancel queued\n requests you no longer need.\n7. Capacity is a cooperative budget, not an enforced one. If the budget seems\n wrong for this machine, check load-lock inspect_machine (real cores/memory,\n including container limits) and adjust with configure (explain why). Don't\n assume N cores = N parallel heavy jobs.\n\nDetailed guide: the \"load-lock\" skill or `load-lock agent-guide`.";
|
|
15
15
|
export declare function policyWithContext(statusSummary: string, configSummary?: string): string;
|
|
16
16
|
export declare function statusLine(st: {
|
|
17
17
|
pools: {
|
package/dist/policy/text.js
CHANGED
|
@@ -36,6 +36,12 @@ Rules:
|
|
|
36
36
|
5. Hold the permit until the heavy process actually finishes — including when
|
|
37
37
|
it runs in the background beyond this tool call or session. Optionally
|
|
38
38
|
attach its pid (load-lock attach_pid) so a dead process auto-releases.
|
|
39
|
+
Out of light work while it runs? Sleep honestly with wait_for_completion
|
|
40
|
+
(MCP/plugin: run_with_permit job_id or tracked request_id; or run_with_permit
|
|
41
|
+
with wait:true) or 'load-lock wait --job <request-id>' (CLI) — a bounded
|
|
42
|
+
sleep that wakes promptly when the job finishes (exit code + output tail;
|
|
43
|
+
the permit is freed before you are woken). On still_running it is a
|
|
44
|
+
checkpoint: call again. Do NOT guess sleeps or poll job_status in a loop.
|
|
39
45
|
6. Release immediately when done (success or failure), and cancel queued
|
|
40
46
|
requests you no longer need.
|
|
41
47
|
7. Capacity is a cooperative budget, not an enforced one. If the budget seems
|
|
@@ -130,6 +130,8 @@ export interface EntrySnapshot {
|
|
|
130
130
|
leaseDeadline?: number;
|
|
131
131
|
lastSeen?: number;
|
|
132
132
|
heldFor?: number;
|
|
133
|
+
/** When execution started (start_job / mark_started), epoch ms. Absent before that. */
|
|
134
|
+
startedAt?: number;
|
|
133
135
|
}
|
|
134
136
|
/** Owner-scoped snapshot: includes tokens. Never returned to non-owners. */
|
|
135
137
|
export interface OwnerEntrySnapshot extends EntrySnapshot {
|
|
@@ -394,6 +396,36 @@ export interface WaitForTurnResult {
|
|
|
394
396
|
nextAction: string;
|
|
395
397
|
detail?: string;
|
|
396
398
|
}
|
|
399
|
+
export type WaitForCompletionState = "finished" | "still_running" | "timeout" | "job_unknown" | "stopped_by_request";
|
|
400
|
+
export interface WaitForCompletionParams {
|
|
401
|
+
/** Request id whose tracked job to wait for. */
|
|
402
|
+
requestId: string;
|
|
403
|
+
/** Bounded wait budget for this call, ms. 0 = check-only (immediate return). */
|
|
404
|
+
timeoutMs?: number;
|
|
405
|
+
}
|
|
406
|
+
export interface WaitForCompletionResult {
|
|
407
|
+
state: WaitForCompletionState;
|
|
408
|
+
/** The job id (adapter-parented run_with_permit job). */
|
|
409
|
+
jobId?: string;
|
|
410
|
+
/** The request/permit id whose job was waited on. */
|
|
411
|
+
requestId?: string;
|
|
412
|
+
/** Adapter-parented jobs only: the child's exit code. */
|
|
413
|
+
exitCode?: number | null;
|
|
414
|
+
/** Adapter-parented jobs only: last lines of combined output. */
|
|
415
|
+
outputTail?: string;
|
|
416
|
+
/** Wall time from execution start to job end (ms). */
|
|
417
|
+
durationMs?: number;
|
|
418
|
+
/** True when the daemon confirms the permit is no longer held. */
|
|
419
|
+
permitReleased?: boolean;
|
|
420
|
+
/** How long the wait actually ran before resolving (ms). */
|
|
421
|
+
waitedMs: number;
|
|
422
|
+
/** The clamped budget actually applied, when it differed from the requested one. */
|
|
423
|
+
effectiveWaitMs?: number;
|
|
424
|
+
/** When execution started, when known (epoch ms). */
|
|
425
|
+
startedAt?: number;
|
|
426
|
+
nextAction: string;
|
|
427
|
+
detail?: string;
|
|
428
|
+
}
|
|
397
429
|
/** Recovery diagnostics: held permits with holder + job information. */
|
|
398
430
|
export interface HolderDiag {
|
|
399
431
|
entry: EntrySnapshot;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { WaitForCompletionResult } from "./protocol.js";
|
|
2
|
+
import { LLClient } from "./client.js";
|
|
3
|
+
/**
|
|
4
|
+
* What adapters expose for a run_with_permit job so it can be waited on.
|
|
5
|
+
* `exitPromise` must resolve when the child exits, and the exit handler must
|
|
6
|
+
* assign `releasePromise` BEFORE resolving it, so a woken waiter can observe
|
|
7
|
+
* the permit-release round-trip.
|
|
8
|
+
*/
|
|
9
|
+
export interface WaitableAdapterJob {
|
|
10
|
+
id: string;
|
|
11
|
+
/** The permit's request id, when known (for cross-referencing with the daemon). */
|
|
12
|
+
requestId?: string;
|
|
13
|
+
startedAt: number;
|
|
14
|
+
exited: boolean;
|
|
15
|
+
exitedAt?: number;
|
|
16
|
+
exitCode?: number | null;
|
|
17
|
+
output: string[];
|
|
18
|
+
/** Set by job_stop before killing — a later wake reports stopped_by_request. */
|
|
19
|
+
stopRequested?: boolean;
|
|
20
|
+
/** True once the daemon confirmed the release RPC. */
|
|
21
|
+
permitReleased?: boolean;
|
|
22
|
+
/** Resolves with the child's exit code when it exits. */
|
|
23
|
+
exitPromise?: Promise<number | null>;
|
|
24
|
+
/** The permit-release round-trip started by the exit handler. */
|
|
25
|
+
releasePromise?: Promise<unknown>;
|
|
26
|
+
}
|
|
27
|
+
export interface WaitForTrackedJobOpts {
|
|
28
|
+
client: LLClient;
|
|
29
|
+
/** The request id whose daemon-tracked job to wait for. */
|
|
30
|
+
requestId: string;
|
|
31
|
+
/**
|
|
32
|
+
* Requested bound (ms), clamped to the machine's maxTurnWaitMs config.
|
|
33
|
+
* 0 = check-only (immediate return). Default 600_000.
|
|
34
|
+
*/
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
/** Poll cadence for the old-daemon fallback loop (default 200ms). */
|
|
37
|
+
pollMs?: number;
|
|
38
|
+
/** Caller cancellation — resolves as { state: "timeout" }; the job is untouched. */
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
}
|
|
41
|
+
export interface WaitForLocalJobOpts {
|
|
42
|
+
/**
|
|
43
|
+
* Bound for this call (ms). 0 = check-only. Default 600_000. NOT clamped
|
|
44
|
+
* by machine config — the adapter already has the exit event, so the wait
|
|
45
|
+
* is bounded only by the caller's own budget (documented divergence).
|
|
46
|
+
*/
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
/** Caller cancellation — resolves as { state: "timeout" }; the job is untouched. */
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Bounded blocking wait for a daemon-tracked job. Uses the daemon's
|
|
53
|
+
* server-side wait_for_completion hold when available; falls back to a light
|
|
54
|
+
* client-side poll against the SAME wire events on old daemons; retries
|
|
55
|
+
* through transient daemon unavailability (tracked permits survive
|
|
56
|
+
* restarts via the journal). Returns a structured result on every path.
|
|
57
|
+
*/
|
|
58
|
+
export declare function waitForTrackedJob(opts: WaitForTrackedJobOpts): Promise<WaitForCompletionResult>;
|
|
59
|
+
/**
|
|
60
|
+
* Bounded, event-driven wait for a run_with_permit child the adapter owns.
|
|
61
|
+
* No polling: the child's exit event wakes the waiter directly, with the
|
|
62
|
+
* exact exit code and output tail. The permit-release round-trip is awaited
|
|
63
|
+
* before reporting finished, so permitReleased is truthful (the daemon
|
|
64
|
+
* releases the permit before adapter waits wake by construction of the
|
|
65
|
+
* adapters' exit handlers).
|
|
66
|
+
*/
|
|
67
|
+
export declare function waitForLocalJob(job: WaitableAdapterJob, opts?: WaitForLocalJobOpts): Promise<WaitForCompletionResult>;
|