@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.
Files changed (58) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/agent/host.js +1 -1
  3. package/dist/agent/notifications.js +10 -8
  4. package/dist/agent/observe-layout.js +3 -3
  5. package/dist/agent/parking.js +0 -1
  6. package/dist/agent/process.js +1 -4
  7. package/dist/agent/runner.js +0 -2
  8. package/dist/agent/shutdown.js +0 -1
  9. package/dist/agent/typebox.js +2 -2
  10. package/dist/build-info.js +1 -1
  11. package/dist/pi-intercom/bridge-presence.js +0 -1
  12. package/dist/pi-intercom/bridge-protocol.js +0 -2
  13. package/dist/pi-intercom/bridge-session.js +0 -1
  14. package/dist/pi-intercom/bridge-wake.js +0 -1
  15. package/dist/pi-intercom/broker/client.js +0 -2
  16. package/dist/pi-intercom/broker/spawn.js +0 -3
  17. package/dist/pi-intercom/inbound-delivery.js +2 -6
  18. package/dist/pi-intercom/reply-tracker.js +47 -55
  19. package/dist/pi-intercom/session-events.js +2 -0
  20. package/dist/pi-intercom/tool-registration.js +1 -2
  21. package/dist/pi-intercom/validation.js +0 -2
  22. package/dist/shared/cooperative.js +0 -1
  23. package/docs/code-quality.md +96 -68
  24. package/docs/intercom.md +6 -2
  25. package/package.json +9 -6
  26. package/scripts/build.mjs +11 -13
  27. package/scripts/check-compat.mjs +3 -2
  28. package/scripts/compat-host.mjs +5 -5
  29. package/scripts/intercom-bridge-credentials.mjs +4 -3
  30. package/scripts/intercom-remote.mjs +0 -1
  31. package/scripts/native-package-smoke-assertions.mjs +25 -23
  32. package/scripts/native-package-smoke.mjs +38 -54
  33. package/scripts/package-smoke.mjs +39 -30
  34. package/scripts/prepare.mjs +16 -10
  35. package/scripts/real-pi-events.mjs +92 -0
  36. package/scripts/real-pi-live.mjs +28 -108
  37. package/scripts/real-pi-processes.mjs +77 -82
  38. package/scripts/real-pi-smoke.mjs +20 -18
  39. package/src/agent/host.ts +1 -1
  40. package/src/agent/notifications.ts +11 -8
  41. package/src/agent/observe-layout.ts +3 -3
  42. package/src/agent/parking.ts +0 -1
  43. package/src/agent/process.ts +1 -4
  44. package/src/agent/runner.ts +0 -2
  45. package/src/agent/shutdown.ts +0 -1
  46. package/src/agent/typebox.ts +2 -2
  47. package/src/pi-intercom/bridge-presence.ts +0 -1
  48. package/src/pi-intercom/bridge-protocol.ts +0 -2
  49. package/src/pi-intercom/bridge-session.ts +0 -1
  50. package/src/pi-intercom/bridge-wake.ts +0 -1
  51. package/src/pi-intercom/broker/client.ts +0 -2
  52. package/src/pi-intercom/broker/spawn.ts +0 -3
  53. package/src/pi-intercom/inbound-delivery.ts +2 -6
  54. package/src/pi-intercom/reply-tracker.ts +50 -58
  55. package/src/pi-intercom/session-events.ts +3 -1
  56. package/src/pi-intercom/tool-registration.ts +1 -2
  57. package/src/pi-intercom/validation.ts +0 -2
  58. 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 -->
@@ -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 instanceof Set) {
12
- const values = existing;
13
- const strings = [...values].filter((value) => typeof value === "string");
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,
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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));
@@ -1 +1 @@
1
- export const EXTENSION_BUILD = Object.freeze({"version":"0.52.16","sha256":"ec0886af93d91687cfae1ed692e1e2e31127d3fbf98abadf192034a044e44d51"});
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({ ...replyTarget, receivedAt: Date.now() });
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
- pendingTurnContexts = [];
59
- currentTurnContext = null;
60
- activeAgentContext = null;
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(context) {
74
- if (context.message.expectsReply !== true) {
73
+ queueTurnContext(messageId) {
74
+ if (!this.pendingAsks.has(messageId) || this.hasTurnContext(messageId)) {
75
75
  return;
76
76
  }
77
- if (this.hasTurnContext(context.message.id)) {
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.currentTurnContext = this.pendingTurnContexts.shift() ?? null;
85
- if (this.currentTurnContext) {
86
- this.activeAgentContext = this.currentTurnContext;
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
- return this.currentTurnContext ?? this.activeAgentContext;
87
+ const id = this.currentTurnId ?? this.activeAgentId;
88
+ return id === null ? null : (this.pendingAsks.get(id) ?? null);
91
89
  }
92
90
  endTurn() {
93
- this.currentTurnContext = null;
91
+ this.currentTurnId = null;
94
92
  }
95
93
  endAgent() {
96
- this.currentTurnContext = null;
97
- this.activeAgentContext = null;
94
+ this.currentTurnId = null;
95
+ this.activeAgentId = null;
98
96
  }
99
97
  reset() {
100
98
  this.pendingAsks.clear();
101
- this.pendingTurnContexts.length = 0;
102
- this.currentTurnContext = null;
103
- this.activeAgentContext = null;
99
+ this.pendingTurnIds.length = 0;
100
+ this.endAgent();
104
101
  }
105
102
  expireSender(sessionId) {
106
- const expires = (context) => context.from.id === sessionId;
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 [messageId, context] of this.pendingAsks) {
109
- if (expires(context)) {
110
- this.pendingAsks.delete(messageId);
111
- expired += 1;
112
- }
107
+ for (const messageId of expiring) {
108
+ this.pendingAsks.delete(messageId);
109
+ expired += 1;
113
110
  }
114
- const beforeQueued = this.pendingTurnContexts.length;
115
- for (let index = this.pendingTurnContexts.length - 1; index >= 0; index -= 1) {
116
- const context = this.pendingTurnContexts[index];
117
- if (context !== undefined && expires(context)) {
118
- this.pendingTurnContexts.splice(index, 1);
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.pendingTurnContexts.length;
122
- if (this.currentTurnContext && expires(this.currentTurnContext)) {
123
- this.currentTurnContext = null;
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.activeAgentContext && expires(this.activeAgentContext)) {
127
- this.activeAgentContext = null;
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(contexts, replyTo, to);
135
+ return explicitReply(pending, replyTo, to);
145
136
  }
146
137
  if (to !== "") {
147
- return replyBySender(contexts, to);
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
- if (this.currentTurnContext) {
154
- return this.currentTurnContext;
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.pendingTurnContexts.some((context) => context.message.id === messageId) ||
174
- this.currentTurnContext?.message.id === messageId ||
175
- this.activeAgentContext?.message.id === messageId);
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.pendingTurnContexts.length - 1; index >= 0; index -= 1) {
180
- if (this.pendingTurnContexts[index]?.message.id === messageId) {
181
- this.pendingTurnContexts.splice(index, 1);
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.currentTurnContext?.message.id === messageId) {
185
- this.currentTurnContext = null;
176
+ if (this.currentTurnId === messageId) {
177
+ this.currentTurnId = null;
186
178
  }
187
- if (this.activeAgentContext?.message.id === messageId) {
188
- this.activeAgentContext = null;
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
- // oxlint-disable-next-line max-params
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
  }