@fitchmultz/pi-subagents 0.50.0 → 0.52.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/CHANGELOG.md +63 -2
- package/README.md +36 -5
- package/dist/agent/child-runtime.js +80 -0
- package/dist/agent/contracts.js +15 -24
- package/dist/agent/host.js +3 -16
- package/dist/agent/index.js +212 -192
- package/dist/agent/journal-tail.js +115 -156
- package/dist/agent/launch.js +20 -8
- package/dist/agent/legacy.js +65 -23
- package/dist/agent/notifications.js +138 -47
- package/dist/agent/observation-state.js +246 -0
- package/dist/agent/observation-view.js +258 -0
- package/dist/agent/observation.js +350 -0
- package/dist/agent/observe-card.js +243 -0
- package/dist/agent/observe-chrome.js +85 -0
- package/dist/agent/observe-client.js +285 -0
- package/dist/agent/observe-layout.js +65 -0
- package/dist/agent/observe-reading.js +84 -0
- package/dist/agent/observe-screen.js +400 -96
- package/dist/agent/observe-transcript.js +382 -0
- package/dist/agent/observe.js +250 -43
- package/dist/agent/parking.js +253 -0
- package/dist/agent/process.js +228 -28
- package/dist/agent/receipts.js +92 -20
- package/dist/agent/runner.js +229 -126
- package/dist/agent/shell.js +2 -1
- package/dist/agent/storage.js +47 -12
- package/dist/agent/tool-schemas.js +20 -0
- package/dist/agent/typebox.js +10 -5
- package/dist/agent/usage.js +0 -115
- package/dist/agent/view.js +46 -16
- package/dist/agent/wake.js +99 -0
- package/dist/build-info.js +1 -1
- package/dist/intercom-client.d.ts +26 -0
- package/dist/intercom-client.js +45 -0
- package/dist/pi-intercom/agent-dir.d.ts +1 -0
- package/dist/pi-intercom/bridge-session.js +2 -1
- package/dist/pi-intercom/broker/broker.js +62 -43
- package/dist/pi-intercom/broker/client-last.d.ts +24 -0
- package/dist/pi-intercom/broker/client-last.js +42 -5
- package/dist/pi-intercom/broker/client-park.d.ts +9 -0
- package/dist/pi-intercom/broker/client-park.js +21 -0
- package/dist/pi-intercom/broker/client.d.ts +59 -0
- package/dist/pi-intercom/broker/client.js +55 -29
- package/dist/pi-intercom/broker/connection.d.ts +29 -0
- package/dist/pi-intercom/broker/framing.d.ts +17 -0
- package/dist/pi-intercom/broker/framing.js +10 -7
- package/dist/pi-intercom/broker/guard.js +23 -0
- package/dist/pi-intercom/broker/last.d.ts +30 -0
- package/dist/pi-intercom/broker/last.js +87 -40
- package/dist/pi-intercom/broker/parked.js +107 -0
- package/dist/pi-intercom/broker/paths.d.ts +13 -0
- package/dist/pi-intercom/broker/paths.js +35 -12
- package/dist/pi-intercom/broker/pid.js +5 -1
- package/dist/pi-intercom/broker/server-connection.js +20 -1
- package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
- package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
- package/dist/pi-intercom/broker/targets.d.ts +9 -0
- package/dist/pi-intercom/connection.js +71 -20
- package/dist/pi-intercom/identity.js +6 -5
- package/dist/pi-intercom/inbound-delivery.js +16 -60
- package/dist/pi-intercom/inbound-journal.js +83 -6
- package/dist/pi-intercom/inbound-record.js +18 -1
- package/dist/pi-intercom/inbound.js +22 -37
- package/dist/pi-intercom/index.js +41 -0
- package/dist/pi-intercom/last-message.js +4 -16
- package/dist/pi-intercom/lifecycle.js +88 -3
- package/dist/pi-intercom/outbound.js +5 -15
- package/dist/pi-intercom/reply-tracker.js +7 -6
- package/dist/pi-intercom/reply-wait.js +2 -2
- package/dist/pi-intercom/session-targets.d.ts +24 -0
- package/dist/pi-intercom/tool-registration.js +4 -1
- package/dist/pi-intercom/tool-results.js +5 -7
- package/dist/pi-intercom/tool-schemas.js +12 -26
- package/dist/pi-intercom/types.d.ts +212 -0
- package/dist/pi-intercom/types.js +54 -3
- package/dist/pi-intercom/validation.d.ts +12 -0
- package/dist/pi-intercom/validation.js +3 -0
- package/dist/shared/bounded-json.js +151 -0
- package/dist/shared/compact-cards.js +66 -0
- package/dist/shared/journal-frames.js +3 -6
- package/dist/shared/json-projection.js +267 -129
- package/dist/shared/native-journal.js +123 -56
- package/dist/shared/parking.js +12 -0
- package/dist/shared/post-exit-stdio-guard.js +120 -7
- package/dist/shared/tui-action.js +29 -0
- package/dist/shared/unknown.d.ts +9 -0
- package/docs/code-quality.md +5 -3
- package/docs/development.md +42 -3
- package/docs/intercom-bridge.md +9 -4
- package/docs/intercom.md +228 -7
- package/docs/performance.md +118 -14
- package/docs/reference.md +168 -20
- package/docs/retired-history-maintenance.md +252 -0
- package/package.json +28 -15
- package/scripts/archive-retired-history.mjs +464 -0
- package/scripts/build.mjs +48 -28
- package/scripts/measure-size.mjs +1 -1
- package/scripts/native-package-smoke.mjs +153 -12
- package/scripts/package-smoke.mjs +80 -11
- package/scripts/prepare.mjs +2 -2
- package/scripts/real-pi-live.mjs +377 -0
- package/scripts/real-pi-processes.mjs +6 -1
- package/scripts/real-pi-smoke.mjs +6 -282
- package/scripts/run-tests.mjs +4 -2
- package/scripts/typescript.mjs +35 -0
- package/skills/pi-intercom/SKILL.md +2 -1
- package/skills/pi-subagents/SKILL.md +23 -5
- package/src/agent/child-runtime.ts +105 -0
- package/src/agent/contracts.ts +18 -34
- package/src/agent/host.ts +39 -20
- package/src/agent/index.ts +234 -207
- package/src/agent/journal-tail.ts +141 -153
- package/src/agent/launch.ts +33 -9
- package/src/agent/legacy.ts +85 -31
- package/src/agent/notifications.ts +192 -67
- package/src/agent/observation-state.ts +277 -0
- package/src/agent/observation-view.ts +267 -0
- package/src/agent/observation.ts +409 -0
- package/src/agent/observe-card.ts +304 -0
- package/src/agent/observe-chrome.ts +170 -0
- package/src/agent/observe-client.ts +318 -0
- package/src/agent/observe-layout.ts +79 -0
- package/src/agent/observe-reading.ts +97 -0
- package/src/agent/observe-screen.ts +443 -129
- package/src/agent/observe-transcript.ts +434 -0
- package/src/agent/observe.ts +304 -71
- package/src/agent/parking.ts +341 -0
- package/src/agent/process.ts +280 -37
- package/src/agent/receipts.ts +114 -24
- package/src/agent/runner.ts +227 -149
- package/src/agent/shell.ts +2 -1
- package/src/agent/storage.ts +44 -11
- package/src/agent/tool-schemas.ts +33 -0
- package/src/agent/typebox.ts +12 -7
- package/src/agent/usage.ts +0 -130
- package/src/agent/view.ts +70 -20
- package/src/agent/wake.ts +107 -0
- package/src/intercom-client.ts +108 -0
- package/src/pi-intercom/bridge-session.ts +2 -2
- package/src/pi-intercom/broker/broker.ts +64 -53
- package/src/pi-intercom/broker/client-last.ts +65 -12
- package/src/pi-intercom/broker/client-park.ts +23 -0
- package/src/pi-intercom/broker/client.ts +63 -45
- package/src/pi-intercom/broker/framing.ts +11 -8
- package/src/pi-intercom/broker/guard.ts +33 -0
- package/src/pi-intercom/broker/last.ts +131 -43
- package/src/pi-intercom/broker/parked.ts +130 -0
- package/src/pi-intercom/broker/paths.ts +39 -12
- package/src/pi-intercom/broker/pid.ts +6 -1
- package/src/pi-intercom/broker/server-connection.ts +27 -2
- package/src/pi-intercom/connection.ts +103 -39
- package/src/pi-intercom/identity.ts +8 -5
- package/src/pi-intercom/inbound-delivery.ts +20 -66
- package/src/pi-intercom/inbound-journal.ts +103 -7
- package/src/pi-intercom/inbound-record.ts +21 -4
- package/src/pi-intercom/inbound.ts +32 -49
- package/src/pi-intercom/index.ts +44 -0
- package/src/pi-intercom/last-message.ts +11 -20
- package/src/pi-intercom/lifecycle.ts +117 -14
- package/src/pi-intercom/outbound.ts +7 -16
- package/src/pi-intercom/reply-tracker.ts +8 -6
- package/src/pi-intercom/reply-wait.ts +3 -2
- package/src/pi-intercom/runtime-types.ts +0 -1
- package/src/pi-intercom/tool-registration.ts +4 -1
- package/src/pi-intercom/tool-results.ts +10 -9
- package/src/pi-intercom/tool-schemas.ts +12 -44
- package/src/pi-intercom/types.ts +115 -4
- package/src/pi-intercom/validation.ts +4 -0
- package/src/shared/bounded-json.ts +162 -0
- package/src/shared/compact-cards.ts +118 -0
- package/src/shared/journal-frames.ts +3 -19
- package/src/shared/json-projection.ts +267 -147
- package/src/shared/native-journal.ts +157 -75
- package/src/shared/parking.ts +22 -0
- package/src/shared/post-exit-stdio-guard.ts +165 -7
- package/src/shared/tui-action.ts +39 -0
- package/tsconfig.client.json +8 -0
- package/tsconfig.json +1 -0
- package/src/shared/stream-json.d.ts +0 -10
package/dist/agent/runner.js
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { alive, ownIdentity, shellCommand } from "./process.js";
|
|
3
|
+
import { commandSchema } from "./contracts.js";
|
|
4
|
+
import { launchAt, missing, read, runDirectory, write } from "./storage.js";
|
|
5
|
+
import { ownIdentity, ParentCheck, shells, signalProcess } from "./process.js";
|
|
7
6
|
import { errorText, loadSDK } from "./host.js";
|
|
7
|
+
import { Parking } from "./parking.js";
|
|
8
8
|
import { childUsage } from "./usage.js";
|
|
9
|
-
import { initializeCwd
|
|
9
|
+
import { initializeCwd } from "./cwd.js";
|
|
10
|
+
import { childRuntime } from "./child-runtime.js";
|
|
11
|
+
import { ActorObservation } from "./observation.js";
|
|
12
|
+
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
|
+
}
|
|
10
25
|
function completionStatus(stopping, message, error) {
|
|
11
26
|
if (stopping || message?.stopReason === "aborted") {
|
|
12
27
|
return "stopped";
|
|
@@ -16,30 +31,24 @@ function completionStatus(stopping, message, error) {
|
|
|
16
31
|
}
|
|
17
32
|
return "completed";
|
|
18
33
|
}
|
|
19
|
-
function guard(pi, directory, token) {
|
|
20
|
-
pi.on("tool_call", (event) => {
|
|
21
|
-
if (event.toolName === "delegate" || event.toolName === "agent_runs") {
|
|
22
|
-
return { block: true, reason: "Only a main session may delegate or control children." };
|
|
23
|
-
}
|
|
24
|
-
const owner = pi.getAllTools().find((tool) => tool.name === "bash")?.sourceInfo;
|
|
25
|
-
if (event.toolName === "bash" &&
|
|
26
|
-
typeof event.input.command === "string" &&
|
|
27
|
-
owner &&
|
|
28
|
-
(owner.source === "builtin" || localCwdOwner(owner))) {
|
|
29
|
-
// Preserve the selected local owner's cwd/env and leave remote/custom tools alone.
|
|
30
|
-
const input = event.input;
|
|
31
|
-
input.command = shellCommand(event.input.command, directory, token);
|
|
32
|
-
}
|
|
33
|
-
return;
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
34
|
class Actor {
|
|
37
35
|
active;
|
|
38
36
|
stopping = false;
|
|
37
|
+
stopDeadline;
|
|
38
|
+
nextParentCheck = 0;
|
|
39
|
+
parking = false;
|
|
40
|
+
parent = new ParentCheck();
|
|
41
|
+
// Raised when durable commands may need admission: change hints and stop.
|
|
42
|
+
wake = new Wake();
|
|
43
|
+
// A deferred run waits for an idle session, which has no command-file event.
|
|
44
|
+
retry = false;
|
|
39
45
|
current;
|
|
40
46
|
errors = [];
|
|
41
47
|
started = new Set();
|
|
48
|
+
// Actor-local validation metadata only; never retain command prompts or receipt authority.
|
|
49
|
+
validated = new Map();
|
|
42
50
|
session;
|
|
51
|
+
observation;
|
|
43
52
|
context;
|
|
44
53
|
base;
|
|
45
54
|
initial;
|
|
@@ -105,65 +114,40 @@ class Actor {
|
|
|
105
114
|
.map((block) => block.text)
|
|
106
115
|
.join("\n") ?? "");
|
|
107
116
|
}
|
|
108
|
-
createRuntime(sdk) {
|
|
109
|
-
return async ({ sessionManager }) => {
|
|
110
|
-
const launch = this.initial;
|
|
111
|
-
if (!fs.existsSync(launch.sessionFile)) {
|
|
112
|
-
throw new Error("Missing saved native session; refusing to create another.");
|
|
113
|
-
}
|
|
114
|
-
const settingsManager = sdk.SettingsManager.create(launch.cwd, launch.agentDir, {
|
|
115
|
-
projectTrusted: launch.trusted,
|
|
116
|
-
});
|
|
117
|
-
const services = await sdk.createAgentSessionServices({
|
|
118
|
-
cwd: launch.cwd,
|
|
119
|
-
agentDir: launch.agentDir,
|
|
120
|
-
settingsManager,
|
|
121
|
-
resourceLoaderOptions: {
|
|
122
|
-
additionalExtensionPaths: [launch.entry],
|
|
123
|
-
extensionFactories: [
|
|
124
|
-
{
|
|
125
|
-
name: "codemode",
|
|
126
|
-
builtin: true,
|
|
127
|
-
factory: sdk.createCodemodeExtension({ mode: "on" }),
|
|
128
|
-
},
|
|
129
|
-
{ name: "tool_search", builtin: true, factory: sdk.createToolSearchExtension() },
|
|
130
|
-
{ name: "mcp", builtin: true, factory: sdk.createMcpExtension() },
|
|
131
|
-
(pi) => {
|
|
132
|
-
guard(pi, this.dir, this.token);
|
|
133
|
-
pi.on("session_start", (_event, ctx) => {
|
|
134
|
-
this.context = { pi, ctx };
|
|
135
|
-
});
|
|
136
|
-
},
|
|
137
|
-
],
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
const diagnostics = services.resourceLoader.getExtensions().errors;
|
|
141
|
-
if (diagnostics.length > 0) {
|
|
142
|
-
throw new Error(`Child extension load failed: ${JSON.stringify(diagnostics)}`);
|
|
143
|
-
}
|
|
144
|
-
const model = services.modelRuntime.getModel(launch.provider, launch.model);
|
|
145
|
-
if (!model) {
|
|
146
|
-
throw new Error(`Saved model ${launch.provider}/${launch.model} is unavailable; no fallback was used.`);
|
|
147
|
-
}
|
|
148
|
-
return {
|
|
149
|
-
...(await sdk.createAgentSessionFromServices({
|
|
150
|
-
services,
|
|
151
|
-
sessionManager,
|
|
152
|
-
model,
|
|
153
|
-
thinkingLevel: launch.thinking,
|
|
154
|
-
})),
|
|
155
|
-
services,
|
|
156
|
-
diagnostics: services.diagnostics,
|
|
157
|
-
};
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
117
|
stop = () => {
|
|
161
|
-
|
|
118
|
+
if (this.stopping) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
162
121
|
this.stopping = true;
|
|
122
|
+
this.wake.raise();
|
|
123
|
+
write(path.join(this.dir, `stopping-${this.token}.json`), { at: Date.now() });
|
|
124
|
+
write(path.join(this.dir, "stop-acknowledged.json"), { at: Date.now(), id: this.current?.id });
|
|
163
125
|
this.session?.abort().catch((error) => {
|
|
164
126
|
this.errors.push(errorText(error));
|
|
165
127
|
});
|
|
128
|
+
this.stopShells("SIGTERM");
|
|
129
|
+
// Parent death leaves no parent-side escalation timer to stop an uncooperative tool.
|
|
130
|
+
this.stopDeadline = setTimeout(() => {
|
|
131
|
+
this.stopShells("SIGKILL");
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}, 3000);
|
|
134
|
+
this.stopDeadline.unref();
|
|
166
135
|
};
|
|
136
|
+
stopShells(signal) {
|
|
137
|
+
try {
|
|
138
|
+
for (const identity of shells(this.base, this.initial, this.token)) {
|
|
139
|
+
try {
|
|
140
|
+
signalProcess(identity, signal);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
this.errors.push(errorText(error));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
this.errors.push(errorText(error));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
167
151
|
run(launch) {
|
|
168
152
|
const session = this.session;
|
|
169
153
|
if (!session) {
|
|
@@ -171,6 +155,7 @@ class Actor {
|
|
|
171
155
|
}
|
|
172
156
|
this.current = launch;
|
|
173
157
|
const baseline = new Set(session.sessionManager.getEntries().map((entry) => entry.id));
|
|
158
|
+
this.observation?.working(true);
|
|
174
159
|
this.active = (async () => {
|
|
175
160
|
try {
|
|
176
161
|
if (launch.provider !== this.initial.provider ||
|
|
@@ -188,6 +173,7 @@ class Actor {
|
|
|
188
173
|
finally {
|
|
189
174
|
this.active = undefined;
|
|
190
175
|
this.current = undefined;
|
|
176
|
+
this.observation?.working(false);
|
|
191
177
|
}
|
|
192
178
|
})();
|
|
193
179
|
}
|
|
@@ -219,6 +205,7 @@ class Actor {
|
|
|
219
205
|
}
|
|
220
206
|
if (command.action === "run" &&
|
|
221
207
|
(this.stopping || this.active !== undefined || !session.isIdle)) {
|
|
208
|
+
this.retry = true;
|
|
222
209
|
return;
|
|
223
210
|
}
|
|
224
211
|
this.started.add(command.id);
|
|
@@ -263,6 +250,22 @@ class Actor {
|
|
|
263
250
|
timestamp: Date.now(),
|
|
264
251
|
});
|
|
265
252
|
}
|
|
253
|
+
pendingCommand(file) {
|
|
254
|
+
const identity = commandIdentity(file);
|
|
255
|
+
const cached = this.validated.get(file);
|
|
256
|
+
if (identity !== undefined &&
|
|
257
|
+
cached?.identity === identity &&
|
|
258
|
+
fs.existsSync(path.join(this.dir, "admissions", `${cached.id}.json`))) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.validated.delete(file);
|
|
262
|
+
const command = read(file, commandSchema);
|
|
263
|
+
// A replacement during validation must not inherit the old body's validated identity.
|
|
264
|
+
if (command && identity !== undefined && commandIdentity(file) === identity) {
|
|
265
|
+
this.validated.set(file, { identity, id: command.id });
|
|
266
|
+
}
|
|
267
|
+
return command;
|
|
268
|
+
}
|
|
266
269
|
pending() {
|
|
267
270
|
const dir = path.join(this.dir, "commands");
|
|
268
271
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
@@ -270,23 +273,81 @@ class Actor {
|
|
|
270
273
|
.readdirSync(dir)
|
|
271
274
|
.filter((file) => file.endsWith(".json"))
|
|
272
275
|
.flatMap((file) => {
|
|
273
|
-
const command =
|
|
276
|
+
const command = this.pendingCommand(path.join(dir, file));
|
|
274
277
|
return command ? [command] : [];
|
|
275
278
|
})
|
|
276
279
|
.sort((a, b) => a.createdAt - b.createdAt);
|
|
277
280
|
}
|
|
278
281
|
shouldExit() {
|
|
279
|
-
|
|
280
|
-
|
|
282
|
+
const now = performance.now();
|
|
283
|
+
if (!this.stopping && now >= this.nextParentCheck) {
|
|
284
|
+
this.nextParentCheck = now + 1000;
|
|
285
|
+
// One observation at a time; a slow one keeps the regular schedule.
|
|
286
|
+
const file = path.join(this.base, "attachment.json");
|
|
287
|
+
this.parent.check(file, this.stop).catch((error) => {
|
|
288
|
+
this.errors.push(errorText(error));
|
|
289
|
+
});
|
|
281
290
|
}
|
|
282
|
-
|
|
283
|
-
|
|
291
|
+
return this.stopping && this.active === undefined && (this.session?.isIdle ?? true);
|
|
292
|
+
}
|
|
293
|
+
/** Durable command readiness, not elapsed time, defines admission. */
|
|
294
|
+
async admitCommands(runtime, parked) {
|
|
295
|
+
// Queued commands create this directory; otherwise pending() does and the next pass arms.
|
|
296
|
+
const directory = path.join(this.dir, "commands");
|
|
297
|
+
const commands = new DirectoryWatch(directory, () => true, this.wake.raise);
|
|
298
|
+
try {
|
|
299
|
+
while (!this.shouldExit()) {
|
|
300
|
+
// Each liveness tick also rechecks the directory for change hints the OS dropped.
|
|
301
|
+
const watch = commands.ensure();
|
|
302
|
+
if ((this.wake.take() || this.retry || watch !== "watching") && !this.stopping) {
|
|
303
|
+
this.retry = false;
|
|
304
|
+
this.pending().forEach((command) => this.accept(command));
|
|
305
|
+
}
|
|
306
|
+
// oxlint-disable-next-line no-await-in-loop
|
|
307
|
+
if (await this.park(runtime, parked)) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const polling = this.retry || this.stopping || watch === "unwatched";
|
|
311
|
+
const tick = Math.max(1, this.nextParentCheck - performance.now());
|
|
312
|
+
// oxlint-disable-next-line no-await-in-loop
|
|
313
|
+
await this.wake.sleep(polling ? 100 : tick);
|
|
314
|
+
}
|
|
284
315
|
}
|
|
285
|
-
|
|
286
|
-
|
|
316
|
+
finally {
|
|
317
|
+
// A verdict settling after the loop ends must not act.
|
|
318
|
+
this.parent.cancel();
|
|
319
|
+
commands.close();
|
|
287
320
|
}
|
|
288
|
-
|
|
289
|
-
|
|
321
|
+
}
|
|
322
|
+
/** Parks a final-idle actor; the parent/stop owner stays live while preparation awaits. */
|
|
323
|
+
async park(runtime, parked) {
|
|
324
|
+
const owner = new AbortController();
|
|
325
|
+
const watch = setInterval(() => {
|
|
326
|
+
if (this.shouldExit() || this.stopping) {
|
|
327
|
+
owner.abort();
|
|
328
|
+
}
|
|
329
|
+
}, 100);
|
|
330
|
+
try {
|
|
331
|
+
this.parking = await parked.park(runtime, {
|
|
332
|
+
signal: owner.signal,
|
|
333
|
+
pi: this.context?.pi,
|
|
334
|
+
busy: this.stopping || this.active !== undefined || this.observation?.watched === true,
|
|
335
|
+
// Raced commands and viewers keep this actor resident; the loop admits after release.
|
|
336
|
+
quiet: () => {
|
|
337
|
+
const queued = this.pending().some((command) => this.commandLaunch(command));
|
|
338
|
+
if (queued) {
|
|
339
|
+
this.wake.raise();
|
|
340
|
+
}
|
|
341
|
+
const idle = this.active === undefined && !this.stopping;
|
|
342
|
+
return !queued && idle && this.observation?.watched !== true;
|
|
343
|
+
},
|
|
344
|
+
errors: this.errors,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
finally {
|
|
348
|
+
clearInterval(watch);
|
|
349
|
+
}
|
|
350
|
+
return this.parking;
|
|
290
351
|
}
|
|
291
352
|
failure(error) {
|
|
292
353
|
const launch = this.current ?? this.initial;
|
|
@@ -304,62 +365,104 @@ class Actor {
|
|
|
304
365
|
}
|
|
305
366
|
process.exitCode = 1;
|
|
306
367
|
}
|
|
368
|
+
async start(sdk, parked) {
|
|
369
|
+
const sessionManager = parked.open();
|
|
370
|
+
const factory = childRuntime(sdk, this.initial, {
|
|
371
|
+
directory: this.dir,
|
|
372
|
+
token: this.token,
|
|
373
|
+
saved: parked.saved,
|
|
374
|
+
started: (pi, ctx) => {
|
|
375
|
+
this.context = { pi, ctx };
|
|
376
|
+
},
|
|
377
|
+
tool: (event) => this.observation?.tool(event),
|
|
378
|
+
});
|
|
379
|
+
const runtime = await sdk.createAgentSessionRuntime(factory, {
|
|
380
|
+
cwd: this.initial.cwd,
|
|
381
|
+
agentDir: this.initial.agentDir,
|
|
382
|
+
sessionManager,
|
|
383
|
+
});
|
|
384
|
+
try {
|
|
385
|
+
await this.ready(runtime, parked);
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
// A runtime that never became ready is still disposed, so the failed actor exits.
|
|
389
|
+
await runtime.dispose();
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
return runtime;
|
|
393
|
+
}
|
|
394
|
+
async ready(runtime, parked) {
|
|
395
|
+
this.session = runtime.session;
|
|
396
|
+
await this.session.bindExtensions({
|
|
397
|
+
mode: "print",
|
|
398
|
+
onError: (error) => {
|
|
399
|
+
this.errors.push(error.error);
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
if (!this.context) {
|
|
403
|
+
throw new Error("Child extension context is unavailable.");
|
|
404
|
+
}
|
|
405
|
+
initializeCwd(this.context.pi, this.context.ctx, this.initial.cwd, this.dir);
|
|
406
|
+
if (this.initial.name !== undefined && !parked.saved) {
|
|
407
|
+
this.session.sessionManager.appendSessionInfo(this.initial.name);
|
|
408
|
+
}
|
|
409
|
+
// Optional live view, bound before readiness; its faults never change native execution.
|
|
410
|
+
this.observation = await ActorObservation.open(this.session, {
|
|
411
|
+
token: this.token,
|
|
412
|
+
rootId: this.initial.rootId,
|
|
413
|
+
sessionId: this.session.sessionManager.getSessionId(),
|
|
414
|
+
});
|
|
415
|
+
write(path.join(this.dir, "actor.lock", "process.json"), {
|
|
416
|
+
...ownIdentity(),
|
|
417
|
+
token: this.token,
|
|
418
|
+
});
|
|
419
|
+
write(path.join(this.dir, "ready.json"), {
|
|
420
|
+
pid: process.pid,
|
|
421
|
+
token: this.token,
|
|
422
|
+
sessionId: this.session.sessionManager.getSessionId(),
|
|
423
|
+
at: Date.now(),
|
|
424
|
+
});
|
|
425
|
+
parked.adopt();
|
|
426
|
+
}
|
|
307
427
|
async serve() {
|
|
308
428
|
const sdk = await loadSDK(this.initial.sdk);
|
|
429
|
+
const parked = new Parking(this.base, this.initial, this.token, sdk);
|
|
309
430
|
let runtime;
|
|
310
431
|
try {
|
|
311
|
-
runtime = await
|
|
312
|
-
cwd: this.initial.cwd,
|
|
313
|
-
agentDir: this.initial.agentDir,
|
|
314
|
-
sessionManager: sdk.SessionManager.open(this.initial.sessionFile, undefined, this.initial.cwd),
|
|
315
|
-
});
|
|
316
|
-
this.session = runtime.session;
|
|
317
|
-
await this.session.bindExtensions({
|
|
318
|
-
mode: "print",
|
|
319
|
-
onError: (error) => {
|
|
320
|
-
this.errors.push(error.error);
|
|
321
|
-
},
|
|
322
|
-
});
|
|
323
|
-
if (!this.context) {
|
|
324
|
-
throw new Error("Child extension context is unavailable.");
|
|
325
|
-
}
|
|
326
|
-
initializeCwd(this.context.pi, this.context.ctx, this.initial.cwd, this.dir);
|
|
327
|
-
if (this.initial.name !== undefined) {
|
|
328
|
-
this.session.sessionManager.appendSessionInfo(this.initial.name);
|
|
329
|
-
}
|
|
330
|
-
write(path.join(this.dir, "actor.lock", "process.json"), {
|
|
331
|
-
...ownIdentity(),
|
|
332
|
-
token: this.token,
|
|
333
|
-
});
|
|
334
|
-
write(path.join(this.dir, "ready.json"), {
|
|
335
|
-
pid: process.pid,
|
|
336
|
-
token: this.token,
|
|
337
|
-
sessionId: this.session.sessionManager.getSessionId(),
|
|
338
|
-
at: Date.now(),
|
|
339
|
-
});
|
|
432
|
+
runtime = await this.start(sdk, parked);
|
|
340
433
|
process.on("SIGTERM", this.stop);
|
|
341
434
|
process.on("SIGINT", this.stop);
|
|
342
|
-
|
|
343
|
-
this.pending().forEach((command) => this.accept(command));
|
|
344
|
-
// Durable command readiness, not elapsed time, defines admission.
|
|
345
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
346
|
-
await delay(100);
|
|
347
|
-
} while (!this.shouldExit());
|
|
435
|
+
await this.admitCommands(runtime, parked);
|
|
348
436
|
await this.active;
|
|
349
437
|
}
|
|
350
438
|
catch (error) {
|
|
351
439
|
this.failure(error);
|
|
352
440
|
}
|
|
353
441
|
finally {
|
|
354
|
-
|
|
355
|
-
|
|
442
|
+
await this.finish(runtime);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
async finish(runtime) {
|
|
446
|
+
process.off("SIGTERM", this.stop);
|
|
447
|
+
process.off("SIGINT", this.stop);
|
|
448
|
+
const observation = this.observation?.close();
|
|
449
|
+
if (!this.parking) {
|
|
356
450
|
await runtime?.dispose();
|
|
357
|
-
write(path.join(this.dir, `shutdown-${this.token}.json`), {
|
|
358
|
-
at: Date.now(),
|
|
359
|
-
errors: this.errors,
|
|
360
|
-
});
|
|
361
|
-
// Keep the process claim until a launcher proves this exact actor is gone.
|
|
362
451
|
}
|
|
452
|
+
if (this.stopping) {
|
|
453
|
+
this.stopShells("SIGKILL");
|
|
454
|
+
}
|
|
455
|
+
clearTimeout(this.stopDeadline);
|
|
456
|
+
write(path.join(this.dir, `shutdown-${this.token}.json`), {
|
|
457
|
+
at: Date.now(),
|
|
458
|
+
errors: this.errors,
|
|
459
|
+
...(observation === undefined ? {} : { observation }),
|
|
460
|
+
});
|
|
461
|
+
if (this.parking) {
|
|
462
|
+
// Parked authority requires actual exit; no leftover extension handle may keep it alive.
|
|
463
|
+
process.exit();
|
|
464
|
+
}
|
|
465
|
+
// Keep the process claim until a launcher proves this exact actor is gone.
|
|
363
466
|
}
|
|
364
467
|
}
|
|
365
468
|
const base = process.argv.at(2);
|
package/dist/agent/shell.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
|
-
import { readChildProcessIdentity } from "../shared/post-exit-stdio-guard.js";
|
|
5
|
+
import { readChildProcessIdentity, readProcessClock } from "../shared/post-exit-stdio-guard.js";
|
|
6
6
|
import { read, write } from "./storage.js";
|
|
7
7
|
import { processSchema } from "./contracts.js";
|
|
8
8
|
import { verifyProcess } from "./process.js";
|
|
@@ -39,5 +39,6 @@ write(path.join(directory, "shells", token, `${randomUUID()}.json`), {
|
|
|
39
39
|
pid,
|
|
40
40
|
identity,
|
|
41
41
|
token,
|
|
42
|
+
clock: readProcessClock(),
|
|
42
43
|
}, true);
|
|
43
44
|
verifyOwner(directory, token);
|
package/dist/agent/storage.js
CHANGED
|
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { readJsonProjection } from "../shared/journal-frames.js";
|
|
5
5
|
import { Assert } from "./typebox.js";
|
|
6
6
|
import { launchSchema, successorSchema } from "./contracts.js";
|
|
7
|
+
import { hasErrorCode } from "../shared/unknown.js";
|
|
7
8
|
export function missing(error) {
|
|
8
9
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
9
10
|
}
|
|
@@ -19,31 +20,47 @@ export function read(file, schema, select = () => true) {
|
|
|
19
20
|
throw error;
|
|
20
21
|
}
|
|
21
22
|
Assert(schema, value);
|
|
22
|
-
// TypeBox's generic Static<T> is intentionally the asserted schema's type.
|
|
23
|
-
// oxlint-disable-next-line typescript/no-unsafe-return
|
|
24
23
|
return value;
|
|
25
24
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const temp = `${file}.${randomUUID()}.tmp`;
|
|
29
|
-
const fd = fs.openSync(temp, "wx", 0o600);
|
|
25
|
+
/** Publish complete bytes without replacing a winner, even where Android denies hard links. */
|
|
26
|
+
function publishExclusive(temp, file) {
|
|
30
27
|
try {
|
|
31
|
-
fs.
|
|
32
|
-
|
|
28
|
+
fs.linkSync(temp, file);
|
|
29
|
+
return false;
|
|
33
30
|
}
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (!["EACCES", "EPERM", "ENOTSUP"].some((code) => hasErrorCode(error, code))) {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
// The immutable, private backing file must outlive its atomic no-clobber symlink.
|
|
36
|
+
fs.symlinkSync(path.basename(temp), file);
|
|
37
|
+
return true;
|
|
36
38
|
}
|
|
39
|
+
}
|
|
40
|
+
export function write(file, value, exclusive = false) {
|
|
41
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
42
|
+
const temp = `${file}.${randomUUID()}.${exclusive ? "data" : "tmp"}`;
|
|
43
|
+
const fd = fs.openSync(temp, "wx", 0o600);
|
|
44
|
+
let retained = false;
|
|
37
45
|
try {
|
|
46
|
+
try {
|
|
47
|
+
fs.writeFileSync(fd, `${JSON.stringify(value)}\n`);
|
|
48
|
+
fs.fsyncSync(fd);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
fs.closeSync(fd);
|
|
52
|
+
}
|
|
38
53
|
if (exclusive) {
|
|
39
|
-
|
|
54
|
+
retained = publishExclusive(temp, file);
|
|
40
55
|
}
|
|
41
56
|
else {
|
|
42
57
|
fs.renameSync(temp, file);
|
|
43
58
|
}
|
|
44
59
|
}
|
|
45
60
|
finally {
|
|
46
|
-
|
|
61
|
+
if (!retained) {
|
|
62
|
+
fs.rmSync(temp, { force: true });
|
|
63
|
+
}
|
|
47
64
|
}
|
|
48
65
|
}
|
|
49
66
|
export function safeId(id) {
|
|
@@ -65,6 +82,24 @@ export function launchAt(base, id, owner) {
|
|
|
65
82
|
}
|
|
66
83
|
return launch;
|
|
67
84
|
}
|
|
85
|
+
/** Claims the single successor of a saved run; undefined when another caller already has. */
|
|
86
|
+
export function successor(base, launch, prompt) {
|
|
87
|
+
const id = randomUUID();
|
|
88
|
+
const next = { ...launch, id, predecessor: launch.id, prompt, createdAt: Date.now() };
|
|
89
|
+
fs.mkdirSync(runDirectory(base, id), { mode: 0o700 });
|
|
90
|
+
write(path.join(runDirectory(base, id), "launch.json"), next, true);
|
|
91
|
+
try {
|
|
92
|
+
write(path.join(runDirectory(base, launch.id), "successor.json"), { id }, true);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
fs.rmSync(runDirectory(base, id), { recursive: true });
|
|
96
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
return next;
|
|
102
|
+
}
|
|
68
103
|
export function latest(base, initial) {
|
|
69
104
|
const seen = new Set();
|
|
70
105
|
let launch = initial;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { Type } from "./typebox.js";
|
|
3
|
+
// Main-session tool parameters only; the detached runner must not load pi-ai values.
|
|
4
|
+
export const delegateSchema = Type.Object({
|
|
5
|
+
prompt: Type.String({ minLength: 1 }),
|
|
6
|
+
name: Type.Optional(Type.String({ minLength: 1 })),
|
|
7
|
+
cwd: Type.Optional(Type.String({ minLength: 1 })),
|
|
8
|
+
model: Type.Optional(Type.String({
|
|
9
|
+
minLength: 1,
|
|
10
|
+
description: "Exact provider/model ID (:thinking and any case need host support); no fuzzy match or fallback.",
|
|
11
|
+
})),
|
|
12
|
+
context: Type.Optional(StringEnum(["fresh", "fork"])),
|
|
13
|
+
}, { additionalProperties: false });
|
|
14
|
+
export const runsSchema = Type.Object({
|
|
15
|
+
action: StringEnum(["list", "inspect", "continue", "stop"]),
|
|
16
|
+
id: Type.Optional(Type.String()),
|
|
17
|
+
prompt: Type.Optional(Type.String({ minLength: 1 })),
|
|
18
|
+
offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
19
|
+
length: Type.Optional(Type.Integer({ minimum: 4, maximum: 262144 })),
|
|
20
|
+
}, { additionalProperties: false });
|
package/dist/agent/typebox.js
CHANGED
|
@@ -9,12 +9,17 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { pathToFileURL } from "node:url";
|
|
12
|
-
import { packageRoot } from "./host.js";
|
|
13
|
-
|
|
14
|
-
//
|
|
12
|
+
import { isChild, packageRoot } from "./host.js";
|
|
13
|
+
// Pi aliases these imports to its own instance (including bundled virtual modules).
|
|
14
|
+
// Only the detached runner needs absolute imports from the selected host package.
|
|
15
|
+
const require = isChild() ? createRequire(path.join(packageRoot(), "dist/index.js")) : undefined;
|
|
15
16
|
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
16
|
-
const typebox =
|
|
17
|
+
const typebox = require === undefined
|
|
18
|
+
? await import("typebox")
|
|
19
|
+
: await import(__rewriteRelativeImportExtension(pathToFileURL(require.resolve("typebox")).href));
|
|
17
20
|
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
18
|
-
const value =
|
|
21
|
+
const value = require === undefined
|
|
22
|
+
? await import("typebox/value")
|
|
23
|
+
: await import(__rewriteRelativeImportExtension(pathToFileURL(require.resolve("typebox/value")).href));
|
|
19
24
|
export const { Type } = typebox;
|
|
20
25
|
export const Assert = value.Assert;
|