@fitchmultz/pi-subagents 0.52.2 → 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.
@@ -196,14 +196,24 @@ export class Mailbox {
196
196
  }
197
197
  ack(ids: readonly string[]): UnknownRecord {
198
198
  const acked: string[] = [];
199
- for (const id of ids) {
200
- const entry = this.get(id);
201
- if (entry) {
202
- this.notificationAccepted(id);
203
- this.remove(id);
204
- acked.push(id);
199
+ this.stored(() => {
200
+ // One FULL commit per ACK group; a rollback leaves every listed payload and intent pending.
201
+ this.db?.exec("BEGIN IMMEDIATE");
202
+ try {
203
+ for (const id of ids) {
204
+ const entry = this.get(id);
205
+ if (entry) {
206
+ this.notificationAccepted(id);
207
+ this.remove(id);
208
+ acked.push(id);
209
+ }
210
+ }
211
+ this.db?.exec("COMMIT");
212
+ } catch (error) {
213
+ this.db?.exec("ROLLBACK");
214
+ throw error;
205
215
  }
206
- }
216
+ });
207
217
  return { acked };
208
218
  }
209
219
  page(after: number, limit: number): InboxPage {
@@ -138,21 +138,20 @@ export class WakeDeliveries {
138
138
 
139
139
  private pending(delivery: Delivery): PendingNotification | undefined {
140
140
  let after = 0;
141
- let next: PendingNotification | undefined;
142
141
  let page;
143
142
  do {
144
- page = delivery.mailbox.pendingNotifications(after);
143
+ // Decode only the retired prefix and the oldest actionable entry, not the whole backlog.
144
+ page = delivery.mailbox.pendingNotifications(after, 1);
145
145
  for (const entry of page.messages) {
146
- if (!actionable(entry)) {
147
- // Retire only notification intent. Payload/reply obligations require actual ACK/reply.
148
- delivery.mailbox.notificationAccepted(entry.message.id);
149
- } else if (!next) {
150
- next = entry;
146
+ if (actionable(entry)) {
147
+ return entry;
151
148
  }
149
+ // Retire only notification intent. Payload/reply obligations require actual ACK/reply.
150
+ delivery.mailbox.notificationAccepted(entry.message.id);
152
151
  }
153
152
  after = Number(page.nextCursor);
154
153
  } while (page.hasMore);
155
- return next;
154
+ return undefined;
156
155
  }
157
156
 
158
157
  private groups(): Map<string, Candidate | undefined> {
@@ -8,7 +8,12 @@ import {
8
8
  type InboundMessageEntry,
9
9
  type PendingInboundMessage,
10
10
  } from "./runtime-types.ts";
11
- import { isInboundCheckpoint, publicationMetadata, requestedDelivery } from "./inbound-record.ts";
11
+ import {
12
+ checkpointMessageId,
13
+ isInboundCheckpoint,
14
+ publicationMetadata,
15
+ requestedDelivery,
16
+ } from "./inbound-record.ts";
12
17
  import {
13
18
  isReceiptQuery,
14
19
  type NativeReceipt,
@@ -66,6 +71,7 @@ export class InboundJournal {
66
71
  if (
67
72
  item.type === "custom" &&
68
73
  item.customType === INBOUND_CHECKPOINT_TYPE &&
74
+ checkpointMessageId(item.data) === query.messageId &&
69
75
  isInboundCheckpoint(item.data) &&
70
76
  item.data.sessionId === query.rootSessionId
71
77
  ) {
@@ -82,7 +88,7 @@ export class InboundJournal {
82
88
  data: InboundCheckpoint,
83
89
  ): NativeReceipt | undefined {
84
90
  if ("entry" in data) {
85
- if (receipt || data.entry.message.id !== query.messageId) {
91
+ if (receipt) {
86
92
  return receipt;
87
93
  }
88
94
  const { from, message, stage, receivedAt } = data.entry;
@@ -106,7 +112,7 @@ export class InboundJournal {
106
112
  discarded: false,
107
113
  };
108
114
  }
109
- if (!receipt || data.messageId !== query.messageId) {
115
+ if (!receipt) {
110
116
  return receipt;
111
117
  }
112
118
  return {
@@ -97,3 +97,15 @@ export function isInboundCheckpoint(
97
97
  value.stage === "reply-retired")
98
98
  );
99
99
  }
100
+ /** Unvalidated: lets a receipt skip checkpoints for other messages before full validation. */
101
+ export function checkpointMessageId(value: unknown): unknown {
102
+ if (!isRecord(value)) {
103
+ return;
104
+ }
105
+ if ("entry" in value) {
106
+ return isRecord(value.entry) && isRecord(value.entry.message)
107
+ ? value.entry.message.id
108
+ : undefined;
109
+ }
110
+ return value.messageId;
111
+ }
@@ -11,10 +11,19 @@ function localForkStartCommand(): string {
11
11
  return `pi --name worker --extension ${shellQuote(path.join(PACKAGE_ROOT, "src", "index.ts"))}`;
12
12
  }
13
13
  function duplicateNames(sessions: readonly SessionInfo[]): ReadonlySet<string> {
14
- const names = sessions
15
- .map((session) => session.name?.toLowerCase())
16
- .filter((name): name is string => name !== undefined && name !== "");
17
- return new Set(names.filter((name, index) => names.indexOf(name) !== index));
14
+ const seen = new Set<string>();
15
+ const duplicates = new Set<string>();
16
+ for (const session of sessions) {
17
+ const name = session.name?.toLowerCase();
18
+ if (name === undefined || name === "") {
19
+ continue;
20
+ }
21
+ if (seen.has(name)) {
22
+ duplicates.add(name);
23
+ }
24
+ seen.add(name);
25
+ }
26
+ return duplicates;
18
27
  }
19
28
  export function formatDuration(seconds: number): string {
20
29
  if (seconds < 60) {
@@ -79,7 +88,7 @@ function healthTags(session: SessionInfo, now: number): readonly string[] {
79
88
  interface RowOptions {
80
89
  readonly currentCwd: string;
81
90
  readonly self: boolean;
82
- readonly duplicates: (name: string) => boolean;
91
+ readonly duplicates: ReadonlySet<string>;
83
92
  readonly allSessions: readonly SessionInfo[];
84
93
  readonly now: number;
85
94
  }
@@ -88,7 +97,7 @@ function row(session: SessionInfo, options: RowOptions): string {
88
97
  const duplicate =
89
98
  session.name !== undefined &&
90
99
  session.name !== "" &&
91
- options.duplicates(session.name.toLowerCase());
100
+ options.duplicates.has(session.name.toLowerCase());
92
101
  let location: string | undefined;
93
102
  if (options.self) {
94
103
  location = "self";
@@ -132,7 +141,7 @@ export function formatSessionListSections(
132
141
  const others = scoped.filter((session) => session.id !== id);
133
142
  const options = {
134
143
  currentCwd: current.cwd,
135
- duplicates: (name: string) => duplicateNames(allSessions).has(name),
144
+ duplicates: duplicateNames(allSessions),
136
145
  allSessions,
137
146
  now: Date.now(),
138
147
  };