@fitchmultz/pi-subagents 0.52.16 → 0.52.18
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 +30 -0
- package/dist/agent/host.js +1 -1
- package/dist/agent/notifications.js +10 -8
- package/dist/agent/observe-layout.js +3 -3
- package/dist/agent/parking.js +0 -1
- package/dist/agent/process.js +1 -4
- package/dist/agent/runner.js +0 -2
- package/dist/agent/shutdown.js +0 -1
- package/dist/agent/typebox.js +2 -2
- package/dist/build-info.js +1 -1
- package/dist/pi-intercom/bridge-presence.js +0 -1
- package/dist/pi-intercom/bridge-protocol.js +0 -2
- package/dist/pi-intercom/bridge-session.js +0 -1
- package/dist/pi-intercom/bridge-wake.js +0 -1
- package/dist/pi-intercom/broker/client.js +0 -2
- package/dist/pi-intercom/broker/spawn.js +0 -3
- package/dist/pi-intercom/inbound-delivery.js +2 -6
- package/dist/pi-intercom/reply-tracker.js +47 -55
- package/dist/pi-intercom/session-events.js +2 -0
- package/dist/pi-intercom/tool-registration.js +1 -2
- package/dist/pi-intercom/validation.js +0 -2
- package/dist/shared/cooperative.js +0 -1
- package/docs/code-quality.md +96 -68
- package/docs/intercom.md +6 -2
- package/package.json +9 -6
- package/scripts/build.mjs +11 -13
- package/scripts/check-compat.mjs +3 -2
- package/scripts/compat-host.mjs +5 -5
- package/scripts/intercom-bridge-credentials.mjs +4 -3
- package/scripts/intercom-remote.mjs +0 -1
- package/scripts/native-package-smoke-assertions.mjs +25 -23
- package/scripts/native-package-smoke.mjs +38 -54
- package/scripts/package-smoke.mjs +39 -30
- package/scripts/prepare.mjs +16 -10
- package/scripts/real-pi-events.mjs +92 -0
- package/scripts/real-pi-live.mjs +28 -108
- package/scripts/real-pi-processes.mjs +77 -82
- package/scripts/real-pi-smoke.mjs +20 -18
- package/src/agent/host.ts +1 -1
- package/src/agent/notifications.ts +11 -8
- package/src/agent/observe-layout.ts +3 -3
- package/src/agent/parking.ts +0 -1
- package/src/agent/process.ts +1 -4
- package/src/agent/runner.ts +0 -2
- package/src/agent/shutdown.ts +0 -1
- package/src/agent/typebox.ts +2 -2
- package/src/pi-intercom/bridge-presence.ts +0 -1
- package/src/pi-intercom/bridge-protocol.ts +0 -2
- package/src/pi-intercom/bridge-session.ts +0 -1
- package/src/pi-intercom/bridge-wake.ts +0 -1
- package/src/pi-intercom/broker/client.ts +0 -2
- package/src/pi-intercom/broker/spawn.ts +0 -3
- package/src/pi-intercom/inbound-delivery.ts +2 -6
- package/src/pi-intercom/reply-tracker.ts +50 -58
- package/src/pi-intercom/session-events.ts +3 -1
- package/src/pi-intercom/tool-registration.ts +1 -2
- package/src/pi-intercom/validation.ts +0 -2
- package/src/shared/cooperative.ts +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## 0.52.18 - 2026-11-06
|
|
6
|
+
|
|
7
|
+
<!-- npm-release:unreleased:start -->
|
|
8
|
+
|
|
9
|
+
- Keep replied or retired asks out of reply attribution when their queued input reaches a later native turn.
|
|
10
|
+
- Preserve unanswered queued asks through abort recovery without sending a premature recipient-failure reply.
|
|
11
|
+
|
|
12
|
+
<!-- npm-release:unreleased:end -->
|
|
13
|
+
|
|
14
|
+
- Keep retired asks out of native reply attribution (#341) ([27a93f9](https://github.com/fitchmultz/pi-subagents/commit/27a93f9bc52aaa9e454ffc593017b66b01461bc4))
|
|
15
|
+
|
|
16
|
+
### What's Changed
|
|
17
|
+
|
|
18
|
+
- Keep retired asks out of native reply attribution by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/341
|
|
19
|
+
|
|
20
|
+
**Full Changelog**: https://github.com/fitchmultz/pi-subagents/compare/v0.52.17...v0.52.18
|
|
21
|
+
|
|
22
|
+
## 0.52.17 - 2026-11-05
|
|
23
|
+
|
|
24
|
+
<!-- npm-release:unreleased:start -->
|
|
25
|
+
<!-- npm-release:unreleased:end -->
|
|
26
|
+
|
|
27
|
+
- Adopt strict Oxlint/Oxfmt v5.2 without finding baselines (#339) ([8de822c](https://github.com/fitchmultz/pi-subagents/commit/8de822c14a56fdad3e13961abb02a106dac71a4f))
|
|
28
|
+
|
|
29
|
+
### What's Changed
|
|
30
|
+
|
|
31
|
+
- Adopt strict Oxlint/Oxfmt v5.2 without finding baselines by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/339
|
|
32
|
+
|
|
33
|
+
**Full Changelog**: https://github.com/fitchmultz/pi-subagents/compare/v0.52.16...v0.52.17
|
|
34
|
+
|
|
5
35
|
## 0.52.16 - 2026-11-03
|
|
6
36
|
|
|
7
37
|
<!-- npm-release:unreleased:start -->
|
package/dist/agent/host.js
CHANGED
|
@@ -75,7 +75,7 @@ export async function loadSDK(root) {
|
|
|
75
75
|
return resolvedSDK;
|
|
76
76
|
}
|
|
77
77
|
// Selected host SDK, not a second package-local runtime hidden behind Jiti aliases.
|
|
78
|
-
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
78
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment -- QX-004: selected host module must retain its runtime identity.
|
|
79
79
|
const sdk = await import(__rewriteRelativeImportExtension(pathToFileURL(path.join(root, "dist/index.js")).href));
|
|
80
80
|
return sdk;
|
|
81
81
|
}
|
|
@@ -5,17 +5,19 @@ import { readOutputPage } from "../shared/output-page.js";
|
|
|
5
5
|
import { journalStamp } from "../shared/native-journal.js";
|
|
6
6
|
import { launchAt, list, runDirectory } from "./storage.js";
|
|
7
7
|
import { readCanonicalResult, readResult } from "./usage.js";
|
|
8
|
+
function isStringSet(value) {
|
|
9
|
+
if (!(value instanceof Set)) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const entries = value;
|
|
13
|
+
return [...entries].every((entry) => typeof entry === "string");
|
|
14
|
+
}
|
|
8
15
|
function queuedAdmissions() {
|
|
9
16
|
const store = globalThis;
|
|
10
17
|
const existing = store.__pi_single_agent_admissions__;
|
|
11
|
-
if (existing
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (strings.length === values.size) {
|
|
15
|
-
// Keep one live queue identity across native extension reloads.
|
|
16
|
-
// oxlint-disable-next-line typescript/no-unsafe-return
|
|
17
|
-
return existing;
|
|
18
|
-
}
|
|
18
|
+
if (isStringSet(existing)) {
|
|
19
|
+
// Keep one live queue identity across native extension reloads.
|
|
20
|
+
return existing;
|
|
19
21
|
}
|
|
20
22
|
const queued = new Set();
|
|
21
23
|
store.__pi_single_agent_admissions__ = queued;
|
|
@@ -19,11 +19,11 @@ if (manifest === undefined) {
|
|
|
19
19
|
throw new Error("Cannot locate the native Pi TUI package.");
|
|
20
20
|
}
|
|
21
21
|
const root = pathToFileURL(path.join(path.dirname(manifest), "dist/index.js"));
|
|
22
|
-
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
22
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment -- QX-004: resolve the selected host TUI, not a second package instance.
|
|
23
23
|
const tuiPromise = import(__rewriteRelativeImportExtension(root.href));
|
|
24
|
-
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
24
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment -- QX-004: layout must share the selected host TUI instance.
|
|
25
25
|
const layoutPromise = import(__rewriteRelativeImportExtension(new URL("./layout.js", root).href));
|
|
26
|
-
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
26
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment -- QX-004: dispatch must share the selected host TUI instance.
|
|
27
27
|
const dispatchPromise = import(__rewriteRelativeImportExtension(new URL("./tui.js", root).href));
|
|
28
28
|
const [tui, layout, dispatch, sdk] = await Promise.all([
|
|
29
29
|
tuiPromise,
|
package/dist/agent/parking.js
CHANGED
|
@@ -297,7 +297,6 @@ async function awaitWake(base, launch, signal, legacy) {
|
|
|
297
297
|
throw new Error(failure);
|
|
298
298
|
}
|
|
299
299
|
// DirectoryWatch reconciles dropped hints without imposing a deadline on native startup.
|
|
300
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
301
300
|
await wake.sleep(200);
|
|
302
301
|
}
|
|
303
302
|
}
|
package/dist/agent/process.js
CHANGED
|
@@ -38,9 +38,7 @@ export async function observeAlive(identities) {
|
|
|
38
38
|
for (const claim of identities) {
|
|
39
39
|
// One `ps` at a time: each spawn briefly holds the event loop, so concurrent spawns add up.
|
|
40
40
|
const { pid, identity, clock } = claim;
|
|
41
|
-
verdicts.push(probe(claim) ??
|
|
42
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
43
|
-
!(await observeChildProcessReplaced(pid, identity, claim.published, clock)));
|
|
41
|
+
verdicts.push(probe(claim) ?? !(await observeChildProcessReplaced(pid, identity, claim.published, clock)));
|
|
44
42
|
}
|
|
45
43
|
return verdicts;
|
|
46
44
|
}
|
|
@@ -198,7 +196,6 @@ async function awaitStopping(dir, existing, signal) {
|
|
|
198
196
|
throw new Error("Previous actor is still stopping; no replacement was launched.");
|
|
199
197
|
}
|
|
200
198
|
// The old actor must actually exit before replacing its generation.
|
|
201
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
202
199
|
await delay(25);
|
|
203
200
|
}
|
|
204
201
|
}
|
package/dist/agent/runner.js
CHANGED
|
@@ -310,13 +310,11 @@ class Actor {
|
|
|
310
310
|
this.retry = false;
|
|
311
311
|
this.pending().forEach((command) => this.accept(command));
|
|
312
312
|
}
|
|
313
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
314
313
|
if (await this.park(runtime, parked)) {
|
|
315
314
|
return;
|
|
316
315
|
}
|
|
317
316
|
const polling = this.retry || this.stopping || watch === "unwatched";
|
|
318
317
|
const tick = Math.max(1, this.nextParentCheck - performance.now());
|
|
319
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
320
318
|
await this.wake.sleep(polling ? 100 : tick);
|
|
321
319
|
}
|
|
322
320
|
}
|
package/dist/agent/shutdown.js
CHANGED
|
@@ -37,7 +37,6 @@ async function waitForExit(targets, timeout) {
|
|
|
37
37
|
const deadline = Date.now() + timeout;
|
|
38
38
|
while (Date.now() < deadline && targets.some(alive)) {
|
|
39
39
|
// Join actual process exit while native abort persists the interrupted turn.
|
|
40
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
41
40
|
await delay(25);
|
|
42
41
|
}
|
|
43
42
|
}
|
package/dist/agent/typebox.js
CHANGED
|
@@ -13,11 +13,11 @@ import { isChild, packageRoot } from "./host.js";
|
|
|
13
13
|
// Pi aliases these imports to its own instance (including bundled virtual modules).
|
|
14
14
|
// Only the detached runner needs absolute imports from the selected host package.
|
|
15
15
|
const require = isChild() ? createRequire(path.join(packageRoot(), "dist/index.js")) : undefined;
|
|
16
|
-
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
16
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment -- QX-004: detached runners resolve the selected host schema instance.
|
|
17
17
|
const typeboxPromise = require === undefined
|
|
18
18
|
? import("typebox")
|
|
19
19
|
: import(__rewriteRelativeImportExtension(pathToFileURL(require.resolve("typebox")).href));
|
|
20
|
-
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
20
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment -- QX-004: validation must use the selected host schema instance.
|
|
21
21
|
const valuePromise = require === undefined
|
|
22
22
|
? import("typebox/value")
|
|
23
23
|
: import(__rewriteRelativeImportExtension(pathToFileURL(require.resolve("typebox/value")).href));
|
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.18","sha256":"83db07f9acbdd6ff5985c46e1f5f5411f68b6766697a2dfab019b3d00dbba1c1"});
|
|
@@ -88,7 +88,6 @@ export class PersistentPresence {
|
|
|
88
88
|
}
|
|
89
89
|
try {
|
|
90
90
|
// Each identity has one connection attempt; preserve the native broker's spawn safeguards.
|
|
91
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
92
91
|
await this.owner.connect(identity, registration.expires);
|
|
93
92
|
}
|
|
94
93
|
catch {
|
|
@@ -25,7 +25,6 @@ export function object(value, fields) {
|
|
|
25
25
|
}
|
|
26
26
|
export function label(value, field, max = 512) {
|
|
27
27
|
// Labels are single-line identifiers and paths; reject C0/C1 controls and DEL.
|
|
28
|
-
// oxlint-disable-next-line no-control-regex
|
|
29
28
|
const controls = /[\u0000-\u001f\u007f-\u009f]/;
|
|
30
29
|
if (typeof value !== "string" ||
|
|
31
30
|
value.trim() === "" ||
|
|
@@ -37,7 +36,6 @@ export function label(value, field, max = 512) {
|
|
|
37
36
|
}
|
|
38
37
|
export function text(value) {
|
|
39
38
|
// Message text permits tab/newline/CR, but rejects other C0/C1 controls and DEL.
|
|
40
|
-
// oxlint-disable-next-line no-control-regex
|
|
41
39
|
const controls = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/;
|
|
42
40
|
if (typeof value !== "string" || value.trim() === "" || controls.test(value)) {
|
|
43
41
|
throw new Fault(400, "invalid_input", "Invalid message.");
|
|
@@ -303,7 +303,6 @@ export class Session {
|
|
|
303
303
|
log("inbox_wait", { ...this.fields(), requestId: context.requestId, after: operation.after });
|
|
304
304
|
try {
|
|
305
305
|
// A nonmatching commit must rearm the same cursor, not start parallel readers.
|
|
306
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
307
306
|
await delay(remaining, undefined, { signal });
|
|
308
307
|
}
|
|
309
308
|
catch (error) {
|
|
@@ -167,7 +167,6 @@ export class WakeDeliveries {
|
|
|
167
167
|
// ponytail: two cold Macs can each admit one wake. Literal cross-Mac single admission
|
|
168
168
|
// requires a neutral quota authority governing every writer, not a URL hash or HTTP 2xx.
|
|
169
169
|
try {
|
|
170
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
171
170
|
await this.post(candidate);
|
|
172
171
|
}
|
|
173
172
|
finally {
|
|
@@ -148,12 +148,10 @@ export class IntercomClient extends EventEmitter {
|
|
|
148
148
|
async restoreTopics(topics, subscriptions) {
|
|
149
149
|
for (const topic of topics ?? []) {
|
|
150
150
|
// Restore frames are individually acknowledged to preserve order and bound socket payloads.
|
|
151
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
152
151
|
await this.updateTopics({ action: "restore", topic });
|
|
153
152
|
}
|
|
154
153
|
for (const subscription of subscriptions ?? []) {
|
|
155
154
|
// Subscriptions are committed before connect resolves and subsequent messages can arrive.
|
|
156
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
157
155
|
await this.updateTopics({ action: "restore", subscription });
|
|
158
156
|
}
|
|
159
157
|
}
|
|
@@ -138,7 +138,6 @@ async function previousBrokerWarning() {
|
|
|
138
138
|
const previousSockets = getPreviousBrokerSocketPaths();
|
|
139
139
|
for (const socketPath of previousSockets) {
|
|
140
140
|
// Migration probes are sequential and never register or send to old peers.
|
|
141
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
142
141
|
if (isOwnedBrokerSocket(socketPath) && (await checkOneSocket(socketPath))) {
|
|
143
142
|
return "An older agent-directory Intercom broker is still running. Its peers are not part of shared same-user discovery until they reload with the updated extension. Existing sessions and accepted work are untouched; no legacy fallback is used.";
|
|
144
143
|
}
|
|
@@ -240,12 +239,10 @@ async function waitForBroker(timeoutMs = 5000) {
|
|
|
240
239
|
const start = Date.now();
|
|
241
240
|
while (Date.now() - start < timeoutMs) {
|
|
242
241
|
// Each probe must finish before the next to avoid parallel socket attempts.
|
|
243
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
244
242
|
if (await checkSocketConnectable()) {
|
|
245
243
|
return;
|
|
246
244
|
}
|
|
247
245
|
// Polling waits between probes rather than starting an unbounded batch.
|
|
248
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
249
246
|
await sleep(100);
|
|
250
247
|
}
|
|
251
248
|
throw new Error("Broker failed to start within timeout");
|
|
@@ -131,7 +131,7 @@ export class InboundDeliveryOwner {
|
|
|
131
131
|
const trigger = index === -1 ? undefined : rest.splice(index, 1)[0];
|
|
132
132
|
const replyTarget = entries.find((entry) => entry.message.expectsReply === true) ?? trigger;
|
|
133
133
|
if (replyTarget && this.journal.get(replyTarget.message.id) === replyTarget) {
|
|
134
|
-
this.hooks.replies.queueTurnContext(
|
|
134
|
+
this.hooks.replies.queueTurnContext(replyTarget.message.id);
|
|
135
135
|
}
|
|
136
136
|
for (const entry of rest) {
|
|
137
137
|
if (!this.live(generation)) {
|
|
@@ -218,11 +218,7 @@ export class InboundDeliveryOwner {
|
|
|
218
218
|
this.journal.markNative(entry.message.id);
|
|
219
219
|
}
|
|
220
220
|
if (delivery !== "passive") {
|
|
221
|
-
this.hooks.replies.queueTurnContext(
|
|
222
|
-
from: entry.from,
|
|
223
|
-
message: entry.message,
|
|
224
|
-
receivedAt: Date.now(),
|
|
225
|
-
});
|
|
221
|
+
this.hooks.replies.queueTurnContext(entry.message.id);
|
|
226
222
|
}
|
|
227
223
|
}
|
|
228
224
|
nativeMessage(entry) {
|
|
@@ -55,9 +55,9 @@ function replyBySender(contexts, to) {
|
|
|
55
55
|
}
|
|
56
56
|
export class ReplyTracker {
|
|
57
57
|
pendingAsks = new Map();
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
pendingTurnIds = [];
|
|
59
|
+
currentTurnId = null;
|
|
60
|
+
activeAgentId = null;
|
|
61
61
|
askTimeoutMs;
|
|
62
62
|
constructor(askTimeoutMs = 10 * 60 * 1000) {
|
|
63
63
|
this.askTimeoutMs = askTimeoutMs;
|
|
@@ -70,61 +70,58 @@ export class ReplyTracker {
|
|
|
70
70
|
}
|
|
71
71
|
return context;
|
|
72
72
|
}
|
|
73
|
-
queueTurnContext(
|
|
74
|
-
if (
|
|
73
|
+
queueTurnContext(messageId) {
|
|
74
|
+
if (!this.pendingAsks.has(messageId) || this.hasTurnContext(messageId)) {
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
|
-
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
this.pendingTurnContexts.push(context);
|
|
77
|
+
this.pendingTurnIds.push(messageId);
|
|
81
78
|
}
|
|
82
79
|
beginTurn(now = Date.now()) {
|
|
83
80
|
this.pruneExpired(now);
|
|
84
|
-
this.
|
|
85
|
-
if (this.
|
|
86
|
-
this.
|
|
81
|
+
this.currentTurnId = this.pendingTurnIds.shift() ?? null;
|
|
82
|
+
if (this.currentTurnId !== null) {
|
|
83
|
+
this.activeAgentId = this.currentTurnId;
|
|
87
84
|
}
|
|
88
85
|
}
|
|
89
86
|
currentTurn() {
|
|
90
|
-
|
|
87
|
+
const id = this.currentTurnId ?? this.activeAgentId;
|
|
88
|
+
return id === null ? null : (this.pendingAsks.get(id) ?? null);
|
|
91
89
|
}
|
|
92
90
|
endTurn() {
|
|
93
|
-
this.
|
|
91
|
+
this.currentTurnId = null;
|
|
94
92
|
}
|
|
95
93
|
endAgent() {
|
|
96
|
-
this.
|
|
97
|
-
this.
|
|
94
|
+
this.currentTurnId = null;
|
|
95
|
+
this.activeAgentId = null;
|
|
98
96
|
}
|
|
99
97
|
reset() {
|
|
100
98
|
this.pendingAsks.clear();
|
|
101
|
-
this.
|
|
102
|
-
this.
|
|
103
|
-
this.activeAgentContext = null;
|
|
99
|
+
this.pendingTurnIds.length = 0;
|
|
100
|
+
this.endAgent();
|
|
104
101
|
}
|
|
105
102
|
expireSender(sessionId) {
|
|
106
|
-
const
|
|
103
|
+
const expiring = new Set([...this.pendingAsks.values()]
|
|
104
|
+
.filter((context) => context.from.id === sessionId)
|
|
105
|
+
.map((context) => context.message.id));
|
|
107
106
|
let expired = 0;
|
|
108
|
-
for (const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
expired += 1;
|
|
112
|
-
}
|
|
107
|
+
for (const messageId of expiring) {
|
|
108
|
+
this.pendingAsks.delete(messageId);
|
|
109
|
+
expired += 1;
|
|
113
110
|
}
|
|
114
|
-
const beforeQueued = this.
|
|
115
|
-
for (let index = this.
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
118
|
-
this.
|
|
111
|
+
const beforeQueued = this.pendingTurnIds.length;
|
|
112
|
+
for (let index = this.pendingTurnIds.length - 1; index >= 0; index -= 1) {
|
|
113
|
+
const id = this.pendingTurnIds[index];
|
|
114
|
+
if (id !== undefined && expiring.has(id)) {
|
|
115
|
+
this.pendingTurnIds.splice(index, 1);
|
|
119
116
|
}
|
|
120
117
|
}
|
|
121
|
-
expired += beforeQueued - this.
|
|
122
|
-
if (this.
|
|
123
|
-
this.
|
|
118
|
+
expired += beforeQueued - this.pendingTurnIds.length;
|
|
119
|
+
if (this.currentTurnId !== null && expiring.has(this.currentTurnId)) {
|
|
120
|
+
this.currentTurnId = null;
|
|
124
121
|
expired += 1;
|
|
125
122
|
}
|
|
126
|
-
if (this.
|
|
127
|
-
this.
|
|
123
|
+
if (this.activeAgentId !== null && expiring.has(this.activeAgentId)) {
|
|
124
|
+
this.activeAgentId = null;
|
|
128
125
|
expired += 1;
|
|
129
126
|
}
|
|
130
127
|
return expired;
|
|
@@ -132,26 +129,21 @@ export class ReplyTracker {
|
|
|
132
129
|
resolveReplyTarget(options, now = Date.now()) {
|
|
133
130
|
this.pruneExpired(now);
|
|
134
131
|
const pending = Array.from(this.pendingAsks.values());
|
|
135
|
-
const contexts = this.currentTurnContext
|
|
136
|
-
? [
|
|
137
|
-
this.currentTurnContext,
|
|
138
|
-
...pending.filter((context) => context.message.id !== this.currentTurnContext?.message.id),
|
|
139
|
-
]
|
|
140
|
-
: pending;
|
|
141
132
|
const replyTo = options.replyTo ?? "";
|
|
142
133
|
const to = options.to ?? "";
|
|
143
134
|
if (replyTo !== "") {
|
|
144
|
-
return explicitReply(
|
|
135
|
+
return explicitReply(pending, replyTo, to);
|
|
145
136
|
}
|
|
146
137
|
if (to !== "") {
|
|
147
|
-
return replyBySender(
|
|
138
|
+
return replyBySender(pending, to);
|
|
148
139
|
}
|
|
149
140
|
return this.implicitReplyTarget(pending);
|
|
150
141
|
}
|
|
151
142
|
/** Without an explicit target, the turn context wins, then a sole pending ask. */
|
|
152
143
|
implicitReplyTarget(pending) {
|
|
153
|
-
|
|
154
|
-
|
|
144
|
+
const current = this.currentTurnId === null ? undefined : this.pendingAsks.get(this.currentTurnId);
|
|
145
|
+
if (current) {
|
|
146
|
+
return current;
|
|
155
147
|
}
|
|
156
148
|
const [solePending] = pending;
|
|
157
149
|
if (pending.length === 1 && solePending !== undefined) {
|
|
@@ -170,22 +162,22 @@ export class ReplyTracker {
|
|
|
170
162
|
return Array.from(this.pendingAsks.values()).sort((a, b) => a.receivedAt - b.receivedAt);
|
|
171
163
|
}
|
|
172
164
|
hasTurnContext(messageId) {
|
|
173
|
-
return (this.
|
|
174
|
-
this.
|
|
175
|
-
this.
|
|
165
|
+
return (this.pendingTurnIds.includes(messageId) ||
|
|
166
|
+
this.currentTurnId === messageId ||
|
|
167
|
+
this.activeAgentId === messageId);
|
|
176
168
|
}
|
|
177
169
|
removeContext(messageId) {
|
|
178
170
|
this.pendingAsks.delete(messageId);
|
|
179
|
-
for (let index = this.
|
|
180
|
-
if (this.
|
|
181
|
-
this.
|
|
171
|
+
for (let index = this.pendingTurnIds.length - 1; index >= 0; index -= 1) {
|
|
172
|
+
if (this.pendingTurnIds[index] === messageId) {
|
|
173
|
+
this.pendingTurnIds.splice(index, 1);
|
|
182
174
|
}
|
|
183
175
|
}
|
|
184
|
-
if (this.
|
|
185
|
-
this.
|
|
176
|
+
if (this.currentTurnId === messageId) {
|
|
177
|
+
this.currentTurnId = null;
|
|
186
178
|
}
|
|
187
|
-
if (this.
|
|
188
|
-
this.
|
|
179
|
+
if (this.activeAgentId === messageId) {
|
|
180
|
+
this.activeAgentId = null;
|
|
189
181
|
}
|
|
190
182
|
}
|
|
191
183
|
pruneExpired(now) {
|
|
@@ -46,6 +46,8 @@ export class IntercomSessionEvents {
|
|
|
46
46
|
if (client?.isConnected() !== true ||
|
|
47
47
|
!context ||
|
|
48
48
|
context.message.expectsReply !== true ||
|
|
49
|
+
// A native turn can start without consuming its queued input after an abort.
|
|
50
|
+
journal.get(context.message.id) !== undefined ||
|
|
49
51
|
error === null ||
|
|
50
52
|
error === "") {
|
|
51
53
|
return;
|
|
@@ -9,8 +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
|
-
|
|
13
|
-
async execute(_id, params, signal, _update, ctx) {
|
|
12
|
+
async execute(...[, params, signal, , ctx]) {
|
|
14
13
|
const result = throwIfToolError(await tools.execute(params, signal, ctx));
|
|
15
14
|
return { content: [...result.content], details: result.details };
|
|
16
15
|
},
|
|
@@ -34,10 +34,8 @@ export function asError(error) {
|
|
|
34
34
|
return error instanceof Error ? error : new Error(errorMessage(error));
|
|
35
35
|
}
|
|
36
36
|
// Protocol text permits line feeds and tabs, but rejects other C0/C1 controls.
|
|
37
|
-
// oxlint-disable-next-line no-control-regex
|
|
38
37
|
const UNSAFE_TEXT_CONTROLS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/;
|
|
39
38
|
// Protocol labels are single-line fields and reject every C0/C1 control.
|
|
40
|
-
// oxlint-disable-next-line no-control-regex
|
|
41
39
|
const UNSAFE_LABEL_CONTROLS = /[\u0000-\u001f\u007f-\u009f]/;
|
|
42
40
|
export function safeText(value) {
|
|
43
41
|
return typeof value === "string" && !UNSAFE_TEXT_CONTROLS.test(value);
|
|
@@ -14,7 +14,6 @@ export async function runCooperatively(steps) {
|
|
|
14
14
|
// single-record stall off-thread rather than increasing this batch size.
|
|
15
15
|
if (++count === 32) {
|
|
16
16
|
// Yield between ordered atomic recovery records; concurrency would reorder delivery.
|
|
17
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
18
17
|
await yieldToInput();
|
|
19
18
|
count = 0;
|
|
20
19
|
}
|