@pasko70/pibo 2.5.2 → 3.0.1

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 (100) hide show
  1. package/context/pibo-native-tooling.md +1 -0
  2. package/dist/agent-runtime/contract.js +2 -1
  3. package/dist/agent-runtime/history.js +53 -1
  4. package/dist/agent-runtime/routed-session.js +100 -26
  5. package/dist/agent-runtimes/codex-native/adapter.js +304 -30
  6. package/dist/agent-runtimes/codex-native/client.js +22 -3
  7. package/dist/agent-runtimes/codex-native/first-use.js +294 -0
  8. package/dist/agent-runtimes/codex-native/history.js +26 -7
  9. package/dist/agent-runtimes/codex-native/process.js +14 -0
  10. package/dist/agent-runtimes/codex-native/thread.js +59 -25
  11. package/dist/agent-runtimes/codex-native/turn.js +27 -0
  12. package/dist/agent-runtimes/history-proof.js +8 -0
  13. package/dist/agent-runtimes/omp/adapter.js +61 -14
  14. package/dist/agent-runtimes/omp/history.js +189 -34
  15. package/dist/agent-runtimes/omp/thread.js +82 -36
  16. package/dist/agent-runtimes/pi/adapter.js +30 -3
  17. package/dist/agent-runtimes/pi/history.js +66 -5
  18. package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
  19. package/dist/agent-runtimes/pi/routed-session.js +344 -37
  20. package/dist/apps/chat/chat-api-routes.js +1 -1
  21. package/dist/apps/chat/chat-files.js +130 -9
  22. package/dist/apps/chat/chat-settings-routes.js +27 -2
  23. package/dist/apps/chat/chat-speech.js +100 -0
  24. package/dist/apps/chat/data/chat-data-mappers.js +25 -5
  25. package/dist/apps/chat/data/timeline-query-service.js +89 -1
  26. package/dist/apps/chat/trace-v2.js +210 -8
  27. package/dist/apps/chat/trace.js +3 -0
  28. package/dist/apps/chat/web-app.js +108 -3
  29. package/dist/apps/chat-ui/assets/{dist-DTWJf37A.js → dist-BBDMeU5-.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-CkYWKnvy.js → dist-BDIATCQt.js} +1 -1
  31. package/dist/apps/chat-ui/assets/{dist-DZ8Wpj-H.js → dist-JnW4v8UE.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-Dmblz15N.js → dist-lRiLP9rr.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-DjcwoyNY.js → dist-mqCviI-c.js} +1 -1
  34. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
  35. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
  36. package/dist/apps/chat-ui/index.html +2 -2
  37. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
  38. package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
  39. package/dist/apps/chat-vscode-web/index.html +2 -2
  40. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  41. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.1.vsix +0 -0
  42. package/dist/cli.js +49 -5
  43. package/dist/config/config.js +59 -8
  44. package/dist/core/preview-server-settings.js +20 -0
  45. package/dist/core/session-router.js +464 -37
  46. package/dist/core/user-settings.js +10 -0
  47. package/dist/data/payload-store.js +57 -1
  48. package/dist/data/schema.js +18 -1
  49. package/dist/debug/trace.js +5 -0
  50. package/dist/gateway/client.js +368 -65
  51. package/dist/gateway/protocol.js +2 -0
  52. package/dist/gateway/server.js +8 -0
  53. package/dist/gateway/web.js +2 -0
  54. package/dist/index.js +1 -1
  55. package/dist/loops/store.js +6 -4
  56. package/dist/mcp/config.js +89 -17
  57. package/dist/mcp/daemon-client.js +473 -153
  58. package/dist/mcp/daemon.js +401 -37
  59. package/dist/plugins/builtin.js +2 -2
  60. package/dist/plugins/codex-native.js +2 -0
  61. package/dist/plugins/registry.js +259 -1
  62. package/dist/previews/cli.js +345 -0
  63. package/dist/previews/config.js +61 -0
  64. package/dist/previews/manager.js +644 -0
  65. package/dist/previews/network.js +198 -0
  66. package/dist/previews/plugin.js +11 -0
  67. package/dist/previews/process-supervisor.js +38 -0
  68. package/dist/previews/proxy.js +387 -0
  69. package/dist/previews/store.js +750 -0
  70. package/dist/previews/types.js +1 -0
  71. package/dist/previews/web-app.js +446 -0
  72. package/dist/runs/registry.js +22 -0
  73. package/dist/runs/tools.js +46 -7
  74. package/dist/session-ui/terminalRows.js +187 -13
  75. package/dist/sessions/pibo-data-store.js +37 -0
  76. package/dist/sessions/runtime-binding-persistence.js +34 -0
  77. package/dist/sessions/runtime-binding.js +6 -0
  78. package/dist/sessions/sqlite-store.js +37 -0
  79. package/dist/shared/trace-engine.js +12 -64
  80. package/dist/shared/trace-event-projection.js +16 -0
  81. package/dist/shared/trace-history.js +684 -184
  82. package/dist/shared/trace-limits.js +7 -0
  83. package/dist/shared/trace-live-patch.js +61 -0
  84. package/dist/shared/trace-tool-identity.js +43 -0
  85. package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
  86. package/dist/speech/openai-codex.js +407 -0
  87. package/dist/speech/types.js +8 -0
  88. package/dist/subagents/context.js +5 -3
  89. package/dist/subagents/observations.js +94 -6
  90. package/dist/subagents/tool.js +103 -20
  91. package/dist/tools/codex-image-artifacts.js +24 -0
  92. package/dist/tools/codex-image-generation.js +4 -22
  93. package/dist/web/channel.js +88 -4
  94. package/dist/web/http.js +2 -1
  95. package/npm-shrinkwrap.json +2 -2
  96. package/package.json +1 -1
  97. package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
  98. package/dist/apps/chat-ui/assets/index-DsgTm1Aw.js +0 -228
  99. package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
  100. package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.2.vsix +0 -0
@@ -1,15 +1,18 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { isDeepStrictEqual } from "node:util";
2
3
  import { InitialSessionContext, } from "./profiles.js";
3
4
  import { createDefaultPiboPluginRegistry, createPiboProfileFromRegistryOrDefault, resolvePiboProfileNameFromRegistryOrDefault, selectDefaultPiboProfileName } from "../plugins/builtin.js";
4
5
  import { RuntimeRoutedSession as RoutedSession, } from "../agent-runtime/routed-session.js";
5
6
  import { runtimeSessionErrorDetails } from "./session-errors.js";
