@fitchmultz/pi-subagents 0.52.19 → 0.52.21
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 +44 -0
- package/dist/agent/contracts.js +12 -3
- package/dist/agent/index.js +9 -9
- package/dist/agent/legacy.js +5 -5
- package/dist/agent/notifications.js +21 -14
- package/dist/agent/parent-check.js +38 -0
- package/dist/agent/process.js +45 -52
- package/dist/agent/runner.js +59 -64
- package/dist/agent/storage.js +16 -4
- package/dist/agent/view.js +1 -3
- package/dist/build-info.js +1 -1
- package/dist/pi-intercom/bridge-presence.js +30 -18
- package/dist/pi-intercom/bridge-wake.js +60 -37
- package/dist/pi-intercom/broker/broker.js +4 -26
- package/dist/pi-intercom/broker/client-last.js +6 -18
- package/dist/pi-intercom/broker/client-park.d.ts +1 -1
- package/dist/pi-intercom/broker/client-park.js +22 -6
- package/dist/pi-intercom/broker/client.d.ts +3 -2
- package/dist/pi-intercom/broker/client.js +104 -112
- package/dist/pi-intercom/broker/connection.d.ts +3 -5
- package/dist/pi-intercom/broker/connection.js +48 -33
- package/dist/pi-intercom/broker/framing.d.ts +1 -1
- package/dist/pi-intercom/broker/framing.js +19 -12
- package/dist/pi-intercom/broker/pending-requests.d.ts +12 -0
- package/dist/pi-intercom/broker/pending-requests.js +26 -0
- package/dist/pi-intercom/broker/replace-deliveries.js +38 -0
- package/dist/pi-intercom/broker/snapshot-reader.d.ts +3 -2
- package/dist/pi-intercom/broker/snapshot-reader.js +34 -15
- package/dist/pi-intercom/connection.js +5 -8
- package/dist/pi-intercom/inbound-journal.js +5 -2
- package/dist/shared/journal-frames.js +19 -24
- package/docs/development.md +6 -0
- package/package.json +1 -1
- package/src/agent/contracts.ts +12 -3
- package/src/agent/index.ts +10 -10
- package/src/agent/legacy.ts +5 -5
- package/src/agent/notifications.ts +35 -15
- package/src/agent/parent-check.ts +40 -0
- package/src/agent/process.ts +53 -55
- package/src/agent/runner.ts +65 -65
- package/src/agent/storage.ts +18 -5
- package/src/agent/view.ts +1 -4
- package/src/pi-intercom/bridge-presence.ts +37 -18
- package/src/pi-intercom/bridge-wake.ts +70 -38
- package/src/pi-intercom/broker/broker.ts +5 -37
- package/src/pi-intercom/broker/client-last.ts +14 -25
- package/src/pi-intercom/broker/client-park.ts +27 -6
- package/src/pi-intercom/broker/client.ts +127 -126
- package/src/pi-intercom/broker/connection.ts +58 -39
- package/src/pi-intercom/broker/framing.ts +24 -12
- package/src/pi-intercom/broker/pending-requests.ts +37 -0
- package/src/pi-intercom/broker/replace-deliveries.ts +56 -0
- package/src/pi-intercom/broker/snapshot-reader.ts +44 -16
- package/src/pi-intercom/connection.ts +9 -9
- package/src/pi-intercom/inbound-journal.ts +5 -2
- package/src/shared/journal-frames.ts +22 -23
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## 0.52.21 - 2026-11-07
|
|
6
|
+
|
|
7
|
+
<!-- npm-release:unreleased:start -->
|
|
8
|
+
|
|
9
|
+
- Join broker socket acquisition during disconnect, share repeated close work, and make registration and parking acknowledgement ownership explicit.
|
|
10
|
+
- Make socket framing failures terminal and represent JSONL record parsing as blank, reading, or invalid without changing publication policies.
|
|
11
|
+
- Represent actor work as idle or running with its exact launch and task; validate command payloads by action.
|
|
12
|
+
- Name absent, launcher-only, live, exited, and uncertain actor claims while keeping shell evidence and signal-time birth checks separate.
|
|
13
|
+
- Give broker-staged replacement messages one timer owner and name recipient-side supersession before native handoff.
|
|
14
|
+
- Share explicit completion-delivery decisions between canonical and legacy results while keeping native file and memory receipts distinct.
|
|
15
|
+
|
|
16
|
+
<!-- npm-release:unreleased:end -->
|
|
17
|
+
|
|
18
|
+
- refactor: give staged replacement delivery one owner (#358) ([f292627](https://github.com/fitchmultz/pi-subagents/commit/f292627a357a5eb99cc9ad2e7a1e9b9622eaa082))
|
|
19
|
+
|
|
20
|
+
### What's Changed
|
|
21
|
+
|
|
22
|
+
- refactor: give staged replacement delivery one owner by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/358
|
|
23
|
+
- refactor: make actor work and command payload states explicit by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/345
|
|
24
|
+
- refactor: share completion delivery decisions by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/357
|
|
25
|
+
- refactor: make framing and JSONL record states explicit by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/359
|
|
26
|
+
- refactor: share process claim inspection by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/364
|
|
27
|
+
- refactor: own socket acquisition through joined disconnect by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/362
|
|
28
|
+
|
|
29
|
+
**Full Changelog**: https://github.com/fitchmultz/pi-subagents/compare/v0.52.20...v0.52.21
|
|
30
|
+
|
|
31
|
+
## 0.52.20 - 2026-11-07
|
|
32
|
+
|
|
33
|
+
<!-- npm-release:unreleased:start -->
|
|
34
|
+
|
|
35
|
+
- Give bridge wake and presence schedulers explicit timer, running, and stopped ownership while retaining durable wake policy.
|
|
36
|
+
- Centralize broker request settlement and close multipart snapshot readers after completion or failure.
|
|
37
|
+
|
|
38
|
+
<!-- npm-release:unreleased:end -->
|
|
39
|
+
|
|
40
|
+
- refactor: centralize request settlement and snapshot phases (#354) ([335c7e2](https://github.com/fitchmultz/pi-subagents/commit/335c7e269936467672f7e8d70cc67bf875a0a1c6))
|
|
41
|
+
|
|
42
|
+
### What's Changed
|
|
43
|
+
|
|
44
|
+
- refactor: centralize broker request settlement by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/354
|
|
45
|
+
- refactor: make bridge scheduler ownership explicit by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/355
|
|
46
|
+
|
|
47
|
+
**Full Changelog**: https://github.com/fitchmultz/pi-subagents/compare/v0.52.19...v0.52.20
|
|
48
|
+
|
|
5
49
|
## 0.52.19 - 2026-11-07
|
|
6
50
|
|
|
7
51
|
<!-- npm-release:unreleased:start -->
|
package/dist/agent/contracts.js
CHANGED
|
@@ -80,14 +80,23 @@ export const resultSchema = Type.Object({
|
|
|
80
80
|
baseline: Type.Array(Type.String()),
|
|
81
81
|
})),
|
|
82
82
|
});
|
|
83
|
-
export const
|
|
83
|
+
export const commandMetadataSchema = Type.Object({
|
|
84
84
|
id: Type.String(),
|
|
85
85
|
runId: Type.String(),
|
|
86
86
|
action: Type.Union([Type.Literal("run"), Type.Literal("steer"), Type.Literal("stop")]),
|
|
87
|
-
prompt: Type.Optional(Type.String()),
|
|
88
|
-
actorToken: Type.Optional(Type.String()),
|
|
89
87
|
createdAt: Type.Number(),
|
|
90
88
|
});
|
|
89
|
+
const commandFields = Type.Omit(commandMetadataSchema, ["action"]).properties;
|
|
90
|
+
export const commandSchema = Type.Union([
|
|
91
|
+
Type.Object({ ...commandFields, action: Type.Literal("run") }),
|
|
92
|
+
Type.Object({ ...commandFields, action: Type.Literal("steer"), prompt: Type.String() }),
|
|
93
|
+
Type.Object({
|
|
94
|
+
...commandFields,
|
|
95
|
+
action: Type.Literal("stop"),
|
|
96
|
+
// Historical unbound Stops parse but cannot target any current actor.
|
|
97
|
+
actorToken: Type.Optional(Type.String()),
|
|
98
|
+
}),
|
|
99
|
+
]);
|
|
91
100
|
export const successorSchema = Type.Object({ id: Type.String() });
|
|
92
101
|
/** Authority to restore exact working state, valid only after the named actor has exited. */
|
|
93
102
|
/** Exact parked authority: one exited generation's state bytes for this session and leaf. */
|
package/dist/agent/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import * as loadedSDK from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import {
|
|
5
|
+
import { inspectActorClaim, possiblyLive, ensureActor, ownIdentity, parkFailure, requestStop, signalProcess, } from "./process.js";
|
|
6
6
|
import { stopChildren } from "./shutdown.js";
|
|
7
7
|
import { isChild, errorText, managedHost, rememberSDK, retainsChildren, } from "./host.js";
|
|
8
8
|
import { serveParked } from "./parking.js";
|
|
@@ -261,7 +261,7 @@ class AgentRuntime {
|
|
|
261
261
|
executionCwd: executionCwd(this.pi, context),
|
|
262
262
|
});
|
|
263
263
|
this.checkMain();
|
|
264
|
-
enqueue(this.base, launch, "run");
|
|
264
|
+
enqueue(this.base, launch, { action: "run" });
|
|
265
265
|
await ensureActor(this.base, launch, this.shutdown.signal);
|
|
266
266
|
return renderReceipt(this.base, launch, this.ctx, {
|
|
267
267
|
receipt: "launch",
|
|
@@ -286,10 +286,10 @@ class AgentRuntime {
|
|
|
286
286
|
return;
|
|
287
287
|
}
|
|
288
288
|
async retryContinuation(launch, input) {
|
|
289
|
-
const
|
|
289
|
+
const claim = inspectActorClaim(this.base, launch);
|
|
290
290
|
const command = read(path.join(conversation(this.base, launch), "commands", `${launch.id}.json`), commandSchema);
|
|
291
291
|
if (input.prompt !== launch.prompt ||
|
|
292
|
-
(
|
|
292
|
+
possiblyLive(claim) ||
|
|
293
293
|
command?.id !== launch.id ||
|
|
294
294
|
command.runId !== launch.id ||
|
|
295
295
|
command.action !== "run") {
|
|
@@ -310,9 +310,9 @@ class AgentRuntime {
|
|
|
310
310
|
if (completed === "pending") {
|
|
311
311
|
return this.retryContinuation(launch, input);
|
|
312
312
|
}
|
|
313
|
-
const
|
|
314
|
-
if (
|
|
315
|
-
const command = enqueue(this.base, launch, "steer",
|
|
313
|
+
const claim = completed ? undefined : inspectActorClaim(this.base, launch);
|
|
314
|
+
if (possiblyLive(claim)) {
|
|
315
|
+
const command = enqueue(this.base, launch, { action: "steer", prompt: input.prompt });
|
|
316
316
|
return renderReceipt(this.base, launch, this.ctx, {
|
|
317
317
|
receipt: "guidance",
|
|
318
318
|
command,
|
|
@@ -327,7 +327,7 @@ class AgentRuntime {
|
|
|
327
327
|
});
|
|
328
328
|
}
|
|
329
329
|
// Retain successor claims on ambiguous after-launch failures: no replay.
|
|
330
|
-
enqueue(this.base, next, "run");
|
|
330
|
+
enqueue(this.base, next, { action: "run" });
|
|
331
331
|
await ensureActor(this.base, next, this.shutdown.signal);
|
|
332
332
|
return renderReceipt(this.base, next, this.ctx, { receipt: "continuation" });
|
|
333
333
|
}
|
|
@@ -344,7 +344,7 @@ class AgentRuntime {
|
|
|
344
344
|
});
|
|
345
345
|
if (created) {
|
|
346
346
|
// The durable adoption claim survives ambiguous after-launch failures.
|
|
347
|
-
enqueue(this.base, launch, "run");
|
|
347
|
+
enqueue(this.base, launch, { action: "run" });
|
|
348
348
|
await ensureActor(this.base, launch, this.shutdown.signal);
|
|
349
349
|
}
|
|
350
350
|
return renderReceipt(this.base, launch, this.ctx, {
|
package/dist/agent/legacy.js
CHANGED
|
@@ -5,7 +5,7 @@ import { journalStamp } from "../shared/native-journal.js";
|
|
|
5
5
|
import { receiptId } from "./receipts.js";
|
|
6
6
|
import { resolveTempRootDir } from "../shared/types.js";
|
|
7
7
|
import { isRecord } from "../shared/unknown.js";
|
|
8
|
-
import { release,
|
|
8
|
+
import { release, completionDelivery } from "./notifications.js";
|
|
9
9
|
import { missing } from "./storage.js";
|
|
10
10
|
// Retain only foreign classifications, never their saved output, for each live native manager.
|
|
11
11
|
const ignoredInputs = new WeakMap();
|
|
@@ -102,19 +102,19 @@ function published(ctx, input) {
|
|
|
102
102
|
}
|
|
103
103
|
function notification(ctx, input) {
|
|
104
104
|
const identity = `legacy:${input.id}:${input.key}`;
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
105
|
+
const state = completionDelivery(input.owner, identity, published(ctx, input));
|
|
106
|
+
if (state.kind === "published") {
|
|
107
107
|
release(input.owner, identity);
|
|
108
108
|
// Retire only an unchanged recovery input whose receipt is LF-published in the session file.
|
|
109
109
|
// An in-memory receipt stops redelivery but never removes the saved input.
|
|
110
|
-
if (where === "file" &&
|
|
110
|
+
if (state.where === "file" &&
|
|
111
111
|
journalStamp(fs.lstatSync(input.file, { bigint: true })) === input.stamp &&
|
|
112
112
|
fs.readFileSync(input.file, "utf8") === input.raw) {
|
|
113
113
|
fs.unlinkSync(input.file);
|
|
114
114
|
}
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
|
-
if (
|
|
117
|
+
if (state.kind === "held") {
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
120
|
return {
|
|
@@ -25,18 +25,19 @@ function queuedAdmissions() {
|
|
|
25
25
|
}
|
|
26
26
|
const admissions = queuedAdmissions();
|
|
27
27
|
function hold(owner, id) {
|
|
28
|
-
|
|
29
|
-
if (admissions.has(key)) {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
admissions.add(key);
|
|
33
|
-
return true;
|
|
28
|
+
admissions.add(`${owner}/${id}`);
|
|
34
29
|
}
|
|
35
30
|
export function release(owner, id) {
|
|
36
31
|
admissions.delete(`${owner}/${id}`);
|
|
37
32
|
}
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
/** Canonical result readers establish availability before deciding delivery. */
|
|
34
|
+
function deliveryDecision(receipt, held) {
|
|
35
|
+
return receipt === undefined
|
|
36
|
+
? { kind: held ? "held" : "deliverable" }
|
|
37
|
+
: { kind: "published", where: receipt };
|
|
38
|
+
}
|
|
39
|
+
export function completionDelivery(owner, id, receipt) {
|
|
40
|
+
return deliveryDecision(receipt, admissions.has(`${owner}/${id}`));
|
|
40
41
|
}
|
|
41
42
|
/** Returns whether any admission was released for reconciliation. */
|
|
42
43
|
export function settle(owner, pending) {
|
|
@@ -197,14 +198,20 @@ export function notifications(ctx, base, owner) {
|
|
|
197
198
|
const awaited = new Set();
|
|
198
199
|
const runs = completed(base, owner, awaited);
|
|
199
200
|
const receipts = runs.length === 0 ? undefined : completionReceipts(ctx.sessionManager, owner);
|
|
201
|
+
const where = ctx.sessionManager.getSessionFile() === undefined ? "memory" : "file";
|
|
200
202
|
for (const run of runs) {
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
const state = completionDelivery(owner, run.id, receipts?.get(run.id) === undefined ? undefined : where);
|
|
204
|
+
switch (state.kind) {
|
|
205
|
+
case "published":
|
|
206
|
+
release(owner, run.id);
|
|
207
|
+
continue;
|
|
208
|
+
case "held":
|
|
209
|
+
continue;
|
|
210
|
+
case "deliverable":
|
|
211
|
+
break;
|
|
205
212
|
}
|
|
206
|
-
const loaded =
|
|
207
|
-
if (loaded
|
|
213
|
+
const loaded = run.loaded ?? load(base, run.id, owner);
|
|
214
|
+
if (loaded.result !== undefined) {
|
|
208
215
|
notices.push(notification(base, loaded.launch, loaded.result));
|
|
209
216
|
}
|
|
210
217
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { attachmentSchema } from "./contracts.js";
|
|
2
|
+
import { observeAlive, published } from "./process.js";
|
|
3
|
+
import { read } from "./storage.js";
|
|
4
|
+
/** A parent liveness verdict acts only while its check and attachment generation are current. */
|
|
5
|
+
export class ParentCheck {
|
|
6
|
+
current;
|
|
7
|
+
check(file, detached) {
|
|
8
|
+
if (this.current !== undefined) {
|
|
9
|
+
return Promise.resolve();
|
|
10
|
+
}
|
|
11
|
+
const attachment = published(file, read(file, attachmentSchema));
|
|
12
|
+
if (attachment === undefined || !attachment.attached) {
|
|
13
|
+
detached();
|
|
14
|
+
return Promise.resolve();
|
|
15
|
+
}
|
|
16
|
+
const check = {};
|
|
17
|
+
this.current = check;
|
|
18
|
+
const settle = async () => {
|
|
19
|
+
try {
|
|
20
|
+
const [live] = await observeAlive([attachment]);
|
|
21
|
+
if (this.current === check &&
|
|
22
|
+
live !== true &&
|
|
23
|
+
read(file, attachmentSchema)?.generation === attachment.generation) {
|
|
24
|
+
detached();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
if (this.current === check) {
|
|
29
|
+
this.current = undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return settle();
|
|
34
|
+
}
|
|
35
|
+
cancel() {
|
|
36
|
+
this.current = undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/agent/process.js
CHANGED
|
@@ -7,7 +7,7 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
7
7
|
import { isChildProcessReplaced, observeChildProcessReplaced, readChildProcessIdentity, readProcessClock, trySignalChildTree, } from "../shared/post-exit-stdio-guard.js";
|
|
8
8
|
import { agentObservationSocketPath } from "../pi-intercom/broker/paths.js";
|
|
9
9
|
import { hasErrorCode } from "../shared/unknown.js";
|
|
10
|
-
import {
|
|
10
|
+
import { parkedSchema, processSchema, wakeFailureSchema, } from "./contracts.js";
|
|
11
11
|
import { conversation, enqueue, read, write } from "./storage.js";
|
|
12
12
|
import { errorText } from "./host.js";
|
|
13
13
|
/** Owner records follow their process's birth; stat after read only moves the bound later. */
|
|
@@ -27,10 +27,19 @@ function probe(identity) {
|
|
|
27
27
|
}
|
|
28
28
|
return identity.identity === "unverified" ? true : undefined;
|
|
29
29
|
}
|
|
30
|
+
function presence(identity) {
|
|
31
|
+
const probed = probe(identity);
|
|
32
|
+
if (probed !== undefined) {
|
|
33
|
+
return probed ? "uncertain" : "exited";
|
|
34
|
+
}
|
|
35
|
+
if (readChildProcessIdentity(identity.pid) === identity.identity) {
|
|
36
|
+
return "live";
|
|
37
|
+
}
|
|
38
|
+
return replaced(identity) ? "exited" : "uncertain";
|
|
39
|
+
}
|
|
30
40
|
export function alive(identity) {
|
|
31
|
-
//
|
|
32
|
-
return (
|
|
33
|
-
(readChildProcessIdentity(identity.pid) === identity.identity || !replaced(identity)));
|
|
41
|
+
// Uncertain identity remains possibly owned; only death/replacement authorizes reuse.
|
|
42
|
+
return presence(identity) !== "exited";
|
|
34
43
|
}
|
|
35
44
|
/** Routine `alive` without waiting on `ps`; signals still use the synchronous proof. */
|
|
36
45
|
export async function observeAlive(identities) {
|
|
@@ -42,50 +51,6 @@ export async function observeAlive(identities) {
|
|
|
42
51
|
}
|
|
43
52
|
return verdicts;
|
|
44
53
|
}
|
|
45
|
-
/**
|
|
46
|
-
* A child's routine parent check, one observation at a time. A dead parent detaches the child
|
|
47
|
-
* only while its check is current and the attachment generation is unchanged: a newer parent
|
|
48
|
-
* may adopt the child meanwhile.
|
|
49
|
-
*/
|
|
50
|
-
export class ParentCheck {
|
|
51
|
-
current;
|
|
52
|
-
/**
|
|
53
|
-
* Reads the attachment synchronously, so an unreadable one still fails the caller at once.
|
|
54
|
-
* Missing or released attachments detach immediately; a check in flight makes this a no-op.
|
|
55
|
-
*/
|
|
56
|
-
check(file, detached) {
|
|
57
|
-
if (this.current !== undefined) {
|
|
58
|
-
return Promise.resolve();
|
|
59
|
-
}
|
|
60
|
-
const attachment = published(file, read(file, attachmentSchema));
|
|
61
|
-
if (attachment === undefined || !attachment.attached) {
|
|
62
|
-
detached();
|
|
63
|
-
return Promise.resolve();
|
|
64
|
-
}
|
|
65
|
-
const check = {};
|
|
66
|
-
this.current = check;
|
|
67
|
-
const settle = async () => {
|
|
68
|
-
try {
|
|
69
|
-
const [live] = await observeAlive([attachment]);
|
|
70
|
-
if (this.current === check &&
|
|
71
|
-
live !== true &&
|
|
72
|
-
read(file, attachmentSchema)?.generation === attachment.generation) {
|
|
73
|
-
detached();
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
finally {
|
|
77
|
-
if (this.current === check) {
|
|
78
|
-
this.current = undefined;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
return settle();
|
|
83
|
-
}
|
|
84
|
-
/** A check settling later no longer acts. */
|
|
85
|
-
cancel() {
|
|
86
|
-
this.current = undefined;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
54
|
function replaced(identity) {
|
|
90
55
|
return (identity.identity !== "unverified" &&
|
|
91
56
|
isChildProcessReplaced(identity.pid, identity.identity, identity.published, identity.clock));
|
|
@@ -103,6 +68,33 @@ export function actor(base, launch) {
|
|
|
103
68
|
const file = path.join(conversation(base, launch), "actor.lock", "process.json");
|
|
104
69
|
return published(file, read(file, processSchema));
|
|
105
70
|
}
|
|
71
|
+
export function possiblyLive(claim) {
|
|
72
|
+
return (claim?.kind === "actor-live" || (claim?.kind === "uncertain" && claim.process !== undefined));
|
|
73
|
+
}
|
|
74
|
+
/** Launcher claims do not prove actor identity; shell ownership is checked separately. */
|
|
75
|
+
export function inspectActorClaim(base, launch) {
|
|
76
|
+
const process = actor(base, launch);
|
|
77
|
+
if (process !== undefined) {
|
|
78
|
+
switch (presence(process)) {
|
|
79
|
+
case "live":
|
|
80
|
+
return { kind: "actor-live", process };
|
|
81
|
+
case "exited":
|
|
82
|
+
return { kind: "actor-exited", process };
|
|
83
|
+
case "uncertain":
|
|
84
|
+
return { kind: "uncertain", process };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const lock = path.join(conversation(base, launch), "actor.lock");
|
|
88
|
+
if (!fs.existsSync(lock)) {
|
|
89
|
+
return { kind: "absent", process: undefined };
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
kind: read(path.join(lock, "claim.json"), processSchema) === undefined
|
|
93
|
+
? "uncertain"
|
|
94
|
+
: "launcher-only",
|
|
95
|
+
process: undefined,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
106
98
|
function childEnvironment(launch) {
|
|
107
99
|
const env = { ...process.env };
|
|
108
100
|
for (const key of Object.keys(env)) {
|
|
@@ -248,8 +240,9 @@ export async function ensureActor(base, launch, signal) {
|
|
|
248
240
|
await awaitStopping(dir, actor(base, launch), signal);
|
|
249
241
|
signal?.throwIfAborted();
|
|
250
242
|
// Another parallel tool may replace the claim while this invocation yields.
|
|
251
|
-
const
|
|
252
|
-
|
|
243
|
+
const claim = inspectActorClaim(base, launch);
|
|
244
|
+
const existing = claim.process;
|
|
245
|
+
if (possiblyLive(claim)) {
|
|
253
246
|
return;
|
|
254
247
|
}
|
|
255
248
|
parkFailure(base, launch, existing);
|
|
@@ -372,12 +365,12 @@ export function requestStop(base, launch, onError) {
|
|
|
372
365
|
return false;
|
|
373
366
|
}
|
|
374
367
|
};
|
|
375
|
-
const identity =
|
|
368
|
+
const identity = inspectActorClaim(base, launch).process;
|
|
376
369
|
const targets = identity ? [identity, ...shells(base, launch, identity.token)] : [];
|
|
377
370
|
// Interactive Stop validates the whole set; shutdown reports and skips only ambiguous targets.
|
|
378
371
|
const live = targets.filter((target) => attempt(() => verifyProcess(target)));
|
|
379
372
|
const command = identity && live.includes(identity)
|
|
380
|
-
? enqueue(base, launch, "stop",
|
|
373
|
+
? enqueue(base, launch, { action: "stop", actorToken: identity.token })
|
|
381
374
|
: undefined;
|
|
382
375
|
if (identity && live.length > 0) {
|
|
383
376
|
write(path.join(conversation(base, launch), `stopping-${identity.token}.json`), {
|
package/dist/agent/runner.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { commandSchema, parkedSchema, } from "./contracts.js";
|
|
4
|
-
import {
|
|
5
|
-
import { ownIdentity,
|
|
4
|
+
import { commandIdentity, launchAt, read, runDirectory, write } from "./storage.js";
|
|
5
|
+
import { ownIdentity, shells, signalProcess } from "./process.js";
|
|
6
|
+
import { ParentCheck } from "./parent-check.js";
|
|
6
7
|
import { errorText, loadSDK } from "./host.js";
|
|
7
8
|
import { Parking } from "./parking.js";
|
|
8
9
|
import { accountingFields, UsageIndex } from "./usage.js";
|
|
@@ -10,18 +11,6 @@ import { initializeCwd } from "./cwd.js";
|
|
|
10
11
|
import { childRuntime } from "./child-runtime.js";
|
|
11
12
|
import { ActorObservation } from "./observation.js";
|
|
12
13
|
import { DirectoryWatch, Wake } from "./wake.js";
|
|
13
|
-
function commandIdentity(file) {
|
|
14
|
-
try {
|
|
15
|
-
const stat = fs.statSync(file, { bigint: true });
|
|
16
|
-
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
17
|
-
}
|
|
18
|
-
catch (error) {
|
|
19
|
-
if (missing(error)) {
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
throw error;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
14
|
function completionStatus(stopping, message, error) {
|
|
26
15
|
if (stopping || message?.stopReason === "aborted") {
|
|
27
16
|
return "stopped";
|
|
@@ -36,7 +25,9 @@ const failureReason = (error, message) => error ??
|
|
|
36
25
|
message?.errorMessage ??
|
|
37
26
|
(message ? undefined : "Native run produced no terminal assistant message.");
|
|
38
27
|
class Actor {
|
|
39
|
-
|
|
28
|
+
work = {
|
|
29
|
+
kind: "idle",
|
|
30
|
+
};
|
|
40
31
|
stopping = false;
|
|
41
32
|
stopDeadline;
|
|
42
33
|
nextParentCheck = 0;
|
|
@@ -46,7 +37,6 @@ class Actor {
|
|
|
46
37
|
wake = new Wake();
|
|
47
38
|
// A deferred run waits for an idle session, which has no command-file event.
|
|
48
39
|
retry = false;
|
|
49
|
-
current;
|
|
50
40
|
errors = [];
|
|
51
41
|
started = new Set();
|
|
52
42
|
// Actor-local validation metadata only; never retain command prompts or receipt authority.
|
|
@@ -130,7 +120,10 @@ class Actor {
|
|
|
130
120
|
this.stopping = true;
|
|
131
121
|
this.wake.raise();
|
|
132
122
|
write(path.join(this.dir, `stopping-${this.token}.json`), { at: Date.now() });
|
|
133
|
-
write(path.join(this.dir, "stop-acknowledged.json"), {
|
|
123
|
+
write(path.join(this.dir, "stop-acknowledged.json"), {
|
|
124
|
+
at: Date.now(),
|
|
125
|
+
id: this.work.kind === "running" ? this.work.launch.id : undefined,
|
|
126
|
+
});
|
|
134
127
|
this.session?.abort().catch((error) => {
|
|
135
128
|
this.errors.push(errorText(error));
|
|
136
129
|
});
|
|
@@ -162,29 +155,29 @@ class Actor {
|
|
|
162
155
|
if (!session) {
|
|
163
156
|
throw new Error("Native child session is not ready.");
|
|
164
157
|
}
|
|
165
|
-
this.current = launch;
|
|
166
158
|
const baseline = new Set(session.sessionManager.getEntries().map((entry) => entry.id));
|
|
159
|
+
const task = Promise.resolve().then(() => this.executeRun(launch, session, baseline));
|
|
160
|
+
this.work = { kind: "running", launch, task };
|
|
167
161
|
this.observation?.working(true);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
await session.prompt(launch.prompt, { expandPromptTemplates: false });
|
|
176
|
-
await session.waitForIdle();
|
|
177
|
-
this.saveResult(launch, baseline);
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
this.saveResult(launch, baseline, errorText(error));
|
|
181
|
-
}
|
|
182
|
-
finally {
|
|
183
|
-
this.active = undefined;
|
|
184
|
-
this.current = undefined;
|
|
185
|
-
this.observation?.working(false);
|
|
162
|
+
}
|
|
163
|
+
async executeRun(launch, session, baseline) {
|
|
164
|
+
try {
|
|
165
|
+
if (launch.provider !== this.initial.provider ||
|
|
166
|
+
launch.model !== this.initial.model ||
|
|
167
|
+
launch.thinking !== this.initial.thinking) {
|
|
168
|
+
throw new Error("Continuation model snapshot changed unexpectedly.");
|
|
186
169
|
}
|
|
187
|
-
|
|
170
|
+
await session.prompt(launch.prompt, { expandPromptTemplates: false });
|
|
171
|
+
await session.waitForIdle();
|
|
172
|
+
this.saveResult(launch, baseline);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
this.saveResult(launch, baseline, errorText(error));
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
this.work = { kind: "idle" };
|
|
179
|
+
this.observation?.working(false);
|
|
180
|
+
}
|
|
188
181
|
}
|
|
189
182
|
commandLaunch(command) {
|
|
190
183
|
const receipt = path.join(this.dir, "admissions", `${command.id}.json`);
|
|
@@ -213,7 +206,7 @@ class Actor {
|
|
|
213
206
|
return;
|
|
214
207
|
}
|
|
215
208
|
if (command.action === "run" &&
|
|
216
|
-
(this.stopping || this.
|
|
209
|
+
(this.stopping || this.work.kind === "running" || !session.isIdle)) {
|
|
217
210
|
this.retry = true;
|
|
218
211
|
return;
|
|
219
212
|
}
|
|
@@ -233,31 +226,31 @@ class Actor {
|
|
|
233
226
|
this.applyCommand(command, launch, session);
|
|
234
227
|
}
|
|
235
228
|
staleCommand(command, launch, session) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
229
|
+
switch (command.action) {
|
|
230
|
+
case "stop":
|
|
231
|
+
return command.actorToken !== this.token;
|
|
232
|
+
case "steer":
|
|
233
|
+
return this.work.kind !== "running" || session.isIdle || this.work.launch.id !== launch.id;
|
|
234
|
+
case "run":
|
|
235
|
+
return false;
|
|
239
236
|
}
|
|
240
|
-
return (command.action === "steer" &&
|
|
241
|
-
(this.active === undefined || session.isIdle || this.current?.id !== launch.id));
|
|
242
237
|
}
|
|
243
238
|
applyCommand(command, launch, session) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
239
|
+
switch (command.action) {
|
|
240
|
+
case "stop":
|
|
241
|
+
this.stop();
|
|
242
|
+
return;
|
|
243
|
+
case "run":
|
|
244
|
+
this.run(launch);
|
|
245
|
+
return;
|
|
246
|
+
case "steer":
|
|
247
|
+
// Synchronous native queue admission cannot turn into an idle prompt after an await.
|
|
248
|
+
session.agent.steer({
|
|
249
|
+
role: "user",
|
|
250
|
+
content: [{ type: "text", text: command.prompt }],
|
|
251
|
+
timestamp: Date.now(),
|
|
252
|
+
});
|
|
251
253
|
}
|
|
252
|
-
if (command.prompt === undefined) {
|
|
253
|
-
throw new Error("Missing guidance.");
|
|
254
|
-
}
|
|
255
|
-
// Synchronous native queue admission cannot turn into an idle prompt after an await.
|
|
256
|
-
session.agent.steer({
|
|
257
|
-
role: "user",
|
|
258
|
-
content: [{ type: "text", text: command.prompt }],
|
|
259
|
-
timestamp: Date.now(),
|
|
260
|
-
});
|
|
261
254
|
}
|
|
262
255
|
pendingCommand(file) {
|
|
263
256
|
const identity = commandIdentity(file);
|
|
@@ -297,7 +290,7 @@ class Actor {
|
|
|
297
290
|
this.errors.push(errorText(error));
|
|
298
291
|
});
|
|
299
292
|
}
|
|
300
|
-
return this.stopping && this.
|
|
293
|
+
return this.stopping && this.work.kind === "idle" && (this.session?.isIdle ?? true);
|
|
301
294
|
}
|
|
302
295
|
/** Durable command readiness, not elapsed time, defines admission. */
|
|
303
296
|
async admitCommands(runtime, parked) {
|
|
@@ -338,14 +331,14 @@ class Actor {
|
|
|
338
331
|
this.parking = await parked.park(runtime, {
|
|
339
332
|
signal: owner.signal,
|
|
340
333
|
pi: this.context?.pi,
|
|
341
|
-
busy: this.stopping || this.
|
|
334
|
+
busy: this.stopping || this.work.kind === "running" || this.observation?.watched === true,
|
|
342
335
|
// Raced commands and viewers keep this actor resident; the loop admits after release.
|
|
343
336
|
quiet: () => {
|
|
344
337
|
const queued = this.pending().some((command) => this.commandLaunch(command));
|
|
345
338
|
if (queued) {
|
|
346
339
|
this.wake.raise();
|
|
347
340
|
}
|
|
348
|
-
const idle = this.
|
|
341
|
+
const idle = this.work.kind === "idle" && !this.stopping;
|
|
349
342
|
return !queued && idle && this.observation?.watched !== true;
|
|
350
343
|
},
|
|
351
344
|
errors: this.errors,
|
|
@@ -357,7 +350,7 @@ class Actor {
|
|
|
357
350
|
return this.parking !== "resident";
|
|
358
351
|
}
|
|
359
352
|
failure(error, unopened) {
|
|
360
|
-
const launch = this.
|
|
353
|
+
const launch = this.work.kind === "running" ? this.work.launch : this.initial;
|
|
361
354
|
const result = {
|
|
362
355
|
id: launch.id,
|
|
363
356
|
owner: launch.owner,
|
|
@@ -454,7 +447,9 @@ class Actor {
|
|
|
454
447
|
process.on("SIGTERM", this.stop);
|
|
455
448
|
process.on("SIGINT", this.stop);
|
|
456
449
|
await this.admitCommands(runtime, parked);
|
|
457
|
-
|
|
450
|
+
if (this.work.kind === "running") {
|
|
451
|
+
await this.work.task;
|
|
452
|
+
}
|
|
458
453
|
}
|
|
459
454
|
catch (error) {
|
|
460
455
|
this.failure(error, parked?.opened === true ? undefined : parkedToken);
|
package/dist/agent/storage.js
CHANGED
|
@@ -118,13 +118,25 @@ export function latest(base, initial) {
|
|
|
118
118
|
export function conversation(base, launch) {
|
|
119
119
|
return runDirectory(base, launch.rootId);
|
|
120
120
|
}
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
/** Metadata only: replacing command bytes invalidates the actor's parsed-command cache. */
|
|
122
|
+
export function commandIdentity(file) {
|
|
123
|
+
try {
|
|
124
|
+
const stat = fs.statSync(file, { bigint: true });
|
|
125
|
+
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (missing(error)) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export function enqueue(base, launch, input) {
|
|
135
|
+
const id = input.action === "run" ? launch.id : randomUUID();
|
|
123
136
|
const command = {
|
|
124
137
|
id,
|
|
125
138
|
runId: launch.id,
|
|
126
|
-
|
|
127
|
-
...options,
|
|
139
|
+
...input,
|
|
128
140
|
createdAt: Date.now(),
|
|
129
141
|
};
|
|
130
142
|
write(path.join(conversation(base, launch), "commands", `${id}.json`), command, true);
|