@fitchmultz/pi-subagents 0.44.6 → 0.44.7

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
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.44.7] - 2026-10-09
4
+
5
+ ### Fixed
6
+ - Keep newer Agents history, including consumed human answers, from being overwritten by an older in-flight page. Recheck the owning session, index, selected attempt, source and published page before applying async history or saved-result reads; preserve requested paging and reading anchors when reloading an invalidated snapshot.
7
+ - Consume refresh notifications received while the latest conversation page is loading, without pulling scrolled readers forward or automatically retrying history errors.
8
+ - Emit the Intercom bridge `session_closed` audit record after broker disconnect completes and the closing session is removed; cancellation still rejects pending and new requests for that identity immediately.
9
+
3
10
  ## [0.44.6] - 2026-10-09
4
11
 
5
12
  ### Fixed
@@ -1 +1 @@
1
- export const EXTENSION_BUILD = Object.freeze({"version":"0.44.6","sha256":"e9a0bfe1bf57b2b67e3b32c6f449996dadf8bc897fe2577eea3d919158b40466"});
1
+ export const EXTENSION_BUILD = Object.freeze({"version":"0.44.7","sha256":"86c62961d20949874e872f49d9044371c4ae3d0b1b18b9d3b1186c29d1522bb0"});
@@ -295,8 +295,8 @@ class Session {
295
295
  await this.disconnect();
296
296
  if (this.bridge.sessions.get(this.identity.fingerprint256) === this)
297
297
  this.bridge.sessions.delete(this.identity.fingerprint256);
298
+ log("session_closed", { ...this.fields(), result: error.code });
298
299
  })();
299
- log("session_closed", { ...this.fields(), result: error.code });
300
300
  }
301
301
  receive(from, message) {
302
302
  if (this.closed)
@@ -315,7 +315,7 @@ export class AgentViewController {
315
315
  activity: { ...prior.child.activity, ...child.activity } } : child;
316
316
  return { key, label: child.identityUnavailable ? "Saved assignment unavailable" : sameAttempt ? agentTaskLabel(child) : prior?.label ?? agentTaskLabel(child), run: view, child: displayChild,
317
317
  model: agentModel(displayChild, child.nativeConfiguration ?? (canonical && sameAttempt && child.sessionFile === prior.child.sessionFile ? prior.page?.configuration : undefined)), history: sameAttempt ? prior.history : [], historyIds: sameAttempt ? prior.historyIds : [],
318
- page: sameAttempt ? prior.page : undefined, metadataAt: observed ? prior.metadataAt : undefined, historyLoading: sameAttempt ? prior.historyLoading : false, finalId: sameAttempt ? prior.finalId : undefined, unavailable: child.missingSession && child.state !== "live" ? "Saved conversation unavailable." : view.diagnosis,
318
+ page: sameAttempt ? prior.page : undefined, historySource: sameAttempt ? prior.historySource : undefined, metadataAt: observed ? prior.metadataAt : undefined, historyLoading: sameAttempt ? prior.historyLoading : false, finalId: sameAttempt ? prior.finalId : undefined, unavailable: child.missingSession && child.state !== "live" ? "Saved conversation unavailable." : view.diagnosis,
319
319
  question: child.identityUnavailable ? undefined : questions.findLast((question) => question.index === child.index && ["awaiting_input", "answer_pending"].includes(question.state)),
320
320
  unread: observed ? prior.unread : Boolean(visit && (child.activity?.lastActivityAt ?? view.updatedAt) > (visit.seenActivityAt ?? 0)), replied: sameAttempt ? prior.replied : false };
321
321
  });
