@fitchmultz/pi-subagents 0.52.1 → 0.52.3

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 CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.52.3] - 2026-10-29
6
+
7
+ ### Fixed
8
+
9
+ - Build Intercom's duplicate-name set once per session list instead of once per row. Session names, duplicate labels and target resolution are unchanged.
10
+ - Hash parked child state in bounded chunks when answering history requests or restoring a child. The digest and native state validation are unchanged.
11
+ - Skip the bash-owner inventory lookup for child tool calls that are not bash commands. Bash ownership checks and child delegation restrictions are unchanged.
12
+ - Avoid revalidating unrelated incoming messages when looking up an Intercom receipt. Receipt contents and branch-wide history remain unchanged.
13
+ - Reuse the child-status widget's display between inventory updates instead of rebuilding it on every parent frame. Theme changes, activity, errors and click targets remain visible.
14
+ - Batch optional bridge acknowledgements in one SQLite transaction and stop reading wake notifications once the next actionable message is known. A failed acknowledgement leaves the group pending. Later non-wake cleanup can wait behind a pending wake.
15
+ - Stop the live child viewer from rebuilding native renderers after each streamed update. Assistant and tool cards now keep the native component's own update result and only refresh the card's outer layout. On the maintained fork, unchanged thinking or prose is no longer re-parsed while a later block streams. On both hosts, tool-argument and progress updates run native tool rendering once per update instead of twice. Visible output, theme changes, resize, selection and expansion are unchanged.
16
+ - Stop each child continuation from rereading its whole native journal to record usage. A resident child keeps the IDs and billing fields of records it already validated and parses only records appended after its previous LF boundary. Unchanged journals are not reread; same-size edits, shrink, inode replacement, non-v3 headers and failed reads rebuild from the start. Inherited baselines, terminal stop, required usage and attribution still apply to every run. A restarted or restored child and the parent's pending-accounting repair still read the journal once from the start.
17
+ - Reuse the running Pi's SDK for the first delegation, `/agents` and parked-history restore in production-only installs, instead of importing a second copy of the same SDK from disk. In five alternating pairs per host on a busy macOS machine, the parent no longer grew by 66–85 MiB during its first `delegate` call. The median time to that call's receipt fell from 1.4 s to 0.1 s on the maintained fork. Official Pi's median fell from 1.9 s to 0.1 s, but the size of that gain is inconclusive under the measured load: one run with the change took 2.3 s. Installs with their own SDK copy, a set `PI_PACKAGE_DIR` and detached children still load the selected Pi package directly.
18
+
19
+ ## [0.52.2] - 2026-10-29
20
+
21
+ ### Fixed
22
+
23
+ - Avoid rereading historical shell records on every idle parking check while a live shell still blocks parking. Retain one publication path and revalidate its current record, owner token and process birth. Read the full inventory again when that witness disappears or dies. Stop and cold continuation keep their complete inventory checks. No records are pruned and live jobs are not interrupted.
24
+
5
25
  ## [0.52.1] - 2026-10-29
6
26
 
7
27
  ### Fixed
