@fitchmultz/pi-subagents 0.52.4 → 0.52.5
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 +11 -0
- package/README.md +5 -4
- package/dist/agent/completion-watches.js +61 -0
- package/dist/agent/contracts.js +9 -0
- package/dist/agent/host.js +5 -0
- package/dist/agent/index.js +20 -51
- package/dist/agent/legacy.js +41 -11
- package/dist/agent/notifications.js +2 -2
- package/dist/agent/parking.js +121 -13
- package/dist/agent/process.js +107 -63
- package/dist/agent/receipts.js +21 -4
- package/dist/agent/runner.js +19 -5
- package/dist/agent/shutdown.js +43 -0
- package/dist/agent/view.js +5 -1
- package/dist/build-info.js +1 -1
- package/dist/pi-intercom/broker/broker.js +7 -6
- package/dist/pi-intercom/broker/client-wake.d.ts +17 -0
- package/dist/pi-intercom/broker/client-wake.js +54 -0
- package/dist/pi-intercom/broker/client.d.ts +2 -0
- package/dist/pi-intercom/broker/client.js +23 -16
- package/dist/pi-intercom/broker/parked.js +122 -5
- package/dist/pi-intercom/broker/spawn.js +10 -6
- package/dist/pi-intercom/config.js +3 -2
- package/dist/pi-intercom/connection.js +72 -11
- package/dist/pi-intercom/inbound-delivery.js +10 -5
- package/dist/pi-intercom/index.js +41 -6
- package/dist/pi-intercom/outbound.js +24 -7
- package/dist/pi-intercom/reply-wait.js +6 -1
- package/dist/pi-intercom/tool-results.js +2 -0
- package/dist/pi-intercom/types.d.ts +6 -0
- package/dist/shared/background-reporting.js +72 -0
- package/dist/shared/parking.js +6 -0
- package/docs/development.md +2 -0
- package/docs/intercom.md +14 -0
- package/docs/performance.md +2 -2
- package/docs/reference.md +13 -5
- package/package.json +1 -1
- package/src/agent/completion-watches.ts +73 -0
- package/src/agent/contracts.ts +11 -0
- package/src/agent/host.ts +6 -0
- package/src/agent/index.ts +24 -54
- package/src/agent/legacy.ts +44 -12
- package/src/agent/notifications.ts +2 -3
- package/src/agent/parking.ts +148 -12
- package/src/agent/process.ts +130 -66
- package/src/agent/receipts.ts +25 -4
- package/src/agent/runner.ts +25 -5
- package/src/agent/shutdown.ts +50 -0
- package/src/agent/view.ts +9 -0
- package/src/pi-intercom/broker/broker.ts +8 -7
- package/src/pi-intercom/broker/client-wake.ts +61 -0
- package/src/pi-intercom/broker/client.ts +23 -17
- package/src/pi-intercom/broker/parked.ts +152 -10
- package/src/pi-intercom/broker/spawn.ts +10 -9
- package/src/pi-intercom/config.ts +5 -2
- package/src/pi-intercom/connection.ts +88 -12
- package/src/pi-intercom/inbound-delivery.ts +21 -6
- package/src/pi-intercom/index.ts +64 -21
- package/src/pi-intercom/outbound.ts +30 -6
- package/src/pi-intercom/reply-wait.ts +6 -1
- package/src/pi-intercom/tool-results.ts +1 -0
- package/src/pi-intercom/types.ts +4 -0
- package/src/shared/background-reporting.ts +78 -0
- package/src/shared/parking.ts +16 -0
package/dist/shared/parking.js
CHANGED
|
@@ -7,6 +7,12 @@ export const PARKED_HISTORY = "pi-subagents:parked-history";
|
|
|
7
7
|
export function isParkRequest(value) {
|
|
8
8
|
return isRecord(value) && typeof value.park === "boolean" && typeof value.reply === "function";
|
|
9
9
|
}
|
|
10
|
+
export function isWakeRequest(value) {
|
|
11
|
+
return (isRecord(value) &&
|
|
12
|
+
typeof value.peer === "string" &&
|
|
13
|
+
typeof value.reply === "function" &&
|
|
14
|
+
(value.signal === undefined || value.signal instanceof AbortSignal));
|
|
15
|
+
}
|
|
10
16
|
export function isHistoryRequest(value) {
|
|
11
17
|
return isRecord(value) && typeof value.peer === "string" && typeof value.reply === "function";
|
|
12
18
|
}
|
package/docs/development.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Development
|
|
2
2
|
|
|
3
|
+
Node.js 24.21 or newer is required.
|
|
4
|
+
|
|
3
5
|
The supported Intercom client's package export uses emitted declarations for consumers.
|
|
4
6
|
The repository's native `development` type condition points at its TypeScript source so
|
|
5
7
|
cold lint and type checks do not require a prior build. Packed-consumer checks compile
|
package/docs/intercom.md
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
child-only supervisor tool, peer overlay, topic registry, or separate extension
|
|
5
5
|
to install. Ordinary and delegated sessions use the same messaging tool.
|
|
6
6
|
|
|
7
|
+
Background failures appear as native notifications when a UI is active and on
|
|
8
|
+
stderr in print or JSON mode, including during headless shutdown. In UI sessions,
|
|
9
|
+
failures during shutdown or after the UI context is unavailable are recorded in
|
|
10
|
+
`intercom/background-errors.log` under `PI_CODING_AGENT_DIR` (normally `~/.pi/agent`).
|
|
11
|
+
If that directory is not writable, the fallback cannot record them. Repeated
|
|
12
|
+
identical failures are reported once until the affected operation recovers.
|
|
13
|
+
|
|
7
14
|
## Discovery and groups
|
|
8
15
|
|
|
9
16
|
```js
|
|
@@ -139,6 +146,13 @@ active or sole pending ask when unambiguous; otherwise specify `replyTo` and the
|
|
|
139
146
|
intended sender. Inspect `pending` for the exact correlation rather than guessing
|
|
140
147
|
from text.
|
|
141
148
|
|
|
149
|
+
Messages accepted for a parked child remain queued until it returns. If its
|
|
150
|
+
owner reports a failed wake, a waiting ask returns the failure and says that the
|
|
151
|
+
accepted question may still arrive later. Check the child before resending to
|
|
152
|
+
avoid a duplicate question. A later active message requests another wake. Inspect
|
|
153
|
+
the child's run for its last recorded wake failure. With older brokers or
|
|
154
|
+
clients that cannot report wake outcomes, a waiting ask keeps its usual timeout.
|
|
155
|
+
|
|
142
156
|
This ordinary request/reply mechanism does not recreate the retired durable
|
|
143
157
|
supervisor approval/revival workflow. An expired wait is not an answered question
|
|
144
158
|
or a completed delegated run.
|
package/docs/performance.md
CHANGED
|
@@ -144,7 +144,7 @@ switch request through an immediate successful `get_state` response changed from
|
|
|
144
144
|
it now pays for cold receipt indexing. This is a change to when recovery work
|
|
145
145
|
runs, not an installed macOS or general delegation-latency claim.
|
|
146
146
|
|
|
147
|
-
The [issue 248 evidence](evidence/issue-248/
|
|
147
|
+
The [issue 248 evidence](evidence/issue-248/report.md) includes both waits,
|
|
148
148
|
switch-to-first-tool measurements, ranges, profiler results, exact runtime and
|
|
149
149
|
build identities, reproducible commands, and retained synthetic RPC transcripts.
|
|
150
150
|
|
|
@@ -167,7 +167,7 @@ after `session_start`, chiefly the intercom broker and owner attachment. In two
|
|
|
167
167
|
official-host 40 s floor/`0b6da9b` pairs, the later windows of about 10 s each
|
|
168
168
|
stayed within 1.1 ms of the paired floor. There is no fork long run, and this
|
|
169
169
|
is not an indefinite-idle bound. The
|
|
170
|
-
[issue 238 evidence](evidence/issue-238-idle/
|
|
170
|
+
[issue 238 evidence](evidence/issue-238-idle/report.md) has the samples, host
|
|
171
171
|
hashes, native causal chain and limits.
|
|
172
172
|
|
|
173
173
|
### Idle parking shell records
|
package/docs/reference.md
CHANGED
|
@@ -106,7 +106,12 @@ not wake it and report `publication_unavailable`. A
|
|
|
106
106
|
normal message, ask or continuation restores the exact saved state into a new
|
|
107
107
|
process, which then receives retained messages in order. If the final save
|
|
108
108
|
fails, the child records `park-failed.json` and nothing restarts implicitly;
|
|
109
|
-
retained messages stay queued and continuing reports the failure.
|
|
109
|
+
retained messages stay queued and continuing reports the failure. A failed wake
|
|
110
|
+
is recorded as the run's `lastWakeFailure` in `agent_runs list` and `inspect`.
|
|
111
|
+
This is a historical failure, not a claim about the child's current state. A
|
|
112
|
+
later active message requests another wake. The saved working directory must be
|
|
113
|
+
available; no other directory is substituted. A pre-existing ambiguous actor
|
|
114
|
+
claim still requires inspection and is not removed automatically. Official Pi
|
|
110
115
|
keeps idle children resident. Parent exit or session replacement stops its actors and verified owned
|
|
111
116
|
native shells, allowing native abort a short grace period before escalation.
|
|
112
117
|
Actors also watch for parent death, including a hard kill. Ordinary extension
|
|
@@ -227,10 +232,13 @@ New-runtime backlogs are sorted by completion time, then launch time and ID—no
|
|
|
227
232
|
directory order. A later scan cannot reorder messages already admitted to Pi.
|
|
228
233
|
|
|
229
234
|
Admission is still not publication or model consumption. Native physical receipts
|
|
230
|
-
suppress redelivery across reload/resume, and dropped queues can recover.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
235
|
+
suppress redelivery across reload/resume, and dropped queues can recover. A
|
|
236
|
+
`--no-session` parent has no session file, so Pi's in-memory transcript holds its
|
|
237
|
+
receipts for the life of that process; such a receipt never removes a retained
|
|
238
|
+
legacy result file. Receipt polling reuses unchanged verified
|
|
239
|
+
journal metadata; changed files validate the committed prefix before parsing
|
|
240
|
+
appended records. Replacement, truncation, changed prefixes, malformed records,
|
|
241
|
+
and missing final newlines do not fabricate receipts.
|
|
234
242
|
Completion delivery appends context; it does not replace prior prompts, tools,
|
|
235
243
|
model settings, or the active conversation.
|
|
236
244
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import type { BackgroundReporter } from "../shared/background-reporting.ts";
|
|
3
|
+
import { errorMessage } from "../shared/unknown.ts";
|
|
4
|
+
import { DirectoryWatch, type WatchState } from "./wake.ts";
|
|
5
|
+
|
|
6
|
+
/** Owns change hints for completion inputs and reports failures without losing polling. */
|
|
7
|
+
export class CompletionWatches {
|
|
8
|
+
private readonly watches = new Map<string, DirectoryWatch>();
|
|
9
|
+
private awaited: ReadonlySet<string> = new Set();
|
|
10
|
+
private readonly reporter: BackgroundReporter;
|
|
11
|
+
private readonly wake: () => void;
|
|
12
|
+
constructor(reporter: BackgroundReporter, wake: () => void) {
|
|
13
|
+
this.reporter = reporter;
|
|
14
|
+
this.wake = wake;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
update(
|
|
18
|
+
base: string,
|
|
19
|
+
legacy: readonly string[],
|
|
20
|
+
awaited: ReadonlySet<string>,
|
|
21
|
+
): "poll" | "rescan" | "hints" {
|
|
22
|
+
this.awaited = awaited;
|
|
23
|
+
const directories = [
|
|
24
|
+
...new Set([base, ...legacy, ...[...awaited].map((file) => path.dirname(file))]),
|
|
25
|
+
];
|
|
26
|
+
for (const [directory, watch] of this.watches) {
|
|
27
|
+
if (!directories.includes(directory)) {
|
|
28
|
+
watch.close();
|
|
29
|
+
this.watches.delete(directory);
|
|
30
|
+
this.reporter.recover(`watch:${directory}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const states = directories.map((directory): [string, WatchState] => [
|
|
34
|
+
directory,
|
|
35
|
+
this.ensure(directory, directory === base || legacy.includes(directory)),
|
|
36
|
+
]);
|
|
37
|
+
if (states.some(([directory, state]) => state === "unwatched" && !legacy.includes(directory))) {
|
|
38
|
+
return "poll";
|
|
39
|
+
}
|
|
40
|
+
return states.some(([, state]) => state === "armed") ? "rescan" : "hints";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private ensure(directory: string, every: boolean): WatchState {
|
|
44
|
+
let watch = this.watches.get(directory);
|
|
45
|
+
if (watch === undefined) {
|
|
46
|
+
watch = new DirectoryWatch(
|
|
47
|
+
directory,
|
|
48
|
+
(file) => every || file === null || this.awaited.has(path.join(directory, file)),
|
|
49
|
+
this.wake,
|
|
50
|
+
);
|
|
51
|
+
this.watches.set(directory, watch);
|
|
52
|
+
}
|
|
53
|
+
const owner = `watch:${directory}`;
|
|
54
|
+
try {
|
|
55
|
+
const state = watch.ensure();
|
|
56
|
+
this.reporter.recover(owner);
|
|
57
|
+
return state;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
watch.close();
|
|
60
|
+
this.reporter.report(owner, `Agent directory watch ${directory}: ${errorMessage(error)}`);
|
|
61
|
+
return "unwatched";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
close(): void {
|
|
66
|
+
for (const [directory, watch] of this.watches) {
|
|
67
|
+
watch.close();
|
|
68
|
+
this.reporter.recover(`watch:${directory}`);
|
|
69
|
+
}
|
|
70
|
+
this.watches.clear();
|
|
71
|
+
this.awaited = new Set();
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/agent/contracts.ts
CHANGED
|
@@ -113,6 +113,17 @@ export const parkedSchema = Type.Object({
|
|
|
113
113
|
leafId: Type.Union([Type.String(), Type.Null()]),
|
|
114
114
|
});
|
|
115
115
|
export type Parked = Static<typeof parkedSchema>;
|
|
116
|
+
export const wakeFailureSchema = Type.Object({
|
|
117
|
+
at: Type.Number(),
|
|
118
|
+
reason: Type.String(),
|
|
119
|
+
beforeOpen: Type.Optional(
|
|
120
|
+
Type.Object({
|
|
121
|
+
actorToken: Type.String(),
|
|
122
|
+
parkedToken: Type.String(),
|
|
123
|
+
}),
|
|
124
|
+
),
|
|
125
|
+
});
|
|
126
|
+
export const readySchema = Type.Object({ token: Type.String() });
|
|
116
127
|
export const exitSchema = Type.Object({
|
|
117
128
|
pid: Type.Integer(),
|
|
118
129
|
code: Type.Union([Type.Integer(), Type.Null()]),
|
package/src/agent/host.ts
CHANGED
|
@@ -9,6 +9,12 @@ import { readIntercomLineage } from "../pi-intercom/identity.ts";
|
|
|
9
9
|
|
|
10
10
|
export { errorMessage as errorText } from "../shared/unknown.ts";
|
|
11
11
|
|
|
12
|
+
/** A launcher may still own children while handing off or replacing a worker. */
|
|
13
|
+
export function retainsChildren(event: object): boolean {
|
|
14
|
+
const disposition: unknown = Reflect.get(event, "disposition");
|
|
15
|
+
return disposition === "handoff" || disposition === "provisional";
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
/** Fork-only exact native working state; official Pi lacks it, so its children stay resident. */
|
|
13
19
|
interface LocalWorkingState {
|
|
14
20
|
readonly header: { readonly id: string };
|
package/src/agent/index.ts
CHANGED
|
@@ -15,9 +15,9 @@ import {
|
|
|
15
15
|
parkFailure,
|
|
16
16
|
requestStop,
|
|
17
17
|
signalProcess,
|
|
18
|
-
stopChildren,
|
|
19
18
|
} from "./process.ts";
|
|
20
|
-
import {
|
|
19
|
+
import { stopChildren } from "./shutdown.ts";
|
|
20
|
+
import { isChild, errorText, rememberSDK, retainsChildren } from "./host.ts";
|
|
21
21
|
import { serveParked } from "./parking.ts";
|
|
22
22
|
import {
|
|
23
23
|
resultSchema,
|
|
@@ -45,23 +45,20 @@ import {
|
|
|
45
45
|
import { notifications, deliver, settle } from "./notifications.ts";
|
|
46
46
|
import { prepareLaunch } from "./launch.ts";
|
|
47
47
|
import { renderReceipt, renderInventory, renderInspection } from "./view.ts";
|
|
48
|
-
import { legacyDirectory,
|
|
49
|
-
import {
|
|
48
|
+
import { legacyDirectory, reconcileLegacy } from "./legacy.ts";
|
|
49
|
+
import { pathStamp } from "./wake.ts";
|
|
50
|
+
import { CompletionWatches } from "./completion-watches.ts";
|
|
50
51
|
import { savedContinuation } from "./saved.ts";
|
|
51
52
|
import { executionCwd } from "./cwd.ts";
|
|
52
53
|
import { getPiAgentDir } from "../pi-intercom/agent-dir.ts";
|
|
53
54
|
import { registerCompactMessages, renderCompactResult } from "../shared/compact-cards.ts";
|
|
55
|
+
import { BackgroundReporter } from "../shared/background-reporting.ts";
|
|
54
56
|
|
|
55
57
|
/** Native fork lifetime owner (`ctx.managedHost`); read structurally so official Pi stays supported. */
|
|
56
58
|
interface ManagedHost {
|
|
57
59
|
readonly owner: { readonly pid: number };
|
|
58
60
|
readonly ready: Promise<boolean>;
|
|
59
61
|
}
|
|
60
|
-
/** `handoff` and `provisional` mean another worker under the same launcher may still own these children. */
|
|
61
|
-
function retainsChildren(event: object): boolean {
|
|
62
|
-
const disposition: unknown = Reflect.get(event, "disposition");
|
|
63
|
-
return disposition === "handoff" || disposition === "provisional";
|
|
64
|
-
}
|
|
65
62
|
function managedHost(context: ExtensionContext): ManagedHost | undefined {
|
|
66
63
|
const host: unknown = Reflect.get(context, "managedHost");
|
|
67
64
|
if (typeof host !== "object" || host === null) {
|
|
@@ -86,6 +83,7 @@ function legacyInput(): string[] {
|
|
|
86
83
|
}
|
|
87
84
|
class AgentRuntime {
|
|
88
85
|
private ctx?: ExtensionContext;
|
|
86
|
+
private readonly reporter = new BackgroundReporter(() => this.ctx);
|
|
89
87
|
// A managed launcher owns children across worker replacement; otherwise this process does.
|
|
90
88
|
private host?: ManagedHost;
|
|
91
89
|
private accepted = false;
|
|
@@ -96,7 +94,7 @@ class AgentRuntime {
|
|
|
96
94
|
private timer?: NodeJS.Timeout;
|
|
97
95
|
private woken?: NodeJS.Immediate;
|
|
98
96
|
// Change hints for the owner directory, legacy results and every awaited canonical file.
|
|
99
|
-
private readonly watches
|
|
97
|
+
private readonly watches: CompletionWatches;
|
|
100
98
|
private awaited: ReadonlySet<string> = new Set();
|
|
101
99
|
// Input stamps the last reconciliation saw; a one-second probe recovers dropped hints.
|
|
102
100
|
private observed = new Map<string, string>();
|
|
@@ -109,6 +107,7 @@ class AgentRuntime {
|
|
|
109
107
|
private readonly pi: ExtensionAPI;
|
|
110
108
|
constructor(pi: ExtensionAPI) {
|
|
111
109
|
this.pi = pi;
|
|
110
|
+
this.watches = new CompletionWatches(this.reporter, this.wake);
|
|
112
111
|
}
|
|
113
112
|
private checkMain(): void {
|
|
114
113
|
if (isChild()) {
|
|
@@ -143,19 +142,19 @@ class AgentRuntime {
|
|
|
143
142
|
try {
|
|
144
143
|
const { notices, awaited } = notifications(this.ctx, this.base, this.owner);
|
|
145
144
|
this.awaited = awaited;
|
|
146
|
-
|
|
147
|
-
notices.unshift(...legacyNotifications(this.ctx));
|
|
148
|
-
} catch (error) {
|
|
149
|
-
console.error(`Legacy completion reconciliation: ${errorText(error)}`);
|
|
150
|
-
}
|
|
145
|
+
notices.unshift(...reconcileLegacy(this.ctx, this.reporter));
|
|
151
146
|
deliver(this.pi, this.ctx, notices);
|
|
147
|
+
this.reporter.recover("completion-scan");
|
|
152
148
|
} catch (error) {
|
|
153
149
|
reconciled = false;
|
|
154
|
-
|
|
150
|
+
this.reporter.report(
|
|
151
|
+
"completion-scan",
|
|
152
|
+
`Agent completion reconciliation: ${errorText(error)}`,
|
|
153
|
+
);
|
|
155
154
|
}
|
|
156
155
|
// Newly awaited files were absent when read, or are rechecked once by the probe.
|
|
157
156
|
this.observed = new Map(inputs().map((file) => [file, before.get(file) ?? ""]));
|
|
158
|
-
const next = this.
|
|
157
|
+
const next = this.watches.update(this.base, legacyInput(), this.awaited);
|
|
159
158
|
if (!reconciled || next === "poll") {
|
|
160
159
|
this.timer = setTimeout(this.scan, 200).unref();
|
|
161
160
|
} else if (next === "rescan") {
|
|
@@ -163,49 +162,17 @@ class AgentRuntime {
|
|
|
163
162
|
this.wake();
|
|
164
163
|
}
|
|
165
164
|
};
|
|
166
|
-
/** Watches every directory that can publish an awaited input; legacy results may never exist. */
|
|
167
|
-
private watch(): "poll" | "rescan" | "hints" {
|
|
168
|
-
const legacy = legacyInput();
|
|
169
|
-
const directories = [
|
|
170
|
-
...new Set([this.base, ...legacy, ...[...this.awaited].map((file) => path.dirname(file))]),
|
|
171
|
-
];
|
|
172
|
-
for (const [directory, watch] of this.watches) {
|
|
173
|
-
if (!directories.includes(directory)) {
|
|
174
|
-
watch.close();
|
|
175
|
-
this.watches.delete(directory);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
const states = directories.map((directory): [string, WatchState] => {
|
|
179
|
-
let watch = this.watches.get(directory);
|
|
180
|
-
if (watch === undefined) {
|
|
181
|
-
const every = directory === this.base || legacy.includes(directory);
|
|
182
|
-
watch = new DirectoryWatch(
|
|
183
|
-
directory,
|
|
184
|
-
(file) => every || file === null || this.awaited.has(path.join(directory, file)),
|
|
185
|
-
this.wake,
|
|
186
|
-
);
|
|
187
|
-
this.watches.set(directory, watch);
|
|
188
|
-
}
|
|
189
|
-
return [directory, watch.ensure()];
|
|
190
|
-
});
|
|
191
|
-
if (states.some(([directory, state]) => state === "unwatched" && !legacy.includes(directory))) {
|
|
192
|
-
return "poll";
|
|
193
|
-
}
|
|
194
|
-
return states.some(([, state]) => state === "armed") ? "rescan" : "hints";
|
|
195
|
-
}
|
|
196
165
|
private unwatch(): void {
|
|
197
166
|
clearTimeout(this.timer);
|
|
198
167
|
clearInterval(this.probe);
|
|
199
168
|
this.observed.clear();
|
|
200
169
|
clearImmediate(this.woken);
|
|
201
170
|
this.woken = undefined;
|
|
202
|
-
|
|
203
|
-
watch.close();
|
|
204
|
-
}
|
|
205
|
-
this.watches.clear();
|
|
171
|
+
this.watches.close();
|
|
206
172
|
this.awaited = new Set();
|
|
207
173
|
}
|
|
208
174
|
private start(context: ExtensionContext): Promise<void> | undefined {
|
|
175
|
+
this.reporter.start();
|
|
209
176
|
this.host = managedHost(context);
|
|
210
177
|
if (this.host === undefined || this.accepted || this.child) {
|
|
211
178
|
this.reset = this.reset.then(() => this.attach(context));
|
|
@@ -224,7 +191,7 @@ class AgentRuntime {
|
|
|
224
191
|
}
|
|
225
192
|
})
|
|
226
193
|
.catch((error: unknown) => {
|
|
227
|
-
|
|
194
|
+
this.reporter.report(undefined, `Agent runtime attachment: ${errorText(error)}`);
|
|
228
195
|
});
|
|
229
196
|
return undefined;
|
|
230
197
|
}
|
|
@@ -254,6 +221,7 @@ class AgentRuntime {
|
|
|
254
221
|
this.probe = setInterval(this.recover, 1000).unref();
|
|
255
222
|
}
|
|
256
223
|
private async dispose(retain: boolean): Promise<void> {
|
|
224
|
+
this.reporter.shutdown();
|
|
257
225
|
this.closing = true;
|
|
258
226
|
this.unwatch();
|
|
259
227
|
this.shutdown.abort();
|
|
@@ -267,7 +235,9 @@ class AgentRuntime {
|
|
|
267
235
|
generation: attachment.generation,
|
|
268
236
|
attached: false,
|
|
269
237
|
});
|
|
270
|
-
await stopChildren(this.base, this.owner)
|
|
238
|
+
await stopChildren(this.base, this.owner, (message) =>
|
|
239
|
+
this.reporter.report(undefined, message),
|
|
240
|
+
);
|
|
271
241
|
}
|
|
272
242
|
}
|
|
273
243
|
this.ctx = undefined;
|
|
@@ -296,7 +266,7 @@ class AgentRuntime {
|
|
|
296
266
|
signalProcess(target, "SIGKILL");
|
|
297
267
|
});
|
|
298
268
|
} catch (error) {
|
|
299
|
-
|
|
269
|
+
this.reporter.report(undefined, `Agent stop escalation ${launch.id}: ${errorText(error)}`);
|
|
300
270
|
}
|
|
301
271
|
}, 3000);
|
|
302
272
|
escalation.unref();
|
package/src/agent/legacy.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { errorText } from "./host.ts";
|
|
4
|
+
import type { BackgroundReporter } from "../shared/background-reporting.ts";
|
|
3
5
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
6
|
import { journalStamp } from "../shared/native-journal.ts";
|
|
5
7
|
import { receiptId } from "./receipts.ts";
|
|
@@ -7,7 +9,6 @@ import { resolveTempRootDir } from "../shared/types.ts";
|
|
|
7
9
|
import { isRecord } from "../shared/unknown.ts";
|
|
8
10
|
import { release, isHeld, type Completion } from "./notifications.ts";
|
|
9
11
|
import { missing } from "./storage.ts";
|
|
10
|
-
import { errorText } from "./host.ts";
|
|
11
12
|
|
|
12
13
|
interface LegacyInput {
|
|
13
14
|
file: string;
|
|
@@ -123,18 +124,24 @@ function receipt(body: unknown, input: LegacyInput): boolean {
|
|
|
123
124
|
}
|
|
124
125
|
return body.type === "message" && legacyWait(body.message, input);
|
|
125
126
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
)
|
|
127
|
+
/** Where Pi recorded this input's receipt: its session file, or memory under `--no-session`. */
|
|
128
|
+
function published(ctx: ExtensionContext, input: LegacyInput): "file" | "memory" | undefined {
|
|
129
|
+
const file = ctx.sessionManager.getSessionFile();
|
|
130
|
+
const matches = (body: unknown) => receipt(body, input);
|
|
131
|
+
if (file === undefined) {
|
|
132
|
+
return ctx.sessionManager.getEntries().some(matches) ? "memory" : undefined;
|
|
133
|
+
}
|
|
134
|
+
return receiptId(file, input.owner, matches) === undefined ? undefined : "file";
|
|
131
135
|
}
|
|
132
136
|
function notification(ctx: ExtensionContext, input: LegacyInput): Completion | undefined {
|
|
133
137
|
const identity = `legacy:${input.id}:${input.key}`;
|
|
134
|
-
|
|
138
|
+
const where = published(ctx, input);
|
|
139
|
+
if (where !== undefined) {
|
|
135
140
|
release(input.owner, identity);
|
|
136
|
-
// Retire only
|
|
141
|
+
// Retire only an unchanged recovery input whose receipt is LF-published in the session file.
|
|
142
|
+
// An in-memory receipt stops redelivery but never removes the saved input.
|
|
137
143
|
if (
|
|
144
|
+
where === "file" &&
|
|
138
145
|
journalStamp(fs.lstatSync(input.file, { bigint: true })) === input.stamp &&
|
|
139
146
|
fs.readFileSync(input.file, "utf8") === input.raw
|
|
140
147
|
) {
|
|
@@ -161,12 +168,15 @@ function legacyNotice(
|
|
|
161
168
|
ctx: ExtensionContext,
|
|
162
169
|
file: string,
|
|
163
170
|
scan: LegacyScan,
|
|
171
|
+
report: (file: string, error?: unknown) => void,
|
|
164
172
|
): Completion | undefined {
|
|
165
173
|
try {
|
|
166
174
|
const input = legacyInput(file, scan);
|
|
167
|
-
|
|
175
|
+
const result = input ? notification(ctx, input) : undefined;
|
|
176
|
+
report(file);
|
|
177
|
+
return result;
|
|
168
178
|
} catch (error) {
|
|
169
|
-
|
|
179
|
+
report(file, error);
|
|
170
180
|
return;
|
|
171
181
|
}
|
|
172
182
|
}
|
|
@@ -188,7 +198,18 @@ function startScan(ctx: ExtensionContext, directory: string): LegacyScan {
|
|
|
188
198
|
export function legacyDirectory(): string {
|
|
189
199
|
return path.join(resolveTempRootDir(), "async-subagent-results");
|
|
190
200
|
}
|
|
191
|
-
export function legacyNotifications(
|
|
201
|
+
export function legacyNotifications(
|
|
202
|
+
ctx: ExtensionContext,
|
|
203
|
+
reporter: BackgroundReporter,
|
|
204
|
+
): Completion[] {
|
|
205
|
+
const report = (file: string, error?: unknown) => {
|
|
206
|
+
const owner = `legacy:${file}`;
|
|
207
|
+
if (error === undefined) {
|
|
208
|
+
reporter.recover(owner);
|
|
209
|
+
} else {
|
|
210
|
+
reporter.report(owner, `Skipped legacy completion ${file}: ${errorText(error)}`);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
192
213
|
const notices: Completion[] = [];
|
|
193
214
|
const directory = legacyDirectory();
|
|
194
215
|
const root = path.dirname(directory);
|
|
@@ -212,10 +233,21 @@ export function legacyNotifications(ctx: ExtensionContext): Completion[] {
|
|
|
212
233
|
if (!name.endsWith(".json")) {
|
|
213
234
|
continue;
|
|
214
235
|
}
|
|
215
|
-
const notice = legacyNotice(ctx, path.join(directory, name), scan);
|
|
236
|
+
const notice = legacyNotice(ctx, path.join(directory, name), scan, report);
|
|
216
237
|
if (notice) {
|
|
217
238
|
notices.push(notice);
|
|
218
239
|
}
|
|
219
240
|
}
|
|
220
241
|
return notices;
|
|
221
242
|
}
|
|
243
|
+
|
|
244
|
+
export function reconcileLegacy(ctx: ExtensionContext, reporter: BackgroundReporter): Completion[] {
|
|
245
|
+
try {
|
|
246
|
+
const notices = legacyNotifications(ctx, reporter);
|
|
247
|
+
reporter.recover("legacy-scan");
|
|
248
|
+
return notices;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
reporter.report("legacy-scan", `Legacy completion reconciliation: ${errorText(error)}`);
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -54,7 +54,7 @@ export function settle(owner: string, pending: boolean): boolean {
|
|
|
54
54
|
return released;
|
|
55
55
|
}
|
|
56
56
|
export function published(ctx: ExtensionContext, launch: Launch): string | undefined {
|
|
57
|
-
return completionReceipts(ctx.sessionManager
|
|
57
|
+
return completionReceipts(ctx.sessionManager, launch.owner).get(launch.id);
|
|
58
58
|
}
|
|
59
59
|
export interface Completion {
|
|
60
60
|
readonly owner: string;
|
|
@@ -239,8 +239,7 @@ export function notifications(ctx: ExtensionContext, base: string, owner: string
|
|
|
239
239
|
const notices: Completion[] = [];
|
|
240
240
|
const awaited = new Set<string>();
|
|
241
241
|
const runs = completed(base, owner, awaited);
|
|
242
|
-
const receipts =
|
|
243
|
-
runs.length === 0 ? undefined : completionReceipts(ctx.sessionManager.getSessionFile(), owner);
|
|
242
|
+
const receipts = runs.length === 0 ? undefined : completionReceipts(ctx.sessionManager, owner);
|
|
244
243
|
for (const run of runs) {
|
|
245
244
|
const key = `${owner}/${run.id}`;
|
|
246
245
|
if (receipts?.get(run.id) !== undefined) {
|