@fitchmultz/pi-subagents 0.52.4 → 0.52.6
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 +29 -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 +15 -3
- package/docs/intercom.md +14 -0
- package/docs/performance.md +2 -2
- package/docs/reference.md +13 -5
- package/package.json +1 -1
- package/scripts/package-smoke.mjs +21 -3
- 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/agent/process.js
CHANGED
|
@@ -7,8 +7,8 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
7
7
|
import { isChildProcessReplaced, observeChildProcessReplaced, readChildProcessIdentity, readProcessClock, trySignalChildTree, } from "../shared/post-exit-stdio-guard.js";
|
|
8
8
|
import { agentObservationSocketPath } from "../pi-intercom/broker/paths.js";
|
|
9
9
|
import { hasErrorCode } from "../shared/unknown.js";
|
|
10
|
-
import { attachmentSchema, parkedSchema, processSchema, } from "./contracts.js";
|
|
11
|
-
import { conversation, enqueue,
|
|
10
|
+
import { attachmentSchema, parkedSchema, processSchema, wakeFailureSchema, } from "./contracts.js";
|
|
11
|
+
import { conversation, enqueue, read, write } from "./storage.js";
|
|
12
12
|
import { errorText } from "./host.js";
|
|
13
13
|
/** Owner records follow their process's birth; stat after read only moves the bound later. */
|
|
14
14
|
export function published(file, record) {
|
|
@@ -119,33 +119,64 @@ function childEnvironment(launch) {
|
|
|
119
119
|
PI_INTERCOM_PARENT_SESSION_ID: launch.owner,
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
-
function
|
|
122
|
+
function saveSpawnFailure(dir, error) {
|
|
123
|
+
try {
|
|
124
|
+
write(path.join(dir, "spawn-error.json"), { error: error.message });
|
|
125
|
+
return error;
|
|
126
|
+
}
|
|
127
|
+
catch (failure) {
|
|
128
|
+
return Object.assign(new Error(`${error.message}; could not save the spawn diagnostic: ${errorText(failure)}`, {
|
|
129
|
+
cause: error,
|
|
130
|
+
}), "code" in error ? { code: error.code } : {});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function spawnActor(base, launch, token, claim) {
|
|
123
134
|
const dir = conversation(base, launch);
|
|
124
|
-
const log = fs.openSync(path.join(dir, "runner.log"), "a", 0o600);
|
|
125
|
-
const runner = fileURLToPath(new URL(import.meta.url.endsWith(".ts") ? "./runner.ts" : "./runner.js", import.meta.url));
|
|
126
135
|
let child;
|
|
136
|
+
let spawnError;
|
|
127
137
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
138
|
+
const log = fs.openSync(path.join(dir, "runner.log"), "a", 0o600);
|
|
139
|
+
try {
|
|
140
|
+
const runner = fileURLToPath(new URL(import.meta.url.endsWith(".ts") ? "./runner.ts" : "./runner.js", import.meta.url));
|
|
141
|
+
child = spawn(process.execPath, [runner, base, launch.rootId, token, launch.id], {
|
|
142
|
+
cwd: launch.cwd,
|
|
143
|
+
env: childEnvironment(launch),
|
|
144
|
+
detached: true,
|
|
145
|
+
stdio: ["ignore", log, log],
|
|
146
|
+
});
|
|
147
|
+
if (child.pid === undefined) {
|
|
148
|
+
spawnError = new Promise((resolve) => {
|
|
149
|
+
child.once("error", (error) => resolve(saveSpawnFailure(dir, error)));
|
|
150
|
+
});
|
|
151
|
+
claim.release();
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
claim.spawned();
|
|
155
|
+
child.on("error", (error) => {
|
|
156
|
+
write(path.join(dir, "spawn-error.json"), { error: error.message });
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
fs.closeSync(log);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
claim.release();
|
|
166
|
+
throw error;
|
|
134
167
|
}
|
|
135
|
-
|
|
136
|
-
|
|
168
|
+
if (child.pid === undefined && spawnError) {
|
|
169
|
+
throw await spawnError;
|
|
170
|
+
}
|
|
171
|
+
if (child.pid === undefined) {
|
|
172
|
+
claim.spawned();
|
|
173
|
+
throw new Error("Actor spawn outcome is ambiguous.");
|
|
137
174
|
}
|
|
138
|
-
child.on("error", (error) => {
|
|
139
|
-
write(path.join(dir, "spawn-error.json"), { error: error.message });
|
|
140
|
-
});
|
|
141
175
|
child.on("exit", (code, signal) => {
|
|
142
176
|
write(path.join(dir, `exit-${token}.json`), { pid: child.pid, code, signal, at: Date.now() });
|
|
143
177
|
trySignalChildTree(child, "SIGKILL");
|
|
144
178
|
});
|
|
145
179
|
child.unref();
|
|
146
|
-
if (child.pid === undefined) {
|
|
147
|
-
throw new Error("Actor was not spawned; retained launch claim requires inspection.");
|
|
148
|
-
}
|
|
149
180
|
write(path.join(dir, "actor.lock", "process.json"), {
|
|
150
181
|
pid: child.pid,
|
|
151
182
|
identity: readChildProcessIdentity(child.pid) ?? "unverified",
|
|
@@ -170,6 +201,48 @@ async function awaitStopping(dir, existing, signal) {
|
|
|
170
201
|
}
|
|
171
202
|
}
|
|
172
203
|
}
|
|
204
|
+
function releaseClaim(lock, identity, token) {
|
|
205
|
+
try {
|
|
206
|
+
const current = fs.lstatSync(lock, { throwIfNoEntry: false });
|
|
207
|
+
if (current && identity && current.dev === identity.dev && current.ino === identity.ino) {
|
|
208
|
+
const file = path.join(lock, "claim.json");
|
|
209
|
+
const claim = read(file, processSchema);
|
|
210
|
+
if (claim === undefined || claim.token === token) {
|
|
211
|
+
fs.rmSync(file, { force: true });
|
|
212
|
+
fs.rmdirSync(lock);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// Keep the original failure; an unremovable claim remains safely ambiguous.
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function launchClaimedActor(base, launch, lock) {
|
|
221
|
+
let identity;
|
|
222
|
+
let token;
|
|
223
|
+
let state = "claimed";
|
|
224
|
+
const claim = {
|
|
225
|
+
spawned: () => {
|
|
226
|
+
state = "spawned";
|
|
227
|
+
},
|
|
228
|
+
release: () => {
|
|
229
|
+
if (state === "claimed") {
|
|
230
|
+
state = "released";
|
|
231
|
+
releaseClaim(lock, identity, token);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
try {
|
|
236
|
+
identity = fs.lstatSync(lock);
|
|
237
|
+
token = randomUUID();
|
|
238
|
+
write(path.join(lock, "claim.json"), { ...ownIdentity(), token });
|
|
239
|
+
await spawnActor(base, launch, token, claim);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
claim.release();
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
173
246
|
export async function ensureActor(base, launch, signal) {
|
|
174
247
|
const dir = conversation(base, launch);
|
|
175
248
|
const lock = path.join(dir, "actor.lock");
|
|
@@ -199,16 +272,26 @@ export async function ensureActor(base, launch, signal) {
|
|
|
199
272
|
}
|
|
200
273
|
throw error;
|
|
201
274
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
275
|
+
await launchClaimedActor(base, launch, lock);
|
|
276
|
+
}
|
|
277
|
+
/** A failed restore can hand the old cut forward only before native open and after death. */
|
|
278
|
+
function failedBeforeOpenToken(dir, exited, current) {
|
|
279
|
+
const failure = read(path.join(dir, `wake-failure-${exited.token}.json`), wakeFailureSchema);
|
|
280
|
+
if (current?.token === exited.token &&
|
|
281
|
+
!alive(current) &&
|
|
282
|
+
failure?.beforeOpen?.actorToken === exited.token) {
|
|
283
|
+
return failure.beforeOpen.parkedToken;
|
|
284
|
+
}
|
|
285
|
+
return undefined;
|
|
205
286
|
}
|
|
206
287
|
/** A failed park keeps its input queued; exact state is lost, so nothing restarts it implicitly. */
|
|
207
288
|
export function parkFailure(base, launch, exited) {
|
|
208
289
|
const dir = conversation(base, launch);
|
|
209
290
|
const parked = read(path.join(dir, "parked.json"), parkedSchema);
|
|
210
|
-
if (exited &&
|
|
211
|
-
|
|
291
|
+
if (exited &&
|
|
292
|
+
parked &&
|
|
293
|
+
parked.token !== exited.token &&
|
|
294
|
+
failedBeforeOpenToken(dir, exited, actor(base, launch)) !== parked.token) {
|
|
212
295
|
throw new Error("Parked working state belongs to another actor generation; nothing was reopened.");
|
|
213
296
|
}
|
|
214
297
|
const file = path.join(dir, "park-failed.json");
|
|
@@ -307,45 +390,6 @@ export function requestStop(base, launch, onError) {
|
|
|
307
390
|
});
|
|
308
391
|
return { targets: live, command };
|
|
309
392
|
}
|
|
310
|
-
export async function stopChildren(base, owner) {
|
|
311
|
-
const roots = new Set();
|
|
312
|
-
const targets = [];
|
|
313
|
-
for (const id of list(base)) {
|
|
314
|
-
try {
|
|
315
|
-
const launch = launchAt(base, id, owner);
|
|
316
|
-
if (!roots.has(launch.rootId)) {
|
|
317
|
-
roots.add(launch.rootId);
|
|
318
|
-
targets.push(...requestStop(base, launch, (error) => {
|
|
319
|
-
console.error(`Child shutdown ${id}: ${errorText(error)}`);
|
|
320
|
-
}).targets);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
catch (error) {
|
|
324
|
-
console.error(`Child shutdown ${id}: ${errorText(error)}`);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
await waitForExit(targets, 3000);
|
|
328
|
-
for (const target of targets) {
|
|
329
|
-
try {
|
|
330
|
-
signalProcess(target, "SIGKILL");
|
|
331
|
-
}
|
|
332
|
-
catch (error) {
|
|
333
|
-
console.error(`Child shutdown ${target.pid}: ${errorText(error)}`);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
await waitForExit(targets, 1000);
|
|
337
|
-
if (targets.some(alive)) {
|
|
338
|
-
console.error("Child shutdown could not confirm every owned process exit.");
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
async function waitForExit(targets, timeout) {
|
|
342
|
-
const deadline = Date.now() + timeout;
|
|
343
|
-
while (Date.now() < deadline && targets.some(alive)) {
|
|
344
|
-
// Join actual process exit while native abort persists the interrupted turn.
|
|
345
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
346
|
-
await delay(25);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
393
|
export function shell(file, token) {
|
|
350
394
|
const identity = published(file, read(file, processSchema));
|
|
351
395
|
if (identity && identity.token !== token) {
|
package/dist/agent/receipts.js
CHANGED
|
@@ -135,13 +135,30 @@ function owned(file, owner) {
|
|
|
135
135
|
const header = journal.entries.at(0);
|
|
136
136
|
return header?.type === "session" && header.id === owner ? journal : undefined;
|
|
137
137
|
}
|
|
138
|
+
/** Without a session file (`--no-session`), Pi's append-only memory is the whole native transcript. */
|
|
139
|
+
function remembered(manager, owner) {
|
|
140
|
+
const header = manager.getHeader();
|
|
141
|
+
if (header?.id !== owner) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const journal = { end: 0, entries: [], completions: new Map() };
|
|
145
|
+
// Index the same record frames the physical journal scan accepts.
|
|
146
|
+
for (const entry of [header, ...manager.getEntries()]) {
|
|
147
|
+
if (isRecord(entry)) {
|
|
148
|
+
addEntry(journal, entry);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return journal;
|
|
152
|
+
}
|
|
138
153
|
export function receiptId(file, owner, matches) {
|
|
139
154
|
const found = owned(file, owner)?.entries.find((entry) => entry.type !== "session" && matches(entry));
|
|
140
155
|
return typeof found?.id === "string" ? found.id : undefined;
|
|
141
156
|
}
|
|
142
|
-
/**
|
|
143
|
-
* A matching entry without an ID is unpublished.
|
|
157
|
+
/** Completion IDs by run from the native transcript: the session file when Pi persists one,
|
|
158
|
+
* otherwise Pi's in-memory entries. A matching entry without an ID is unpublished.
|
|
144
159
|
*/
|
|
145
|
-
export function completionReceipts(
|
|
146
|
-
|
|
160
|
+
export function completionReceipts(manager, owner) {
|
|
161
|
+
const file = manager.getSessionFile();
|
|
162
|
+
const journal = file === undefined ? remembered(manager, owner) : owned(file, owner);
|
|
163
|
+
return journal?.completions ?? new Map();
|
|
147
164
|
}
|
package/dist/agent/runner.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { commandSchema } from "./contracts.js";
|
|
3
|
+
import { commandSchema, parkedSchema, } from "./contracts.js";
|
|
4
4
|
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";
|
|
@@ -351,7 +351,7 @@ class Actor {
|
|
|
351
351
|
}
|
|
352
352
|
return this.parking;
|
|
353
353
|
}
|
|
354
|
-
failure(error) {
|
|
354
|
+
failure(error, unopened) {
|
|
355
355
|
const launch = this.current ?? this.initial;
|
|
356
356
|
const result = {
|
|
357
357
|
id: launch.id,
|
|
@@ -361,6 +361,16 @@ class Actor {
|
|
|
361
361
|
error: errorText(error),
|
|
362
362
|
};
|
|
363
363
|
const directory = runDirectory(this.base, launch.id);
|
|
364
|
+
if (fs.existsSync(path.join(this.dir, "parked.json"))) {
|
|
365
|
+
const parked = read(path.join(this.dir, "parked.json"), parkedSchema);
|
|
366
|
+
write(path.join(this.dir, `wake-failure-${this.token}.json`), {
|
|
367
|
+
at: Date.now(),
|
|
368
|
+
reason: errorText(error),
|
|
369
|
+
...(unopened !== undefined && parked?.token === unopened
|
|
370
|
+
? { beforeOpen: { actorToken: this.token, parkedToken: unopened } }
|
|
371
|
+
: {}),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
364
374
|
if (!fs.existsSync(path.join(directory, "result.json"))) {
|
|
365
375
|
fs.writeFileSync(path.join(directory, "output.txt"), "", { mode: 0o600 });
|
|
366
376
|
write(path.join(directory, "result.json"), result);
|
|
@@ -427,10 +437,14 @@ class Actor {
|
|
|
427
437
|
parked.adopt();
|
|
428
438
|
}
|
|
429
439
|
async serve() {
|
|
430
|
-
const sdk = await loadSDK(this.initial.sdk);
|
|
431
|
-
const parked = new Parking(this.base, this.initial, this.token, sdk);
|
|
432
440
|
let runtime;
|
|
441
|
+
let parked;
|
|
442
|
+
let parkedToken;
|
|
433
443
|
try {
|
|
444
|
+
// Capture the old generation before SDK import or any native working-state operation.
|
|
445
|
+
parkedToken = read(path.join(this.dir, "parked.json"), parkedSchema)?.token;
|
|
446
|
+
const sdk = await loadSDK(this.initial.sdk);
|
|
447
|
+
parked = new Parking(this.base, this.initial, this.token, sdk);
|
|
434
448
|
runtime = await this.start(sdk, parked);
|
|
435
449
|
process.on("SIGTERM", this.stop);
|
|
436
450
|
process.on("SIGINT", this.stop);
|
|
@@ -438,7 +452,7 @@ class Actor {
|
|
|
438
452
|
await this.active;
|
|
439
453
|
}
|
|
440
454
|
catch (error) {
|
|
441
|
-
this.failure(error);
|
|
455
|
+
this.failure(error, parked?.opened === true ? undefined : parkedToken);
|
|
442
456
|
}
|
|
443
457
|
finally {
|
|
444
458
|
await this.finish(runtime);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import { errorText } from "./host.js";
|
|
3
|
+
import { alive, requestStop, signalProcess } from "./process.js";
|
|
4
|
+
import { launchAt, list } from "./storage.js";
|
|
5
|
+
export async function stopChildren(base, owner, report) {
|
|
6
|
+
const roots = new Set();
|
|
7
|
+
const targets = [];
|
|
8
|
+
for (const id of list(base)) {
|
|
9
|
+
try {
|
|
10
|
+
const launch = launchAt(base, id, owner);
|
|
11
|
+
if (!roots.has(launch.rootId)) {
|
|
12
|
+
roots.add(launch.rootId);
|
|
13
|
+
targets.push(...requestStop(base, launch, (error) => {
|
|
14
|
+
report(`Child shutdown ${id}: ${errorText(error)}`);
|
|
15
|
+
}).targets);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
report(`Child shutdown ${id}: ${errorText(error)}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
await waitForExit(targets, 3000);
|
|
23
|
+
for (const target of targets) {
|
|
24
|
+
try {
|
|
25
|
+
signalProcess(target, "SIGKILL");
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
report(`Child shutdown ${target.pid}: ${errorText(error)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
await waitForExit(targets, 1000);
|
|
32
|
+
if (targets.some(alive)) {
|
|
33
|
+
report("Child shutdown could not confirm every owned process exit.");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function waitForExit(targets, timeout) {
|
|
37
|
+
const deadline = Date.now() + timeout;
|
|
38
|
+
while (Date.now() < deadline && targets.some(alive)) {
|
|
39
|
+
// Join actual process exit while native abort persists the interrupted turn.
|
|
40
|
+
// oxlint-disable-next-line no-await-in-loop
|
|
41
|
+
await delay(25);
|
|
42
|
+
}
|
|
43
|
+
}
|
package/dist/agent/view.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { readOutputPage } from "../shared/output-page.js";
|
|
4
4
|
import { actor, alive, observeAlive } from "./process.js";
|
|
5
|
-
import { admissionSchema, commandSchema, exitSchema, successorSchema, } from "./contracts.js";
|
|
5
|
+
import { admissionSchema, commandSchema, exitSchema, successorSchema, wakeFailureSchema, } from "./contracts.js";
|
|
6
6
|
import { conversation, read, runDirectory } from "./storage.js";
|
|
7
7
|
import { published } from "./notifications.js";
|
|
8
8
|
import { readCanonicalResult, readResult } from "./usage.js";
|
|
@@ -98,6 +98,7 @@ function metadata(base, launch, result, options) {
|
|
|
98
98
|
result,
|
|
99
99
|
nativeReceipt: options.ctx ? (published(options.ctx, launch) ?? null) : null,
|
|
100
100
|
process,
|
|
101
|
+
lastWakeFailure: read(path.join(conversation(base, launch), "wake-failure.json"), wakeFailureSchema),
|
|
101
102
|
sessionId: launch.sessionId,
|
|
102
103
|
sessionFile: launch.sessionFile,
|
|
103
104
|
outputFile,
|
|
@@ -178,6 +179,9 @@ function resultText(details) {
|
|
|
178
179
|
`Agent run ${details.id}: ${details.status}; admission ${details.admission}; process ${details.process.live ? "live" : "not observed live"}.`,
|
|
179
180
|
receiptText(details),
|
|
180
181
|
exitText(details),
|
|
182
|
+
details.lastWakeFailure
|
|
183
|
+
? `Last wake failure at ${new Date(details.lastWakeFailure.at).toISOString()}: ${details.lastWakeFailure.reason} Retained intercom messages may still arrive after a later wake.`
|
|
184
|
+
: "",
|
|
181
185
|
`Guidance (latest 20):\n${guidance.length > 0 ? guidance : " none"}`,
|
|
182
186
|
"Guidance states: pending = not yet admitted; accepted = native queue admission, not journal publication or model consumption; stale = rejected, no new work was started (retry the entire prompt as a continuation).",
|
|
183
187
|
details.result?.error ?? "",
|
package/dist/build-info.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const EXTENSION_BUILD = Object.freeze({"version":"0.52.
|
|
1
|
+
export const EXTENSION_BUILD = Object.freeze({"version":"0.52.6","sha256":"ccb2b2cb4725bfa7f5e813494a2007dc60ed9c04e677502d08df23612cc30af8"});
|
|
@@ -71,9 +71,7 @@ class IntercomBroker {
|
|
|
71
71
|
if (currentId === null && msg.type !== "register") {
|
|
72
72
|
throw new Error(`Received ${msg.type} before register`);
|
|
73
73
|
}
|
|
74
|
-
|
|
75
|
-
this.touchActivity(currentId, false);
|
|
76
|
-
}
|
|
74
|
+
this.touchActivity(currentId, false);
|
|
77
75
|
if (msg.type === "register") {
|
|
78
76
|
if (currentId !== null) {
|
|
79
77
|
throw new Error("Received duplicate register message");
|
|
@@ -107,7 +105,8 @@ class IntercomBroker {
|
|
|
107
105
|
break;
|
|
108
106
|
case "park":
|
|
109
107
|
case "unpark":
|
|
110
|
-
|
|
108
|
+
case "wake_result":
|
|
109
|
+
this.parking.handle(socket, currentId, msg);
|
|
111
110
|
break;
|
|
112
111
|
default:
|
|
113
112
|
throw new Error(`Unknown client message type: ${msg.type}`);
|
|
@@ -126,7 +125,7 @@ class IntercomBroker {
|
|
|
126
125
|
topics: resumed?.topics ?? new TopicSnapshot(info),
|
|
127
126
|
topicFrames,
|
|
128
127
|
lastMessages: msg.lastMessages === true,
|
|
129
|
-
parking
|
|
128
|
+
...this.parking.features(msg),
|
|
130
129
|
receiptReads: msg.receiptReads === 1,
|
|
131
130
|
});
|
|
132
131
|
setId(id);
|
|
@@ -139,6 +138,7 @@ class IntercomBroker {
|
|
|
139
138
|
sessionId: id,
|
|
140
139
|
lastMessages: true,
|
|
141
140
|
parking: msg.parking === true,
|
|
141
|
+
...this.parking.capabilities(msg),
|
|
142
142
|
receiptReads: 1,
|
|
143
143
|
...(msg.guardedDelivery === 1 ? { guardedDelivery: 1 } : {}),
|
|
144
144
|
...(topicFrames ? { topicsSupported: true, topicFrames: true } : {}),
|
|
@@ -331,12 +331,13 @@ class IntercomBroker {
|
|
|
331
331
|
accepted: reason === null,
|
|
332
332
|
delivered: reason === null && !queued,
|
|
333
333
|
...(queued ? { queued } : {}),
|
|
334
|
+
...(queued ? { reason: this.parking.queueReason(target, message.id) } : {}),
|
|
334
335
|
...(reason !== null ? { reason } : {}),
|
|
335
336
|
};
|
|
336
337
|
}
|
|
337
338
|
/** Update liveness/intercom-activity timestamps for a connected session. */
|
|
338
339
|
touchActivity(sessionId, comms) {
|
|
339
|
-
const session = this.sessions.get(sessionId);
|
|
340
|
+
const session = sessionId === null ? undefined : this.sessions.get(sessionId);
|
|
340
341
|
if (!session) {
|
|
341
342
|
return;
|
|
342
343
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type net from "node:net";
|
|
2
|
+
import type { EventEmitter } from "node:events";
|
|
3
|
+
import type { WakeResult } from "../types.ts";
|
|
4
|
+
import type { UnknownRecord } from "../../shared/unknown.ts";
|
|
5
|
+
/** Negotiated wake outcomes; old brokers and clients keep the original wake event. */
|
|
6
|
+
export declare class WakeMessages {
|
|
7
|
+
supported: boolean;
|
|
8
|
+
private readonly events;
|
|
9
|
+
private readonly socket;
|
|
10
|
+
constructor(events: EventEmitter, socket: () => net.Socket);
|
|
11
|
+
registration(): {
|
|
12
|
+
wakeResults?: 1;
|
|
13
|
+
wakeNotices?: 1;
|
|
14
|
+
};
|
|
15
|
+
receive(msg: UnknownRecord): void;
|
|
16
|
+
respond(requestId: string, result: WakeResult): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { validateIntercomMessageSize, writeMessage } from "./framing.js";
|
|
2
|
+
/** Negotiated wake outcomes; old brokers and clients keep the original wake event. */
|
|
3
|
+
export class WakeMessages {
|
|
4
|
+
supported = false;
|
|
5
|
+
events;
|
|
6
|
+
socket;
|
|
7
|
+
constructor(events, socket) {
|
|
8
|
+
this.events = events;
|
|
9
|
+
this.socket = socket;
|
|
10
|
+
}
|
|
11
|
+
registration() {
|
|
12
|
+
return {
|
|
13
|
+
...(this.events.listenerCount("wake_request") > 0 ? { wakeResults: 1 } : {}),
|
|
14
|
+
...(this.events.listenerCount("wake_failed") > 0 ? { wakeNotices: 1 } : {}),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
receive(msg) {
|
|
18
|
+
if (msg.type === "wake_registered") {
|
|
19
|
+
if (typeof msg.requestId !== "string") {
|
|
20
|
+
throw new Error("Invalid wake registration");
|
|
21
|
+
}
|
|
22
|
+
this.events.emit("wake_registered", msg.requestId);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (typeof msg.sessionId !== "string") {
|
|
26
|
+
throw new Error("Invalid wake message");
|
|
27
|
+
}
|
|
28
|
+
if (msg.type === "wake") {
|
|
29
|
+
if (this.supported && typeof msg.requestId === "string") {
|
|
30
|
+
this.events.emit("wake_request", msg.sessionId, msg.requestId);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
this.events.emit("wake", msg.sessionId);
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (typeof msg.messageId !== "string" || typeof msg.reason !== "string") {
|
|
38
|
+
throw new Error("Invalid wake failure");
|
|
39
|
+
}
|
|
40
|
+
this.events.emit("wake_failed", msg.sessionId, msg.messageId, msg.reason);
|
|
41
|
+
}
|
|
42
|
+
respond(requestId, result) {
|
|
43
|
+
if (!this.supported) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const frame = { type: "wake_result", requestId, result };
|
|
47
|
+
writeMessage(this.socket(), validateIntercomMessageSize(frame) === null
|
|
48
|
+
? frame
|
|
49
|
+
: {
|
|
50
|
+
...frame,
|
|
51
|
+
result: { status: "failed", reason: "Wake failed; inspect the agent run for details." },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
|
+
import { WakeMessages } from "./client-wake.ts";
|
|
2
3
|
import { type SessionInfo, type TopicUpdate, type TopicChange, type SessionSnapshot, type SendResult, type LastMessageQuery, type LastMessageResult, type SendOptions, type ReceiptQuery, type ReceiptResult } from "../types.ts";
|
|
3
4
|
export type { SendResult } from "../types.ts";
|
|
4
5
|
export interface IntercomClientOptions {
|
|
@@ -12,6 +13,7 @@ export declare class IntercomClient extends EventEmitter {
|
|
|
12
13
|
private _sessionId;
|
|
13
14
|
private _topicsSupported;
|
|
14
15
|
private readonly parking;
|
|
16
|
+
readonly wakes: WakeMessages;
|
|
15
17
|
private guardedDeliverySupported;
|
|
16
18
|
private readonly lastRequests;
|
|
17
19
|
private readonly pendingSends;
|
|
@@ -5,6 +5,7 @@ import { BrokerConnection, connectBrokerSocket } from "./connection.js";
|
|
|
5
5
|
import { SnapshotReader } from "./snapshot-reader.js";
|
|
6
6
|
import { LastMessageRequests, lastRequest } from "./client-last.js";
|
|
7
7
|
import { ParkRequests } from "./client-park.js";
|
|
8
|
+
import { WakeMessages } from "./client-wake.js";
|
|
8
9
|
import { isNativeReadResult } from "./last.js";
|
|
9
10
|
import { errorMessage, isRecord, isUnknownArray, } from "../../shared/unknown.js";
|
|
10
11
|
import { compactTopicMessage, normalizeMessage, normalizeSessionInfo, } from "../types.js";
|
|
@@ -32,6 +33,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
32
33
|
_sessionId = null;
|
|
33
34
|
_topicsSupported = false;
|
|
34
35
|
parking = new ParkRequests();
|
|
36
|
+
wakes = new WakeMessages(this, () => this.requireActiveSocket());
|
|
35
37
|
guardedDeliverySupported = false;
|
|
36
38
|
lastRequests;
|
|
37
39
|
pendingSends = new Map();
|
|
@@ -106,6 +108,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
106
108
|
this._topicsSupported = false;
|
|
107
109
|
this.parking.settle(false);
|
|
108
110
|
this.guardedDeliverySupported = false;
|
|
111
|
+
this.wakes.supported = false;
|
|
109
112
|
this.disconnecting = false;
|
|
110
113
|
this.failPending(error);
|
|
111
114
|
if (established && !expected) {
|
|
@@ -125,6 +128,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
125
128
|
parking: true,
|
|
126
129
|
receiptReads: 1,
|
|
127
130
|
guardedDelivery: 1,
|
|
131
|
+
...this.wakes.registration(),
|
|
128
132
|
});
|
|
129
133
|
}
|
|
130
134
|
catch (error) {
|
|
@@ -157,44 +161,46 @@ export class IntercomClient extends EventEmitter {
|
|
|
157
161
|
if (!isRecord(msg) || typeof msg.type !== "string") {
|
|
158
162
|
throw new Error("Invalid broker message");
|
|
159
163
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (this._sessionId === null && type !== "registered") {
|
|
163
|
-
throw new Error(`Received ${type} before registered`);
|
|
164
|
+
if (this._sessionId === null && msg.type !== "registered") {
|
|
165
|
+
throw new Error(`Received ${msg.type} before registered`);
|
|
164
166
|
}
|
|
165
|
-
switch (type) {
|
|
167
|
+
switch (msg.type) {
|
|
166
168
|
case "registered":
|
|
167
|
-
this.receiveRegistration(
|
|
169
|
+
this.receiveRegistration(msg);
|
|
168
170
|
break;
|
|
169
171
|
case "sessions":
|
|
170
|
-
this.receiveSessions(
|
|
172
|
+
this.receiveSessions(msg);
|
|
171
173
|
break;
|
|
172
174
|
case "last_request":
|
|
173
|
-
this.receiveLastRequest(
|
|
175
|
+
this.receiveLastRequest(msg);
|
|
174
176
|
break;
|
|
175
177
|
case "last_response":
|
|
176
|
-
this.lastRequests.receive(
|
|
178
|
+
this.lastRequests.receive(msg);
|
|
177
179
|
break;
|
|
178
180
|
case "message":
|
|
179
|
-
this.receiveMessage(
|
|
181
|
+
this.receiveMessage(msg);
|
|
180
182
|
break;
|
|
181
183
|
case "delivered":
|
|
182
184
|
case "delivery_queued":
|
|
183
185
|
case "delivery_failed":
|
|
184
|
-
this.receiveDelivery(
|
|
186
|
+
this.receiveDelivery(msg);
|
|
185
187
|
break;
|
|
186
188
|
case "parked":
|
|
187
189
|
this.parking.settle(true);
|
|
188
190
|
break;
|
|
189
|
-
case "wake":
|
|
190
191
|
case "session_left":
|
|
191
|
-
if (typeof
|
|
192
|
-
throw new Error(`Invalid ${type} message`);
|
|
192
|
+
if (typeof msg.sessionId !== "string") {
|
|
193
|
+
throw new Error(`Invalid ${msg.type} message`);
|
|
193
194
|
}
|
|
194
|
-
this.emit(type,
|
|
195
|
+
this.emit(msg.type, msg.sessionId);
|
|
196
|
+
break;
|
|
197
|
+
case "wake":
|
|
198
|
+
case "wake_failed":
|
|
199
|
+
case "wake_registered":
|
|
200
|
+
this.wakes.receive(msg);
|
|
195
201
|
break;
|
|
196
202
|
default:
|
|
197
|
-
throw new Error(`Unknown broker message type: ${type}`);
|
|
203
|
+
throw new Error(`Unknown broker message type: ${msg.type}`);
|
|
198
204
|
}
|
|
199
205
|
}
|
|
200
206
|
receiveRegistration(msg) {
|
|
@@ -210,6 +216,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
210
216
|
this.parking.supported = msg.parking === true;
|
|
211
217
|
this.lastRequests.receiptReads = msg.receiptReads === 1;
|
|
212
218
|
this.guardedDeliverySupported = msg.guardedDelivery === 1;
|
|
219
|
+
this.wakes.supported = msg.wakeResults === 1;
|
|
213
220
|
this.connection?.confirmRegistration();
|
|
214
221
|
this.emit("_registered", { type: "registered", sessionId: msg.sessionId });
|
|
215
222
|
}
|