6
- import { normalizePiboAgentObservationCursor, normalizePiboAgentObservationLimit, normalizePiboAgentObservationOrder, parsePiboAgentObservationTimestamp, piboAgentObservationDetails, piboAgentObservationKind, piboAgentObservationRole, piboAgentObservationSourceFromEvent, piboAgentObservationText, } from "../subagents/observations.js";
7
+ import { normalizePiboAgentSessionName, } from "../subagents/tool.js";
8
+ import { PIBO_AGENT_OBSERVATION_DEFAULT_EVENT_TYPES, PIBO_AGENT_OBSERVATION_DEFAULT_TOOL_EVENT_TYPES, normalizePiboAgentObservationCursor, normalizePiboAgentObservationLimit, normalizePiboAgentObservationOrder, normalizePiboAgentObservationToolDetail, parsePiboAgentObservationTimestamp, piboAgentObservationDetails, piboAgentObservationKind, piboAgentObservationRole, piboAgentObservationSourceFromEvent, piboAgentObservationText, piboAgentObservationToolSummary, } from "../subagents/observations.js";
7
9
  import { PiboRunRegistry } from "../runs/registry.js";
8
10
  import { PiboRunCancellationError, PiboRunCancelledError, PiboRunExecutionTimeoutError, waitForRunCancellationSettlement } from "../runs/lifecycle.js";
9
11
  import { PiboRunResourceLimitError } from "../runs/resource-isolation.js";
10
12
  import { createPiboSignalRegistry } from "../signals/registry.js";
11
13
  import { createDefaultPiboReliabilityStore } from "../reliability/store.js";
12
- import { InMemoryPiboSessionStore, } from "../sessions/store.js";
14
+ import { InMemoryPiboSessionStore, createPiboSessionId, } from "../sessions/store.js";
15
+ import { createAgentRuntimeBindingPersistence } from "../sessions/runtime-binding-persistence.js";
13
16
  import { createLegacyPiRuntimeSessionBinding, RuntimeSessionBindingConflictError, } from "../sessions/runtime-binding.js";
14
17
  import { AgentRuntimeBindingMissingError, AgentRuntimeUnavailableError } from "../agent-runtime/errors.js";
15
18
  import { validateAgentRuntimeProfileCapabilities } from "../agent-runtime/profile-validation.js";