@@ -485,7 +485,19 @@ export class AgentViewController {
485
485
  const task = this.task(key), generation = this.generation;
486
486
  if (!task || !this.live() || task.child.identityUnavailable)
487
487
  return;
488
+ const published = task.page, source = task.historySource, sessionFile = task.child.sessionFile, childState = task.child.state;
488
489
  const input = this.historyInput(task), index = await runHistoryIndex(this.state);
490
+ // Versions can reset on index reopen. Fence the actual owner/read baseline,
491
+ // including metadata-only source transitions, rather than ordering unrelated versions.
492
+ const isCurrent = () => {
493
+ const current = this.task(key);
494
+ if (!this.live(generation) || this.state.historyIndex !== index || index.failure || !current || current.child.identityUnavailable
495
+ || current.page !== published || current.historySource !== source || current.child.sessionFile !== sessionFile || current.child.state !== childState)
496
+ return false;
497
+ const boundary = this.historyInput(current);
498
+ return boundary.runId === input.runId && boundary.index === input.index && boundary.leaf === input.leaf
499
+ && boundary.terminalEntryId === input.terminalEntryId && boundary.endedAt === input.endedAt;
500
+ };
489
501
  const boundary = { runId: task.run.runId, index: task.child.index, terminalEntryId: input.terminalEntryId, endedAt: input.endedAt };
490
502
  const marker = typeof input.readThrough === "string" && !input.readThrough.startsWith("result:")
491
503
  ? await index.entry({ ...boundary, entryId: input.readThrough.replace(/:(?:\d+|error)$/, "") }) : undefined;
@@ -499,7 +511,7 @@ export class AgentViewController {
499
511
  const result = await indexedHistory(index, { ...input, limit: 100, ...paging });
500
512
  if (!this.live(generation) || this.task(key)?.run.runId !== task.run.runId)
501
513
  return;
502
- return { ...result, latestPage: paging.after !== undefined ? !result.page.hasMore : paging.before === undefined && paging.cursor === undefined,
514
+ return { ...result, isCurrent, latestPage: paging.after !== undefined ? !result.page.hasMore : paging.before === undefined && paging.cursor === undefined,
503
515
  readThroughSequence: marker?.sequence ?? (input.readThrough == null ? -1 : undefined) };
504
516
  }
505
517
  async savedResult(key, id) {
@@ -522,6 +534,7 @@ export class AgentViewController {
522
534
  }
523
535
  applyMetadata(task, page) {
524
536
  const visit = this.visits.get(task.key), delivered = new Map(page.deliveredMessages);
537
+ task.historySource = JSON.stringify([page.sourceId, page.generation, page.sessionId]);
525
538
  if (page.freshness.state !== "catching-up")
526
539
  task.metadataAt = task.run.updatedAt;
527
540
  task.model = agentModel(task.child, page.configuration);
@@ -1009,6 +1022,7 @@ export class AgentConversation extends Container {
1009
1022
  pageRequest = 0;
1010
1023
  detailRequest = 0;
1011
1024
  loadingPage = false;
1025
+ refreshPending = false;
1012
1026
  pageError;
1013
1027
  latestPage = true;
1014
1028
  readSequence = -1;
@@ -1108,6 +1122,7 @@ export class AgentConversation extends Container {
1108
1122
  if (this.closed)
1109
1123
  return;
1110
1124
  const request = ++this.pageRequest;
1125
+ this.refreshPending = false;
1111
1126
  this.loadingPage = true;
1112
1127
  this.pageError = undefined;
1113
1128
  if (this.task)
@@ -1126,11 +1141,8 @@ export class AgentConversation extends Container {
1126
1141
  result = reportPage;
1127
1142
  }
1128
1143
  let task = this.task;
1129
- if (result.readThroughSequence !== undefined)
1130
- this.readSequence = result.readThroughSequence;
1131
- this.latestPage = result.latestPage;
1132
1144
  let history = result.history;
1133
- if ((this.initialPosition || this.latestPage || history.finalId && history.entryIds.includes(history.finalId)) && task.child.state !== "live" && task.child.result && getSingleResultOutput(task.child.result)) {
1145
+ if ((this.initialPosition || result.latestPage || history.finalId && history.entryIds.includes(history.finalId)) && task.child.state !== "live" && task.child.result && getSingleResultOutput(task.child.result)) {
1134
1146
  const report = await this.controller.savedResult(this.key, `result:${task.run.runId}`);
1135
1147
  const currentTask = this.task;
1136
1148
  if (this.closed || request !== this.pageRequest || !currentTask || currentTask.run.runId !== task.run.runId)
@@ -1138,6 +1150,13 @@ export class AgentConversation extends Container {
1138
1150
  task = currentTask;
1139
1151
  history = withFinalResult(history, report.text, task.run.runId, task.run.updatedAt);
1140
1152
  }
1153
+ if (!result.isCurrent()) {
1154
+ void this.loadPage(paging, anchor);
1155
+ return;
1156
+ }
1157
+ if (result.readThroughSequence !== undefined)
1158
+ this.readSequence = result.readThroughSequence;
1159
+ this.latestPage = result.latestPage;
1141
1160
  task.history = history.items;
1142
1161
  task.historyIds = history.entryIds;
1143
1162
  task.finalId = history.finalId;
@@ -1159,6 +1178,10 @@ export class AgentConversation extends Container {
1159
1178
  this.loadingPage = false;
1160
1179
  if (this.task)
1161
1180
  this.task.historyLoading = false;
1181
+ if (this.refreshPending && !this.pageError) {
1182
+ this.refreshPending = false;
1183
+ this.refresh();
1184
+ }
1162
1185
  this.tui.requestRender();
1163
1186
  }
1164
1187
  }
@@ -1178,8 +1201,12 @@ export class AgentConversation extends Container {
1178
1201
  this.detail = this.assignment();
1179
1202
  if (!this.initialPosition && !this.scroll.isFollowingEnd)
1180
1203
  this.restoreAnchor = this.anchor();
1181
- if (!this.loadingPage && this.latestPage && this.scroll.isFollowingEnd && !this.detail && !this.pageError)
1182
- void this.loadPage();
1204
+ if (this.latestPage && this.scroll.isFollowingEnd && !this.detail && !this.pageError) {
1205
+ if (this.loadingPage)
1206
+ this.refreshPending = true;
1207
+ else
1208
+ void this.loadPage();
1209
+ }
1183
1210
  this.tui.requestRender();
1184
1211
  }
1185
1212
  syncDraft() { if (!this.closed && this.editor.getExpandedText() !== this.visit.draft)
@@ -14,7 +14,7 @@ Optional HTTPS messaging proxy for a remote agent such as **Grok Bot**. Pi-inter
14
14
 
15
15
  **Delivery/lifecycle:** one broker peer per authorized certificate, registered lazily on the first API call, with a five-minute inactivity lease. Poll inbox to maintain presence. Send returns broker acceptance, not proof a model consumed it. Ask steers the recipient and waits at most two minutes for both the exact recipient ID and exact `replyTo`. One outgoing ask per identity. Revocation/expiry/shutdown/client disconnect terminate waits. Inbox is bounded and volatile; overflow is reported, not silently described as successful receipt. Restarting the helper loses inbox contents and pending replies. Upgrade to a durable inbox journal if restart recovery is required; existing Pi recipients still save their own messages normally.
16
16
 
17
- **Audit:** JSONL metadata on stdout: request/connection identity, certificate fingerprint, operation, peer/message IDs and outcome. Message/attachment bodies and keys are not logged. Protect logs (0700 directory, 0600 files); rotate them with your normal log tooling. Never publish private config/keys or raw inbox/list output as smoke evidence.
17
+ **Audit:** JSONL metadata on stdout: request/connection identity, certificate fingerprint, operation, peer/message IDs and outcome. `session_closed` confirms broker disconnection and removal of the retiring session; cancellation still rejects requests immediately. Message/attachment bodies and keys are not logged. Protect logs (0700 directory, 0600 files); rotate them with your normal log tooling. Never publish private config/keys or raw inbox/list output as smoke evidence.
18
18
 
19
19
  ## Mac setup
20
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitchmultz/pi-subagents",
3
- "version": "0.44.6",
3
+ "version": "0.44.7",
4
4
  "description": "Pi extension for subagent orchestration and local session coordination",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -287,8 +287,8 @@ class Session {
287
287
  try { await this.connecting; } catch { /* The original request receives the connect failure. */ }
288
288
  await this.disconnect();
289
289
  if (this.bridge.sessions.get(this.identity.fingerprint256) === this) this.bridge.sessions.delete(this.identity.fingerprint256);
290
+ log("session_closed", { ...this.fields(), result: error.code });
290
291
  })();
291
- log("session_closed", { ...this.fields(), result: error.code });
292
292
  }
293
293
 
294
294
  private receive(from: SessionInfo, message: Message): void {
@@ -70,6 +70,7 @@ export interface AgentTask {
70
70
  replied: boolean;
71
71
  page?: HistoryPage;
72
72
  historyLoading?: boolean;
73
+ historySource?: string;
73
74
  metadataAt?: number;
74
75
  }
75
76
 
@@ -332,7 +333,7 @@ export class AgentViewController {
332
333
  activity: { ...prior.child.activity, ...child.activity } } : child;
333
334
  return { key, label: child.identityUnavailable ? "Saved assignment unavailable" : sameAttempt ? agentTaskLabel(child) : prior?.label ?? agentTaskLabel(child), run: view, child: displayChild,
334
335
  model: agentModel(displayChild, child.nativeConfiguration ?? (canonical && sameAttempt && child.sessionFile === prior.child.sessionFile ? prior.page?.configuration : undefined)), history: sameAttempt ? prior.history : [], historyIds: sameAttempt ? prior.historyIds : [],
335
- page: sameAttempt ? prior.page : undefined, metadataAt: observed ? prior.metadataAt : undefined, historyLoading: sameAttempt ? prior.historyLoading : false, finalId: sameAttempt ? prior.finalId : undefined, unavailable: child.missingSession && child.state !== "live" ? "Saved conversation unavailable." : view.diagnosis,
336
+ page: sameAttempt ? prior.page : undefined, historySource: sameAttempt ? prior.historySource : undefined, metadataAt: observed ? prior.metadataAt : undefined, historyLoading: sameAttempt ? prior.historyLoading : false, finalId: sameAttempt ? prior.finalId : undefined, unavailable: child.missingSession && child.state !== "live" ? "Saved conversation unavailable." : view.diagnosis,
336
337
  question: child.identityUnavailable ? undefined : questions.findLast((question) => question.index === child.index && ["awaiting_input", "answer_pending"].includes(question.state)),
337
338
  unread: observed ? prior.unread : Boolean(visit && (child.activity?.lastActivityAt ?? view.updatedAt) > (visit.seenActivityAt ?? 0)), replied: sameAttempt ? prior.replied : false };
338
339
  });
@@ -457,10 +458,21 @@ export class AgentViewController {
457
458
  if (this.live(generation)) await this.refresh();
458
459
  }
459
460
 
460
- async historyPage(key: string, paging: Pick<HistoryPageInput, "before" | "after" | "cursor"> = {}, anchor?: string | null): Promise<{ history: AgentHistory; page: HistoryPage; latestPage: boolean; readThroughSequence?: number } | undefined> {
461
+ async historyPage(key: string, paging: Pick<HistoryPageInput, "before" | "after" | "cursor"> = {}, anchor?: string | null): Promise<{ history: AgentHistory; page: HistoryPage; latestPage: boolean; readThroughSequence?: number; isCurrent: () => boolean } | undefined> {
461
462
  const task = this.task(key), generation = this.generation;
462
463
  if (!task || !this.live() || task.child.identityUnavailable) return;
464
+ const published = task.page, source = task.historySource, sessionFile = task.child.sessionFile, childState = task.child.state;
463
465
  const input = this.historyInput(task), index = await runHistoryIndex(this.state);
466
+ // Versions can reset on index reopen. Fence the actual owner/read baseline,
467
+ // including metadata-only source transitions, rather than ordering unrelated versions.
468
+ const isCurrent = () => {
469
+ const current = this.task(key);
470
+ if (!this.live(generation) || this.state.historyIndex !== index || index.failure || !current || current.child.identityUnavailable
471
+ || current.page !== published || current.historySource !== source || current.child.sessionFile !== sessionFile || current.child.state !== childState) return false;
472
+ const boundary = this.historyInput(current);
473
+ return boundary.runId === input.runId && boundary.index === input.index && boundary.leaf === input.leaf
474
+ && boundary.terminalEntryId === input.terminalEntryId && boundary.endedAt === input.endedAt;
475
+ };
464
476
  const boundary = { runId: task.run.runId, index: task.child.index, terminalEntryId: input.terminalEntryId, endedAt: input.endedAt };
465
477
  const marker = typeof input.readThrough === "string" && !input.readThrough.startsWith("result:")
466
478
  ? await index.entry({ ...boundary, entryId: input.readThrough.replace(/:(?:\d+|error)$/, "") }) : undefined;
@@ -471,7 +483,7 @@ export class AgentViewController {
471
483
  }
472
484
  const result = await indexedHistory(index, { ...input, limit: 100, ...paging });
473
485
  if (!this.live(generation) || this.task(key)?.run.runId !== task.run.runId) return;
474
- return { ...result, latestPage: paging.after !== undefined ? !result.page.hasMore : paging.before === undefined && paging.cursor === undefined,
486
+ return { ...result, isCurrent, latestPage: paging.after !== undefined ? !result.page.hasMore : paging.before === undefined && paging.cursor === undefined,
475
487
  readThroughSequence: marker?.sequence ?? (input.readThrough == null ? -1 : undefined) };
476
488
  }
477
489
 
@@ -494,6 +506,7 @@ export class AgentViewController {
494
506
 
495
507
  applyMetadata(task: AgentTask, page: HistoryPage): void {
496
508
  const visit = this.visits.get(task.key), delivered = new Map(page.deliveredMessages);
509
+ task.historySource = JSON.stringify([page.sourceId, page.generation, page.sessionId]);
497
510
  if (page.freshness.state !== "catching-up") task.metadataAt = task.run.updatedAt;
498
511
  task.model = agentModel(task.child, page.configuration);
499
512
  task.unread = Boolean(page.unreadAfter || visit && (task.child.activity?.lastActivityAt ?? 0) > (visit.seenActivityAt ?? 0));
@@ -865,6 +878,7 @@ export class AgentConversation extends Container {
865
878
  private pageRequest = 0;
866
879
  private detailRequest = 0;
867
880
  private loadingPage = false;
881
+ private refreshPending = false;
868
882
  private pageError?: string;
869
883
  private latestPage = true;
870
884
  private readSequence = -1;
@@ -953,6 +967,7 @@ export class AgentConversation extends Container {
953
967
  private async loadPage(paging: Pick<HistoryPageInput, "before" | "after" | "cursor"> = {}, anchor?: string | null): Promise<void> {
954
968
  if (this.closed) return;
955
969
  const request = ++this.pageRequest;
970
+ this.refreshPending = false;
956
971
  this.loadingPage = true; this.pageError = undefined;
957
972
  if (this.task) this.task.historyLoading = true;
958
973
  this.latestPage = paging.before === undefined && paging.after === undefined && paging.cursor === undefined;
@@ -966,16 +981,17 @@ export class AgentConversation extends Container {
966
981
  if (reportPage) result = reportPage;
967
982
  }
968
983
  let task = this.task;
969
- if (result.readThroughSequence !== undefined) this.readSequence = result.readThroughSequence;
970
- this.latestPage = result.latestPage;
971
984
  let history = result.history;
972
- if ((this.initialPosition || this.latestPage || history.finalId && history.entryIds.includes(history.finalId)) && task.child.state !== "live" && task.child.result && getSingleResultOutput(task.child.result)) {
985
+ if ((this.initialPosition || result.latestPage || history.finalId && history.entryIds.includes(history.finalId)) && task.child.state !== "live" && task.child.result && getSingleResultOutput(task.child.result)) {
973
986
  const report = await this.controller.savedResult(this.key, `result:${task.run.runId}`);
974
987
  const currentTask = this.task;
975
988
  if (this.closed || request !== this.pageRequest || !currentTask || currentTask.run.runId !== task.run.runId) return;
976
989
  task = currentTask;
977
990
  history = withFinalResult(history, report.text, task.run.runId, task.run.updatedAt);
978
991
  }
992
+ if (!result.isCurrent()) { void this.loadPage(paging, anchor); return; }
993
+ if (result.readThroughSequence !== undefined) this.readSequence = result.readThroughSequence;
994
+ this.latestPage = result.latestPage;
979
995
  task.history = history.items; task.historyIds = history.entryIds; task.finalId = history.finalId; task.page = result.page;
980
996
  for (const item of task.history) if (item.id === `result:${task.run.runId}`) item.load = () => this.controller.savedResult(this.key, item.id);
981
997
  this.seenSource ||= result.page.count > 0;
@@ -985,7 +1001,11 @@ export class AgentConversation extends Container {
985
1001
  } catch (error) {
986
1002
  if (!this.closed && request === this.pageRequest) this.pageError = `History unavailable: ${error instanceof Error ? error.message : String(error)}. F5 retries the selected run.`;
987
1003
  } finally {
988
- if (!this.closed && request === this.pageRequest) { this.loadingPage = false; if (this.task) this.task.historyLoading = false; this.tui.requestRender(); }
1004
+ if (!this.closed && request === this.pageRequest) {
1005
+ this.loadingPage = false; if (this.task) this.task.historyLoading = false;
1006
+ if (this.refreshPending && !this.pageError) { this.refreshPending = false; this.refresh(); }
1007
+ this.tui.requestRender();
1008
+ }
989
1009
  }
990
1010
  }
991
1011
 
@@ -1001,7 +1021,10 @@ export class AgentConversation extends Container {
1001
1021
  if (this.closed) return;
1002
1022
  if (this.detail?.id === "assignment" && this.task) this.detail = this.assignment();
1003
1023
  if (!this.initialPosition && !this.scroll.isFollowingEnd) this.restoreAnchor = this.anchor();
1004
- if (!this.loadingPage && this.latestPage && this.scroll.isFollowingEnd && !this.detail && !this.pageError) void this.loadPage();
1024
+ if (this.latestPage && this.scroll.isFollowingEnd && !this.detail && !this.pageError) {
1025
+ if (this.loadingPage) this.refreshPending = true;
1026
+ else void this.loadPage();
1027
+ }
1005
1028
  this.tui.requestRender();
1006
1029
  }
1007
1030
  syncDraft(): void { if (!this.closed && this.editor.getExpandedText() !== this.visit.draft) this.editor.setText(this.visit.draft); }