@fitchmultz/pi-subagents 0.52.7 → 0.52.8
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 +15 -0
- package/dist/agent/launch.js +1 -1
- package/dist/agent/observation-view.js +15 -7
- package/dist/agent/observe-card.js +2 -2
- package/dist/agent/observe-client.js +1 -1
- package/dist/agent/observe.js +4 -1
- package/dist/agent/process.js +5 -3
- package/dist/agent/runner.js +9 -4
- package/dist/agent/saved.js +16 -6
- package/dist/agent/view.js +5 -2
- package/dist/build-info.js +1 -1
- package/dist/pi-intercom/bridge-routes.js +1 -1
- package/dist/pi-intercom/bridge.js +4 -3
- package/dist/pi-intercom/broker/broker.js +1 -1
- package/dist/pi-intercom/broker/client-last.d.ts +1 -1
- package/dist/pi-intercom/broker/client.d.ts +9 -9
- package/dist/pi-intercom/broker/framing.js +6 -0
- package/dist/pi-intercom/broker/last.d.ts +5 -1
- package/dist/pi-intercom/broker/last.js +33 -15
- package/dist/pi-intercom/broker/parked.js +1 -1
- package/dist/pi-intercom/outbound.js +1 -0
- package/dist/pi-intercom/reply-tracker.js +11 -5
- package/dist/pi-intercom/session-targets.d.ts +2 -2
- package/dist/pi-intercom/session-targets.js +3 -2
- package/dist/pi-intercom/tool-registration.js +1 -0
- package/dist/pi-intercom/types.d.ts +37 -37
- package/dist/pi-intercom/types.js +9 -7
- package/dist/shared/output-page.js +1 -1
- package/package.json +5 -4
- package/scripts/archive-retired-history.mjs +3 -1
- package/src/agent/index.ts +3 -3
- package/src/agent/journal-tail.ts +2 -2
- package/src/agent/launch.ts +1 -1
- package/src/agent/notifications.ts +3 -3
- package/src/agent/observation-state.ts +2 -2
- package/src/agent/observation-view.ts +20 -7
- package/src/agent/observation.ts +4 -4
- package/src/agent/observe-card.ts +7 -3
- package/src/agent/observe-chrome.ts +2 -2
- package/src/agent/observe-client.ts +5 -5
- package/src/agent/observe-reading.ts +1 -1
- package/src/agent/observe-screen.ts +4 -4
- package/src/agent/observe-transcript.ts +4 -4
- package/src/agent/observe.ts +13 -8
- package/src/agent/parking.ts +1 -1
- package/src/agent/process.ts +6 -4
- package/src/agent/runner.ts +15 -7
- package/src/agent/saved.ts +17 -7
- package/src/agent/usage.ts +1 -1
- package/src/agent/view.ts +18 -9
- package/src/agent/wake.ts +2 -2
- package/src/pi-intercom/bridge-config.ts +2 -2
- package/src/pi-intercom/bridge-mailbox.ts +1 -1
- package/src/pi-intercom/bridge-presence.ts +1 -1
- package/src/pi-intercom/bridge-routes.ts +5 -5
- package/src/pi-intercom/bridge-session.ts +5 -5
- package/src/pi-intercom/bridge-wake.ts +2 -2
- package/src/pi-intercom/bridge.ts +4 -3
- package/src/pi-intercom/broker/broker.ts +1 -1
- package/src/pi-intercom/broker/client-last.ts +1 -1
- package/src/pi-intercom/broker/client-park.ts +1 -1
- package/src/pi-intercom/broker/client.ts +9 -9
- package/src/pi-intercom/broker/framing.ts +6 -0
- package/src/pi-intercom/broker/last.ts +62 -18
- package/src/pi-intercom/broker/parked.ts +8 -5
- package/src/pi-intercom/config.ts +1 -1
- package/src/pi-intercom/outbound.ts +1 -0
- package/src/pi-intercom/reply-tracker.ts +13 -6
- package/src/pi-intercom/reply-wait.ts +3 -3
- package/src/pi-intercom/runtime-types.ts +1 -1
- package/src/pi-intercom/session-targets.ts +5 -4
- package/src/pi-intercom/tool-registration.ts +1 -0
- package/src/pi-intercom/tool-results.ts +1 -1
- package/src/pi-intercom/types.ts +48 -43
- package/src/shared/journal-frames.ts +1 -1
- package/src/shared/json-projection.ts +2 -2
- package/src/shared/native-journal.ts +2 -2
- package/src/shared/output-page.ts +3 -3
- package/src/shared/post-exit-stdio-guard.ts +4 -4
- package/tsconfig.json +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## 0.52.8 - 2026-11-02
|
|
6
|
+
|
|
7
|
+
<!-- npm-release:unreleased:start -->
|
|
8
|
+
<!-- npm-release:unreleased:end -->
|
|
9
|
+
|
|
10
|
+
- Merge pull request #321 from fitchmultz/chore/oxlint-v512-strict-ts ([600fcaa](https://github.com/fitchmultz/pi-subagents/commit/600fcaa9b22d59d4111179a8563332ff0420f9d0))
|
|
11
|
+
|
|
12
|
+
### What's Changed
|
|
13
|
+
|
|
14
|
+
- Align Oxlint toolchain and baseline config with the strict standard (1/5) by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/319
|
|
15
|
+
- Narrow the node:test registration allowance to package-origin distinct names (2/5) by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/320
|
|
16
|
+
- Adopt the strict compiler options and clear every strict-mode error (3/5) by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/321
|
|
17
|
+
|
|
18
|
+
**Full Changelog**: https://github.com/fitchmultz/pi-subagents/compare/v0.52.7...v0.52.8
|
|
19
|
+
|
|
5
20
|
## 0.52.7 - 2026-11-01
|
|
6
21
|
|
|
7
22
|
<!-- npm-release:unreleased:start -->
|
package/dist/agent/launch.js
CHANGED
|
@@ -89,7 +89,7 @@ export async function prepareLaunch(options) {
|
|
|
89
89
|
model: selection.model.id,
|
|
90
90
|
thinking: selection.thinking,
|
|
91
91
|
trusted,
|
|
92
|
-
name: input.name,
|
|
92
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
93
93
|
createdAt: Date.now(),
|
|
94
94
|
};
|
|
95
95
|
// Retain even unpublished native journals on failure, including full fork evidence.
|
|
@@ -197,7 +197,7 @@ export class Viewer {
|
|
|
197
197
|
}
|
|
198
198
|
message(frame, state) {
|
|
199
199
|
const live = state.message;
|
|
200
|
-
if (live === undefined) {
|
|
200
|
+
if (live === undefined || live.message === undefined) {
|
|
201
201
|
return undefined;
|
|
202
202
|
}
|
|
203
203
|
const reset = this.key !== live.key;
|
|
@@ -213,18 +213,26 @@ export class Viewer {
|
|
|
213
213
|
this.ended = live.ended;
|
|
214
214
|
const limit = Math.min(content.length, BLOCKS);
|
|
215
215
|
this.blocks.length = Math.min(this.blocks.length, limit);
|
|
216
|
-
|
|
217
|
-
const item = this.cursor(index, content[index]).next(index, frame, state);
|
|
218
|
-
if (item !== undefined) {
|
|
219
|
-
frame.blocks.push(item);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
216
|
+
this.appendBlocks(frame, state, content, limit);
|
|
222
217
|
if (!changed && frame.blocks.length === 0) {
|
|
223
218
|
return undefined;
|
|
224
219
|
}
|
|
225
220
|
// An ended message stays live until its session entry is found and handed over.
|
|
226
221
|
return `{"key":${live.key},"reset":${reset},"ended":${live.ended},"count":${content.length},"blocks":[${frame.blocks.join(",")}]}`;
|
|
227
222
|
}
|
|
223
|
+
/** Refill the frame from live content blocks, reusing cursors by block identity. */
|
|
224
|
+
appendBlocks(frame, state, content, limit) {
|
|
225
|
+
for (let index = 0; index < limit && !frame.full; index++) {
|
|
226
|
+
const block = content[index];
|
|
227
|
+
if (block === undefined) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const item = this.cursor(index, block).next(index, frame, state);
|
|
231
|
+
if (item !== undefined) {
|
|
232
|
+
frame.blocks.push(item);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
228
236
|
cursor(index, block) {
|
|
229
237
|
const existing = this.blocks.at(index);
|
|
230
238
|
if (existing?.ref === block) {
|
|
@@ -119,7 +119,7 @@ export class Card {
|
|
|
119
119
|
this.tool.updateResult({
|
|
120
120
|
content: blocks(message.content)
|
|
121
121
|
.filter((part) => part.type === "text")
|
|
122
|
-
.map((part) =>
|
|
122
|
+
.map((part) => part.text === undefined ? { type: "text" } : { type: "text", text: part.text }),
|
|
123
123
|
isError: message.isError === true,
|
|
124
124
|
});
|
|
125
125
|
this.tool.setExpanded(this.expanded);
|
|
@@ -169,7 +169,7 @@ export class Card {
|
|
|
169
169
|
this.tool?.updateResult({
|
|
170
170
|
content: blocks(result.content)
|
|
171
171
|
.filter((part) => part.type === "text")
|
|
172
|
-
.map((part) =>
|
|
172
|
+
.map((part) => part.text === undefined ? { type: "text" } : { type: "text", text: part.text }),
|
|
173
173
|
isError,
|
|
174
174
|
}, partial);
|
|
175
175
|
}
|
|
@@ -259,7 +259,7 @@ export class ObservationClient {
|
|
|
259
259
|
}
|
|
260
260
|
/** Retire live state only after its exact journal entries have been indexed. */
|
|
261
261
|
settle(has) {
|
|
262
|
-
while (this.handovers.length > 0 && has(this.handovers[0][1])) {
|
|
262
|
+
while (this.handovers.length > 0 && has(this.handovers[0]?.[1] ?? "")) {
|
|
263
263
|
const item = this.handovers.shift();
|
|
264
264
|
if (item) {
|
|
265
265
|
this.retire(item[2], item[3]);
|
package/dist/agent/observe.js
CHANGED
|
@@ -199,7 +199,10 @@ class Observer {
|
|
|
199
199
|
if (this.ctx !== ctx) {
|
|
200
200
|
return;
|
|
201
201
|
}
|
|
202
|
-
const live = new Map(claims.
|
|
202
|
+
const live = new Map(claims.flatMap(({ root }, index) => {
|
|
203
|
+
const verdict = verdicts[index];
|
|
204
|
+
return verdict === undefined ? [] : [[root, verdict]];
|
|
205
|
+
}));
|
|
203
206
|
for (const commit of commits) {
|
|
204
207
|
commit(live);
|
|
205
208
|
}
|
package/dist/agent/process.js
CHANGED
|
@@ -70,7 +70,7 @@ export class ParentCheck {
|
|
|
70
70
|
try {
|
|
71
71
|
const [live] = await observeAlive([attachment]);
|
|
72
72
|
if (this.current === check &&
|
|
73
|
-
|
|
73
|
+
live !== true &&
|
|
74
74
|
read(file, attachmentSchema)?.generation === attachment.generation) {
|
|
75
75
|
detached();
|
|
76
76
|
}
|
|
@@ -94,10 +94,11 @@ function replaced(identity) {
|
|
|
94
94
|
}
|
|
95
95
|
export function ownIdentity(pid = process.pid) {
|
|
96
96
|
// An ordinary main or managed launcher need not be a group leader. Ambiguous live PIDs fail closed.
|
|
97
|
+
const clock = readProcessClock();
|
|
97
98
|
return {
|
|
98
99
|
pid,
|
|
99
100
|
identity: readChildProcessIdentity(pid) ?? "unverified",
|
|
100
|
-
clock:
|
|
101
|
+
...(clock !== undefined ? { clock } : {}),
|
|
101
102
|
};
|
|
102
103
|
}
|
|
103
104
|
export function actor(base, launch) {
|
|
@@ -177,11 +178,12 @@ async function spawnActor(base, launch, token, claim) {
|
|
|
177
178
|
trySignalChildTree(child, "SIGKILL");
|
|
178
179
|
});
|
|
179
180
|
child.unref();
|
|
181
|
+
const clock = readProcessClock();
|
|
180
182
|
write(path.join(dir, "actor.lock", "process.json"), {
|
|
181
183
|
pid: child.pid,
|
|
182
184
|
identity: readChildProcessIdentity(child.pid) ?? "unverified",
|
|
183
185
|
token,
|
|
184
|
-
clock:
|
|
186
|
+
...(clock !== undefined ? { clock } : {}),
|
|
185
187
|
});
|
|
186
188
|
}
|
|
187
189
|
async function awaitStopping(dir, existing, signal) {
|
package/dist/agent/runner.js
CHANGED
|
@@ -31,6 +31,10 @@ function completionStatus(stopping, message, error) {
|
|
|
31
31
|
}
|
|
32
32
|
return "completed";
|
|
33
33
|
}
|
|
34
|
+
/** The recorded failure: the launch error, the model's error, or the missing terminal message. */
|
|
35
|
+
const failureReason = (error, message) => error ??
|
|
36
|
+
message?.errorMessage ??
|
|
37
|
+
(message ? undefined : "Native run produced no terminal assistant message.");
|
|
34
38
|
class Actor {
|
|
35
39
|
active;
|
|
36
40
|
stopping = false;
|
|
@@ -81,16 +85,17 @@ class Actor {
|
|
|
81
85
|
const message = terminal?.message;
|
|
82
86
|
const accounting = this.usage(launch, baseline, terminal?.id);
|
|
83
87
|
const output = this.output(message);
|
|
88
|
+
const failure = failureReason(error, message);
|
|
84
89
|
const result = {
|
|
85
90
|
id: launch.id,
|
|
86
91
|
owner: launch.owner,
|
|
87
92
|
status: completionStatus(this.stopping, message, error),
|
|
88
93
|
endedAt: Date.now(),
|
|
89
|
-
|
|
94
|
+
...(accounting.accountingError === undefined && terminal?.id !== undefined
|
|
95
|
+
? { terminalEntryId: terminal.id }
|
|
96
|
+
: {}),
|
|
90
97
|
...accounting,
|
|
91
|
-
error:
|
|
92
|
-
message?.errorMessage ??
|
|
93
|
-
(message ? undefined : "Native run produced no terminal assistant message."),
|
|
98
|
+
...(failure !== undefined ? { error: failure } : {}),
|
|
94
99
|
};
|
|
95
100
|
fs.writeFileSync(path.join(runDirectory(this.base, launch.id), "output.txt"), output, {
|
|
96
101
|
mode: 0o600,
|
package/dist/agent/saved.js
CHANGED
|
@@ -42,9 +42,19 @@ function ownedRun(context, id) {
|
|
|
42
42
|
throw new Error("Legacy continuation requires a published native owner.");
|
|
43
43
|
}
|
|
44
44
|
const journal = new NativeJournal(file, "live");
|
|
45
|
-
|
|
45
|
+
const [opening] = journal.records;
|
|
46
|
+
if (opening === undefined || opening.value.id !== context.sessionManager.getSessionId()) {
|
|
46
47
|
throw new Error("Native owner journal identity changed; nothing was launched.");
|
|
47
48
|
}
|
|
49
|
+
const owned = findOwnedRun(journal, id);
|
|
50
|
+
if (owned?.ownerSessionId !== context.sessionManager.getSessionId()) {
|
|
51
|
+
throw new Error("Missing saved native owner or foreign legacy run; nothing was launched.");
|
|
52
|
+
}
|
|
53
|
+
Assert(singleSchema, owned);
|
|
54
|
+
return owned;
|
|
55
|
+
}
|
|
56
|
+
/** The most recent saved subagent-run record for this run id, if one exists. */
|
|
57
|
+
function findOwnedRun(journal, id) {
|
|
48
58
|
let owned;
|
|
49
59
|
for (const record of journal.records) {
|
|
50
60
|
if (record.value.type === "custom" && record.value.customType === "subagent-run") {
|
|
@@ -54,10 +64,6 @@ function ownedRun(context, id) {
|
|
|
54
64
|
}
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
|
-
if (owned?.ownerSessionId !== context.sessionManager.getSessionId()) {
|
|
58
|
-
throw new Error("Missing saved native owner or foreign legacy run; nothing was launched.");
|
|
59
|
-
}
|
|
60
|
-
Assert(singleSchema, owned);
|
|
61
67
|
return owned;
|
|
62
68
|
}
|
|
63
69
|
function requireExited(pid) {
|
|
@@ -95,7 +101,11 @@ function nativeSession(contract) {
|
|
|
95
101
|
throw new Error("Legacy continuation requires the exact saved native journal.");
|
|
96
102
|
}
|
|
97
103
|
const journal = new NativeJournal(contract.sessionFile, "live");
|
|
98
|
-
const
|
|
104
|
+
const [opening] = journal.records;
|
|
105
|
+
if (opening === undefined) {
|
|
106
|
+
throw new Error("Not a readable native Pi session.");
|
|
107
|
+
}
|
|
108
|
+
const sessionId = journal.body(opening).id;
|
|
99
109
|
if (typeof sessionId !== "string" ||
|
|
100
110
|
sessionId.length === 0 ||
|
|
101
111
|
(contract.nativeSessionId !== undefined && contract.nativeSessionId !== sessionId)) {
|
package/dist/agent/view.js
CHANGED
|
@@ -111,7 +111,10 @@ async function summaries(base, launches, ctx) {
|
|
|
111
111
|
...new Map(launches.map((launch) => [launch.rootId, actor(base, launch)])).values(),
|
|
112
112
|
].filter((identity) => identity !== undefined);
|
|
113
113
|
const verdicts = await observeAlive(identities);
|
|
114
|
-
const observed = new Map(identities.
|
|
114
|
+
const observed = new Map(identities.flatMap((identity, index) => {
|
|
115
|
+
const verdict = verdicts[index];
|
|
116
|
+
return verdict === undefined ? [] : [[identity.token, verdict]];
|
|
117
|
+
}));
|
|
115
118
|
return launches.map((launch) => {
|
|
116
119
|
const view = metadata(base, launch, readCanonicalResult(base, launch), {
|
|
117
120
|
ctx,
|
|
@@ -125,7 +128,7 @@ async function summaries(base, launches, ctx) {
|
|
|
125
128
|
? {
|
|
126
129
|
status: view.result.status,
|
|
127
130
|
endedAt: view.result.endedAt,
|
|
128
|
-
error: view.result.error
|
|
131
|
+
...(view.result.error !== undefined ? { error: view.result.error.slice(0, 2048) } : {}),
|
|
129
132
|
}
|
|
130
133
|
: undefined,
|
|
131
134
|
};
|
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.8","sha256":"afeb6d4b7e13dfe796c199d39eddf3836e8f7ee7185f302254f4182659a0388c"});
|
|
@@ -16,7 +16,7 @@ export function routeFor(req) {
|
|
|
16
16
|
Object.keys(req.headers).some((key) => key.startsWith("sec-fetch-"))) {
|
|
17
17
|
throw new Fault(403, "browser_forbidden", "Browser access is not supported.");
|
|
18
18
|
}
|
|
19
|
-
const route = `${req.method ?? ""} ${(req.url ?? "").split("?")[0]}`;
|
|
19
|
+
const route = `${req.method ?? ""} ${(req.url ?? "").split("?")[0] ?? ""}`;
|
|
20
20
|
if (!isRoute(route)) {
|
|
21
21
|
throw new Fault(404, "not_found", "Unknown method or path.");
|
|
22
22
|
}
|
|
@@ -366,17 +366,18 @@ class Bridge {
|
|
|
366
366
|
}
|
|
367
367
|
function main() {
|
|
368
368
|
const args = process.argv.slice(2);
|
|
369
|
-
|
|
369
|
+
const [flag, configPath] = args;
|
|
370
|
+
if (args.length === 1 && (flag === "-h" || flag === "--help")) {
|
|
370
371
|
process.stdout.write("Usage: node dist/pi-intercom/bridge.js --config PATH\n\nOptional loopback HTTPS/mTLS bridge for the local pi-intercom broker.\nConfig and TLS key must be owned by you with mode 0600.\nWithout stateDir, messaging never starts the broker. Durable presence safely starts\none only when none is running; it never restarts or replaces a live broker.\nSIGHUP reloads the client allowlist (invalid reload denies all clients).\nRestart this helper for TLS/listener rotation. SIGINT/SIGTERM disconnect peers.\n\nExample: node dist/pi-intercom/bridge.js --config ~/.pi/agent/intercom/bridge.json\n");
|
|
371
372
|
return;
|
|
372
373
|
}
|
|
373
|
-
if (args.length !== 2 ||
|
|
374
|
+
if (args.length !== 2 || flag !== "--config" || configPath === undefined || configPath === "") {
|
|
374
375
|
process.stderr.write("Usage: node dist/pi-intercom/bridge.js --config PATH (or --help)\n");
|
|
375
376
|
process.exitCode = 2;
|
|
376
377
|
return;
|
|
377
378
|
}
|
|
378
379
|
try {
|
|
379
|
-
const path = resolve(
|
|
380
|
+
const path = resolve(configPath);
|
|
380
381
|
new Bridge(loadConfig(path), path).start();
|
|
381
382
|
}
|
|
382
383
|
catch (error) {
|
|
@@ -250,7 +250,7 @@ class IntercomBroker {
|
|
|
250
250
|
}
|
|
251
251
|
const targets = findSessions(this.sessions, msg.to, currentId);
|
|
252
252
|
const target = targets[0];
|
|
253
|
-
if (targets.length === 1) {
|
|
253
|
+
if (targets.length === 1 && target !== undefined) {
|
|
254
254
|
const guardError = guardedDeliveryError(this.sessions, msg.to, message);
|
|
255
255
|
if (guardError !== null) {
|
|
256
256
|
writeMessage(socket, {
|
|
@@ -7,7 +7,7 @@ export declare function lastRequest(msg: UnknownRecord): {
|
|
|
7
7
|
readonly requestId: string;
|
|
8
8
|
readonly from: SessionInfo;
|
|
9
9
|
readonly query: NativeReadQuery;
|
|
10
|
-
readonly parked?: SessionInfo;
|
|
10
|
+
readonly parked?: SessionInfo | undefined;
|
|
11
11
|
};
|
|
12
12
|
export declare class LastMessageRequests {
|
|
13
13
|
/** Negotiated at registration; a failed connection supports neither. */
|
|
@@ -48,14 +48,14 @@ export declare class IntercomClient extends EventEmitter {
|
|
|
48
48
|
private requireSendSocket;
|
|
49
49
|
send(to: string, options: SendOptions): Promise<SendResult>;
|
|
50
50
|
updatePresence(updates: {
|
|
51
|
-
readonly applicationGuard?: string | null;
|
|
52
|
-
readonly name?: string;
|
|
53
|
-
readonly status?: string;
|
|
54
|
-
readonly model?: string;
|
|
55
|
-
readonly pendingAsks?: number;
|
|
56
|
-
readonly acceptsAsks?: boolean;
|
|
57
|
-
readonly lastIntercomActivity?: number;
|
|
58
|
-
readonly subscriptions?: SessionInfo["subscriptions"];
|
|
59
|
-
readonly topics?: readonly TopicUpdate[];
|
|
51
|
+
readonly applicationGuard?: string | null | undefined;
|
|
52
|
+
readonly name?: string | undefined;
|
|
53
|
+
readonly status?: string | undefined;
|
|
54
|
+
readonly model?: string | undefined;
|
|
55
|
+
readonly pendingAsks?: number | undefined;
|
|
56
|
+
readonly acceptsAsks?: boolean | undefined;
|
|
57
|
+
readonly lastIntercomActivity?: number | undefined;
|
|
58
|
+
readonly subscriptions?: SessionInfo["subscriptions"] | undefined;
|
|
59
|
+
readonly topics?: readonly TopicUpdate[] | undefined;
|
|
60
60
|
}): void;
|
|
61
61
|
}
|
|
@@ -48,10 +48,16 @@ class MessageBuffer {
|
|
|
48
48
|
if (size === 0) {
|
|
49
49
|
return Buffer.alloc(0);
|
|
50
50
|
}
|
|
51
|
+
if (first === undefined) {
|
|
52
|
+
throw new Error("Frame reader has no buffered chunk for a non-empty read.");
|
|
53
|
+
}
|
|
51
54
|
const result = first.length >= size ? first.subarray(0, size) : Buffer.allocUnsafe(size);
|
|
52
55
|
let written = 0;
|
|
53
56
|
while (written < size) {
|
|
54
57
|
const chunk = this.chunks[this.index];
|
|
58
|
+
if (chunk === undefined) {
|
|
59
|
+
throw new Error("Frame reader ran out of buffered chunks mid-read.");
|
|
60
|
+
}
|
|
55
61
|
const count = Math.min(chunk.length, size - written);
|
|
56
62
|
if (first.length < size) {
|
|
57
63
|
chunk.copy(result, written, 0, count);
|
|
@@ -14,7 +14,7 @@ interface LastPeer {
|
|
|
14
14
|
readonly receiptReads: boolean;
|
|
15
15
|
readonly parked?: {
|
|
16
16
|
readonly closed: boolean;
|
|
17
|
-
};
|
|
17
|
+
} | undefined;
|
|
18
18
|
}
|
|
19
19
|
/** Correlation belongs to the broker, not the requester or the recipient's model queue. */
|
|
20
20
|
export declare class LastMessageRelay {
|
|
@@ -23,6 +23,10 @@ export declare class LastMessageRelay {
|
|
|
23
23
|
constructor(sessions: ReadonlyMap<string, LastPeer>);
|
|
24
24
|
/** `reader` names another peer that answers for an exited target from its captured history. */
|
|
25
25
|
request(fromId: string, input: UnknownRecord, reader: (id: string) => LastPeer | undefined): void;
|
|
26
|
+
/** Register the correlated wait, then forward the read to the reader or the recipient. */
|
|
27
|
+
private forward;
|
|
28
|
+
/** A sole matched target may delegate the read to another peer's captured history. */
|
|
29
|
+
private delegatedReader;
|
|
26
30
|
respond(fromId: string, message: UnknownRecord): void;
|
|
27
31
|
disconnected(sessionId: string): void;
|
|
28
32
|
private finish;
|
|
@@ -75,21 +75,22 @@ function exitedRefusal(target, query, via) {
|
|
|
75
75
|
return via === undefined ? "Parked session history is unavailable." : undefined;
|
|
76
76
|
}
|
|
77
77
|
function unavailable(targets, name, pending, { query, via }) {
|
|
78
|
-
|
|
78
|
+
const [target] = targets;
|
|
79
|
+
if (target === undefined) {
|
|
79
80
|
return "Session not found";
|
|
80
81
|
}
|
|
81
82
|
if (targets.length > 1) {
|
|
82
83
|
return `Multiple sessions named "${name}" are connected. Use the session ID instead.`;
|
|
83
84
|
}
|
|
84
|
-
const exited = exitedRefusal(
|
|
85
|
+
const exited = exitedRefusal(target, query, via);
|
|
85
86
|
if (exited !== undefined) {
|
|
86
87
|
return exited;
|
|
87
88
|
}
|
|
88
|
-
if (!
|
|
89
|
+
if (!target.lastMessages) {
|
|
89
90
|
return "Recipient does not support last-message reads; reload its extension.";
|
|
90
91
|
}
|
|
91
92
|
if ("messageId" in query &&
|
|
92
|
-
(!
|
|
93
|
+
(!target.receiptReads || target.info.rootSessionId !== query.rootSessionId)) {
|
|
93
94
|
return "publication_unavailable";
|
|
94
95
|
}
|
|
95
96
|
if (pending >= 128) {
|
|
@@ -135,19 +136,19 @@ export class LastMessageRelay {
|
|
|
135
136
|
request(fromId, input, reader) {
|
|
136
137
|
const message = lastRequest(input);
|
|
137
138
|
const targets = findSessions(this.sessions, message.to, fromId);
|
|
138
|
-
const target = targets[0];
|
|
139
139
|
const from = this.sessions.get(fromId);
|
|
140
140
|
if (!from) {
|
|
141
141
|
return;
|
|
142
142
|
}
|
|
143
143
|
const token = randomUUID();
|
|
144
|
-
const
|
|
144
|
+
const delegated = this.delegatedReader(targets, reader);
|
|
145
|
+
const via = delegated?.via;
|
|
145
146
|
const frame = {
|
|
146
147
|
type: "last_request",
|
|
147
148
|
requestId: token,
|
|
148
149
|
from: messageSender(from.info),
|
|
149
150
|
query: message.query,
|
|
150
|
-
parked:
|
|
151
|
+
parked: delegated === undefined ? undefined : messageSender(delegated.target.info),
|
|
151
152
|
};
|
|
152
153
|
const error = unavailable(targets, message.to, this.pending.size, { query: message.query, via }) ??
|
|
153
154
|
validateIntercomMessageSize(frame)?.message;
|
|
@@ -159,22 +160,39 @@ export class LastMessageRelay {
|
|
|
159
160
|
});
|
|
160
161
|
return;
|
|
161
162
|
}
|
|
162
|
-
const
|
|
163
|
+
const [recipient] = targets;
|
|
164
|
+
if (recipient === undefined) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
this.forward({ token, fromId, requestId: message.requestId, query: message.query }, recipient, via, frame);
|
|
168
|
+
}
|
|
169
|
+
/** Register the correlated wait, then forward the read to the reader or the recipient. */
|
|
170
|
+
forward(wait, recipient, via, frame) {
|
|
171
|
+
const timer = setTimeout(() => this.finish(wait.token, { error: "Last-message read timed out" }), 5000);
|
|
163
172
|
timer.unref();
|
|
164
|
-
this.pending.set(token, {
|
|
165
|
-
fromId,
|
|
166
|
-
toId:
|
|
173
|
+
this.pending.set(wait.token, {
|
|
174
|
+
fromId: wait.fromId,
|
|
175
|
+
toId: recipient.info.id,
|
|
167
176
|
via: via?.info.id,
|
|
168
|
-
requestId:
|
|
169
|
-
query:
|
|
177
|
+
requestId: wait.requestId,
|
|
178
|
+
query: wait.query,
|
|
170
179
|
timer,
|
|
171
180
|
});
|
|
172
181
|
try {
|
|
173
|
-
writeMessage((via ??
|
|
182
|
+
writeMessage((via ?? recipient).socket, frame);
|
|
174
183
|
}
|
|
175
184
|
catch {
|
|
176
|
-
this.finish(token, { error: "Recipient disconnected before last-message read" });
|
|
185
|
+
this.finish(wait.token, { error: "Recipient disconnected before last-message read" });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** A sole matched target may delegate the read to another peer's captured history. */
|
|
189
|
+
delegatedReader(targets, reader) {
|
|
190
|
+
const [target] = targets;
|
|
191
|
+
if (targets.length !== 1 || target === undefined) {
|
|
192
|
+
return undefined;
|
|
177
193
|
}
|
|
194
|
+
const via = reader(target.info.id);
|
|
195
|
+
return via === undefined ? undefined : { target, via };
|
|
178
196
|
}
|
|
179
197
|
respond(fromId, message) {
|
|
180
198
|
if (typeof message.requestId !== "string") {
|
|
@@ -79,7 +79,7 @@ export class ParkedSessions {
|
|
|
79
79
|
return { id: randomUUID() };
|
|
80
80
|
}
|
|
81
81
|
previous?.socket.destroy();
|
|
82
|
-
return { id, resumed: previous };
|
|
82
|
+
return { id, ...(previous !== undefined ? { resumed: previous } : {}) };
|
|
83
83
|
}
|
|
84
84
|
/** Flushes a restored child's input and re-sends wakes owed to a (re)registered main. */
|
|
85
85
|
registered(id, previous) {
|
|
@@ -47,6 +47,7 @@ export class IntercomOutbound {
|
|
|
47
47
|
case "queue":
|
|
48
48
|
return " (intentionally deferred behind active recipient work)";
|
|
49
49
|
case undefined:
|
|
50
|
+
default:
|
|
50
51
|
return " (defaults to steer; wakes idle recipients and steers active recipients after the current tool call)";
|
|
51
52
|
}
|
|
52
53
|
}
|
|
@@ -44,8 +44,9 @@ function explicitReply(contexts, replyTo, to) {
|
|
|
44
44
|
}
|
|
45
45
|
function replyBySender(contexts, to) {
|
|
46
46
|
const matches = matchingSender(contexts, to);
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
const [onlyMatch] = matches;
|
|
48
|
+
if (matches.length === 1 && onlyMatch !== undefined) {
|
|
49
|
+
return onlyMatch;
|
|
49
50
|
}
|
|
50
51
|
if (matches.length > 1) {
|
|
51
52
|
throw new RecoveryError("ambiguous_reply_target", `Multiple pending asks from "${to}" — use one of: ${pendingSenderOptions(matches, contexts)}.`);
|
|
@@ -113,7 +114,7 @@ export class ReplyTracker {
|
|
|
113
114
|
const beforeQueued = this.pendingTurnContexts.length;
|
|
114
115
|
for (let index = this.pendingTurnContexts.length - 1; index >= 0; index -= 1) {
|
|
115
116
|
const context = this.pendingTurnContexts[index];
|
|
116
|
-
if (expires(context)) {
|
|
117
|
+
if (context !== undefined && expires(context)) {
|
|
117
118
|
this.pendingTurnContexts.splice(index, 1);
|
|
118
119
|
}
|
|
119
120
|
}
|
|
@@ -145,11 +146,16 @@ export class ReplyTracker {
|
|
|
145
146
|
if (to !== "") {
|
|
146
147
|
return replyBySender(contexts, to);
|
|
147
148
|
}
|
|
149
|
+
return this.implicitReplyTarget(pending);
|
|
150
|
+
}
|
|
151
|
+
/** Without an explicit target, the turn context wins, then a sole pending ask. */
|
|
152
|
+
implicitReplyTarget(pending) {
|
|
148
153
|
if (this.currentTurnContext) {
|
|
149
154
|
return this.currentTurnContext;
|
|
150
155
|
}
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
const [solePending] = pending;
|
|
157
|
+
if (pending.length === 1 && solePending !== undefined) {
|
|
158
|
+
return solePending;
|
|
153
159
|
}
|
|
154
160
|
if (pending.length === 0) {
|
|
155
161
|
throw new RecoveryError("no_pending_reply", "No active intercom context to reply to");
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export interface TargetIdentity {
|
|
2
2
|
readonly id: string;
|
|
3
|
-
readonly name?: string;
|
|
3
|
+
readonly name?: string | undefined;
|
|
4
4
|
}
|
|
5
5
|
export interface ProjectSessionIdentity extends TargetIdentity {
|
|
6
6
|
readonly cwd: string;
|
|
7
|
-
readonly projectId?: string;
|
|
7
|
+
readonly projectId?: string | undefined;
|
|
8
8
|
}
|
|
9
9
|
export declare const MIN_SESSION_TARGET_PREFIX_LENGTH = 8;
|
|
10
10
|
export declare function nativePeerId(sessionId: string): string;
|
|
@@ -97,8 +97,9 @@ export function formatPeerAwarenessHint(sessions, currentSessionId) {
|
|
|
97
97
|
: undefined;
|
|
98
98
|
}
|
|
99
99
|
function matchedTargets(matches) {
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
const [target] = matches;
|
|
101
|
+
if (matches.length === 1 && target !== undefined) {
|
|
102
|
+
return { status: "found", target, matches };
|
|
102
103
|
}
|
|
103
104
|
return matches.length > 1 ? { status: "ambiguous", matches } : { status: "none", matches: [] };
|
|
104
105
|
}
|
|
@@ -9,6 +9,7 @@ export function registerIntercomTools(pi, tools) {
|
|
|
9
9
|
description: "Message connected Pi sessions in your group; mains also reach other mains across projects. List shows target IDs. Ask waits for a correlated reply; reply answers the active or sole pending ask. Pending lists asks; status shows connection and inbound queue. Last pages a peer's last completed response without waking it. Broker acceptance is not recipient persistence, consumption or completion.",
|
|
10
10
|
parameters: intercomSchema,
|
|
11
11
|
renderResult: renderCompactResult,
|
|
12
|
+
// oxlint-disable-next-line max-params
|
|
12
13
|
async execute(_id, params, signal, _update, ctx) {
|
|
13
14
|
const result = throwIfToolError(await tools.execute(params, signal, ctx));
|
|
14
15
|
return { content: [...result.content], details: result.details };
|