@@ -162,6 +165,23 @@ function formatRunReminderMessage(notification) {
162
165
  function isRunReminderServiceMessage(event) {
163
166
  return event.source === "service" && event.text.startsWith("<pibo_run_notification>");
164
167
  }
168
+ function isRunReminderContextPressureError(event) {
169
+ return event.errorDetails?.category === "context_overflow"
170
+ || event.errorDetails?.code === "context_length_exceeded"
171
+ || event.errorDetails?.code === "run_reminder_limit_exceeded";
172
+ }
173
+ function runReminderRecoveryGroupKey(notification) {
174
+ if (notification.origin)
175
+ return `origin:${JSON.stringify(notification.origin)}`;
176
+ const runIds = [
177
+ ...notification.completed,
178
+ ...notification.failed,
179
+ ...notification.timedOut,
180
+ ...notification.cancelled,
181
+ ...notification.running,
182
+ ].map((run) => run.runId).sort();
183
+ return `runs:${JSON.stringify(runIds)}`;
184
+ }
165
185
  function yieldedRunOrigin(event) {
166
186
  if (!event?.id || event.provenance?.kind !== "loop-run")
167
187
  return undefined;
@@ -190,6 +210,58 @@ function isTerminalRunStatus(status) {
190
210
  function asJsonObject(value) {
191
211
  return value ?? {};
192
212
  }
213
+ const DERIVED_SESSION_RECONCILIATION_METADATA_KEY = "pibo.sessionIdentityReconciliation.v1";
214
+ function unresolvedDerivedSessionSource(session) {
215
+ const marker = session.metadata?.[DERIVED_SESSION_RECONCILIATION_METADATA_KEY];
216
+ if (!marker || typeof marker !== "object" || Array.isArray(marker))
217
+ return undefined;
218
+ return marker.state === "cleanup-required" && typeof marker.sourcePiboSessionId === "string"
219
+ ? marker.sourcePiboSessionId
220
+ : undefined;
221
+ }
222
+ function derivedSessionMetadata(value) {
223
+ const metadata = { ...asJsonObject(value) };
224
+ for (const key of [
225
+ DERIVED_SESSION_RECONCILIATION_METADATA_KEY,
226
+ "workflowSessionKind",
227
+ "projectSessionKind",
228
+ "subagentName",
229
+ "subagentToolName",
230
+ "agentStatus",
231
+ "threadKey",
232
+ ]) {
233
+ delete metadata[key];
234
+ }
235
+ return metadata;
236
+ }
237
+ function matchesDerivedSessionIntent(session, input) {
238
+ const binding = session.runtimeBinding;
239
+ const expectedBinding = input.runtimeBinding;
240
+ if (!binding || !expectedBinding)
241
+ return false;
242
+ const expectedPiSessionId = expectedBinding.adapterId === "pi" ? expectedBinding.nativeSessionId ?? "" : "";
243
+ return session.id === input.id
244
+ && session.piSessionId === expectedPiSessionId
245
+ && session.channel === input.channel
246
+ && session.kind === input.kind
247
+ && session.profile === input.profile
248
+ && session.parentId === input.parentId
249
+ && session.originId === input.originId
250
+ && session.workspace === input.workspace
251
+ && (session.title === input.title || (input.title === undefined && session.title === "Untitled Session"))
252
+ && isDeepStrictEqual(session.metadata ?? {}, input.metadata ?? {})
253
+ && isDeepStrictEqual(session.activeModel, input.activeModel)
254
+ && binding.piboSessionId === input.id
255
+ && binding.runtimeInstanceId === expectedBinding.runtimeInstanceId
256
+ && binding.adapterId === expectedBinding.adapterId
257
+ && binding.nativeSessionId === expectedBinding.nativeSessionId
258
+ && binding.state === (expectedBinding.state ?? "unbound")
259
+ && binding.protocol === expectedBinding.protocol
260
+ && binding.protocolVersion === expectedBinding.protocolVersion
261
+ && binding.adapterVersion === expectedBinding.adapterVersion
262
+ && isDeepStrictEqual(binding.locator, expectedBinding.locator)
263
+ && isDeepStrictEqual(binding.metadata ?? {}, expectedBinding.metadata ?? {});
264
+ }
193
265
  function shouldResetSessionAfterAction(action, output) {
194
266
  if (action === "login.apikey" || action === "logout")
195
267
  return true;
@@ -273,10 +345,14 @@ export class PiboSessionRouter {
273
345
  agentObservationEvictedThroughByParent = new Map();
274
346
  nextAgentObservationSequence = 1;
275
347
  scheduledRunReminders = new Map();
348
+ runReminderDeliveries = new Map();
349
+ deferredRunReminders = new Map();
350
+ runReminderRecoveries = new Map();
276
351
  runReminderGenerations = new Map();
277
352
  runCancellationHandlers = new Map();
278
353
  activeRunExecutions = new Set();
279
354
  quiescingSessions = new Set();
355
+ unresolvedDerivedSessionTransitions = new Map();
280
356
  disposingSessions = new Map();
281
357
  idleSessionTimers = new Map();
282
358
  routedSessionIdleTimeoutMs;
@@ -356,6 +432,13 @@ export class PiboSessionRouter {
356
432
  async emit(event) {
357
433
  if (this.closing)
358
434
  throw new Error("Pibo session router is disposed.");
435
+ let messageSignalAccepted = false;
436
+ const acceptMessageSignal = () => {
437
+ if (event.type !== "message" || !event.id || messageSignalAccepted)
438
+ return;
439
+ messageSignalAccepted = true;
440
+ this.signalRegistry.project({ type: "message_accepted", piboSessionId: event.piboSessionId, eventId: event.id, source: event.source });
441
+ };
359
442
  const teardownAction = event.type === "execution" && (event.action === "dispose" || event.action === "kill" || event.action === "kill_all");
360
443
  const teardownIds = teardownAction
361
444
  ? [event.piboSessionId, ...this.descendantSessionIds(event.piboSessionId)]
@@ -370,7 +453,13 @@ export class PiboSessionRouter {
370
453
  const stored = this.sessionStore.get(event.piboSessionId);
371
454
  if (stored)
372
455
  this.signalRegistry.project({ type: "session_created", session: stored });
373
- this.signalRegistry.project({ type: "message_accepted", piboSessionId: event.piboSessionId, eventId: event.id, source: event.source });
456
+ // Cold runtime creation and steering retain their established eager
457
+ // signal lifecycle. Cached queued messages are accepted inside the
458
+ // routed session's synchronous admission boundary so an identity
459
+ // reservation can reject without first publishing acceptance.
460
+ if (event.delivery === "steer" || (!this.sessions.has(event.piboSessionId) && !this.pendingSessions.has(event.piboSessionId))) {
461
+ acceptMessageSignal();
462
+ }
374
463
  }
375
464
  let session;
376
465
  try {
@@ -399,9 +488,13 @@ export class PiboSessionRouter {
399
488
  throw new Error(`Pibo session "${event.piboSessionId}" is quiescing.`);
400
489
  }
401
490
  if (event.type === "message") {
402
- return event.delivery === "steer"
403
- ? await session.steerMessage(event)
404
- : session.enqueueMessage(event);
491
+ if (event.delivery === "steer")
492
+ return await session.steerMessage(event);
493
+ const output = session.enqueueMessage(event, acceptMessageSignal);
494
+ // Test doubles and compatibility shims may ignore the optional
495
+ // callback; production routed sessions invoke it before queueing.
496
+ acceptMessageSignal();
497
+ return output;
405
498
  }
406
499
  if (event.action === "abort") {
407
500
  this.signalRegistry.project({ type: "session_interrupted", piboSessionId: event.piboSessionId, reason: "abort action" });
@@ -840,6 +933,15 @@ export class PiboSessionRouter {
840
933
  this.scheduleIdleSessionEvictionIfIdle(piboSessionId);
841
934
  }
842
935
  }
936
+ async getSessionForkCandidates(piboSessionId) {
937
+ const session = await this.getOrCreateSession(piboSessionId);
938
+ try {
939
+ return await session.getForkCandidates();
940
+ }
941
+ finally {
942
+ this.scheduleIdleSessionEvictionIfIdle(piboSessionId);
943
+ }
944
+ }
843
945
  async setLiveSessionActiveModel(piboSessionId, model) {
844
946
  const session = this.sessions.get(piboSessionId);
845
947
  if (!session)
@@ -1224,6 +1326,17 @@ export class PiboSessionRouter {
1224
1326
  this.portableToolSessions.delete(piboSession.id);
1225
1327
  throw error;
1226
1328
  }
1329
+ const bindingSync = { expectedRevision: binding.revision };
1330
+ const runtimeBindingPersistence = createAgentRuntimeBindingPersistence(this.sessionStore, {
1331
+ piboSessionId: piboSession.id,
1332
+ onPersisted: (updated) => {
1333
+ binding = updated;
1334
+ bindingSync.expectedRevision = updated.revision;
1335
+ const updatedSession = this.sessionStore.get(piboSession.id);
1336
+ if (updatedSession)
1337
+ this.signalRegistry.project({ type: "session_created", session: updatedSession });
1338
+ },
1339
+ });
1227
1340
  let runtimeSession;
1228
1341
  try {
1229
1342
  runtimeSession = await runtimeRegistry.openAgentRuntimeSession(binding.runtimeInstanceId, {
@@ -1249,6 +1362,7 @@ export class PiboSessionRouter {
1249
1362
  codeRuntimeToolController,
1250
1363
  portableTools,
1251
1364
  resources,
1365
+ ...(runtimeBindingPersistence ? { runtimeBindingPersistence } : {}),
1252
1366
  compatibility: {
1253
1367
  persistSession: this.options.persistSession,
1254
1368
  thinkingLevel: initialThinkingLevel ?? this.options.thinkingLevel,
@@ -1309,6 +1423,7 @@ export class PiboSessionRouter {
1309
1423
  binding = this.persistSessionRuntimeBinding(piboSession, openedBinding, {
1310
1424
  expectedRevision: binding.revision,
1311
1425
  });
1426
+ bindingSync.expectedRevision = binding.revision;
1312
1427
  }
1313
1428
  }
1314
1429
  catch (error) {
@@ -1331,6 +1446,7 @@ export class PiboSessionRouter {
1331
1446
  })
1332
1447
  : undefined,
1333
1448
  onSessionOperation: (result, event) => this.handleSessionOperation(result, event),
1449
+ onBeforeSessionIdentityOperation: (event) => this.retryUnresolvedDerivedSessionCompensation(event.piboSessionId),
1334
1450
  onKillChildren: (id, opts) => this.killChildSessions(id, opts),
1335
1451
  onStateChange: (state) => {
1336
1452
  this.signalRegistry.project({
@@ -1339,18 +1455,22 @@ export class PiboSessionRouter {
1339
1455
  processing: state.processing,
1340
1456
  queuedMessages: state.queuedMessages,
1341
1457
  });
1342
- if (!state.processing && state.queuedMessages === 0 && !state.disposed) {
1343
- this.syncLiveSessionRuntimeBinding(piboSession.id, runtimeSession);
1458
+ if (!state.processing && state.queuedMessages === 0 && !state.disposed && !state.sessionIdentityOperationInFlight) {
1459
+ this.syncLiveSessionRuntimeBinding(piboSession.id, runtimeSession, bindingSync);
1460
+ this.scheduleRunReminder(piboSession.id, false);
1344
1461
  }
1345
- if (state.disposed || state.processing || state.queuedMessages > 0)
1462
+ if (state.disposed || state.processing || state.queuedMessages > 0 || state.sessionIdentityOperationInFlight)
1346
1463
  this.clearIdleSessionTimer(piboSession.id);
1347
1464
  else
1348
1465
  this.scheduleIdleSessionEvictionIfIdle(piboSession.id);
1349
1466
  },
1350
- onMessagesInterrupted: (messages, reason) => this.telemetryRecorder?.recordMessagesInterrupted(messages, {
1351
- session: this.sessionStore.get(piboSession.id),
1352
- status: this.sessions.get(piboSession.id)?.getStatus(),
1353
- }, reason),
1467
+ onMessagesInterrupted: (messages, reason) => {
1468
+ this.telemetryRecorder?.recordMessagesInterrupted(messages, {
1469
+ session: this.sessionStore.get(piboSession.id),
1470
+ status: this.sessions.get(piboSession.id)?.getStatus(),
1471
+ }, reason);
1472
+ this.handleInterruptedRunReminders(messages);
1473
+ },
1354
1474
  messagePreflight: this.options.messagePreflight,
1355
1475
  getRuntimeAuthStatus: () => runtimeRegistry.getAgentRuntimeAuthStatus(binding.runtimeInstanceId),
1356
1476
  startRuntimeAuth: async (input) => {
@@ -1430,22 +1550,33 @@ export class PiboSessionRouter {
1430
1550
  updatedAt: now,
1431
1551
  };
1432
1552
  }
1433
- syncLiveSessionRuntimeBinding(piboSessionId, runtimeSession) {
1553
+ syncLiveSessionRuntimeBinding(piboSessionId, runtimeSession, state) {
1434
1554
  const session = this.sessionStore.get(piboSessionId);
1435
1555
  if (!session)
1436
1556
  return;
1437
1557
  try {
1438
1558
  const persisted = this.resolveSessionRuntimeBinding(session);
1439
- this.persistSessionRuntimeBinding(session, withPersistedPortableHistoryAuditMetadata(persisted, runtimeSession.getBinding()));
1559
+ const updated = this.persistSessionRuntimeBinding(session, withPersistedPortableHistoryAuditMetadata(persisted, runtimeSession.getBinding()), { expectedRevision: state.expectedRevision });
1560
+ state.expectedRevision = updated.revision;
1440
1561
  }
1441
1562
  catch (error) {
1442
1563
  const message = error instanceof Error ? error.message : String(error);
1564
+ const reset = this.resetCachedSession(piboSessionId, "runtime binding synchronization failed");
1443
1565
  this.emitOutput({
1444
1566
  type: "session_error",
1445
1567
  piboSessionId,
1446
1568
  error: `Failed to persist the live runtime binding: ${message}`,
1447
1569
  errorDetails: runtimeSessionErrorDetails(message),
1448
1570
  });
1571
+ void reset.catch((resetError) => {
1572
+ const resetMessage = resetError instanceof Error ? resetError.message : String(resetError);
1573
+ this.emitOutput({
1574
+ type: "session_error",
1575
+ piboSessionId,
1576
+ error: `Failed to discard a runtime after binding synchronization failed: ${resetMessage}`,
1577
+ errorDetails: runtimeSessionErrorDetails(resetMessage),
1578
+ });
1579
+ });
1449
1580
  }
1450
1581
  }
1451
1582
  assertOpenableRuntimeBinding(binding) {
@@ -1494,21 +1625,44 @@ export class PiboSessionRouter {
1494
1625
  const source = this.resolvePiboSession(event.piboSessionId);
1495
1626
  const previousBinding = this.resolveSessionRuntimeBinding(source);
1496
1627
  const liveBinding = this.sessions.get(event.piboSessionId)?.getRuntimeBinding();
1628
+ const currentNativeSessionId = result.current.piSessionId || undefined;
1497
1629
  const currentBinding = {
1498
1630
  ...previousBinding,
1499
1631
  ...liveBinding,
1500
1632
  piboSessionId: source.id,
1501
- nativeSessionId: result.current.piSessionId,
1502
- state: "bound",
1503
- locator: result.current.sessionFile
1633
+ nativeSessionId: currentNativeSessionId,
1634
+ state: currentNativeSessionId ? "bound" : "unbound",
1635
+ locator: currentNativeSessionId && result.current.sessionFile
1504
1636
  ? { kind: "local-file", value: result.current.sessionFile }
1505
- : liveBinding?.locator ?? previousBinding.locator,
1637
+ : currentNativeSessionId
1638
+ ? liveBinding?.locator ?? previousBinding.locator
1639
+ : undefined,
1640
+ metadata: currentNativeSessionId ? liveBinding?.metadata ?? previousBinding.metadata : undefined,
1506
1641
  };
1507
1642
  if (event.action === "session.fork" || event.action === "session.clone") {
1508
1643
  const action = event.action;
1509
- const created = this.createDerivedSession(result, action, currentBinding);
1510
- result.piboSessionId = created.id;
1511
- await this.resetCachedSession(event.piboSessionId);
1644
+ let transitionError;
1645
+ try {
1646
+ const created = this.createDerivedSession(result, action, currentBinding);
1647
+ result.piboSessionId = created.id;
1648
+ }
1649
+ catch (error) {
1650
+ transitionError = error;
1651
+ }
1652
+ // Always discard the source live handle after derivation. Bound native
1653
+ // branches move that handle to the derived session; first-message Codex
1654
+ // branches intentionally leave it on the source and persist unbound.
1655
+ try {
1656
+ await this.resetCachedSession(event.piboSessionId);
1657
+ }
1658
+ catch (resetError) {
1659
+ if (transitionError) {
1660
+ throw new AggregateError([transitionError, resetError], `Failed to persist and reset ${action}.`);
1661
+ }
1662
+ throw resetError;
1663
+ }
1664
+ if (transitionError)
1665
+ throw transitionError;
1512
1666
  return;
1513
1667
  }
1514
1668
  this.persistSessionRuntimeBinding(source, currentBinding, {
@@ -1519,17 +1673,18 @@ export class PiboSessionRouter {
1519
1673
  }
1520
1674
  createDerivedSession(result, action, currentBinding) {
1521
1675
  const source = this.resolvePiboSession(result.piboSessionId);
1522
- return this.sessionStore.create({
1676
+ const input = {
1677
+ id: createPiboSessionId(),
1523
1678
  channel: source.channel,
1524
1679
  kind: "branch",
1525
1680
  profile: source.profile,
1526
- parentId: source.kind === "subagent" ? source.parentId : undefined,
1681
+ parentId: source.parentId,
1527
1682
  originId: source.id,
1528
1683
  runtimeBinding: {
1529
1684
  runtimeInstanceId: currentBinding.runtimeInstanceId,
1530
1685
  adapterId: currentBinding.adapterId,
1531
- nativeSessionId: currentBinding.nativeSessionId ?? result.current.piSessionId,
1532
- state: "bound",
1686
+ nativeSessionId: currentBinding.nativeSessionId,
1687
+ state: currentBinding.state,
1533
1688
  protocol: currentBinding.protocol,
1534
1689
  protocolVersion: currentBinding.protocolVersion,
1535
1690
  adapterVersion: currentBinding.adapterVersion,
@@ -1540,12 +1695,116 @@ export class PiboSessionRouter {
1540
1695
  title: source.title,
1541
1696
  activeModel: source.activeModel,
1542
1697
  metadata: {
1543
- ...asJsonObject(source.metadata),
1698
+ ...derivedSessionMetadata(source.metadata),
1544
1699
  originAction: action,
1545
1700
  originRuntimeNativeSessionId: result.previous.piSessionId,
1546
1701
  ...(currentBinding.adapterId === "pi" ? { originPiSessionId: result.previous.piSessionId } : {}),
1547
1702
  },
1703
+ };
1704
+ let created;
1705
+ try {
1706
+ created = this.sessionStore.create(input);
1707
+ }
1708
+ catch (error) {
1709
+ let persisted;
1710
+ try {
1711
+ persisted = this.sessionStore.get(input.id);
1712
+ }
1713
+ catch (lookupError) {
1714
+ const failure = new AggregateError([error, lookupError], `Branch persistence for "${input.id}" failed and its commit state could not be reconciled.`);
1715
+ this.unresolvedDerivedSessionTransitions.set(source.id, { piboSessionId: input.id, cause: failure });
1716
+ throw failure;
1717
+ }
1718
+ if (!persisted)
1719
+ throw error;
1720
+ if (matchesDerivedSessionIntent(persisted, input))
1721
+ return persisted;
1722
+ return this.rejectDerivedSessionAfterCompensation(source.id, input.id, error);
1723
+ }
1724
+ if (matchesDerivedSessionIntent(created, input))
1725
+ return created;
1726
+ return this.rejectDerivedSessionAfterCompensation(source.id, input.id, new Error(`Session store returned a branch that does not match the intended derivation "${input.id}".`));
1727
+ }
1728
+ retryUnresolvedDerivedSessionCompensation(sourcePiboSessionId) {
1729
+ const remembered = this.unresolvedDerivedSessionTransitions.get(sourcePiboSessionId);
1730
+ const persisted = this.sessionStore.find({ originId: sourcePiboSessionId })
1731
+ .filter((session) => unresolvedDerivedSessionSource(session) === sourcePiboSessionId)
1732
+ .map((session) => ({
1733
+ piboSessionId: session.id,
1734
+ cause: new Error(`Branch "${session.id}" has a persisted session identity reconciliation marker.`),
1735
+ }));
1736
+ const unresolved = [
1737
+ ...(remembered ? [remembered] : []),
1738
+ ...persisted.filter((candidate) => candidate.piboSessionId !== remembered?.piboSessionId),
1739
+ ];
1740
+ for (const candidate of unresolved) {
1741
+ try {
1742
+ if (!this.sessionStore.get(candidate.piboSessionId)) {
1743
+ if (remembered?.piboSessionId === candidate.piboSessionId) {
1744
+ this.unresolvedDerivedSessionTransitions.delete(sourcePiboSessionId);
1745
+ }
1746
+ continue;
1747
+ }
1748
+ if (!this.sessionStore.delete)
1749
+ throw new Error("Session store does not support compensating deletion.");
1750
+ this.sessionStore.delete(candidate.piboSessionId);
1751
+ if (this.sessionStore.get(candidate.piboSessionId)) {
1752
+ throw new Error(`Compensating deletion left branch "${candidate.piboSessionId}" persisted.`);
1753
+ }
1754
+ if (remembered?.piboSessionId === candidate.piboSessionId) {
1755
+ this.unresolvedDerivedSessionTransitions.delete(sourcePiboSessionId);
1756
+ }
1757
+ }
1758
+ catch (error) {
1759
+ this.unresolvedDerivedSessionTransitions.set(sourcePiboSessionId, candidate);
1760
+ throw new AggregateError([candidate.cause, error], `Branch "${candidate.piboSessionId}" still requires compensation; refusing another session identity operation for source "${sourcePiboSessionId}".`);
1761
+ }
1762
+ }
1763
+ }
1764
+ persistDerivedSessionReconciliationMarker(sourcePiboSessionId, piboSessionId) {
1765
+ const residual = this.sessionStore.get(piboSessionId);
1766
+ if (!residual)
1767
+ return;
1768
+ const updated = this.sessionStore.update(piboSessionId, {
1769
+ metadata: {
1770
+ ...(residual.metadata ?? {}),
1771
+ [DERIVED_SESSION_RECONCILIATION_METADATA_KEY]: {
1772
+ state: "cleanup-required",
1773
+ sourcePiboSessionId,
1774
+ },
1775
+ },
1548
1776
  });
1777
+ if (!updated || unresolvedDerivedSessionSource(updated) !== sourcePiboSessionId) {
1778
+ throw new Error(`Failed to persist the reconciliation marker for branch "${piboSessionId}".`);
1779
+ }
1780
+ }
1781
+ rejectDerivedSessionAfterCompensation(sourcePiboSessionId, piboSessionId, cause) {
1782
+ let cleanupError;
1783
+ try {
1784
+ if (!this.sessionStore.delete) {
1785
+ throw new Error("Session store does not support compensating deletion.");
1786
+ }
1787
+ this.sessionStore.delete(piboSessionId);
1788
+ if (this.sessionStore.get(piboSessionId)) {
1789
+ throw new Error(`Compensating deletion left branch "${piboSessionId}" persisted.`);
1790
+ }
1791
+ }
1792
+ catch (error) {
1793
+ cleanupError = error;
1794
+ }
1795
+ if (cleanupError) {
1796
+ let markerError;
1797
+ try {
1798
+ this.persistDerivedSessionReconciliationMarker(sourcePiboSessionId, piboSessionId);
1799
+ }
1800
+ catch (error) {
1801
+ markerError = error;
1802
+ }
1803
+ const failure = new AggregateError([cause, cleanupError, ...(markerError ? [markerError] : [])], `Branch persistence for "${piboSessionId}" was inconsistent and compensation failed.`);
1804
+ this.unresolvedDerivedSessionTransitions.set(sourcePiboSessionId, { piboSessionId, cause: failure });
1805
+ throw failure;
1806
+ }
1807
+ throw cause;
1549
1808
  }
1550
1809
  runtimeAuthAffectedInstanceIds(runtimeInstanceId) {
1551
1810
  const registry = this.resolveAgentRuntimeRegistry(runtimeInstanceId);
@@ -1661,13 +1920,14 @@ export class PiboSessionRouter {
1661
1920
  }
1662
1921
  createAgentsController(parentPiboSessionId) {
1663
1922
  return {
1664
- sendMessage: async ({ subagent, message, threadKey, toolCallId, requestId, parentProvenance, signal }) => {
1923
+ sendMessage: async ({ subagent, sessionName, message, threadKey, toolCallId, requestId, parentProvenance, signal }) => {
1665
1924
  if (signal?.aborted)
1666
1925
  throw subagentAbortError();
1667
1926
  if (typeof requestId !== "string" || !requestId.trim())
1668
1927
  throw new Error("Delegated agent requestId is required.");
1669
1928
  this.assertSubagentDepth(parentPiboSessionId, subagent);
1670
- const child = this.resolveSubagentSession(parentPiboSessionId, subagent, threadKey);
1929
+ const normalizedSessionName = normalizePiboAgentSessionName(sessionName);
1930
+ const child = this.resolveSubagentSession(parentPiboSessionId, subagent, normalizedSessionName, threadKey);
1671
1931
  const resolvedThreadKey = typeof child.metadata?.threadKey === "string" ? child.metadata.threadKey : "";
1672
1932
  const loopJobId = parentProvenance?.kind === "loop-run"
1673
1933
  ? parentProvenance.jobId
@@ -1762,6 +2022,7 @@ export class PiboSessionRouter {
1762
2022
  agentId: session.id,
1763
2023
  name: typeof session.metadata?.subagentName === "string" ? session.metadata.subagentName : session.profile,
1764
2024
  profile: session.profile,
2025
+ ...(session.title ? { sessionName: session.title } : {}),
1765
2026
  ...(typeof session.metadata?.threadKey === "string" ? { threadKey: session.metadata.threadKey } : {}),
1766
2027
  status: killed ? "killed" : running ? "running" : "idle",
1767
2028
  createdAt: session.createdAt,
@@ -1783,6 +2044,7 @@ export class PiboSessionRouter {
1783
2044
  observeManagedAgents(parentPiboSessionId, input) {
1784
2045
  const order = normalizePiboAgentObservationOrder(input.order);
1785
2046
  const limit = normalizePiboAgentObservationLimit(input.limit);
2047
+ const toolDetail = normalizePiboAgentObservationToolDetail(input.toolDetail);
1786
2048
  const afterSequence = normalizePiboAgentObservationCursor(input.afterSequence);
1787
2049
  const since = parsePiboAgentObservationTimestamp(input.since, "since");
1788
2050
  const until = parsePiboAgentObservationTimestamp(input.until, "until");
@@ -1801,6 +2063,14 @@ export class PiboSessionRouter {
1801
2063
  const kinds = input.kinds ? new Set(input.kinds) : undefined;
1802
2064
  const roles = input.roles ? new Set(input.roles) : undefined;
1803
2065
  const textContains = input.textContains?.toLowerCase();
2066
+ const defaultMessageView = eventTypes === undefined && kinds === undefined;
2067
+ const explicitlySelectsTools = input.eventTypes?.some((eventType) => piboAgentObservationKind(eventType) === "tool") === true
2068
+ || input.kinds?.includes("tool") === true;
2069
+ const includeTools = input.includeTools === true || (input.includeTools === undefined && explicitlySelectsTools);
2070
+ const defaultEventTypes = includeTools
2071
+ ? [...PIBO_AGENT_OBSERVATION_DEFAULT_EVENT_TYPES, ...PIBO_AGENT_OBSERVATION_DEFAULT_TOOL_EVENT_TYPES]
2072
+ : [...PIBO_AGENT_OBSERVATION_DEFAULT_EVENT_TYPES];
2073
+ const defaultEventTypeSet = new Set(defaultEventTypes);
1804
2074
  const matches = this.agentObservations.filter((observation) => {
1805
2075
  if (observation.managingParentId !== parentPiboSessionId)
1806
2076
  return false;
@@ -1812,6 +2082,10 @@ export class PiboSessionRouter {
1812
2082
  return false;
1813
2083
  if (threadKeys && (!observation.threadKey || !threadKeys.has(observation.threadKey)))
1814
2084
  return false;
2085
+ if (observation.kind === "tool" && !includeTools)
2086
+ return false;
2087
+ if (defaultMessageView && !defaultEventTypeSet.has(observation.eventType))
2088
+ return false;
1815
2089
  if (eventTypes && !eventTypes.has(observation.eventType))
1816
2090
  return false;
1817
2091
  if (kinds && !kinds.has(observation.kind))
@@ -1837,7 +2111,10 @@ export class PiboSessionRouter {
1837
2111
  ordered.reverse();
1838
2112
  const selected = ordered.map((observation) => {
1839
2113
  const { managingParentId: _managingParentId, details, ...visible } = observation;
1840
- return input.includeDetails === true && details !== undefined ? { ...visible, details } : visible;
2114
+ const compact = visible.kind === "tool" && toolDetail === "summary"
2115
+ ? { ...visible, text: piboAgentObservationToolSummary(visible.text, visible.isError, details) }
2116
+ : visible;
2117
+ return input.includeDetails === true && details !== undefined ? { ...compact, details } : compact;
1841
2118
  });
1842
2119
  const evictedThrough = this.agentObservationEvictedThroughByParent.get(parentPiboSessionId) ?? 0;
1843
2120
  const retentionTruncated = afterSequence === undefined ? evictedThrough > 0 : afterSequence < evictedThrough;
@@ -1845,9 +2122,12 @@ export class PiboSessionRouter {
1845
2122
  return {
1846
2123
  filters: {
1847
2124
  ...input,
2125
+ ...(defaultMessageView ? { eventTypes: defaultEventTypes } : {}),
1848
2126
  ...(afterSequence !== undefined ? { afterSequence } : {}),
1849
2127
  order,
1850
2128
  limit,
2129
+ includeTools,
2130
+ toolDetail,
1851
2131
  includeDetails: input.includeDetails === true,
1852
2132
  },
1853
2133
  observations: selected,
@@ -2050,7 +2330,7 @@ export class PiboSessionRouter {
2050
2330
  }
2051
2331
  return depth;
2052
2332
  }
2053
- resolveSubagentSession(parentPiboSessionId, subagent, threadKey) {
2333
+ resolveSubagentSession(parentPiboSessionId, subagent, sessionName, threadKey) {
2054
2334
  const targetProfile = resolvePiboProfileNameFromRegistryOrDefault(this.pluginRegistry, subagent.targetProfile);
2055
2335
  const parent = this.resolvePiboSession(parentPiboSessionId);
2056
2336
  const resolvedThreadKey = resolveSubagentThreadKey(threadKey);
@@ -2087,8 +2367,8 @@ export class PiboSessionRouter {
2087
2367
  agentStatus: "active",
2088
2368
  ...(parentChatRoomId ? { chatRoomId: parentChatRoomId } : {}),
2089
2369
  }, "subagent");
2090
- if (JSON.stringify(updatedMetadata) !== JSON.stringify(existing.metadata ?? {})) {
2091
- return this.sessionStore.update(existing.id, { metadata: updatedMetadata }) ?? existing;
2370
+ if (existing.title !== sessionName || JSON.stringify(updatedMetadata) !== JSON.stringify(existing.metadata ?? {})) {
2371
+ return this.sessionStore.update(existing.id, { title: sessionName, metadata: updatedMetadata }) ?? existing;
2092
2372
  }
2093
2373
  return existing;
2094
2374
  }
@@ -2100,6 +2380,7 @@ export class PiboSessionRouter {
2100
2380
  parentId: parent.id,
2101
2381
  runtimeBinding: this.createRuntimeBindingInput(childProfile),
2102
2382
  workspace: parent.workspace,
2383
+ title: sessionName,
2103
2384
  metadata: newSessionMetadata,
2104
2385
  activeModel: subagent.model,
2105
2386
  });
@@ -2162,10 +2443,137 @@ export class PiboSessionRouter {
2162
2443
  for (const listener of this.listeners) {
2163
2444
  listener(event);
2164
2445
  }
2446
+ this.handleRunReminderOutput(event);
2165
2447
  if (event.type === "message_finished" && event.source !== "service") {
2166
2448
  this.scheduleRunReminder(event.piboSessionId, true);
2167
2449
  }
2168
2450
  };
2451
+ handleRunReminderOutput(event) {
2452
+ if (event.type === "message_finished") {
2453
+ const delivery = event.eventId ? this.runReminderDeliveries.get(event.eventId) : undefined;
2454
+ if (event.eventId)
2455
+ this.runReminderDeliveries.delete(event.eventId);
2456
+ if (delivery) {
2457
+ this.clearRunReminderRecovery(delivery);
2458
+ this.scheduleRunReminder(delivery.piboSessionId, false, delivery.generation);
2459
+ }
2460
+ return;
2461
+ }
2462
+ if (event.type === "session_error") {
2463
+ const eventId = event.eventId;
2464
+ if (!eventId)
2465
+ return;
2466
+ const delivery = this.runReminderDeliveries.get(eventId);
2467
+ if (!delivery)
2468
+ return;
2469
+ this.runReminderDeliveries.delete(eventId);
2470
+ if (event.errorDetails?.code === "loop_continuation_invalidated") {
2471
+ this.suppressRunReminderDelivery(delivery);
2472
+ return;
2473
+ }
2474
+ if (!isRunReminderContextPressureError(event)
2475
+ || this.closing
2476
+ || this.quiescingSessions.has(delivery.piboSessionId)
2477
+ || delivery.generation !== this.runReminderGeneration(delivery.piboSessionId)) {
2478
+ this.runRegistry.releaseNotification(delivery.piboSessionId, delivery.notification);
2479
+ return;
2480
+ }
2481
+ if (this.hasRunReminderRecovery(delivery)) {
2482
+ this.suppressRunReminderDelivery(delivery);
2483
+ return;
2484
+ }
2485
+ this.runRegistry.releaseNotification(delivery.piboSessionId, delivery.notification);
2486
+ if (!this.runRegistry.hasPendingNotification(delivery.piboSessionId))
2487
+ return;
2488
+ this.recordRunReminderRecovery(delivery);
2489
+ const alreadyDeferred = this.deferredRunReminders.get(delivery.piboSessionId) === delivery.generation;
2490
+ this.deferredRunReminders.set(delivery.piboSessionId, delivery.generation);
2491
+ this.sessions.get(delivery.piboSessionId)?.removeQueuedMessages(isRunReminderServiceMessage);
2492
+ if (!alreadyDeferred)
2493
+ this.queueRunReminderRecoveryCompaction(delivery.piboSessionId, delivery.generation);
2494
+ return;
2495
+ }
2496
+ if (event.type !== "compaction_end" || event.aborted || event.errorMessage)
2497
+ return;
2498
+ const generation = this.deferredRunReminders.get(event.piboSessionId);
2499
+ if (generation === undefined)
2500
+ return;
2501
+ this.deferredRunReminders.delete(event.piboSessionId);
2502
+ if (this.closing || this.quiescingSessions.has(event.piboSessionId))
2503
+ return;
2504
+ if (generation !== this.runReminderGeneration(event.piboSessionId))
2505
+ return;
2506
+ this.scheduleRunReminder(event.piboSessionId, true, generation);
2507
+ }
2508
+ suppressRunReminderDelivery(delivery) {
2509
+ const released = this.runRegistry.releaseNotification(delivery.piboSessionId, delivery.notification);
2510
+ for (const run of released)
2511
+ this.runRegistry.suppressNotification(delivery.piboSessionId, run.runId);
2512
+ this.clearRunReminderRecovery(delivery);
2513
+ this.deferredRunReminders.delete(delivery.piboSessionId);
2514
+ this.sessions.get(delivery.piboSessionId)?.removeQueuedMessages(isRunReminderServiceMessage);
2515
+ this.scheduleRunReminder(delivery.piboSessionId, false, delivery.generation);
2516
+ }
2517
+ hasRunReminderRecovery(delivery) {
2518
+ const recovery = this.runReminderRecoveries.get(delivery.piboSessionId);
2519
+ return recovery?.generation === delivery.generation
2520
+ && recovery.groups.has(runReminderRecoveryGroupKey(delivery.notification));
2521
+ }
2522
+ recordRunReminderRecovery(delivery) {
2523
+ let recovery = this.runReminderRecoveries.get(delivery.piboSessionId);
2524
+ if (!recovery || recovery.generation !== delivery.generation) {
2525
+ recovery = { generation: delivery.generation, groups: new Set() };
2526
+ this.runReminderRecoveries.set(delivery.piboSessionId, recovery);
2527
+ }
2528
+ recovery.groups.add(runReminderRecoveryGroupKey(delivery.notification));
2529
+ }
2530
+ clearRunReminderRecovery(delivery) {
2531
+ const recovery = this.runReminderRecoveries.get(delivery.piboSessionId);
2532
+ if (!recovery || recovery.generation !== delivery.generation)
2533
+ return;
2534
+ recovery.groups.delete(runReminderRecoveryGroupKey(delivery.notification));
2535
+ if (recovery.groups.size === 0)
2536
+ this.runReminderRecoveries.delete(delivery.piboSessionId);
2537
+ }
2538
+ queueRunReminderRecoveryCompaction(piboSessionId, generation) {
2539
+ const session = this.sessions.get(piboSessionId);
2540
+ if (!session)
2541
+ return;
2542
+ const eventId = randomUUID();
2543
+ void session.executeAction({
2544
+ type: "execution",
2545
+ piboSessionId,
2546
+ id: eventId,
2547
+ action: "compact",
2548
+ params: {
2549
+ customInstructions: "Preserve the current task and pending yielded-run lifecycle, and leave enough context for the deferred run notification.",
2550
+ },
2551
+ }).catch((error) => {
2552
+ if (this.closing || this.quiescingSessions.has(piboSessionId))
2553
+ return;
2554
+ if (generation !== this.runReminderGeneration(piboSessionId))
2555
+ return;
2556
+ const message = error instanceof Error ? error.message : String(error);
2557
+ this.emitOutput({
2558
+ type: "session_error",
2559
+ piboSessionId,
2560
+ eventId,
2561
+ error: message,
2562
+ errorDetails: runtimeSessionErrorDetails(message),
2563
+ });
2564
+ });
2565
+ }
2566
+ handleInterruptedRunReminders(messages) {
2567
+ for (const message of messages) {
2568
+ if (!isRunReminderServiceMessage(message) || !message.id)
2569
+ continue;
2570
+ const delivery = this.runReminderDeliveries.get(message.id);
2571
+ if (!delivery)
2572
+ continue;
2573
+ this.runReminderDeliveries.delete(message.id);
2574
+ this.runRegistry.releaseNotification(delivery.piboSessionId, delivery.notification);
2575
+ }
2576
+ }
2169
2577
  projectKnownSessionSignals() {
2170
2578
  for (const session of this.sessionStore.list?.() ?? []) {
2171
2579
  this.signalRegistry.project({ type: "session_created", session });
@@ -2185,6 +2593,12 @@ export class PiboSessionRouter {
2185
2593
  for (const piboSessionId of piboSessionIds) {
2186
2594
  this.runReminderGenerations.set(piboSessionId, this.runReminderGeneration(piboSessionId) + 1);
2187
2595
  this.scheduledRunReminders.delete(piboSessionId);
2596
+ this.deferredRunReminders.delete(piboSessionId);
2597
+ this.runReminderRecoveries.delete(piboSessionId);
2598
+ for (const [eventId, delivery] of this.runReminderDeliveries) {
2599
+ if (delivery.piboSessionId === piboSessionId)
2600
+ this.runReminderDeliveries.delete(eventId);
2601
+ }
2188
2602
  try {
2189
2603
  this.sessions.get(piboSessionId)?.removeQueuedMessages(isRunReminderServiceMessage);
2190
2604
  }
@@ -2213,6 +2627,11 @@ export class PiboSessionRouter {
2213
2627
  return;
2214
2628
  if (expectedGeneration !== this.runReminderGeneration(piboSessionId))
2215
2629
  return;
2630
+ const deferredGeneration = this.deferredRunReminders.get(piboSessionId);
2631
+ if (deferredGeneration === expectedGeneration)
2632
+ return;
2633
+ if (deferredGeneration !== undefined)
2634
+ this.deferredRunReminders.delete(piboSessionId);
2216
2635
  if (!this.runRegistry.hasPendingNotification(piboSessionId, { includeAlreadyNotified }))
2217
2636
  return;
2218
2637
  const previous = this.scheduledRunReminders.get(piboSessionId);
@@ -2240,23 +2659,31 @@ export class PiboSessionRouter {
2240
2659
  this.scheduledRunReminders.delete(piboSessionId);
2241
2660
  if (this.closing || this.quiescingSessions.has(piboSessionId) || expectedGeneration !== this.runReminderGeneration(piboSessionId))
2242
2661
  return;
2243
- const notification = this.runRegistry.createNotification(piboSessionId, { includeAlreadyNotified: scheduled.includeAlreadyNotified });
2244
- if (!notification)
2245
- return;
2662
+ let notification;
2663
+ let eventId;
2246
2664
  try {
2247
2665
  const session = await this.getOrCreateSession(piboSessionId);
2248
2666
  if (this.closing || this.quiescingSessions.has(piboSessionId) || expectedGeneration !== this.runReminderGeneration(piboSessionId))
2249
2667
  return;
2668
+ notification = this.runRegistry.createNotification(piboSessionId, { includeAlreadyNotified: scheduled.includeAlreadyNotified });
2669
+ if (!notification)
2670
+ return;
2671
+ eventId = randomUUID();
2672
+ this.runReminderDeliveries.set(eventId, { piboSessionId, generation: expectedGeneration, notification });
2250
2673
  session.enqueueMessage({
2251
2674
  type: "message",
2252
2675
  piboSessionId,
2253
2676
  text: formatRunReminderMessage(notification),
2254
2677
  source: "service",
2255
- id: randomUUID(),
2678
+ id: eventId,
2256
2679
  provenance: runReminderProvenance(notification),
2257
2680
  });
2258
2681
  }
2259
2682
  catch (error) {
2683
+ if (eventId)
2684
+ this.runReminderDeliveries.delete(eventId);
2685
+ if (notification)
2686
+ this.runRegistry.releaseNotification(piboSessionId, notification);
2260
2687
  if (this.closing || this.quiescingSessions.has(piboSessionId) || expectedGeneration !== this.runReminderGeneration(piboSessionId))
2261
2688
  return;
2262
2689
  const message = error instanceof Error ? error.message : String(error);