@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
|
@@ -8,12 +8,8 @@ export function isContextRequest(value) {
|
|
|
8
8
|
}
|
|
9
9
|
/** Owns native context validity and occupancy, never journal or socket state. */
|
|
10
10
|
export class IntercomLifecycle {
|
|
11
|
-
|
|
12
|
-
sessionId = null;
|
|
11
|
+
state = { kind: "inactive", epoch: 0 };
|
|
13
12
|
model = "unknown";
|
|
14
|
-
disposed = true;
|
|
15
|
-
epoch = 0;
|
|
16
|
-
incarnation = null;
|
|
17
13
|
peerId = null;
|
|
18
14
|
guardedDelivery = false;
|
|
19
15
|
applicationGuard;
|
|
@@ -29,10 +25,10 @@ export class IntercomLifecycle {
|
|
|
29
25
|
this.lineage = lineage;
|
|
30
26
|
}
|
|
31
27
|
get generation() {
|
|
32
|
-
return this.epoch;
|
|
28
|
+
return this.state.epoch;
|
|
33
29
|
}
|
|
34
30
|
get currentSessionId() {
|
|
35
|
-
return this.sessionId;
|
|
31
|
+
return this.state.kind === "inactive" ? null : this.state.sessionId;
|
|
36
32
|
}
|
|
37
33
|
get toolCount() {
|
|
38
34
|
return this.tools.size;
|
|
@@ -64,7 +60,7 @@ export class IntercomLifecycle {
|
|
|
64
60
|
request.respond(this.snapshot());
|
|
65
61
|
}
|
|
66
62
|
snapshot() {
|
|
67
|
-
if (!this.live()) {
|
|
63
|
+
if (this.state.kind !== "live" || !this.live()) {
|
|
68
64
|
return {
|
|
69
65
|
version: 1,
|
|
70
66
|
rootSessionId: null,
|
|
@@ -76,10 +72,10 @@ export class IntercomLifecycle {
|
|
|
76
72
|
}
|
|
77
73
|
return {
|
|
78
74
|
version: 1,
|
|
79
|
-
rootSessionId: this.lineage?.rootSessionId ?? this.sessionId,
|
|
75
|
+
rootSessionId: this.lineage?.rootSessionId ?? this.state.sessionId,
|
|
80
76
|
parentSessionId: this.lineage?.parentSessionId,
|
|
81
77
|
peerId: this.peerId,
|
|
82
|
-
incarnation: this.incarnation,
|
|
78
|
+
incarnation: this.state.incarnation,
|
|
83
79
|
connected: this.peerId !== null,
|
|
84
80
|
guardedDelivery: this.guardedDelivery ? 1 : null,
|
|
85
81
|
applicationGuard: this.applicationGuard,
|
|
@@ -107,36 +103,40 @@ export class IntercomLifecycle {
|
|
|
107
103
|
this.applicationGuard === guard.applicationGuard);
|
|
108
104
|
}
|
|
109
105
|
start(ctx) {
|
|
110
|
-
this.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
106
|
+
this.state = {
|
|
107
|
+
kind: "live",
|
|
108
|
+
epoch: this.state.epoch + 1,
|
|
109
|
+
context: ctx,
|
|
110
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
111
|
+
incarnation: randomUUID(),
|
|
112
|
+
};
|
|
115
113
|
this.applicationGuard = undefined;
|
|
116
114
|
this.model = ctx.model?.id ?? "unknown";
|
|
117
115
|
this.running = false;
|
|
118
116
|
this.activity = 0;
|
|
119
117
|
this.tools.clear();
|
|
120
118
|
this.transport(null);
|
|
121
|
-
return this.epoch;
|
|
119
|
+
return this.state.epoch;
|
|
122
120
|
}
|
|
123
121
|
invalidate() {
|
|
124
|
-
this.
|
|
125
|
-
this.
|
|
122
|
+
const epoch = this.state.epoch + 1;
|
|
123
|
+
this.state =
|
|
124
|
+
this.state.kind === "inactive"
|
|
125
|
+
? { kind: "inactive", epoch }
|
|
126
|
+
: { kind: "invalidated", epoch, sessionId: this.state.sessionId };
|
|
126
127
|
this.running = false;
|
|
127
128
|
this.tools.clear();
|
|
128
129
|
this.publishContext();
|
|
129
130
|
}
|
|
130
131
|
clear() {
|
|
131
|
-
this.
|
|
132
|
-
this.sessionId = null;
|
|
132
|
+
this.state = { kind: "inactive", epoch: this.state.epoch };
|
|
133
133
|
}
|
|
134
|
-
live(ctx = this.context, generation = this.epoch) {
|
|
135
|
-
if (this.
|
|
134
|
+
live(ctx = this.state.kind === "live" ? this.state.context : null, generation = this.state.epoch) {
|
|
135
|
+
if (this.state.kind !== "live" || generation !== this.state.epoch || !ctx) {
|
|
136
136
|
return null;
|
|
137
137
|
}
|
|
138
138
|
try {
|
|
139
|
-
if (
|
|
139
|
+
if (ctx.sessionManager.getSessionId() !== this.state.sessionId) {
|
|
140
140
|
return null;
|
|
141
141
|
}
|
|
142
142
|
// Reading a native property detects invalidated contexts without modifying them.
|
|
@@ -209,12 +209,12 @@ export class IntercomLifecycle {
|
|
|
209
209
|
}
|
|
210
210
|
async registration(pendingAsks) {
|
|
211
211
|
const live = this.live();
|
|
212
|
-
if (!live || this.
|
|
212
|
+
if (!live || this.state.kind !== "live") {
|
|
213
213
|
throw new Error("Intercom runtime not initialized");
|
|
214
214
|
}
|
|
215
215
|
return {
|
|
216
|
-
...this.identity(this.sessionId),
|
|
217
|
-
incarnation: this.incarnation
|
|
216
|
+
...this.identity(this.state.sessionId),
|
|
217
|
+
incarnation: this.state.incarnation,
|
|
218
218
|
guardedDelivery: 1,
|
|
219
219
|
applicationGuard: this.applicationGuard,
|
|
220
220
|
cwd: live.cwd,
|
|
@@ -280,12 +280,13 @@ export function attachChildProcessLifecycle(child) {
|
|
|
280
280
|
return new OwnedChildLifecycle(child);
|
|
281
281
|
}
|
|
282
282
|
function attachPostExitStdioGuard(child) {
|
|
283
|
-
let
|
|
283
|
+
let state = { kind: "waiting" };
|
|
284
284
|
let stdoutEnded = false;
|
|
285
285
|
let stderrEnded = false;
|
|
286
|
-
let idleTimer;
|
|
287
|
-
let hardTimer;
|
|
288
286
|
const destroyUnendedStdio = () => {
|
|
287
|
+
if (state.kind !== "draining") {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
289
290
|
if (!stdoutEnded) {
|
|
290
291
|
try {
|
|
291
292
|
child.stdout?.destroy();
|
|
@@ -302,51 +303,57 @@ function attachPostExitStdioGuard(child) {
|
|
|
302
303
|
// The process has exited; failed pipe destruction must not prevent other cleanup.
|
|
303
304
|
}
|
|
304
305
|
}
|
|
305
|
-
|
|
306
|
-
const clearTimers = () => {
|
|
307
|
-
if (idleTimer) {
|
|
308
|
-
clearTimeout(idleTimer);
|
|
309
|
-
idleTimer = undefined;
|
|
310
|
-
}
|
|
311
|
-
if (hardTimer) {
|
|
312
|
-
clearTimeout(hardTimer);
|
|
313
|
-
hardTimer = undefined;
|
|
314
|
-
}
|
|
306
|
+
cleanup();
|
|
315
307
|
};
|
|
316
308
|
const armIdleTimer = () => {
|
|
317
|
-
if (
|
|
309
|
+
if (state.kind !== "draining") {
|
|
318
310
|
return;
|
|
319
311
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
idleTimer = setTimeout(destroyUnendedStdio, 2000);
|
|
324
|
-
idleTimer.unref();
|
|
312
|
+
clearTimeout(state.idle);
|
|
313
|
+
state.idle = setTimeout(destroyUnendedStdio, 2000);
|
|
314
|
+
state.idle.unref();
|
|
325
315
|
};
|
|
326
|
-
|
|
327
|
-
child.stderr?.on("data", armIdleTimer);
|
|
328
|
-
child.stdout?.on("end", () => {
|
|
316
|
+
const stdoutEnd = () => {
|
|
329
317
|
stdoutEnded = true;
|
|
330
318
|
if (stderrEnded) {
|
|
331
|
-
|
|
319
|
+
cleanup();
|
|
332
320
|
}
|
|
333
|
-
}
|
|
334
|
-
|
|
321
|
+
};
|
|
322
|
+
const stderrEnd = () => {
|
|
335
323
|
stderrEnded = true;
|
|
336
324
|
if (stdoutEnded) {
|
|
337
|
-
|
|
325
|
+
cleanup();
|
|
338
326
|
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
armIdleTimer();
|
|
343
|
-
if (hardTimer) {
|
|
327
|
+
};
|
|
328
|
+
const exited = () => {
|
|
329
|
+
if (state.kind !== "waiting") {
|
|
344
330
|
return;
|
|
345
331
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
332
|
+
const hard = setTimeout(destroyUnendedStdio, 8000);
|
|
333
|
+
hard.unref();
|
|
334
|
+
state = { kind: "draining", idle: undefined, hard };
|
|
335
|
+
armIdleTimer();
|
|
336
|
+
};
|
|
337
|
+
function cleanup() {
|
|
338
|
+
if (state.kind === "draining") {
|
|
339
|
+
clearTimeout(state.idle);
|
|
340
|
+
clearTimeout(state.hard);
|
|
341
|
+
}
|
|
342
|
+
state = { kind: "closed" };
|
|
343
|
+
child.stdout?.off("data", armIdleTimer);
|
|
344
|
+
child.stderr?.off("data", armIdleTimer);
|
|
345
|
+
child.stdout?.off("end", stdoutEnd);
|
|
346
|
+
child.stderr?.off("end", stderrEnd);
|
|
347
|
+
child.off("exit", exited);
|
|
348
|
+
child.off("close", cleanup);
|
|
349
|
+
child.off("error", cleanup);
|
|
350
|
+
}
|
|
351
|
+
child.stdout?.on("data", armIdleTimer);
|
|
352
|
+
child.stderr?.on("data", armIdleTimer);
|
|
353
|
+
child.stdout?.on("end", stdoutEnd);
|
|
354
|
+
child.stderr?.on("end", stderrEnd);
|
|
355
|
+
child.on("exit", exited);
|
|
356
|
+
child.on("close", cleanup);
|
|
357
|
+
child.on("error", cleanup);
|
|
358
|
+
return cleanup;
|
|
352
359
|
}
|
package/docs/development.md
CHANGED
|
@@ -23,6 +23,12 @@ Run narrow owner-boundary tests locally through `scripts/run-tests.mjs`. Full
|
|
|
23
23
|
`npm run ci` belongs in hosted CI and includes package/install smokes and the
|
|
24
24
|
complete suites. Keep the strict Oxlint/Oxfmt rules; see [code quality](code-quality.md).
|
|
25
25
|
|
|
26
|
+
Builds compile into process-owned staging before replacing `dist`. If a publisher
|
|
27
|
+
exits after retiring the previous build, later builds preserve its complete
|
|
28
|
+
`.previous` tree until a new complete build publishes. A failed build reports
|
|
29
|
+
the retained recovery paths. Recovery never promotes an older backup into a
|
|
30
|
+
concurrent publisher's retirement gap or reaps live or uncertain owners.
|
|
31
|
+
|
|
26
32
|
## Native Android/Termux
|
|
27
33
|
|
|
28
34
|
Use app-private storage, not Android's shared `/sdcard` filesystem. No Termux app
|
|
@@ -80,6 +86,25 @@ npm and attached to the matching GitHub release.
|
|
|
80
86
|
|
|
81
87
|
## Isolation and safety
|
|
82
88
|
|
|
89
|
+
Parent attachment keeps one binding through detached, waiting, attaching and
|
|
90
|
+
attached phases. The existing shutdown signal closes the owner. Managed
|
|
91
|
+
readiness can attach only its own still-current waiting binding; shutdown joins
|
|
92
|
+
admitted publication before releasing the matching generation.
|
|
93
|
+
|
|
94
|
+
Parking returns resident, exit-with-parked-authority or exit-after-failed-save.
|
|
95
|
+
Both exit outcomes have disposed the native runtime and must exit the actor;
|
|
96
|
+
neither permits a second ordinary disposal. Restore tracks unopened, opening
|
|
97
|
+
and adopted phases, keeping the exact saved cut with the phase that owns it.
|
|
98
|
+
|
|
99
|
+
Directory watches distinguish unarmed, armed-to-one-inode and polling-only
|
|
100
|
+
availability. Missing paths can be armed later; unsupported or failed native
|
|
101
|
+
watches keep the existing polling fallback. Each coalesced Wake has one waiting
|
|
102
|
+
owner loop.
|
|
103
|
+
|
|
104
|
+
Smoke child cleanup treats leader exit and pipe closure separately. Pipe draining
|
|
105
|
+
has waiting, draining and closed phases. Closing it cancels its deadlines and
|
|
106
|
+
detaches its listeners, so later events cannot restart cleanup.
|
|
107
|
+
|
|
83
108
|
Every test/smoke broker uses a short, explicit `PI_INTERCOM_RUNTIME_DIR` owned by
|
|
84
109
|
the fixture. Do not touch `/tmp/pi-intercom-<uid>` to test namespace behavior on a
|
|
85
110
|
workstation with active sessions. Tests for the default path inspect it without
|
package/docs/intercom.md
CHANGED
|
@@ -341,6 +341,9 @@ tool to give up the turn.
|
|
|
341
341
|
|
|
342
342
|
Shutdown joins admitted inbound work before detaching its native owner. Stale
|
|
343
343
|
runtime generations must not checkpoint or deliver into a replacement session.
|
|
344
|
+
Context state is inactive, live with its session and incarnation, or invalidated
|
|
345
|
+
while transport cleanup retains the session ID. Clearing cleanup drops that ID;
|
|
346
|
+
transport connection and native tool activity remain separate from context validity.
|
|
344
347
|
The native tests own these contracts; neither a timer nor a source-string test is
|
|
345
348
|
publication evidence.
|
|
346
349
|
|
|
@@ -396,4 +399,14 @@ records include native boot/namespace/start identity to reject proven PID reuse;
|
|
|
396
399
|
ambiguous legacy numeric PIDs remain blocking. A process is never killed merely
|
|
397
400
|
to recover broker startup.
|
|
398
401
|
|
|
402
|
+
Startup distinguishes absent, live and uncertain ownership. An unreadable or
|
|
403
|
+
invalid PID record is uncertain, not permission to launch another broker.
|
|
404
|
+
A startup claim stays owned while its process is live, regardless of age.
|
|
405
|
+
Only a proven exited or replaced owner permits reclaiming it. Linux claims carry
|
|
406
|
+
the same native process identity as broker PID records. Legacy or incomparable
|
|
407
|
+
identities remain blocking; a timestamp alone cannot prove PID reuse.
|
|
408
|
+
Each starter keeps the claim's
|
|
409
|
+
file descriptor open and releases only the same device/inode, leaving a
|
|
410
|
+
replacement claim untouched.
|
|
411
|
+
|
|
399
412
|
Remote authenticated mail is separate: see [bridge setup](intercom-bridge.md).
|
package/docs/reference.md
CHANGED
|
@@ -12,6 +12,7 @@ delegate({
|
|
|
12
12
|
name: "optional-readable-name",
|
|
13
13
|
cwd: "/optional/working/directory",
|
|
14
14
|
model: "provider/model",
|
|
15
|
+
thinking: "low",
|
|
15
16
|
context: "fresh",
|
|
16
17
|
});
|
|
17
18
|
```
|
|
@@ -21,19 +22,27 @@ canned profile. `name` is an optional display name, not a unique identity or rol
|
|
|
21
22
|
Omitted cwd/model inherit the invoking session's effective selection. A supplied
|
|
22
23
|
model must be available; there is no hidden alternate-model retry catalog.
|
|
23
24
|
|
|
25
|
+
`thinking` accepts `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`.
|
|
26
|
+
It overrides parent effort for this child without changing the parent. It works
|
|
27
|
+
with an omitted model or a literal model ID on every supported host, including
|
|
28
|
+
official Pi 1.5.0. Omission preserves inherited effort. Pi applies its native
|
|
29
|
+
model-capability clamping; the launch receipt records the requested value, not
|
|
30
|
+
proof of the child's effective effort.
|
|
31
|
+
|
|
24
32
|
On hosts exporting Pi's native `resolveModelReference`, `model` also accepts
|
|
25
33
|
`provider/model:thinking` (for example `provider/model:low` or `provider/model:off`).
|
|
26
|
-
The explicit suffix overrides inherited thinking for that child only.
|
|
34
|
+
The explicit suffix overrides inherited thinking for that child only. If both
|
|
35
|
+
`thinking` and a suffix specify effort, they must match or the launch fails. Literal
|
|
27
36
|
model IDs take precedence, including IDs containing colons or additional slashes;
|
|
28
37
|
unknown IDs and invalid suffixes fail without fuzzy matching or another provider.
|
|
29
38
|
Native resolution follows Pi's case-insensitive ID matching. Pi still applies the
|
|
30
39
|
selected model's native thinking capabilities.
|
|
31
40
|
|
|
32
41
|
Official Pi, including 1.5.0, does not export that resolver. Older fork runtimes also
|
|
33
|
-
lack the export. Those hosts retain
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
lack the export. Those hosts retain case-sensitive literal-ID overrides and
|
|
43
|
+
support the `thinking` field. Suffix requests fail with an upgrade message rather
|
|
44
|
+
than being parsed by an extension-side replacement. Suffix support requires a
|
|
45
|
+
host with the public resolver export, not merely a matching version number.
|
|
37
46
|
|
|
38
47
|
Fresh context is the default. Fork uses the native conversation, including
|
|
39
48
|
historical tool calls and results; restricting a child's authority does not erase
|
|
@@ -58,6 +67,8 @@ tool or programmatic invocation of `delegate` or `agent_runs`.
|
|
|
58
67
|
|
|
59
68
|
Saved runs belong to the native main session that launched them, not its cwd,
|
|
60
69
|
project, display name, or a different session forked from its history.
|
|
70
|
+
Live guidance and saved-run successors keep the child's saved effort even if the
|
|
71
|
+
parent's effort has changed.
|
|
61
72
|
|
|
62
73
|
| Action | Purpose |
|
|
63
74
|
| ---------- | ------------------------------------------------------------------------------------ |
|
package/package.json
CHANGED
package/scripts/build.mjs
CHANGED
|
@@ -67,21 +67,37 @@ async function hasPublishedDist(/** @type {string} */ path) {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
/**
|
|
71
|
+
* @param {string} cwd
|
|
72
|
+
* @param {import("node:fs").Dirent} entry
|
|
73
|
+
* @returns {{ path: string, kind: "staging" | "previous" } | undefined}
|
|
74
|
+
*/
|
|
75
|
+
function strandedBuild(cwd, entry) {
|
|
71
76
|
// Signal 0 proves only that the owner was gone at probe time. Pid reuse
|
|
72
77
|
// between this probe and removal is inherent to pid-based reaping.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
const match = /^dist\.staging\.(\d+)(\.previous)?$/.exec(entry.name);
|
|
79
|
+
if (!match || !entry.isDirectory()) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const ownerPid = Number(match[1]);
|
|
83
|
+
if (ownerPid === process.pid || !isOwnerGone(ownerPid)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
return { path: join(cwd, entry.name), kind: match[2] === undefined ? "staging" : "previous" };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function reapStrandedBuilds(/** @type {string} */ cwd) {
|
|
90
|
+
const stranded = (await readdir(cwd, { withFileTypes: true })).flatMap((entry) => {
|
|
91
|
+
const build = strandedBuild(cwd, entry);
|
|
92
|
+
return build ? [build] : [];
|
|
93
|
+
});
|
|
94
|
+
for (const entry of stranded) {
|
|
95
|
+
if (entry.kind === "staging") {
|
|
96
|
+
await discardStaging(entry.path);
|
|
81
97
|
}
|
|
82
|
-
// Reap abandoned trees sequentially to bound filesystem removal pressure.
|
|
83
|
-
await discardStaging(join(cwd, entry.name));
|
|
84
98
|
}
|
|
99
|
+
// Never promote rollback material into another live publisher's retirement gap.
|
|
100
|
+
return stranded.filter((entry) => entry.kind === "previous").map((entry) => entry.path);
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
async function compileProject(
|
|
@@ -208,13 +224,24 @@ async function publishStaging(/** @type {string} */ stagingDir, /** @type {strin
|
|
|
208
224
|
async function main() {
|
|
209
225
|
const cwd = process.cwd();
|
|
210
226
|
const compiler = compilerInvocation(process.argv[2]);
|
|
211
|
-
await
|
|
227
|
+
const previous = await reapStrandedBuilds(cwd);
|
|
212
228
|
// Pid-scoped staging isolates concurrent emits; only a complete tree publishes.
|
|
213
229
|
const stagingDir = join(cwd, `dist.staging.${process.pid}`);
|
|
214
230
|
await rm(stagingDir, RM_OPTIONS);
|
|
215
231
|
await rm(`${stagingDir}.previous`, RM_OPTIONS);
|
|
216
|
-
|
|
217
|
-
|
|
232
|
+
try {
|
|
233
|
+
await compileToStaging(cwd, stagingDir, compiler);
|
|
234
|
+
await publishStaging(stagingDir, join(cwd, "dist"));
|
|
235
|
+
} catch (error) {
|
|
236
|
+
for (const retained of previous.filter(existsSync)) {
|
|
237
|
+
console.warn(`Previous build retained for recovery: ${retained}`);
|
|
238
|
+
}
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
// A complete published winner now replaces these older rollback candidates.
|
|
242
|
+
for (const retired of previous) {
|
|
243
|
+
await discardStaging(retired);
|
|
244
|
+
}
|
|
218
245
|
}
|
|
219
246
|
|
|
220
247
|
main().catch((/** @type {unknown} */ error) => {
|
|
@@ -14,6 +14,7 @@ profile discovery, loader, role body, chain DSL, or nested delegation.
|
|
|
14
14
|
delegate({
|
|
15
15
|
prompt: "Complete assignment with constraints and expected output",
|
|
16
16
|
name: "short-name",
|
|
17
|
+
thinking: "low",
|
|
17
18
|
});
|
|
18
19
|
```
|
|
19
20
|
|
|
@@ -23,12 +24,14 @@ expected result, and stop boundary. Do not rely on a hidden profile to add them.
|
|
|
23
24
|
|
|
24
25
|
Fresh context is default; `context:"fork"` preserves the native conversation,
|
|
25
26
|
including prior tool results. A forked child still cannot delegate. Optional
|
|
26
|
-
`cwd` and `
|
|
27
|
-
settings to implement an override.
|
|
27
|
+
`cwd`, `model`, and `thinking` are explicit launch overrides; do not change global
|
|
28
|
+
auth/model settings to implement an override. `thinking` accepts `off`, `minimal`,
|
|
29
|
+
`low`, `medium`, `high`, `xhigh`, or `max`. Omit it to inherit the parent's effort.
|
|
30
|
+
Pi clamps the child's effort to its model capabilities without changing the parent.
|
|
28
31
|
`model:"provider/model:thinking"` uses Pi's native suffix resolver when the host
|
|
29
32
|
exports it; a literal model ID wins before suffix parsing. Without that host API,
|
|
30
|
-
use
|
|
31
|
-
silently.
|
|
33
|
+
use `thinking` with an omitted model or literal ID. If both the field and a native
|
|
34
|
+
suffix supply effort, they must match. Never retry another model silently.
|
|
32
35
|
|
|
33
36
|
Every launch, continuation and guidance call returns a receipt; no tool waits for
|
|
34
37
|
child completion. Continue useful independent work; when child evidence gates the
|
|
@@ -45,6 +48,7 @@ invocation exits after its final turn and stops any remaining children.
|
|
|
45
48
|
- `inspect` reads a run by ID; follow returned offsets for complete output.
|
|
46
49
|
- `continue` takes a complete follow-up `prompt` and keeps the saved child chat.
|
|
47
50
|
Active work receives guidance; completed work gets an immutable successor.
|
|
51
|
+
Both keep the child's saved effort, independent of later parent changes.
|
|
48
52
|
- `stop` requests termination. Inspect actual process evidence; request acceptance
|
|
49
53
|
does not prove actor or command exit.
|
|
50
54
|
|
package/src/agent/contracts.ts
CHANGED
|
@@ -93,6 +93,23 @@ export const resultSchema = Type.Object({
|
|
|
93
93
|
),
|
|
94
94
|
});
|
|
95
95
|
export type Result = Static<typeof resultSchema>;
|
|
96
|
+
export type AccountingState =
|
|
97
|
+
| { readonly kind: "unrecorded"; readonly terminalEntryId: string | undefined }
|
|
98
|
+
| {
|
|
99
|
+
readonly kind: "verified";
|
|
100
|
+
readonly contributions: Contribution[];
|
|
101
|
+
readonly terminalEntryId: string | undefined;
|
|
102
|
+
}
|
|
103
|
+
| {
|
|
104
|
+
readonly kind: "pending";
|
|
105
|
+
readonly pending: NonNullable<Result["accountingPending"]>;
|
|
106
|
+
readonly error: string | undefined;
|
|
107
|
+
}
|
|
108
|
+
| {
|
|
109
|
+
readonly kind: "unavailable";
|
|
110
|
+
readonly error: string;
|
|
111
|
+
readonly terminalEntryId: string | undefined;
|
|
112
|
+
};
|
|
96
113
|
export const commandSchema = Type.Object({
|
|
97
114
|
id: Type.String(),
|
|
98
115
|
runId: Type.String(),
|
package/src/agent/host.ts
CHANGED
|
@@ -9,6 +9,25 @@ import { readIntercomLineage } from "../pi-intercom/identity.ts";
|
|
|
9
9
|
|
|
10
10
|
export { errorMessage as errorText } from "../shared/unknown.ts";
|
|
11
11
|
|
|
12
|
+
/** Native fork lifetime owner; structural detection keeps official Pi supported. */
|
|
13
|
+
export interface ManagedHost {
|
|
14
|
+
readonly owner: { readonly pid: number };
|
|
15
|
+
readonly ready: Promise<boolean>;
|
|
16
|
+
}
|
|
17
|
+
export function managedHost(context: SDK.ExtensionContext): ManagedHost | undefined {
|
|
18
|
+
const host: unknown = Reflect.get(context, "managedHost");
|
|
19
|
+
if (typeof host !== "object" || host === null) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const owner: unknown = Reflect.get(host, "owner");
|
|
23
|
+
const pid: unknown =
|
|
24
|
+
typeof owner === "object" && owner !== null ? Reflect.get(owner, "pid") : undefined;
|
|
25
|
+
const ready: unknown = Reflect.get(host, "ready");
|
|
26
|
+
return typeof pid === "number" && Number.isSafeInteger(pid) && ready instanceof Promise
|
|
27
|
+
? { owner: { pid }, ready: ready.then((accepted) => accepted === true) }
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
/** A launcher may still own children while handing off or replacing a worker. */
|
|
13
32
|
export function retainsChildren(event: object): boolean {
|
|
14
33
|
const disposition: unknown = Reflect.get(event, "disposition");
|