@fitchmultz/pi-subagents 0.52.18 → 0.52.19
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 +34 -0
- package/README.md +4 -0
- package/dist/agent/host.js +12 -0
- package/dist/agent/index.js +70 -56
- package/dist/agent/launch.js +12 -3
- package/dist/agent/notifications.js +5 -5
- package/dist/agent/parking.js +26 -15
- package/dist/agent/runner.js +18 -16
- package/dist/agent/tool-schemas.js +2 -0
- package/dist/agent/usage.js +42 -3
- package/dist/agent/view.js +13 -13
- package/dist/agent/wake.js +17 -15
- package/dist/build-info.js +1 -1
- package/dist/pi-intercom/broker/broker.js +2 -2
- package/dist/pi-intercom/broker/snapshots.d.ts +25 -7
- package/dist/pi-intercom/broker/snapshots.js +56 -25
- package/dist/pi-intercom/broker/spawn-claim.js +89 -0
- package/dist/pi-intercom/broker/spawn.js +31 -78
- package/dist/pi-intercom/lifecycle.js +26 -26
- package/dist/shared/post-exit-stdio-guard.js +44 -37
- package/docs/development.md +25 -0
- package/docs/intercom.md +13 -0
- package/docs/reference.md +16 -5
- package/package.json +1 -1
- package/scripts/build.mjs +41 -14
- package/skills/pi-subagents/SKILL.md +8 -4
- package/src/agent/contracts.ts +17 -0
- package/src/agent/host.ts +19 -0
- package/src/agent/index.ts +91 -62
- package/src/agent/launch.ts +16 -3
- package/src/agent/notifications.ts +5 -5
- package/src/agent/parking.ts +39 -17
- package/src/agent/runner.ts +20 -21
- package/src/agent/tool-schemas.ts +2 -0
- package/src/agent/usage.ts +48 -3
- package/src/agent/view.ts +15 -13
- package/src/agent/wake.ts +19 -15
- package/src/pi-intercom/broker/broker.ts +2 -1
- package/src/pi-intercom/broker/snapshots.ts +82 -37
- package/src/pi-intercom/broker/spawn-claim.ts +98 -0
- package/src/pi-intercom/broker/spawn.ts +42 -82
- package/src/pi-intercom/lifecycle.ts +38 -27
- package/src/shared/post-exit-stdio-guard.ts +48 -40
package/CHANGELOG.md
CHANGED
|
@@ -2,12 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## 0.52.19 - 2026-11-07
|
|
6
|
+
|
|
7
|
+
<!-- npm-release:unreleased:start -->
|
|
8
|
+
|
|
9
|
+
- Make post-exit pipe cleanup terminal across waiting, draining and closed phases so later pipe events cannot restart smoke child cleanup.
|
|
10
|
+
- Distinguish parked and failed actor exit outcomes; both have disposed the native runtime, exit the actor and permit no second ordinary disposal.
|
|
11
|
+
- Bind native intercom context identity to inactive, live or invalidated lifecycle state; invalidated transport cleanup retains the session ID until cleared, separate from transport connection and native tool activity.
|
|
12
|
+
- Bind parent attachment to explicit detached, waiting, attaching and attached phases; managed readiness attaches only its own still-current waiting binding and shutdown joins admitted publication before releasing the matching generation.
|
|
13
|
+
- Add optional `delegate.thinking` to select child effort independently of the parent on all supported hosts, including official Pi 1.5.0. Omission keeps inherited effort. Native model suffixes must agree with an explicit field. Pi still clamps effort to model capabilities; guidance and saved continuations retain the child's saved effort.
|
|
14
|
+
- Represent child accounting as unrecorded, verified, pending or unavailable while keeping execution status and saved result formats unchanged.
|
|
15
|
+
- Make directory-watch availability and the coalesced wake's single-owner contract explicit without changing polling fallback.
|
|
16
|
+
- Preserve stranded last-good builds until a complete replacement publishes, and report retained recovery paths when a later build fails.
|
|
17
|
+
- Give prepared topic transactions action-specific payloads while preserving validate-before-commit and stale-restore normalization.
|
|
18
|
+
|
|
19
|
+
<!-- npm-release:unreleased:end -->
|
|
20
|
+
|
|
21
|
+
- fix: preserve live and uncertain broker startup ownership (#342) ([f6147dd](https://github.com/fitchmultz/pi-subagents/commit/f6147dd7a31775e8499a6fd77b4185d6d918d646))
|
|
22
|
+
|
|
23
|
+
### What's Changed
|
|
24
|
+
|
|
25
|
+
- fix: preserve live and uncertain broker startup ownership by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/342
|
|
26
|
+
- refactor: make post-exit pipe cleanup terminal by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/346
|
|
27
|
+
- refactor: distinguish parked and failed actor exit outcomes by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/348
|
|
28
|
+
- refactor: bind intercom context identity to lifecycle state by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/351
|
|
29
|
+
- refactor: bind parent attachment to explicit lifecycle phases by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/349
|
|
30
|
+
- fix: preserve last-good builds across interrupted publication by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/343
|
|
31
|
+
- refactor: make child accounting outcomes explicit by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/352
|
|
32
|
+
- refactor: make directory watch availability explicit by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/350
|
|
33
|
+
- refactor: make prepared topic transactions explicit by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/353
|
|
34
|
+
- Add explicit child thinking to delegate by @tribble in https://github.com/fitchmultz/pi-subagents/pull/338
|
|
35
|
+
|
|
36
|
+
**Full Changelog**: https://github.com/fitchmultz/pi-subagents/compare/v0.52.18...v0.52.19
|
|
37
|
+
|
|
5
38
|
## 0.52.18 - 2026-11-06
|
|
6
39
|
|
|
7
40
|
<!-- npm-release:unreleased:start -->
|
|
8
41
|
|
|
9
42
|
- Keep replied or retired asks out of reply attribution when their queued input reaches a later native turn.
|
|
10
43
|
- Preserve unanswered queued asks through abort recovery without sending a premature recipient-failure reply.
|
|
44
|
+
- Preserve live or uncertain broker startup ownership; reclaim only exited spawn owners and release only the acquired claim.
|
|
11
45
|
|
|
12
46
|
<!-- npm-release:unreleased:end -->
|
|
13
47
|
|
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ delegate({
|
|
|
14
14
|
prompt:
|
|
15
15
|
"Inspect the authentication changes. Return concrete defects with file and line evidence. Do not edit files.",
|
|
16
16
|
name: "auth-review",
|
|
17
|
+
thinking: "low",
|
|
17
18
|
});
|
|
18
19
|
```
|
|
19
20
|
|
|
@@ -22,6 +23,9 @@ arrives later as a notification. Do other work or end the turn; do not poll.
|
|
|
22
23
|
`context: "fresh"` is the default; `context: "fork"` preserves the native
|
|
23
24
|
conversation, including prior tool results. There is no hidden role prompt,
|
|
24
25
|
report-writing suffix, profile selection, or model fallback catalog.
|
|
26
|
+
Optional `thinking` sets the child's effort without changing the parent. Omit it
|
|
27
|
+
to inherit parent effort. Pi clamps the request to the child's model capabilities.
|
|
28
|
+
Guidance and saved continuations keep the child's saved effort.
|
|
25
29
|
|
|
26
30
|
```js
|
|
27
31
|
agent_runs({ action: "list" });
|
package/dist/agent/host.js
CHANGED
|
@@ -12,6 +12,18 @@ import { findPackageJSON } from "node:module";
|
|
|
12
12
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
13
|
import { readIntercomLineage } from "../pi-intercom/identity.js";
|
|
14
14
|
export { errorMessage as errorText } from "../shared/unknown.js";
|
|
15
|
+
export function managedHost(context) {
|
|
16
|
+
const host = Reflect.get(context, "managedHost");
|
|
17
|
+
if (typeof host !== "object" || host === null) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const owner = Reflect.get(host, "owner");
|
|
21
|
+
const pid = typeof owner === "object" && owner !== null ? Reflect.get(owner, "pid") : undefined;
|
|
22
|
+
const ready = Reflect.get(host, "ready");
|
|
23
|
+
return typeof pid === "number" && Number.isSafeInteger(pid) && ready instanceof Promise
|
|
24
|
+
? { owner: { pid }, ready: ready.then((accepted) => accepted === true) }
|
|
25
|
+
: undefined;
|
|
26
|
+
}
|
|
15
27
|
/** A launcher may still own children while handing off or replacing a worker. */
|
|
16
28
|
export function retainsChildren(event) {
|
|
17
29
|
const disposition = Reflect.get(event, "disposition");
|
package/dist/agent/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import * as loadedSDK from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { actor, alive, ensureActor, ownIdentity, parkFailure, requestStop, signalProcess, } from "./process.js";
|
|
6
6
|
import { stopChildren } from "./shutdown.js";
|
|
7
|
-
import { isChild, errorText, rememberSDK, retainsChildren } from "./host.js";
|
|
7
|
+
import { isChild, errorText, managedHost, rememberSDK, retainsChildren, } from "./host.js";
|
|
8
8
|
import { serveParked } from "./parking.js";
|
|
9
9
|
import { resultSchema, attachmentSchema, admissionSchema, commandSchema, } from "./contracts.js";
|
|
10
10
|
import { delegateSchema, runsSchema } from "./tool-schemas.js";
|
|
@@ -20,18 +20,6 @@ import { executionCwd } from "./cwd.js";
|
|
|
20
20
|
import { getPiAgentDir } from "../pi-intercom/agent-dir.js";
|
|
21
21
|
import { registerCompactMessages, renderCompactResult } from "../shared/compact-cards.js";
|
|
22
22
|
import { BackgroundReporter } from "../shared/background-reporting.js";
|
|
23
|
-
function managedHost(context) {
|
|
24
|
-
const host = Reflect.get(context, "managedHost");
|
|
25
|
-
if (typeof host !== "object" || host === null) {
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
const owner = Reflect.get(host, "owner");
|
|
29
|
-
const pid = typeof owner === "object" && owner !== null ? Reflect.get(owner, "pid") : undefined;
|
|
30
|
-
const ready = Reflect.get(host, "ready");
|
|
31
|
-
return typeof pid === "number" && Number.isSafeInteger(pid) && ready instanceof Promise
|
|
32
|
-
? { owner: { pid }, ready: ready.then((accepted) => accepted === true) }
|
|
33
|
-
: undefined;
|
|
34
|
-
}
|
|
35
23
|
/** Legacy results are optional; their reconciliation reports an invalid configured root. */
|
|
36
24
|
function legacyInput() {
|
|
37
25
|
try {
|
|
@@ -42,14 +30,8 @@ function legacyInput() {
|
|
|
42
30
|
}
|
|
43
31
|
}
|
|
44
32
|
class AgentRuntime {
|
|
45
|
-
|
|
33
|
+
attachment = { kind: "detached" };
|
|
46
34
|
reporter = new BackgroundReporter(() => this.ctx);
|
|
47
|
-
// A managed launcher owns children across worker replacement; otherwise this process does.
|
|
48
|
-
host;
|
|
49
|
-
accepted = false;
|
|
50
|
-
base = "";
|
|
51
|
-
owner = "";
|
|
52
|
-
generation = "";
|
|
53
35
|
// Polling remains only while change hints are unavailable or a reconciliation failed.
|
|
54
36
|
timer;
|
|
55
37
|
woken;
|
|
@@ -59,7 +41,6 @@ class AgentRuntime {
|
|
|
59
41
|
// Input stamps the last reconciliation saw; a one-second probe recovers dropped hints.
|
|
60
42
|
observed = new Map();
|
|
61
43
|
probe;
|
|
62
|
-
closing = false;
|
|
63
44
|
reset = Promise.resolve();
|
|
64
45
|
shutdown = new AbortController();
|
|
65
46
|
child = isChild();
|
|
@@ -69,11 +50,28 @@ class AgentRuntime {
|
|
|
69
50
|
this.pi = pi;
|
|
70
51
|
this.watches = new CompletionWatches(this.reporter, this.wake);
|
|
71
52
|
}
|
|
53
|
+
get binding() {
|
|
54
|
+
if (this.attachment.kind === "detached") {
|
|
55
|
+
throw new Error("Agent runtime is not attached.");
|
|
56
|
+
}
|
|
57
|
+
return this.attachment.binding;
|
|
58
|
+
}
|
|
59
|
+
get ctx() {
|
|
60
|
+
return this.attachment.kind === "attaching" || this.attachment.kind === "attached"
|
|
61
|
+
? this.attachment.binding.context
|
|
62
|
+
: undefined;
|
|
63
|
+
}
|
|
64
|
+
get base() {
|
|
65
|
+
return this.binding.base;
|
|
66
|
+
}
|
|
67
|
+
get owner() {
|
|
68
|
+
return this.binding.owner;
|
|
69
|
+
}
|
|
72
70
|
checkMain() {
|
|
73
71
|
if (isChild()) {
|
|
74
72
|
throw new Error("Only a main session may delegate or control agent runs.");
|
|
75
73
|
}
|
|
76
|
-
if (this.
|
|
74
|
+
if (this.shutdown.signal.aborted || this.attachment.kind !== "attached") {
|
|
77
75
|
throw new Error("Agent runtime is not attached.");
|
|
78
76
|
}
|
|
79
77
|
}
|
|
@@ -93,17 +91,18 @@ class AgentRuntime {
|
|
|
93
91
|
};
|
|
94
92
|
scan = () => {
|
|
95
93
|
clearTimeout(this.timer);
|
|
96
|
-
if (this.
|
|
94
|
+
if (this.shutdown.signal.aborted || this.attachment.kind !== "attached" || this.child) {
|
|
97
95
|
return;
|
|
98
96
|
}
|
|
97
|
+
const { context } = this.attachment.binding;
|
|
99
98
|
const inputs = () => [this.base, ...legacyInput(), ...this.awaited];
|
|
100
99
|
const before = new Map(inputs().map((file) => [file, pathStamp(file)]));
|
|
101
100
|
let reconciled = true;
|
|
102
101
|
try {
|
|
103
|
-
const { notices, awaited } = notifications(
|
|
102
|
+
const { notices, awaited } = notifications(context, this.base, this.owner);
|
|
104
103
|
this.awaited = awaited;
|
|
105
|
-
notices.unshift(...reconcileLegacy(
|
|
106
|
-
deliver(this.pi,
|
|
104
|
+
notices.unshift(...reconcileLegacy(context, this.reporter));
|
|
105
|
+
deliver(this.pi, context, notices);
|
|
107
106
|
this.reporter.recover("completion-scan");
|
|
108
107
|
}
|
|
109
108
|
catch (error) {
|
|
@@ -131,23 +130,32 @@ class AgentRuntime {
|
|
|
131
130
|
this.awaited = new Set();
|
|
132
131
|
}
|
|
133
132
|
start(context) {
|
|
133
|
+
if (this.shutdown.signal.aborted) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
134
136
|
this.reporter.start();
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
const owner = context.sessionManager.getSessionId();
|
|
138
|
+
const host = managedHost(context);
|
|
139
|
+
const binding = { context, owner, base: ownerDirectory(this.agentDir, owner), host };
|
|
140
|
+
if (host === undefined ||
|
|
141
|
+
this.attachment.kind === "attached" ||
|
|
142
|
+
this.attachment.kind === "attaching" ||
|
|
143
|
+
this.child) {
|
|
144
|
+
this.reset = this.reset.then(() => this.attach(binding));
|
|
138
145
|
return this.reset;
|
|
139
146
|
}
|
|
140
147
|
// Attach and publish only after the launcher retires rollback: an unready replacement
|
|
141
148
|
// must neither adopt children nor append completions to a journal it may abandon.
|
|
142
149
|
// Readiness follows session_start, so never await it here.
|
|
143
|
-
|
|
144
|
-
this.
|
|
145
|
-
|
|
146
|
-
.then(
|
|
147
|
-
if (accepted && !this.
|
|
148
|
-
this.
|
|
149
|
-
|
|
150
|
+
const waiting = { kind: "waiting", binding };
|
|
151
|
+
this.attachment = waiting;
|
|
152
|
+
host.ready
|
|
153
|
+
.then((accepted) => {
|
|
154
|
+
if (accepted && !this.shutdown.signal.aborted && this.attachment === waiting) {
|
|
155
|
+
this.reset = this.reset.then(() => this.attach(binding));
|
|
156
|
+
return this.reset;
|
|
150
157
|
}
|
|
158
|
+
return null;
|
|
151
159
|
})
|
|
152
160
|
.catch((error) => {
|
|
153
161
|
this.reporter.report(undefined, `Agent runtime attachment: ${errorText(error)}`);
|
|
@@ -155,37 +163,36 @@ class AgentRuntime {
|
|
|
155
163
|
return undefined;
|
|
156
164
|
}
|
|
157
165
|
ownerIdentity() {
|
|
158
|
-
return ownIdentity(this.host?.owner.pid);
|
|
166
|
+
return ownIdentity(this.binding.host?.owner.pid);
|
|
159
167
|
}
|
|
160
|
-
async attach(
|
|
168
|
+
async attach(binding) {
|
|
161
169
|
this.unwatch();
|
|
162
|
-
|
|
170
|
+
const attaching = { kind: "attaching", binding, generation: randomUUID() };
|
|
171
|
+
this.attachment = attaching;
|
|
163
172
|
if (this.child) {
|
|
173
|
+
this.attachment = { ...attaching, kind: "attached" };
|
|
164
174
|
return;
|
|
165
175
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
write(path.join(this.base, "attachment.json"), {
|
|
171
|
-
...this.ownerIdentity(),
|
|
172
|
-
generation: this.generation,
|
|
176
|
+
await fs.promises.mkdir(binding.base, { recursive: true, mode: 0o700 });
|
|
177
|
+
write(path.join(binding.base, "attachment.json"), {
|
|
178
|
+
...ownIdentity(binding.host?.owner.pid),
|
|
179
|
+
generation: attaching.generation,
|
|
173
180
|
attached: true,
|
|
174
181
|
});
|
|
175
|
-
if (this.
|
|
182
|
+
if (this.shutdown.signal.aborted) {
|
|
176
183
|
return;
|
|
177
184
|
}
|
|
185
|
+
this.attachment = { ...attaching, kind: "attached" };
|
|
178
186
|
// Recovery and watch setup stay off the awaited startup path.
|
|
179
187
|
this.timer = setTimeout(this.scan, 200).unref();
|
|
180
188
|
this.probe = setInterval(this.recover, 1000).unref();
|
|
181
189
|
}
|
|
182
190
|
async dispose(retain) {
|
|
183
191
|
this.reporter.shutdown();
|
|
184
|
-
this.closing = true;
|
|
185
192
|
this.unwatch();
|
|
186
193
|
this.shutdown.abort();
|
|
187
194
|
await this.reset;
|
|
188
|
-
if (!retain && this.
|
|
195
|
+
if (!retain && !this.child && this.attachment.kind !== "detached") {
|
|
189
196
|
const file = path.join(this.base, "attachment.json");
|
|
190
197
|
const attachment = read(file, attachmentSchema);
|
|
191
198
|
if (attachment !== undefined && this.releases(attachment)) {
|
|
@@ -197,14 +204,20 @@ class AgentRuntime {
|
|
|
197
204
|
await stopChildren(this.base, this.owner, (message) => this.reporter.report(undefined, message));
|
|
198
205
|
}
|
|
199
206
|
}
|
|
200
|
-
this.
|
|
207
|
+
this.attachment = { kind: "detached" };
|
|
201
208
|
}
|
|
202
209
|
releases(attachment) {
|
|
203
|
-
|
|
204
|
-
|
|
210
|
+
const state = this.attachment;
|
|
211
|
+
switch (state.kind) {
|
|
212
|
+
case "detached":
|
|
213
|
+
return false;
|
|
214
|
+
case "waiting":
|
|
215
|
+
// An unready replacement can release the launcher's existing generation on quit.
|
|
216
|
+
return attachment.attached && attachment.pid === state.binding.host?.owner.pid;
|
|
217
|
+
case "attaching":
|
|
218
|
+
case "attached":
|
|
219
|
+
return attachment.generation === state.generation;
|
|
205
220
|
}
|
|
206
|
-
// An unready replacement never attached, but exiting still releases the launcher-owned children.
|
|
207
|
-
return (this.generation.length === 0 && attachment.attached && attachment.pid === this.host?.owner.pid);
|
|
208
221
|
}
|
|
209
222
|
stop(launch) {
|
|
210
223
|
const base = this.base;
|
|
@@ -368,7 +381,8 @@ class AgentRuntime {
|
|
|
368
381
|
this.pi.on("session_start", (_event, context) => this.start(context));
|
|
369
382
|
this.pi.on("turn_end", (event, context) => {
|
|
370
383
|
// The public pending flag covers user input; the preview also includes custom messages.
|
|
371
|
-
if (
|
|
384
|
+
if (this.attachment.kind === "attached" &&
|
|
385
|
+
settle(this.owner, context.hasPendingMessages() || event.context.pendingMessages.length > 0)) {
|
|
372
386
|
// Released admissions may have been dropped from the queue rather than published.
|
|
373
387
|
this.wake();
|
|
374
388
|
}
|
|
@@ -389,7 +403,7 @@ class AgentRuntime {
|
|
|
389
403
|
this.pi.registerTool({
|
|
390
404
|
name: "delegate",
|
|
391
405
|
label: "Delegate",
|
|
392
|
-
description: "Run a child on exactly this prompt; main sessions only. Fork copies the full conversation; default is fresh. Returns a receipt; completion is notified later: do other work or end the turn, don't poll.",
|
|
406
|
+
description: "Run a child on exactly this prompt; main sessions only. Fork copies the full conversation; default is fresh. Optional thinking overrides inherited effort for this child. Returns a receipt; completion is notified later: do other work or end the turn, don't poll.",
|
|
393
407
|
parameters: delegateSchema,
|
|
394
408
|
exposure: this.child ? "hidden" : "direct",
|
|
395
409
|
renderResult: renderCompactResult,
|
package/dist/agent/launch.js
CHANGED
|
@@ -3,12 +3,18 @@ import * as path from "node:path";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { loadSDK, packageRoot, runtimeEntry } from "./host.js";
|
|
5
5
|
import { runDirectory, write } from "./storage.js";
|
|
6
|
+
function selectedThinking(requested, inherited, suffix) {
|
|
7
|
+
if (requested !== undefined && suffix !== undefined && requested !== suffix) {
|
|
8
|
+
throw new Error("thinking conflicts with the model's thinking suffix.");
|
|
9
|
+
}
|
|
10
|
+
return suffix ?? requested ?? inherited;
|
|
11
|
+
}
|
|
6
12
|
function selectedModel(input, context, thinking, sdk) {
|
|
7
13
|
if (input.model === undefined) {
|
|
8
14
|
if (!context.model) {
|
|
9
15
|
throw new Error("No selected model; no fallback was used.");
|
|
10
16
|
}
|
|
11
|
-
return { model: context.model, thinking };
|
|
17
|
+
return { model: context.model, thinking: selectedThinking(input.thinking, thinking) };
|
|
12
18
|
}
|
|
13
19
|
const slash = input.model.indexOf("/");
|
|
14
20
|
if (slash < 1) {
|
|
@@ -16,7 +22,7 @@ function selectedModel(input, context, thinking, sdk) {
|
|
|
16
22
|
}
|
|
17
23
|
const model = context.modelRegistry.find(input.model.slice(0, slash), input.model.slice(slash + 1));
|
|
18
24
|
if (model) {
|
|
19
|
-
return { model, thinking };
|
|
25
|
+
return { model, thinking: selectedThinking(input.thinking, thinking) };
|
|
20
26
|
}
|
|
21
27
|
if (typeof sdk.resolveModelReference !== "function") {
|
|
22
28
|
throw new Error(`Model ${input.model} is unavailable; no fallback was used. Check the provider/model ID. This host supports literal IDs only. If you intended a thinking suffix, update Pi for native thinking-suffix support.`);
|
|
@@ -30,7 +36,10 @@ function selectedModel(input, context, thinking, sdk) {
|
|
|
30
36
|
if (resolved.status !== "resolved") {
|
|
31
37
|
throw new Error(`Model ${input.model} is unavailable; no fallback was used.`);
|
|
32
38
|
}
|
|
33
|
-
return {
|
|
39
|
+
return {
|
|
40
|
+
model: resolved.model,
|
|
41
|
+
thinking: selectedThinking(input.thinking, thinking, resolved.thinkingLevel),
|
|
42
|
+
};
|
|
34
43
|
}
|
|
35
44
|
function targetTrust(sdk, cwd, context, agentDir) {
|
|
36
45
|
if (fs.realpathSync(cwd) === fs.realpathSync(context.cwd)) {
|
|
@@ -4,7 +4,7 @@ import { completionReceipts } from "./receipts.js";
|
|
|
4
4
|
import { readOutputPage } from "../shared/output-page.js";
|
|
5
5
|
import { journalStamp } from "../shared/native-journal.js";
|
|
6
6
|
import { launchAt, list, runDirectory } from "./storage.js";
|
|
7
|
-
import { readCanonicalResult, readResult } from "./usage.js";
|
|
7
|
+
import { accountingState, readCanonicalResult, readResult } from "./usage.js";
|
|
8
8
|
function isStringSet(value) {
|
|
9
9
|
if (!(value instanceof Set)) {
|
|
10
10
|
return false;
|
|
@@ -126,7 +126,7 @@ function unchangedPending(pending) {
|
|
|
126
126
|
function load(base, id, owner) {
|
|
127
127
|
const launch = launchAt(base, id, owner);
|
|
128
128
|
const result = readCanonicalResult(base, launch);
|
|
129
|
-
if (result
|
|
129
|
+
if (accountingState(result).kind !== "pending") {
|
|
130
130
|
return { launch, result };
|
|
131
131
|
}
|
|
132
132
|
// Only pending runs need this pre-read stamp to observe concurrent publication.
|
|
@@ -135,7 +135,7 @@ function load(base, id, owner) {
|
|
|
135
135
|
return { launch, result: readResult(base, launch), journal };
|
|
136
136
|
}
|
|
137
137
|
function cacheableResult(loaded) {
|
|
138
|
-
return loaded.result
|
|
138
|
+
return accountingState(loaded.result).kind !== "pending" || loaded.journal !== undefined;
|
|
139
139
|
}
|
|
140
140
|
function completedRun({ base, owner, known, settled }, id) {
|
|
141
141
|
const directory = runDirectory(base, id);
|
|
@@ -156,7 +156,7 @@ function completedRun({ base, owner, known, settled }, id) {
|
|
|
156
156
|
const order = { endedAt: loaded.result.endedAt, createdAt: loaded.launch.createdAt };
|
|
157
157
|
// Pending accounting retries on child changes, even after the completion receipt.
|
|
158
158
|
// Ordinary settled history retains its launch/result-only idle probes.
|
|
159
|
-
const pending = loaded.result.
|
|
159
|
+
const pending = accountingState(loaded.result).kind === "pending" ? loaded : undefined;
|
|
160
160
|
if (launch !== undefined && result !== undefined && cacheableResult(loaded)) {
|
|
161
161
|
settled.set(id, { launch, result, pending, ...order });
|
|
162
162
|
}
|
|
@@ -175,7 +175,7 @@ function completed(base, owner, awaited) {
|
|
|
175
175
|
if (run === undefined) {
|
|
176
176
|
awaited.add(path.join(runDirectory(base, id), "result.json"));
|
|
177
177
|
}
|
|
178
|
-
else if (run.loaded
|
|
178
|
+
else if (run.loaded && accountingState(run.loaded.result).kind === "pending") {
|
|
179
179
|
awaited.add(run.loaded.launch.sessionFile);
|
|
180
180
|
}
|
|
181
181
|
return run ?? [];
|
package/dist/agent/parking.js
CHANGED
|
@@ -85,10 +85,7 @@ export function parkedState(dir, launch, restore) {
|
|
|
85
85
|
}
|
|
86
86
|
/** One actor generation's idle parking and exact restore; commands stay with the runner. */
|
|
87
87
|
export class Parking {
|
|
88
|
-
|
|
89
|
-
/** Set before entering either native open, whose effects may outlive an exception. */
|
|
90
|
-
opened = false;
|
|
91
|
-
restored;
|
|
88
|
+
restore = { kind: "unopened" };
|
|
92
89
|
since = 0;
|
|
93
90
|
leaf;
|
|
94
91
|
shell;
|
|
@@ -108,36 +105,48 @@ export class Parking {
|
|
|
108
105
|
get file() {
|
|
109
106
|
return path.join(this.dir, "parked.json");
|
|
110
107
|
}
|
|
108
|
+
get saved() {
|
|
109
|
+
return this.restore.kind !== "unopened" && this.restore.source.kind === "parked"
|
|
110
|
+
? this.restore.source.saved
|
|
111
|
+
: undefined;
|
|
112
|
+
}
|
|
113
|
+
/** Set before entering either native open, whose effects may outlive an exception. */
|
|
114
|
+
get opened() {
|
|
115
|
+
return this.restore.kind !== "unopened";
|
|
116
|
+
}
|
|
111
117
|
/** Parked authority is exact state or nothing; never a journal-only reopen. */
|
|
112
118
|
open() {
|
|
113
119
|
const { sdk, launch } = this;
|
|
114
120
|
const { openWorkingSession: restore } = sdk;
|
|
115
121
|
const parked = parkedState(this.dir, launch, restore && sdk.readWorkingSession);
|
|
116
|
-
this.
|
|
122
|
+
this.restore = {
|
|
123
|
+
kind: "opening",
|
|
124
|
+
source: parked ? { kind: "parked", ...parked } : { kind: "journal" },
|
|
125
|
+
};
|
|
117
126
|
if (!parked || !restore) {
|
|
118
127
|
return sdk.SessionManager.open(launch.sessionFile, undefined, launch.cwd);
|
|
119
128
|
}
|
|
120
|
-
this.saved = parked.saved;
|
|
121
|
-
this.restored = parked.state;
|
|
122
129
|
return restore(parked.saved);
|
|
123
130
|
}
|
|
124
131
|
/** The ready restored generation owns this state now; a later park writes a new cut. */
|
|
125
132
|
adopt() {
|
|
126
|
-
if (this.
|
|
133
|
+
if (this.restore.kind !== "opening") {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const { source } = this.restore;
|
|
137
|
+
if (source.kind === "parked") {
|
|
127
138
|
fs.rmSync(this.file);
|
|
128
|
-
fs.rmSync(
|
|
139
|
+
fs.rmSync(source.state, { force: true });
|
|
129
140
|
}
|
|
141
|
+
this.restore = { kind: "adopted", source };
|
|
130
142
|
}
|
|
131
|
-
/**
|
|
143
|
+
/** Both exit outcomes have disposed the runtime; only one publishes parked authority. */
|
|
132
144
|
async park(runtime, actor) {
|
|
133
145
|
if (!actor.pi || !this.due(runtime.session, actor.busy) || this.hasShell()) {
|
|
134
|
-
return
|
|
146
|
+
return "resident";
|
|
135
147
|
}
|
|
136
148
|
const hold = await this.prepare(runtime, actor.pi, actor);
|
|
137
|
-
|
|
138
|
-
await this.commit(runtime, hold, actor.errors);
|
|
139
|
-
}
|
|
140
|
-
return hold !== undefined;
|
|
149
|
+
return hold ? this.commit(runtime, hold, actor.errors) : "resident";
|
|
141
150
|
}
|
|
142
151
|
hasShell() {
|
|
143
152
|
const witness = this.shell === undefined ? undefined : shell(this.shell, this.token);
|
|
@@ -201,7 +210,9 @@ export class Parking {
|
|
|
201
210
|
if (read(this.file, parkedSchema)?.token !== this.token) {
|
|
202
211
|
// Truthful boundary: retained input stays queued and nothing restarts implicitly.
|
|
203
212
|
write(path.join(this.dir, "park-failed.json"), { token: this.token, errors });
|
|
213
|
+
return "exit-failed";
|
|
204
214
|
}
|
|
215
|
+
return "exit-parked";
|
|
205
216
|
}
|
|
206
217
|
async capture(session, save) {
|
|
207
218
|
if (!parkable(session)) {
|
package/dist/agent/runner.js
CHANGED
|
@@ -5,7 +5,7 @@ import { launchAt, missing, read, runDirectory, write } from "./storage.js";
|
|
|
5
5
|
import { ownIdentity, ParentCheck, shells, signalProcess } from "./process.js";
|
|
6
6
|
import { errorText, loadSDK } from "./host.js";
|
|
7
7
|
import { Parking } from "./parking.js";
|
|
8
|
-
import { UsageIndex } from "./usage.js";
|
|
8
|
+
import { accountingFields, UsageIndex } from "./usage.js";
|
|
9
9
|
import { initializeCwd } from "./cwd.js";
|
|
10
10
|
import { childRuntime } from "./child-runtime.js";
|
|
11
11
|
import { ActorObservation } from "./observation.js";
|
|
@@ -40,7 +40,7 @@ class Actor {
|
|
|
40
40
|
stopping = false;
|
|
41
41
|
stopDeadline;
|
|
42
42
|
nextParentCheck = 0;
|
|
43
|
-
parking =
|
|
43
|
+
parking = "resident";
|
|
44
44
|
parent = new ParentCheck();
|
|
45
45
|
// Raised when durable commands may need admission: change hints and stop.
|
|
46
46
|
wake = new Wake();
|
|
@@ -91,10 +91,7 @@ class Actor {
|
|
|
91
91
|
owner: launch.owner,
|
|
92
92
|
status: completionStatus(this.stopping, message, error),
|
|
93
93
|
endedAt: Date.now(),
|
|
94
|
-
...(accounting
|
|
95
|
-
? { terminalEntryId: terminal.id }
|
|
96
|
-
: {}),
|
|
97
|
-
...accounting,
|
|
94
|
+
...accountingFields(accounting),
|
|
98
95
|
...(failure !== undefined ? { error: failure } : {}),
|
|
99
96
|
};
|
|
100
97
|
fs.writeFileSync(path.join(runDirectory(this.base, launch.id), "output.txt"), output, {
|
|
@@ -104,16 +101,21 @@ class Actor {
|
|
|
104
101
|
}
|
|
105
102
|
usage(launch, baseline, terminal) {
|
|
106
103
|
try {
|
|
107
|
-
return { contributions: this.accounting.read(launch, baseline, terminal) };
|
|
108
|
-
}
|
|
109
|
-
catch (error) {
|
|
110
104
|
return {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
: {}),
|
|
105
|
+
kind: "verified",
|
|
106
|
+
contributions: this.accounting.read(launch, baseline, terminal),
|
|
107
|
+
terminalEntryId: terminal,
|
|
115
108
|
};
|
|
116
109
|
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return terminal === undefined
|
|
112
|
+
? { kind: "unavailable", error: errorText(error), terminalEntryId: undefined }
|
|
113
|
+
: {
|
|
114
|
+
kind: "pending",
|
|
115
|
+
error: errorText(error),
|
|
116
|
+
pending: { terminalCandidate: terminal, baseline: [...baseline] },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
117
119
|
}
|
|
118
120
|
output(message) {
|
|
119
121
|
return (message?.content
|
|
@@ -352,7 +354,7 @@ class Actor {
|
|
|
352
354
|
finally {
|
|
353
355
|
clearInterval(watch);
|
|
354
356
|
}
|
|
355
|
-
return this.parking;
|
|
357
|
+
return this.parking !== "resident";
|
|
356
358
|
}
|
|
357
359
|
failure(error, unopened) {
|
|
358
360
|
const launch = this.current ?? this.initial;
|
|
@@ -465,7 +467,7 @@ class Actor {
|
|
|
465
467
|
process.off("SIGTERM", this.stop);
|
|
466
468
|
process.off("SIGINT", this.stop);
|
|
467
469
|
const observation = this.observation?.close();
|
|
468
|
-
if (
|
|
470
|
+
if (this.parking === "resident") {
|
|
469
471
|
await runtime?.dispose();
|
|
470
472
|
}
|
|
471
473
|
if (this.stopping) {
|
|
@@ -477,7 +479,7 @@ class Actor {
|
|
|
477
479
|
errors: this.errors,
|
|
478
480
|
...(observation === undefined ? {} : { observation }),
|
|
479
481
|
});
|
|
480
|
-
if (this.parking) {
|
|
482
|
+
if (this.parking !== "resident") {
|
|
481
483
|
// Parked authority requires actual exit; no leftover extension handle may keep it alive.
|
|
482
484
|
process.exit();
|
|
483
485
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
2
|
import { Type } from "./typebox.js";
|
|
3
|
+
import { thinkingSchema } from "./contracts.js";
|
|
3
4
|
// Main-session tool parameters only; the detached runner must not load pi-ai values.
|
|
4
5
|
export const delegateSchema = Type.Object({
|
|
5
6
|
prompt: Type.String({ minLength: 1 }),
|
|
@@ -9,6 +10,7 @@ export const delegateSchema = Type.Object({
|
|
|
9
10
|
minLength: 1,
|
|
10
11
|
description: "Exact provider/model ID (:thinking and any case need host support); no fuzzy match or fallback.",
|
|
11
12
|
})),
|
|
13
|
+
thinking: Type.Optional(thinkingSchema),
|
|
12
14
|
context: Type.Optional(StringEnum(["fresh", "fork"])),
|
|
13
15
|
}, { additionalProperties: false });
|
|
14
16
|
export const runsSchema = Type.Object({
|