@@ -6,11 +6,11 @@ function guard(pi, directory, token) {
6
6
  if (event.toolName === "delegate" || event.toolName === "agent_runs") {
7
7
  return { block: true, reason: "Only a main session may delegate or control children." };
8
8
  }
9
+ if (event.toolName !== "bash" || typeof event.input.command !== "string") {
10
+ return;
11
+ }
9
12
  const owner = pi.getAllTools().find((tool) => tool.name === "bash")?.sourceInfo;
10
- if (event.toolName === "bash" &&
11
- typeof event.input.command === "string" &&
12
- owner &&
13
- (owner.source === "builtin" || localCwdOwner(owner))) {
13
+ if (owner && (owner.source === "builtin" || localCwdOwner(owner))) {
14
14
  // Preserve the selected local owner's cwd/env and leave remote/custom tools alone.
15
15
  const input = event.input;
16
16
  input.command = shellCommand(event.input.command, directory, token);
@@ -55,7 +55,20 @@ export function packageRoot() {
55
55
  }
56
56
  return path.dirname(manifest);
57
57
  }
58
+ // Pi's resolution of this extension's SDK import: the running host in production installs,
59
+ // possibly a package-local copy in development checkouts.
60
+ let resolvedSDK;
61
+ export function rememberSDK(sdk) {
62
+ resolvedSDK = sdk;
63
+ }
58
64
  export async function loadSDK(root) {
65
+ // Reuse it only as the selected root. PI_PACKAGE_DIR replaces getPackageDir()'s own
66
+ // location, so only an unset override proves that identity.
67
+ if (resolvedSDK !== undefined &&
68
+ (process.env.PI_PACKAGE_DIR ?? "") === "" &&
69
+ fs.realpathSync(resolvedSDK.getPackageDir()) === fs.realpathSync(root)) {
70
+ return resolvedSDK;
71
+ }
59
72
  // Selected host SDK, not a second package-local runtime hidden behind Jiti aliases.
60
73
  // oxlint-disable-next-line typescript/no-unsafe-assignment
61
74
  const sdk = await import(__rewriteRelativeImportExtension(pathToFileURL(path.join(root, "dist/index.js")).href));
@@ -1,8 +1,9 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { randomUUID } from "node:crypto";
4
+ import * as loadedSDK from "@earendil-works/pi-coding-agent";
4
5
  import { actor, alive, ensureActor, ownIdentity, parkFailure, requestStop, signalProcess, stopChildren, } from "./process.js";
5
- import { isChild, errorText } from "./host.js";
6
+ import { isChild, errorText, rememberSDK } from "./host.js";
6
7
  import { serveParked } from "./parking.js";
7
8
  import { resultSchema, attachmentSchema, admissionSchema, commandSchema, } from "./contracts.js";
8
9
  import { delegateSchema, runsSchema } from "./tool-schemas.js";
@@ -438,5 +439,7 @@ class AgentRuntime {
438
439
  }
439
440
  }
440
441
  export default function agents(pi) {
442
+ // Delegation, /agents and parked history then avoid importing a second SDK graph.
443
+ rememberSDK(loadedSDK);
441
444
  new AgentRuntime(pi).register();
442
445
  }
@@ -137,7 +137,7 @@ export class Card {
137
137
  if (this.arguments !== value.args) {
138
138
  this.tool.updateArgs(projected(value.args) ?? {});
139
139
  this.arguments = value.args;
140
- this.invalidate();
140
+ this.cache = undefined;
141
141
  }
142
142
  }
143
143
  updateTool(value, options, preparing) {
@@ -156,7 +156,7 @@ export class Card {
156
156
  this.phase = phase;
157
157
  this.value = source.result;
158
158
  this.ended = ended;
159
- this.invalidate();
159
+ this.cache = undefined;
160
160
  }
161
161
  }
162
162
  preview(value, isError, partial) {
@@ -188,7 +188,7 @@ export class Card {
188
188
  this.status?.setText(message.ended ? "Awaiting journal publication" : "In progress");
189
189
  this.blocks = message.blocks.slice();
190
190
  this.ended = message.ended;
191
- this.invalidate();
191
+ this.cache = undefined;
192
192
  }
193
193
  invalidate() {
194
194
  this.cache = undefined;
@@ -176,6 +176,7 @@ class Observer {
176
176
  const scanning = this.update()
177
177
  .catch((error) => {
178
178
  this.error = plainText(errorText(error));
179
+ this.widget();
179
180
  })
180
181
  .finally(() => {
181
182
  if (this.scanning !== scanning) {
@@ -267,21 +268,22 @@ class Observer {
267
268
  this.widgetSignature = "";
268
269
  return;
269
270
  }
270
- const signature = JSON.stringify([this.status(), rows.slice(0, 4), rows.length]);
271
+ const visible = rows.slice(0, 4);
272
+ const more = rows.length - visible.length;
273
+ const snapshot = { status: this.status(), rows: visible, more, error: this.error };
274
+ const signature = JSON.stringify(snapshot);
271
275
  if (signature === this.widgetSignature) {
272
276
  return;
273
277
  }
274
278
  this.widgetSignature = signature;
275
279
  if (this.widgetVisible) {
276
- this.widgetRender?.();
280
+ this.widgetRender?.(snapshot);
277
281
  return;
278
282
  }
279
283
  this.widgetVisible = true;
280
284
  ctx.ui.setWidget(STATUS_KEY, (tui, theme) => {
281
- this.widgetRender = () => {
282
- tui.requestRender();
283
- };
284
285
  const content = new Container();
286
+ let shown = snapshot;
285
287
  const open = (id) => {
286
288
  if (ctx !== this.ctx) {
287
289
  return;
@@ -290,29 +292,31 @@ class Observer {
290
292
  ctx.ui.notify(errorText(error), "error");
291
293
  });
292
294
  };
295
+ const build = () => {
296
+ content.clear();
297
+ content.addChild(actionText(theme.fg("accent", shown.status), () => open("")));
298
+ for (const row of shown.rows) {
299
+ const glyph = row.live ? "●" : "○";
300
+ const color = row.live ? "accent" : "warning";
301
+ content.addChild(actionText(`${theme.fg(color, glyph)} ${theme.bold(row.name)} · ${row.activity ?? row.status}`, () => open(row.id)));
302
+ }
303
+ if (shown.more > 0) {
304
+ content.addChild(actionText(theme.fg("dim", `+${shown.more} more · /agents`), () => open("")));
305
+ }
306
+ if (shown.error.length > 0) {
307
+ content.addChild(new TruncatedText(theme.fg("warning", shown.error), 0, 0));
308
+ }
309
+ };
310
+ build();
311
+ this.widgetRender = (next) => {
312
+ shown = next;
313
+ build();
314
+ tui.requestRender();
315
+ };
293
316
  return {
294
- render: (width) => {
295
- const strip = this.strip();
296
- const visible = strip.slice(0, 4);
297
- content.clear();
298
- content.addChild(actionText(theme.fg("accent", this.status()), () => open("")));
299
- for (const row of visible) {
300
- const glyph = row.live ? "●" : "○";
301
- const color = row.live ? "accent" : "warning";
302
- content.addChild(actionText(`${theme.fg(color, glyph)} ${theme.bold(row.name)} · ${row.activity ?? row.status}`, () => open(row.id)));
303
- }
304
- if (strip.length > visible.length) {
305
- content.addChild(actionText(theme.fg("dim", `+${strip.length - visible.length} more · /agents`), () => open("")));
306
- }
307
- if (this.error.length > 0) {
308
- content.addChild(new TruncatedText(theme.fg("warning", this.error), 0, 0));
309
- }
310
- return content.render(width);
311
- },
317
+ render: (width) => content.render(width),
312
318
  handleMouse: (event) => content.handleMouse(event),
313
- invalidate: () => {
314
- // Render reads current rows, expansion and theme on every frame.
315
- },
319
+ invalidate: build,
316
320
  };
317
321
  }, { placement: "aboveEditor" });
318
322
  }
@@ -4,7 +4,7 @@ import * as path from "node:path";
4
4
  import { parkedSchema } from "./contracts.js";
5
5
  import { errorText, loadSDK } from "./host.js";
6
6
  import { conversation, latest, launchAt, list, read, runDirectory, write } from "./storage.js";
7
- import { alive, shells, wakeActor } from "./process.js";
7
+ import { alive, shell, shells, wakeActor } from "./process.js";
8
8
  import { isHistoryRequest, PARK, PARKED_HISTORY, WAKE, } from "../shared/parking.js";
9
9
  import { nativePeerId } from "../pi-intercom/session-targets.js";
10
10
  function parkable(session) {
@@ -36,7 +36,21 @@ async function cut(pi, session, { quiet, errors, signal }) {
36
36
  return undefined;
37
37
  }
38
38
  const PARK_AFTER_MS = 5 * 60_000;
39
- const digest = (file) => createHash("sha256").update(fs.readFileSync(file)).digest("hex");
39
+ /** SHA-256 of every state byte through one reused chunk, like the native state reader. */
40
+ function digest(file) {
41
+ const hash = createHash("sha256");
42
+ const fd = fs.openSync(file, "r");
43
+ try {
44
+ const chunk = Buffer.allocUnsafe(64 * 1024);
45
+ for (let count = fs.readSync(fd, chunk); count > 0; count = fs.readSync(fd, chunk)) {
46
+ hash.update(chunk.subarray(0, count));
47
+ }
48
+ }
49
+ finally {
50
+ fs.closeSync(fd);
51
+ }
52
+ return hash.digest("hex");
53
+ }
40
54
  /** Parked authority names this session and exactly the saved generation's state bytes. */
41
55
  function exact(restore, parked, state, launch) {
42
56
  if (parked.sessionId !== launch.sessionId ||
@@ -74,6 +88,7 @@ export class Parking {
74
88
  restored;
75
89
  since = 0;
76
90
  leaf;
91
+ shell;
77
92
  base;
78
93
  launch;
79
94
  token;
@@ -111,9 +126,7 @@ export class Parking {
111
126
  }
112
127
  /** True once this generation has exited its runtime with or without parked authority. */
113
128
  async park(runtime, actor) {
114
- if (!actor.pi ||
115
- !this.due(runtime.session, actor.busy) ||
116
- shells(this.base, this.launch, this.token).some(alive)) {
129
+ if (!actor.pi || !this.due(runtime.session, actor.busy) || this.hasShell()) {
117
130
  return false;
118
131
  }
119
132
  const hold = await this.prepare(runtime, actor.pi, actor);
@@ -122,6 +135,13 @@ export class Parking {
122
135
  }
123
136
  return hold !== undefined;
124
137
  }
138
+ hasShell() {
139
+ const witness = this.shell === undefined ? undefined : shell(this.shell, this.token);
140
+ if (!witness || !alive(witness)) {
141
+ this.shell = shells(this.base, this.launch, this.token).find(alive)?.file;
142
+ }
143
+ return this.shell !== undefined;
144
+ }
125
145
  /** Final idle: no run, turn, journal change or viewer since the period began. */
126
146
  due(session, busy) {
127
147
  const leaf = session.sessionManager.getLeafId();
@@ -346,6 +346,13 @@ async function waitForExit(targets, timeout) {
346
346
  await delay(25);
347
347
  }
348
348
  }
349
+ export function shell(file, token) {
350
+ const identity = published(file, read(file, processSchema));
351
+ if (identity && identity.token !== token) {
352
+ throw new Error("Foreign or missing native shell identity.");
353
+ }
354
+ return identity;
355
+ }
349
356
  export function shells(base, launch, token) {
350
357
  const dir = path.join(conversation(base, launch), "shells", token);
351
358
  if (!fs.existsSync(dir)) {
@@ -356,11 +363,11 @@ export function shells(base, launch, token) {
356
363
  .filter((name) => name.endsWith(".json"))
357
364
  .map((name) => {
358
365
  const file = path.join(dir, name);
359
- const identity = published(file, read(file, processSchema));
360
- if (!identity || identity.token !== token) {
366
+ const identity = shell(file, token);
367
+ if (!identity) {
361
368
  throw new Error("Foreign or missing native shell identity.");
362
369
  }
363
- return identity;
370
+ return Object.assign(identity, { file });
364
371
  });
365
372
  }
366
373
  function quote(value) {
@@ -5,7 +5,7 @@ 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";
7
7
  import { Parking } from "./parking.js";
8
- import { childUsage } from "./usage.js";
8
+ import { UsageIndex } from "./usage.js";
9
9
  import { initializeCwd } from "./cwd.js";
10
10
  import { childRuntime } from "./child-runtime.js";
11
11
  import { ActorObservation } from "./observation.js";
@@ -47,6 +47,8 @@ class Actor {
47
47
  started = new Set();
48
48
  // Actor-local validation metadata only; never retain command prompts or receipt authority.
49
49
  validated = new Map();
50
+ // Every run of this conversation shares one native journal; later runs read only its suffix.
51
+ accounting = new UsageIndex();
50
52
  session;
51
53
  observation;
52
54
  context;
@@ -97,7 +99,7 @@ class Actor {
97
99
  }
98
100
  usage(launch, baseline, terminal) {
99
101
  try {
100
- return { contributions: childUsage(launch, baseline, terminal) };
102
+ return { contributions: this.accounting.read(launch, baseline, terminal) };
101
103
  }
102
104
  catch (error) {
103
105
  return {
@@ -1,12 +1,13 @@
1
1
  import * as path from "node:path";
2
2
  import * as fs from "node:fs";
3
- import { journalStamp } from "../shared/native-journal.js";
3
+ import { journalStamp, nativeAppend } from "../shared/native-journal.js";
4
4
  import { nativeProjection } from "../shared/journal-projections.js";
5
5
  import { scanJournal } from "../shared/journal-frames.js";
6
6
  import { isRecord } from "../shared/unknown.js";
7
7
  import { Assert } from "./typebox.js";
8
8
  import { contributionsSchema, resultSchema, } from "./contracts.js";
9
9
  import { read, runDirectory, write } from "./storage.js";
10
+ /** Billing evidence of one native entry. Missing required usage is an error only outside a baseline. */
10
11
  function billing(entry) {
11
12
  if (entry.checkpoint === true) {
12
13
  return;
@@ -16,11 +17,11 @@ function billing(entry) {
16
17
  (entry.type === "message" && !["assistant", "toolResult"].some((role) => role === message.role))) {
17
18
  return;
18
19
  }
19
- const required = entry.type === "usage" || message.role === "assistant";
20
- if (required && message.usage === undefined) {
21
- throw new Error("Required native usage is unavailable.");
20
+ if (message.usage === undefined) {
21
+ return entry.type === "usage" || message.role === "assistant" ? "missing" : undefined;
22
22
  }
23
- return message.usage === undefined ? undefined : message;
23
+ const { role, provider, model, responseModel, usage } = message;
24
+ return { role, provider, model, responseModel, usage };
24
25
  }
25
26
  function attribution(message, launch) {
26
27
  if (message.role === "toolResult") {
@@ -34,56 +35,100 @@ function attribution(message, launch) {
34
35
  model: typeof model === "string" ? model : launch.model,
35
36
  };
36
37
  }
37
- export function childUsage(launch, baseline, terminal) {
38
- const values = [];
39
- const observed = { header: false, ended: false };
40
- const reachedTerminal = new Error("Native terminal boundary reached.");
41
- const fd = fs.openSync(launch.sessionFile, "r");
42
- try {
43
- const stamp = journalStamp(fs.fstatSync(fd, { bigint: true }));
38
+ function identify(header, launch) {
39
+ if (header.type !== "session" || header.id !== launch.sessionId) {
40
+ throw new Error("Foreign native usage journal identity.");
41
+ }
42
+ }
43
+ /** An unchanged or native v3 appended journal keeps its prefix; any other change rebuilds it. */
44
+ function reusable(cached, stat) {
45
+ return cached !== undefined &&
46
+ (journalStamp(cached.stat) === journalStamp(stat) ||
47
+ nativeAppend(cached.stat, stat, cached.header))
48
+ ? cached
49
+ : { stat, end: 0, entries: [] };
50
+ }
51
+ /**
52
+ * Child usage of one physical native journal. A retained index parses only records after its last
53
+ * accepted LF boundary. Baselines, required usage and attribution apply to each read.
54
+ */
55
+ export class UsageIndex {
56
+ prefix;
57
+ read(launch, baseline, terminal) {
58
+ const values = [];
59
+ const reachedTerminal = new Error("Native terminal boundary reached.");
60
+ const charge = (entry) => {
61
+ const billed = baseline.has(entry.id) ? undefined : entry.billing;
62
+ if (billed === "missing") {
63
+ throw new Error("Required native usage is unavailable.");
64
+ }
65
+ if (billed !== undefined) {
66
+ values.push({
67
+ id: `${launch.sessionId}:${entry.id}`,
68
+ ...attribution(billed, launch),
69
+ usage: billed.usage,
70
+ });
71
+ }
72
+ if (entry.id === terminal) {
73
+ throw reachedTerminal;
74
+ }
75
+ };
76
+ const cached = this.prefix;
77
+ // Only a completed read is retained.
78
+ this.prefix = undefined;
79
+ const fd = fs.openSync(launch.sessionFile, "r");
44
80
  try {
45
- scanJournal(fd, nativeProjection, ({ value }) => {
46
- if (!observed.header) {
47
- if (value.type !== "session" || value.id !== launch.sessionId) {
48
- throw new Error("Foreign native usage journal identity.");
49
- }
50
- observed.header = true;
51
- return;
52
- }
53
- if (typeof value.id !== "string" || typeof value.type !== "string") {
54
- throw new Error("Missing native usage entry identity.");
81
+ const stat = fs.fstatSync(fd, { bigint: true });
82
+ const prefix = reusable(cached, stat);
83
+ let ended = false;
84
+ try {
85
+ if (prefix.header !== undefined) {
86
+ identify(prefix.header, launch);
55
87
  }
56
- const message = baseline.has(value.id) ? undefined : billing(value);
57
- if (message) {
58
- values.push({
59
- id: `${launch.sessionId}:${value.id}`,
60
- ...attribution(message, launch),
61
- usage: message.usage,
62
- });
88
+ for (const entry of prefix.entries) {
89
+ charge(entry);
63
90
  }
64
- if (value.id === terminal) {
65
- observed.ended = true;
66
- throw reachedTerminal;
91
+ scanJournal(fd, nativeProjection, ({ value, end }) => {
92
+ if (prefix.header === undefined) {
93
+ identify(value, launch);
94
+ prefix.header = value;
95
+ prefix.end = end;
96
+ return;
97
+ }
98
+ if (typeof value.id !== "string" || typeof value.type !== "string") {
99
+ throw new Error("Missing native usage entry identity.");
100
+ }
101
+ const entry = { id: value.id, billing: billing(value) };
102
+ prefix.entries.push(entry);
103
+ prefix.end = end;
104
+ charge(entry);
105
+ }, { policy: "live", requireNewline: true, start: prefix.end });
106
+ }
107
+ catch (error) {
108
+ if (error !== reachedTerminal) {
109
+ throw error;
67
110
  }
68
- }, { policy: "live", requireNewline: true });
69
- }
70
- catch (error) {
71
- if (error !== reachedTerminal) {
72
- throw error;
111
+ ended = true;
112
+ }
113
+ if (journalStamp(fs.fstatSync(fd, { bigint: true })) !== journalStamp(stat)) {
114
+ throw new Error("Native usage journal changed while reading.");
73
115
  }
116
+ if (prefix.header === undefined || (terminal !== undefined && !ended)) {
117
+ throw new Error("Native terminal or session identity is not LF-published.");
118
+ }
119
+ Assert(contributionsSchema, values);
120
+ prefix.stat = stat;
121
+ this.prefix = prefix;
122
+ return values;
74
123
  }
75
- if (journalStamp(fs.fstatSync(fd, { bigint: true })) !== stamp) {
76
- throw new Error("Native usage journal changed while reading.");
124
+ finally {
125
+ fs.closeSync(fd);
77
126
  }
78
127
  }
79
- finally {
80
- fs.closeSync(fd);
81
- }
82
- if (!observed.header || (terminal !== undefined && !observed.ended)) {
83
- throw new Error("Native terminal or session identity is not LF-published.");
84
- }
85
- Assert(contributionsSchema, values);
86
- return values;
128
+ }
129
+ /** A fresh read for callers without an owner lifetime. */
130
+ export function childUsage(launch, baseline, terminal) {
131
+ return new UsageIndex().read(launch, baseline, terminal);
87
132
  }
88
133
  export function readCanonicalResult(base, launch) {
89
134
  const file = path.join(runDirectory(base, launch.id), "result.json");
@@ -1 +1 @@
1
- export const EXTENSION_BUILD = Object.freeze({"version":"0.52.1","sha256":"d950f3bebb47cbe1dc09b2f7f9cc042535701b14cfe39cc518107a5626698bf4"});
1
+ export const EXTENSION_BUILD = Object.freeze({"version":"0.52.3","sha256":"56e15afa9487e79b68bb9a924bf3cbb4a62a8492cf4fe882d0496263f87a1e33"});
@@ -146,14 +146,25 @@ export class Mailbox {
146
146
  }
147
147
  ack(ids) {
148
148
  const acked = [];
149
- for (const id of ids) {
150
- const entry = this.get(id);
151
- if (entry) {
152
- this.notificationAccepted(id);
153
- this.remove(id);
154
- acked.push(id);
149
+ this.stored(() => {
150
+ // One FULL commit per ACK group; a rollback leaves every listed payload and intent pending.
151
+ this.db?.exec("BEGIN IMMEDIATE");
152
+ try {
153
+ for (const id of ids) {
154
+ const entry = this.get(id);
155
+ if (entry) {
156
+ this.notificationAccepted(id);
157
+ this.remove(id);
158
+ acked.push(id);
159
+ }
160
+ }
161
+ this.db?.exec("COMMIT");
155
162
  }
156
- }
163
+ catch (error) {
164
+ this.db?.exec("ROLLBACK");
165
+ throw error;
166
+ }
167
+ });
157
168
  return { acked };
158
169
  }
159
170
  page(after, limit) {
@@ -110,22 +110,20 @@ export class WakeDeliveries {
110
110
  }
111
111
  pending(delivery) {
112
112
  let after = 0;
113
- let next;
114
113
  let page;
115
114
  do {
116
- page = delivery.mailbox.pendingNotifications(after);
115
+ // Decode only the retired prefix and the oldest actionable entry, not the whole backlog.
116
+ page = delivery.mailbox.pendingNotifications(after, 1);
117
117
  for (const entry of page.messages) {
118
- if (!actionable(entry)) {
119
- // Retire only notification intent. Payload/reply obligations require actual ACK/reply.
120
- delivery.mailbox.notificationAccepted(entry.message.id);
121
- }
122
- else if (!next) {
123
- next = entry;
118
+ if (actionable(entry)) {
119
+ return entry;
124
120
  }
121
+ // Retire only notification intent. Payload/reply obligations require actual ACK/reply.
122
+ delivery.mailbox.notificationAccepted(entry.message.id);
125
123
  }
126
124
  after = Number(page.nextCursor);
127
125
  } while (page.hasMore);
128
- return next;
126
+ return undefined;
129
127
  }
130
128
  groups() {
131
129
  const groups = new Map();
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { isContextRequest } from "./lifecycle.js";
3
3
  import { INBOUND_CHECKPOINT_TYPE, } from "./runtime-types.js";
4
- import { isInboundCheckpoint, publicationMetadata, requestedDelivery } from "./inbound-record.js";
4
+ import { checkpointMessageId, isInboundCheckpoint, publicationMetadata, requestedDelivery, } from "./inbound-record.js";
5
5
  import { isReceiptQuery, } from "./types.js";
6
6
  import { inboundIdFromCustomMessage } from "./message-format.js";
7
7
  import { isRecord } from "./validation.js";
@@ -40,6 +40,7 @@ export class InboundJournal {
40
40
  receiptEntry(receipt, query, item) {
41
41
  if (item.type === "custom" &&
42
42
  item.customType === INBOUND_CHECKPOINT_TYPE &&
43
+ checkpointMessageId(item.data) === query.messageId &&
43
44
  isInboundCheckpoint(item.data) &&
44
45
  item.data.sessionId === query.rootSessionId) {
45
46
  return this.receiptCheckpoint(receipt, query, item.id, item.data);
@@ -50,7 +51,7 @@ export class InboundJournal {
50
51
  }
51
52
  receiptCheckpoint(receipt, query, entryId, data) {
52
53
  if ("entry" in data) {
53
- if (receipt || data.entry.message.id !== query.messageId) {
54
+ if (receipt) {
54
55
  return receipt;
55
56
  }
56
57
  const { from, message, stage, receivedAt } = data.entry;
@@ -74,7 +75,7 @@ export class InboundJournal {
74
75
  discarded: false,
75
76
  };
76
77
  }
77
- if (!receipt || data.messageId !== query.messageId) {
78
+ if (!receipt) {
78
79
  return receipt;
79
80
  }
80
81
  return {
@@ -82,3 +82,15 @@ export function isInboundCheckpoint(value) {
82
82
  value.stage === "discarded" ||
83
83
  value.stage === "reply-retired"));
84
84
  }
85
+ /** Unvalidated: lets a receipt skip checkpoints for other messages before full validation. */
86
+ export function checkpointMessageId(value) {
87
+ if (!isRecord(value)) {
88
+ return;
89
+ }
90
+ if ("entry" in value) {
91
+ return isRecord(value.entry) && isRecord(value.entry.message)
92
+ ? value.entry.message.id
93
+ : undefined;
94
+ }
95
+ return value.messageId;
96
+ }