agy-worker-mcp 0.3.1
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/CHANGELOG.md +141 -0
- package/LICENSE +21 -0
- package/README.md +309 -0
- package/commands/agy-ceiling.md +31 -0
- package/dist/broker/blockers.d.ts +86 -0
- package/dist/broker/blockers.d.ts.map +1 -0
- package/dist/broker/blockers.js +390 -0
- package/dist/broker/blockers.js.map +1 -0
- package/dist/broker/outcome.d.ts +85 -0
- package/dist/broker/outcome.d.ts.map +1 -0
- package/dist/broker/outcome.js +147 -0
- package/dist/broker/outcome.js.map +1 -0
- package/dist/broker/reconcile.d.ts +74 -0
- package/dist/broker/reconcile.d.ts.map +1 -0
- package/dist/broker/reconcile.js +415 -0
- package/dist/broker/reconcile.js.map +1 -0
- package/dist/broker/result.d.ts +88 -0
- package/dist/broker/result.d.ts.map +1 -0
- package/dist/broker/result.js +234 -0
- package/dist/broker/result.js.map +1 -0
- package/dist/broker/verify.d.ts +52 -0
- package/dist/broker/verify.d.ts.map +1 -0
- package/dist/broker/verify.js +174 -0
- package/dist/broker/verify.js.map +1 -0
- package/dist/contract/errors.d.ts +119 -0
- package/dist/contract/errors.d.ts.map +1 -0
- package/dist/contract/errors.js +99 -0
- package/dist/contract/errors.js.map +1 -0
- package/dist/contract/index.d.ts +10 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +10 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/contract/paths.d.ts +139 -0
- package/dist/contract/paths.d.ts.map +1 -0
- package/dist/contract/paths.js +359 -0
- package/dist/contract/paths.js.map +1 -0
- package/dist/contract/schema.sql +64 -0
- package/dist/contract/types.d.ts +1113 -0
- package/dist/contract/types.d.ts.map +1 -0
- package/dist/contract/types.js +114 -0
- package/dist/contract/types.js.map +1 -0
- package/dist/events/cursor.d.ts +65 -0
- package/dist/events/cursor.d.ts.map +1 -0
- package/dist/events/cursor.js +286 -0
- package/dist/events/cursor.js.map +1 -0
- package/dist/events/detect.d.ts +58 -0
- package/dist/events/detect.d.ts.map +1 -0
- package/dist/events/detect.js +183 -0
- package/dist/events/detect.js.map +1 -0
- package/dist/events/normalize.d.ts +30 -0
- package/dist/events/normalize.d.ts.map +1 -0
- package/dist/events/normalize.js +158 -0
- package/dist/events/normalize.js.map +1 -0
- package/dist/events/parse.d.ts +41 -0
- package/dist/events/parse.d.ts.map +1 -0
- package/dist/events/parse.js +89 -0
- package/dist/events/parse.js.map +1 -0
- package/dist/gate/bind.d.ts +28 -0
- package/dist/gate/bind.d.ts.map +1 -0
- package/dist/gate/bind.js +154 -0
- package/dist/gate/bind.js.map +1 -0
- package/dist/gate/gate.d.ts +139 -0
- package/dist/gate/gate.d.ts.map +1 -0
- package/dist/gate/gate.js +488 -0
- package/dist/gate/gate.js.map +1 -0
- package/dist/gate/hooks-file.d.ts +55 -0
- package/dist/gate/hooks-file.d.ts.map +1 -0
- package/dist/gate/hooks-file.js +104 -0
- package/dist/gate/hooks-file.js.map +1 -0
- package/dist/gate.d.ts +3 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +20 -0
- package/dist/gate.js.map +1 -0
- package/dist/policy/ceiling-review.d.ts +22 -0
- package/dist/policy/ceiling-review.d.ts.map +1 -0
- package/dist/policy/ceiling-review.js +170 -0
- package/dist/policy/ceiling-review.js.map +1 -0
- package/dist/policy/ceiling.d.ts +128 -0
- package/dist/policy/ceiling.d.ts.map +1 -0
- package/dist/policy/ceiling.js +273 -0
- package/dist/policy/ceiling.js.map +1 -0
- package/dist/policy/containment.d.ts +100 -0
- package/dist/policy/containment.d.ts.map +1 -0
- package/dist/policy/containment.js +1071 -0
- package/dist/policy/containment.js.map +1 -0
- package/dist/policy/hard-deny.d.ts +28 -0
- package/dist/policy/hard-deny.d.ts.map +1 -0
- package/dist/policy/hard-deny.js +78 -0
- package/dist/policy/hard-deny.js.map +1 -0
- package/dist/policy/profiles.d.ts +82 -0
- package/dist/policy/profiles.d.ts.map +1 -0
- package/dist/policy/profiles.js +317 -0
- package/dist/policy/profiles.js.map +1 -0
- package/dist/policy/rules.d.ts +138 -0
- package/dist/policy/rules.d.ts.map +1 -0
- package/dist/policy/rules.js +1180 -0
- package/dist/policy/rules.js.map +1 -0
- package/dist/policy/seatbelt.d.ts +32 -0
- package/dist/policy/seatbelt.d.ts.map +1 -0
- package/dist/policy/seatbelt.js +59 -0
- package/dist/policy/seatbelt.js.map +1 -0
- package/dist/policy/tools.d.ts +36 -0
- package/dist/policy/tools.d.ts.map +1 -0
- package/dist/policy/tools.js +94 -0
- package/dist/policy/tools.js.map +1 -0
- package/dist/runner/gate-watchdog.d.ts +93 -0
- package/dist/runner/gate-watchdog.d.ts.map +1 -0
- package/dist/runner/gate-watchdog.js +153 -0
- package/dist/runner/gate-watchdog.js.map +1 -0
- package/dist/runner/idle.d.ts +51 -0
- package/dist/runner/idle.d.ts.map +1 -0
- package/dist/runner/idle.js +81 -0
- package/dist/runner/idle.js.map +1 -0
- package/dist/runner/inbox.d.ts +56 -0
- package/dist/runner/inbox.d.ts.map +1 -0
- package/dist/runner/inbox.js +134 -0
- package/dist/runner/inbox.js.map +1 -0
- package/dist/runner/reap.d.ts +65 -0
- package/dist/runner/reap.d.ts.map +1 -0
- package/dist/runner/reap.js +168 -0
- package/dist/runner/reap.js.map +1 -0
- package/dist/runner/runner.d.ts +51 -0
- package/dist/runner/runner.d.ts.map +1 -0
- package/dist/runner/runner.js +289 -0
- package/dist/runner/runner.js.map +1 -0
- package/dist/runner/spawn.d.ts +98 -0
- package/dist/runner/spawn.d.ts.map +1 -0
- package/dist/runner/spawn.js +230 -0
- package/dist/runner/spawn.js.map +1 -0
- package/dist/runner/verify.d.ts +24 -0
- package/dist/runner/verify.d.ts.map +1 -0
- package/dist/runner/verify.js +108 -0
- package/dist/runner/verify.js.map +1 -0
- package/dist/runner.d.ts +3 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +15 -0
- package/dist/runner.js.map +1 -0
- package/dist/server/context.d.ts +41 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +59 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/instructions.d.ts +13 -0
- package/dist/server/instructions.d.ts.map +1 -0
- package/dist/server/instructions.js +121 -0
- package/dist/server/instructions.js.map +1 -0
- package/dist/server/server.d.ts +20 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +55 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/tools/cancel.d.ts +24 -0
- package/dist/server/tools/cancel.d.ts.map +1 -0
- package/dist/server/tools/cancel.js +59 -0
- package/dist/server/tools/cancel.js.map +1 -0
- package/dist/server/tools/capabilities.d.ts +36 -0
- package/dist/server/tools/capabilities.d.ts.map +1 -0
- package/dist/server/tools/capabilities.js +116 -0
- package/dist/server/tools/capabilities.js.map +1 -0
- package/dist/server/tools/ceiling.d.ts +31 -0
- package/dist/server/tools/ceiling.d.ts.map +1 -0
- package/dist/server/tools/ceiling.js +121 -0
- package/dist/server/tools/ceiling.js.map +1 -0
- package/dist/server/tools/index.d.ts +38 -0
- package/dist/server/tools/index.d.ts.map +1 -0
- package/dist/server/tools/index.js +115 -0
- package/dist/server/tools/index.js.map +1 -0
- package/dist/server/tools/listJobs.d.ts +30 -0
- package/dist/server/tools/listJobs.d.ts.map +1 -0
- package/dist/server/tools/listJobs.js +37 -0
- package/dist/server/tools/listJobs.js.map +1 -0
- package/dist/server/tools/logs.d.ts +31 -0
- package/dist/server/tools/logs.d.ts.map +1 -0
- package/dist/server/tools/logs.js +96 -0
- package/dist/server/tools/logs.js.map +1 -0
- package/dist/server/tools/result.d.ts +32 -0
- package/dist/server/tools/result.d.ts.map +1 -0
- package/dist/server/tools/result.js +95 -0
- package/dist/server/tools/result.js.map +1 -0
- package/dist/server/tools/send.d.ts +37 -0
- package/dist/server/tools/send.d.ts.map +1 -0
- package/dist/server/tools/send.js +71 -0
- package/dist/server/tools/send.js.map +1 -0
- package/dist/server/tools/sessions.d.ts +28 -0
- package/dist/server/tools/sessions.d.ts.map +1 -0
- package/dist/server/tools/sessions.js +54 -0
- package/dist/server/tools/sessions.js.map +1 -0
- package/dist/server/tools/start.d.ts +107 -0
- package/dist/server/tools/start.d.ts.map +1 -0
- package/dist/server/tools/start.js +482 -0
- package/dist/server/tools/start.js.map +1 -0
- package/dist/server/tools/wait.d.ts +29 -0
- package/dist/server/tools/wait.d.ts.map +1 -0
- package/dist/server/tools/wait.js +120 -0
- package/dist/server/tools/wait.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +17 -0
- package/dist/server.js.map +1 -0
- package/dist/setup/install.d.ts +52 -0
- package/dist/setup/install.d.ts.map +1 -0
- package/dist/setup/install.js +108 -0
- package/dist/setup/install.js.map +1 -0
- package/dist/setup.d.ts +3 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +24 -0
- package/dist/setup.js.map +1 -0
- package/dist/store/db.d.ts +49 -0
- package/dist/store/db.d.ts.map +1 -0
- package/dist/store/db.js +101 -0
- package/dist/store/db.js.map +1 -0
- package/dist/store/jobs.d.ts +45 -0
- package/dist/store/jobs.d.ts.map +1 -0
- package/dist/store/jobs.js +150 -0
- package/dist/store/jobs.js.map +1 -0
- package/dist/store/locks.d.ts +60 -0
- package/dist/store/locks.d.ts.map +1 -0
- package/dist/store/locks.js +172 -0
- package/dist/store/locks.js.map +1 -0
- package/dist/store/sessions.d.ts +37 -0
- package/dist/store/sessions.d.ts.map +1 -0
- package/dist/store/sessions.js +117 -0
- package/dist/store/sessions.js.map +1 -0
- package/dist/trace/digest.d.ts +77 -0
- package/dist/trace/digest.d.ts.map +1 -0
- package/dist/trace/digest.js +481 -0
- package/dist/trace/digest.js.map +1 -0
- package/package.json +68 -0
- package/skills/agy-ceiling/SKILL.md +72 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { readLinesFrom } from '../events/cursor.js';
|
|
2
|
+
import { isResult, okEvents, parseEventLines } from '../events/parse.js';
|
|
3
|
+
/**
|
|
4
|
+
* Start the watchdog. It never fires before at least one full turn/`result`
|
|
5
|
+
* cycle has been observed: `pendingTurns` starts at 0 and the idle deadline is
|
|
6
|
+
* only armed inside the branch that just decremented it back to 0, so a
|
|
7
|
+
* watchdog created the instant a job spawns cannot race turn 1's own relay.
|
|
8
|
+
*/
|
|
9
|
+
export function startIdleWatchdog(opts) {
|
|
10
|
+
const pollMs = opts.pollMs ?? 250;
|
|
11
|
+
let stopped = false;
|
|
12
|
+
let fired = false;
|
|
13
|
+
let eventsOffset = 0;
|
|
14
|
+
/** Turns relayed to stdin but not yet confirmed complete by a `result` event. */
|
|
15
|
+
let pendingTurns = 0;
|
|
16
|
+
let idleTimer = null;
|
|
17
|
+
let pollTimer = null;
|
|
18
|
+
function clearIdleTimer() {
|
|
19
|
+
if (idleTimer) {
|
|
20
|
+
clearTimeout(idleTimer);
|
|
21
|
+
idleTimer = null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function armIdleTimer() {
|
|
25
|
+
clearIdleTimer();
|
|
26
|
+
idleTimer = setTimeout(() => {
|
|
27
|
+
if (fired || stopped)
|
|
28
|
+
return;
|
|
29
|
+
fired = true;
|
|
30
|
+
stopInternal();
|
|
31
|
+
try {
|
|
32
|
+
opts.stdin.end();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// stdin already gone — nothing left to close.
|
|
36
|
+
}
|
|
37
|
+
opts.onIdleClose();
|
|
38
|
+
}, opts.idleTimeoutMs);
|
|
39
|
+
}
|
|
40
|
+
function poll() {
|
|
41
|
+
if (stopped)
|
|
42
|
+
return;
|
|
43
|
+
const { lines, nextCursor } = readLinesFrom(opts.eventsPath, eventsOffset);
|
|
44
|
+
eventsOffset = nextCursor;
|
|
45
|
+
if (lines.length > 0) {
|
|
46
|
+
const events = okEvents(parseEventLines(lines, 0));
|
|
47
|
+
let completed = 0;
|
|
48
|
+
for (const e of events)
|
|
49
|
+
if (isResult(e))
|
|
50
|
+
completed++;
|
|
51
|
+
if (completed > 0) {
|
|
52
|
+
pendingTurns = Math.max(0, pendingTurns - completed);
|
|
53
|
+
// Only arm right here, on the transition a `result` just produced —
|
|
54
|
+
// never merely because nothing has happened lately.
|
|
55
|
+
if (pendingTurns === 0)
|
|
56
|
+
armIdleTimer();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!stopped)
|
|
60
|
+
pollTimer = setTimeout(poll, pollMs);
|
|
61
|
+
}
|
|
62
|
+
function stopInternal() {
|
|
63
|
+
stopped = true;
|
|
64
|
+
clearIdleTimer();
|
|
65
|
+
if (pollTimer) {
|
|
66
|
+
clearTimeout(pollTimer);
|
|
67
|
+
pollTimer = null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
pollTimer = setTimeout(poll, pollMs);
|
|
71
|
+
return {
|
|
72
|
+
stop: stopInternal,
|
|
73
|
+
noteActivity() {
|
|
74
|
+
if (stopped)
|
|
75
|
+
return;
|
|
76
|
+
pendingTurns++;
|
|
77
|
+
clearIdleTimer();
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=idle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idle.js","sourceRoot":"","sources":["../../src/runner/idle.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AA+CxE;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAyB;IACzD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,GAAG,CAAA;IAEjC,IAAI,OAAO,GAAG,KAAK,CAAA;IACnB,IAAI,KAAK,GAAG,KAAK,CAAA;IACjB,IAAI,YAAY,GAAG,CAAC,CAAA;IACpB,iFAAiF;IACjF,IAAI,YAAY,GAAG,CAAC,CAAA;IACpB,IAAI,SAAS,GAAyC,IAAI,CAAA;IAC1D,IAAI,SAAS,GAAyC,IAAI,CAAA;IAE1D,SAAS,cAAc;QACrB,IAAI,SAAS,EAAE,CAAC;YACd,YAAY,CAAC,SAAS,CAAC,CAAA;YACvB,SAAS,GAAG,IAAI,CAAA;QAClB,CAAC;IACH,CAAC;IAED,SAAS,YAAY;QACnB,cAAc,EAAE,CAAA;QAChB,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE;YAC1B,IAAI,KAAK,IAAI,OAAO;gBAAE,OAAM;YAC5B,KAAK,GAAG,IAAI,CAAA;YACZ,YAAY,EAAE,CAAA;YACd,IAAI,CAAC;gBACH,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAA;YAClB,CAAC;YAAC,MAAM,CAAC;gBACP,8CAA8C;YAChD,CAAC;YACD,IAAI,CAAC,WAAW,EAAE,CAAA;QACpB,CAAC,EAAE,IAAI,CAAC,aAAa,CAAC,CAAA;IACxB,CAAC;IAED,SAAS,IAAI;QACX,IAAI,OAAO;YAAE,OAAM;QACnB,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA;QAC1E,YAAY,GAAG,UAAU,CAAA;QAEzB,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,MAAM,MAAM,GAAG,QAAQ,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAA;YAClD,IAAI,SAAS,GAAG,CAAC,CAAA;YACjB,KAAK,MAAM,CAAC,IAAI,MAAM;gBAAE,IAAI,QAAQ,CAAC,CAAC,CAAC;oBAAE,SAAS,EAAE,CAAA;YACpD,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;gBAClB,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC,CAAA;gBACpD,oEAAoE;gBACpE,oDAAoD;gBACpD,IAAI,YAAY,KAAK,CAAC;oBAAE,YAAY,EAAE,CAAA;YACxC,CAAC;QACH,CAAC;QAED,IAAI,CAAC,OAAO;YAAE,SAAS,GAAG,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACpD,CAAC;IAED,SAAS,YAAY;QACnB,OAAO,GAAG,IAAI,CAAA;QACd,cAAc,EAAE,CAAA;QAChB,IAAI,SAAS,EAAE,CAAC;YACd,YAAY,CAAC,SAAS,CAAC,CAAA;YACvB,SAAS,GAAG,IAAI,CAAA;QAClB,CAAC;IACH,CAAC;IAED,SAAS,GAAG,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IAEpC,OAAO;QACL,IAAI,EAAE,YAAY;QAClB,YAAY;YACV,IAAI,OAAO;gBAAE,OAAM;YACnB,YAAY,EAAE,CAAA;YACd,cAAc,EAAE,CAAA;QAClB,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Writable } from 'node:stream';
|
|
2
|
+
import type { InboxLine, InboxUserLine } from '../contract/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Follow-up turns travel through a file, not a socket.
|
|
5
|
+
*
|
|
6
|
+
* `agy_send` appends a line and returns; the runner tails the file and relays to
|
|
7
|
+
* agy's stdin. The MCP server never touches the process, so a client dying
|
|
8
|
+
* mid-send cannot wedge anything.
|
|
9
|
+
*/
|
|
10
|
+
/** Build the stream-json line for a user turn — byte-identical to agy's schema. */
|
|
11
|
+
export declare function userLine(text: string): InboxUserLine;
|
|
12
|
+
/** Append one queued turn. The only thing `agy_send` does. */
|
|
13
|
+
export declare function appendUserTurn(inboxPath: string, text: string): void;
|
|
14
|
+
/**
|
|
15
|
+
* Append the close directive. The relay consumes it and closes stdin, which is
|
|
16
|
+
* how agy is told the conversation is over (EOF ends the process, §6).
|
|
17
|
+
*/
|
|
18
|
+
export declare function appendClose(inboxPath: string): void;
|
|
19
|
+
export interface InboxRead {
|
|
20
|
+
lines: InboxLine[];
|
|
21
|
+
nextOffset: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Read complete lines from `offset` onward. A half-written trailing line is left
|
|
25
|
+
* for the next call, exactly like the events cursor.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readInboxFrom(inboxPath: string, offset: number): InboxRead;
|
|
28
|
+
export interface InboxRelayOptions {
|
|
29
|
+
inboxPath: string;
|
|
30
|
+
/** agy's stdin. Only exists when the job was spawned with a stdin pipe. */
|
|
31
|
+
stdin: Writable;
|
|
32
|
+
pollMs?: number;
|
|
33
|
+
/** Fired after the close directive has been relayed and stdin ended. */
|
|
34
|
+
onClose?: () => void;
|
|
35
|
+
/**
|
|
36
|
+
* Fired each time a user line is actually written to stdin. The idle
|
|
37
|
+
* watchdog hooks this to know a turn just went "in
|
|
38
|
+
* flight", so it never arms its deadline mid-turn.
|
|
39
|
+
*/
|
|
40
|
+
onUserLineSent?: () => void;
|
|
41
|
+
}
|
|
42
|
+
export interface InboxRelay {
|
|
43
|
+
/** Stop polling. Does not close stdin. */
|
|
44
|
+
stop(): void;
|
|
45
|
+
/** Bytes consumed so far. */
|
|
46
|
+
readonly offset: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Tail the inbox and forward user lines to stdin, filtering out control lines.
|
|
50
|
+
*
|
|
51
|
+
* Queued turns only ever queue: agy finishes the in-flight turn before starting
|
|
52
|
+
* the next one, and there is no interrupt path in print mode (§7). Anything that
|
|
53
|
+
* promises otherwise is wrong.
|
|
54
|
+
*/
|
|
55
|
+
export declare function startInboxRelay(opts: InboxRelayOptions): InboxRelay;
|
|
56
|
+
//# sourceMappingURL=inbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../../src/runner/inbox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAG3C,OAAO,KAAK,EAAoB,SAAS,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAEtF;;;;;;GAMG;AAEH,mFAAmF;AACnF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAQpD;AAED,8DAA8D;AAC9D,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAEpE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAGnD;AAWD,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,SAAS,EAAE,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,CAkC1E;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,2EAA2E;IAC3E,KAAK,EAAE,QAAQ,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;CAC5B;AAED,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,IAAI,IAAI,IAAI,CAAA;IACZ,6BAA6B;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,iBAAiB,GAAG,UAAU,CAoDnE"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { appendJsonLine } from '../contract/paths.js';
|
|
3
|
+
/**
|
|
4
|
+
* Follow-up turns travel through a file, not a socket.
|
|
5
|
+
*
|
|
6
|
+
* `agy_send` appends a line and returns; the runner tails the file and relays to
|
|
7
|
+
* agy's stdin. The MCP server never touches the process, so a client dying
|
|
8
|
+
* mid-send cannot wedge anything.
|
|
9
|
+
*/
|
|
10
|
+
/** Build the stream-json line for a user turn — byte-identical to agy's schema. */
|
|
11
|
+
export function userLine(text) {
|
|
12
|
+
return {
|
|
13
|
+
event: 'user',
|
|
14
|
+
message: {
|
|
15
|
+
role: 'user',
|
|
16
|
+
content: [{ type: 'text', text }],
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Append one queued turn. The only thing `agy_send` does. */
|
|
21
|
+
export function appendUserTurn(inboxPath, text) {
|
|
22
|
+
appendJsonLine(inboxPath, userLine(text));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Append the close directive. The relay consumes it and closes stdin, which is
|
|
26
|
+
* how agy is told the conversation is over (EOF ends the process, §6).
|
|
27
|
+
*/
|
|
28
|
+
export function appendClose(inboxPath) {
|
|
29
|
+
const line = { agy_worker_control: 'close', ts: Date.now() };
|
|
30
|
+
appendJsonLine(inboxPath, line);
|
|
31
|
+
}
|
|
32
|
+
function isControlLine(line) {
|
|
33
|
+
return (typeof line === 'object' &&
|
|
34
|
+
line !== null &&
|
|
35
|
+
'agy_worker_control' in line &&
|
|
36
|
+
line.agy_worker_control === 'close');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Read complete lines from `offset` onward. A half-written trailing line is left
|
|
40
|
+
* for the next call, exactly like the events cursor.
|
|
41
|
+
*/
|
|
42
|
+
export function readInboxFrom(inboxPath, offset) {
|
|
43
|
+
let buf;
|
|
44
|
+
try {
|
|
45
|
+
buf = readFileSync(inboxPath);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return { lines: [], nextOffset: offset };
|
|
49
|
+
}
|
|
50
|
+
if (offset < 0 || offset >= buf.length) {
|
|
51
|
+
return { lines: [], nextOffset: Math.max(offset, 0) };
|
|
52
|
+
}
|
|
53
|
+
const text = buf.subarray(offset).toString('utf8');
|
|
54
|
+
const lines = [];
|
|
55
|
+
let consumedBytes = 0;
|
|
56
|
+
let searchFrom = 0;
|
|
57
|
+
for (;;) {
|
|
58
|
+
const nlIdx = text.indexOf('\n', searchFrom);
|
|
59
|
+
if (nlIdx === -1)
|
|
60
|
+
break;
|
|
61
|
+
const rawLine = text.slice(searchFrom, nlIdx);
|
|
62
|
+
consumedBytes += Buffer.byteLength(rawLine, 'utf8') + 1; // +1 for the newline itself
|
|
63
|
+
searchFrom = nlIdx + 1;
|
|
64
|
+
const trimmed = rawLine.trim();
|
|
65
|
+
if (trimmed.length === 0)
|
|
66
|
+
continue;
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(trimmed);
|
|
69
|
+
lines.push(parsed);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Malformed line: drop it, but still consume the bytes so we don't spin on it.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { lines, nextOffset: offset + consumedBytes };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Tail the inbox and forward user lines to stdin, filtering out control lines.
|
|
79
|
+
*
|
|
80
|
+
* Queued turns only ever queue: agy finishes the in-flight turn before starting
|
|
81
|
+
* the next one, and there is no interrupt path in print mode (§7). Anything that
|
|
82
|
+
* promises otherwise is wrong.
|
|
83
|
+
*/
|
|
84
|
+
export function startInboxRelay(opts) {
|
|
85
|
+
const pollMs = opts.pollMs ?? 250;
|
|
86
|
+
let offset = 0;
|
|
87
|
+
let stopped = false;
|
|
88
|
+
let closed = false;
|
|
89
|
+
let timer = null;
|
|
90
|
+
function tick() {
|
|
91
|
+
if (stopped || closed)
|
|
92
|
+
return;
|
|
93
|
+
const { lines, nextOffset } = readInboxFrom(opts.inboxPath, offset);
|
|
94
|
+
offset = nextOffset;
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
if (isControlLine(line)) {
|
|
97
|
+
closed = true;
|
|
98
|
+
try {
|
|
99
|
+
opts.stdin.end();
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// stdin already gone — nothing left to close.
|
|
103
|
+
}
|
|
104
|
+
opts.onClose?.();
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
opts.stdin.write(JSON.stringify(line) + '\n');
|
|
109
|
+
opts.onUserLineSent?.();
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// agy's stdin is gone (process likely exited); stop relaying rather than
|
|
113
|
+
// throwing out of a timer callback.
|
|
114
|
+
stopped = true;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!stopped && !closed) {
|
|
119
|
+
timer = setTimeout(tick, pollMs);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
timer = setTimeout(tick, pollMs);
|
|
123
|
+
return {
|
|
124
|
+
stop() {
|
|
125
|
+
stopped = true;
|
|
126
|
+
if (timer)
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
},
|
|
129
|
+
get offset() {
|
|
130
|
+
return offset;
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=inbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbox.js","sourceRoot":"","sources":["../../src/runner/inbox.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process liveness primitives. Kept apart from the store so both the runner and
|
|
3
|
+
* `reconcile` can use them without dragging SQLite into the runner.
|
|
4
|
+
*
|
|
5
|
+
* Everything here shells out to `ps` / `pgrep` rather than a native addon
|
|
6
|
+
* (keeping native dependency count at 0). Both tools are present on
|
|
7
|
+
* macOS and Linux; nothing here claims Windows support.
|
|
8
|
+
*/
|
|
9
|
+
/** Milliseconds sleep, used by the poll loops in {@link killProcessGroup}. */
|
|
10
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
11
|
+
/** `kill(pid, 0)`. True also for a process we cannot signal (EPERM) — it exists. */
|
|
12
|
+
export declare function isPidAlive(pid: number): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Opaque per-process start token, compared for equality and never parsed.
|
|
15
|
+
*
|
|
16
|
+
* Together with the pid it identifies a *specific* process. Without it a recycled
|
|
17
|
+
* pid looks like a live lock holder and nothing ever gets reclaimed.
|
|
18
|
+
* Returns null when the process is gone.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getProcStartTime(pid: number): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* True only when the pid is alive **and** its start token still matches.
|
|
23
|
+
* A false here is what turns a stuck lock into a reclaimed one.
|
|
24
|
+
*
|
|
25
|
+
* When `procStartTime` is `null` — nothing was ever recorded for it, e.g. `ps`
|
|
26
|
+
* was unavailable at capture time — there is no token to compare against, so this
|
|
27
|
+
* falls back to liveness alone. That is a documented, deliberate weakening: it
|
|
28
|
+
* only matters on a platform where {@link getProcStartTime} never worked in the
|
|
29
|
+
* first place, which is outside the measured surface.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isSameProcess(pid: number, procStartTime: string | null): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Three-way identity check, for callers that must not treat "the process just
|
|
34
|
+
* exited" as "a different process now owns this pid".
|
|
35
|
+
*
|
|
36
|
+
* {@link isSameProcess} deliberately folds both into `false` — for lock
|
|
37
|
+
* reclamation that is right, since neither case is a live holder. `reconcile`
|
|
38
|
+
* needs them apart: `different` is pid reuse (`orphaned`), while `gone` is an
|
|
39
|
+
* ordinary exit whose `exit_code` may be a few milliseconds from landing.
|
|
40
|
+
*/
|
|
41
|
+
export type ProcessIdentity = 'same' | 'different' | 'gone';
|
|
42
|
+
export declare function checkProcessIdentity(pid: number, procStartTime: string | null): ProcessIdentity;
|
|
43
|
+
/** Process-group id of a pid, or null if it is gone. */
|
|
44
|
+
export declare function getPgid(pid: number): number | null;
|
|
45
|
+
/**
|
|
46
|
+
* Pids currently in the group. Must reach 0 after a kill.
|
|
47
|
+
*
|
|
48
|
+
* `pgrep -g <pgid>` is the check used to verify a kill.
|
|
49
|
+
*/
|
|
50
|
+
export declare function listProcessGroupPids(pgid: number): number[];
|
|
51
|
+
export interface KillGroupOptions {
|
|
52
|
+
/** Milliseconds between SIGTERM and SIGKILL. */
|
|
53
|
+
graceMs?: number;
|
|
54
|
+
pollMs?: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `SIGTERM` the whole group, wait out the grace period, then `SIGKILL`.
|
|
58
|
+
*
|
|
59
|
+
* ⚠ Refuse to signal group 0, group 1, or our own group. A negative-pid kill with
|
|
60
|
+
* a wrong argument takes down the caller and everything around it.
|
|
61
|
+
*
|
|
62
|
+
* @returns true when the group is empty afterwards.
|
|
63
|
+
*/
|
|
64
|
+
export declare function killProcessGroup(pgid: number, opts?: KillGroupOptions): Promise<boolean>;
|
|
65
|
+
//# sourceMappingURL=reap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reap.d.ts","sourceRoot":"","sources":["../../src/runner/reap.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,8EAA8E;AAC9E,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED,oFAAoF;AACpF,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAS/C;AAGD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAW3D;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAKhF;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAA;AAE3D,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,eAAe,CAa/F;AAED,wDAAwD;AACxD,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWlD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAgB3D;AAED,MAAM,WAAW,gBAAgB;IAC/B,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAoBD;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAsB9F"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Process liveness primitives. Kept apart from the store so both the runner and
|
|
4
|
+
* `reconcile` can use them without dragging SQLite into the runner.
|
|
5
|
+
*
|
|
6
|
+
* Everything here shells out to `ps` / `pgrep` rather than a native addon
|
|
7
|
+
* (keeping native dependency count at 0). Both tools are present on
|
|
8
|
+
* macOS and Linux; nothing here claims Windows support.
|
|
9
|
+
*/
|
|
10
|
+
/** Milliseconds sleep, used by the poll loops in {@link killProcessGroup}. */
|
|
11
|
+
export function sleep(ms) {
|
|
12
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
}
|
|
14
|
+
/** `kill(pid, 0)`. True also for a process we cannot signal (EPERM) — it exists. */
|
|
15
|
+
export function isPidAlive(pid) {
|
|
16
|
+
try {
|
|
17
|
+
process.kill(pid, 0);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
const code = e?.code;
|
|
22
|
+
if (code === 'EPERM')
|
|
23
|
+
return true;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Opaque per-process start token, compared for equality and never parsed.
|
|
29
|
+
*
|
|
30
|
+
* Together with the pid it identifies a *specific* process. Without it a recycled
|
|
31
|
+
* pid looks like a live lock holder and nothing ever gets reclaimed.
|
|
32
|
+
* Returns null when the process is gone.
|
|
33
|
+
*/
|
|
34
|
+
export function getProcStartTime(pid) {
|
|
35
|
+
try {
|
|
36
|
+
const out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
39
|
+
});
|
|
40
|
+
const trimmed = out.trim();
|
|
41
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* True only when the pid is alive **and** its start token still matches.
|
|
49
|
+
* A false here is what turns a stuck lock into a reclaimed one.
|
|
50
|
+
*
|
|
51
|
+
* When `procStartTime` is `null` — nothing was ever recorded for it, e.g. `ps`
|
|
52
|
+
* was unavailable at capture time — there is no token to compare against, so this
|
|
53
|
+
* falls back to liveness alone. That is a documented, deliberate weakening: it
|
|
54
|
+
* only matters on a platform where {@link getProcStartTime} never worked in the
|
|
55
|
+
* first place, which is outside the measured surface.
|
|
56
|
+
*/
|
|
57
|
+
export function isSameProcess(pid, procStartTime) {
|
|
58
|
+
if (!isPidAlive(pid))
|
|
59
|
+
return false;
|
|
60
|
+
if (procStartTime === null)
|
|
61
|
+
return true;
|
|
62
|
+
const current = getProcStartTime(pid);
|
|
63
|
+
return current !== null && current === procStartTime;
|
|
64
|
+
}
|
|
65
|
+
export function checkProcessIdentity(pid, procStartTime) {
|
|
66
|
+
if (!isPidAlive(pid))
|
|
67
|
+
return 'gone';
|
|
68
|
+
if (procStartTime === null)
|
|
69
|
+
return 'same';
|
|
70
|
+
const current = getProcStartTime(pid);
|
|
71
|
+
// `ps` returning nothing has two causes: the process disappeared between the
|
|
72
|
+
// liveness check and here (`gone`), or `ps` itself failed to run — measured
|
|
73
|
+
// 2026-09-03 while the runner's verify_command was starting vitest and the
|
|
74
|
+
// machine was fork-saturated, which turned a healthy job into
|
|
75
|
+
// `process_error`. Re-checking liveness tells them apart; a live pid whose
|
|
76
|
+
// token cannot be read is treated as `same`, the documented weakening for a
|
|
77
|
+
// missing token, never as a mismatch.
|
|
78
|
+
if (current === null)
|
|
79
|
+
return isPidAlive(pid) ? 'same' : 'gone';
|
|
80
|
+
return current === procStartTime ? 'same' : 'different';
|
|
81
|
+
}
|
|
82
|
+
/** Process-group id of a pid, or null if it is gone. */
|
|
83
|
+
export function getPgid(pid) {
|
|
84
|
+
try {
|
|
85
|
+
const out = execFileSync('ps', ['-o', 'pgid=', '-p', String(pid)], {
|
|
86
|
+
encoding: 'utf8',
|
|
87
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
88
|
+
});
|
|
89
|
+
const n = Number.parseInt(out.trim(), 10);
|
|
90
|
+
return Number.isFinite(n) ? n : null;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Pids currently in the group. Must reach 0 after a kill.
|
|
98
|
+
*
|
|
99
|
+
* `pgrep -g <pgid>` is the check used to verify a kill.
|
|
100
|
+
*/
|
|
101
|
+
export function listProcessGroupPids(pgid) {
|
|
102
|
+
try {
|
|
103
|
+
const out = execFileSync('pgrep', ['-g', String(pgid)], {
|
|
104
|
+
encoding: 'utf8',
|
|
105
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
106
|
+
});
|
|
107
|
+
return out
|
|
108
|
+
.split('\n')
|
|
109
|
+
.map((s) => s.trim())
|
|
110
|
+
.filter((s) => s.length > 0)
|
|
111
|
+
.map((s) => Number.parseInt(s, 10))
|
|
112
|
+
.filter((n) => Number.isFinite(n));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// pgrep exits non-zero when nothing matches. That is "empty group", not an error.
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function guardTargetPgid(pgid) {
|
|
120
|
+
if (!Number.isInteger(pgid) || pgid <= 1) {
|
|
121
|
+
throw new Error(`refusing to signal process group ${pgid}: group 0 and group 1 are off-limits`);
|
|
122
|
+
}
|
|
123
|
+
const ownPgid = getPgid(process.pid);
|
|
124
|
+
if (ownPgid !== null && ownPgid === pgid) {
|
|
125
|
+
throw new Error(`refusing to signal our own process group ${pgid}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function trySignal(pgid, signal) {
|
|
129
|
+
try {
|
|
130
|
+
process.kill(-pgid, signal);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// ESRCH: group already empty. Nothing to do.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* `SIGTERM` the whole group, wait out the grace period, then `SIGKILL`.
|
|
138
|
+
*
|
|
139
|
+
* ⚠ Refuse to signal group 0, group 1, or our own group. A negative-pid kill with
|
|
140
|
+
* a wrong argument takes down the caller and everything around it.
|
|
141
|
+
*
|
|
142
|
+
* @returns true when the group is empty afterwards.
|
|
143
|
+
*/
|
|
144
|
+
export async function killProcessGroup(pgid, opts) {
|
|
145
|
+
const graceMs = opts?.graceMs ?? 5000;
|
|
146
|
+
const pollMs = opts?.pollMs ?? 200;
|
|
147
|
+
guardTargetPgid(pgid);
|
|
148
|
+
if (listProcessGroupPids(pgid).length === 0)
|
|
149
|
+
return true;
|
|
150
|
+
trySignal(pgid, 'SIGTERM');
|
|
151
|
+
const termDeadline = Date.now() + graceMs;
|
|
152
|
+
while (Date.now() < termDeadline) {
|
|
153
|
+
if (listProcessGroupPids(pgid).length === 0)
|
|
154
|
+
return true;
|
|
155
|
+
await sleep(pollMs);
|
|
156
|
+
}
|
|
157
|
+
if (listProcessGroupPids(pgid).length === 0)
|
|
158
|
+
return true;
|
|
159
|
+
trySignal(pgid, 'SIGKILL');
|
|
160
|
+
const killDeadline = Date.now() + Math.max(pollMs * 3, 1000);
|
|
161
|
+
while (Date.now() < killDeadline) {
|
|
162
|
+
if (listProcessGroupPids(pgid).length === 0)
|
|
163
|
+
return true;
|
|
164
|
+
await sleep(pollMs);
|
|
165
|
+
}
|
|
166
|
+
return listProcessGroupPids(pgid).length === 0;
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=reap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reap.js","sourceRoot":"","sources":["../../src/runner/reap.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runner: a standalone process that outlives whichever MCP server started it.
|
|
3
|
+
*
|
|
4
|
+
* ⚠ This module must not import anything from `src/server/**`. It has to run when
|
|
5
|
+
* every server process is dead — that is its entire reason to exist.
|
|
6
|
+
*/
|
|
7
|
+
export interface RunJobOptions {
|
|
8
|
+
jobId: string;
|
|
9
|
+
/** Canonical project root; the runner re-derives its own paths from this. */
|
|
10
|
+
projectRoot: string;
|
|
11
|
+
}
|
|
12
|
+
export interface RunJobResult {
|
|
13
|
+
jobId: string;
|
|
14
|
+
exitCode: number;
|
|
15
|
+
/** True when the deadline fired and the process group was killed. */
|
|
16
|
+
timedOut: boolean;
|
|
17
|
+
durationMs: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Run one job to completion.
|
|
21
|
+
*
|
|
22
|
+
* Sequence: read `effective-config.json` → spawn detached with file-backed stdio →
|
|
23
|
+
* start the inbox relay when `session_mode === 'session'` → wait for exit, racing
|
|
24
|
+
* `deadline_at` → run `config.verify` once, if set and the deadline did not fire
|
|
25
|
+
* (PR6 §6.2) → **always** write the final `jobs/<id>/state.json`, then
|
|
26
|
+
* `jobs/<id>/exit_code`.
|
|
27
|
+
*
|
|
28
|
+
* `exit_code` existing is the finalization signal every server polls for; if
|
|
29
|
+
* the runner can die without writing it, `reconcile` has to guess, and a job
|
|
30
|
+
* that finished cleanly gets reported as `process_error`. Writing the final
|
|
31
|
+
* `state.json` *first* is what makes that signal trustworthy the instant it
|
|
32
|
+
* appears — see the comment at the write site.
|
|
33
|
+
*/
|
|
34
|
+
export declare function runJob(opts: RunJobOptions): Promise<RunJobResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Enforce the deadline: `SIGTERM` to the process group, grace period, then
|
|
37
|
+
* `SIGKILL`. Signalling the *group* rather than the pid is what reaches agy's own
|
|
38
|
+
* children (verified by checking `pgrep -g` afterwards).
|
|
39
|
+
*
|
|
40
|
+
* Standalone entry point for callers other than {@link runJob} (e.g. `reconcile`
|
|
41
|
+
* discovering a job whose deadline has already passed). `runJob` itself does its
|
|
42
|
+
* own cancellable wait so it is not left holding a dangling timer once the
|
|
43
|
+
* process exits on its own — this function, called directly, always waits out
|
|
44
|
+
* the deadline first.
|
|
45
|
+
*/
|
|
46
|
+
export declare function enforceDeadline(pgid: number, deadlineAt: number, graceMs?: number): Promise<boolean>;
|
|
47
|
+
/** Write the exit code file atomically. Called on every exit path, including throws. */
|
|
48
|
+
export declare function writeExitCode(exitCodePath: string, code: number): void;
|
|
49
|
+
/** `dist/runner.js` entry point. Reads the job id from argv or `AGY_WORKER_JOB_ID`. */
|
|
50
|
+
export declare function main(argv?: string[]): Promise<number>;
|
|
51
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/runner/runner.ts"],"names":[],"mappings":"AAoBA;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,qEAAqE;IACrE,QAAQ,EAAE,OAAO,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;CACnB;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAwOvE;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAI1G;AAED,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAEtE;AAED,uFAAuF;AACvF,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAmB/D"}
|