@pasko70/pibo 2.5.1 → 3.0.0

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 (104) 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 +307 -31
  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/models.js +48 -1
  10. package/dist/agent-runtimes/codex-native/process.js +15 -1
  11. package/dist/agent-runtimes/codex-native/thread.js +75 -31
  12. package/dist/agent-runtimes/codex-native/turn.js +30 -0
  13. package/dist/agent-runtimes/history-proof.js +8 -0
  14. package/dist/agent-runtimes/omp/adapter.js +61 -14
  15. package/dist/agent-runtimes/omp/history.js +189 -34
  16. package/dist/agent-runtimes/omp/thread.js +82 -36
  17. package/dist/agent-runtimes/pi/adapter.js +30 -3
  18. package/dist/agent-runtimes/pi/history.js +66 -5
  19. package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
  20. package/dist/agent-runtimes/pi/routed-session.js +344 -37
  21. package/dist/apps/chat/agent-store.js +3 -0
  22. package/dist/apps/chat/chat-api-routes.js +1 -1
  23. package/dist/apps/chat/chat-files.js +130 -9
  24. package/dist/apps/chat/chat-request-normalizers.js +6 -0
  25. package/dist/apps/chat/chat-settings-routes.js +27 -2
  26. package/dist/apps/chat/chat-speech.js +100 -0
  27. package/dist/apps/chat/data/chat-data-mappers.js +25 -5
  28. package/dist/apps/chat/data/timeline-query-service.js +89 -1
  29. package/dist/apps/chat/trace-v2.js +210 -8
  30. package/dist/apps/chat/trace.js +3 -0
  31. package/dist/apps/chat/web-app.js +108 -3
  32. package/dist/apps/chat-ui/assets/{dist-GS8xUdfg.js → dist-BBDMeU5-.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-BpVSf6ZV.js → dist-BDIATCQt.js} +1 -1
  34. package/dist/apps/chat-ui/assets/{dist-CRv7fzi3.js → dist-JnW4v8UE.js} +1 -1
  35. package/dist/apps/chat-ui/assets/{dist-CbBXbGVi.js → dist-lRiLP9rr.js} +1 -1
  36. package/dist/apps/chat-ui/assets/{dist-HC0xQ92C.js → dist-mqCviI-c.js} +1 -1
  37. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
  38. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
  39. package/dist/apps/chat-ui/index.html +2 -2
  40. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
  41. package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
  42. package/dist/apps/chat-vscode-web/index.html +2 -2
  43. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  44. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.0.vsix +0 -0
  45. package/dist/cli.js +49 -5
  46. package/dist/config/config.js +59 -8
  47. package/dist/core/preview-server-settings.js +20 -0
  48. package/dist/core/profiles.js +1 -0
  49. package/dist/core/session-router.js +476 -44
  50. package/dist/core/user-settings.js +10 -0
  51. package/dist/data/payload-store.js +57 -1
  52. package/dist/data/schema.js +18 -1
  53. package/dist/debug/trace.js +5 -0
  54. package/dist/gateway/client.js +368 -65
  55. package/dist/gateway/protocol.js +2 -0
  56. package/dist/gateway/server.js +8 -0
  57. package/dist/gateway/web.js +2 -0
  58. package/dist/index.js +1 -1
  59. package/dist/loops/store.js +6 -4
  60. package/dist/mcp/config.js +89 -17
  61. package/dist/mcp/daemon-client.js +473 -153
  62. package/dist/mcp/daemon.js +401 -37
  63. package/dist/plugins/builtin.js +2 -2
  64. package/dist/plugins/codex-native.js +2 -0
  65. package/dist/plugins/registry.js +260 -1
  66. package/dist/previews/cli.js +345 -0
  67. package/dist/previews/config.js +61 -0
  68. package/dist/previews/manager.js +644 -0
  69. package/dist/previews/network.js +198 -0
  70. package/dist/previews/plugin.js +11 -0
  71. package/dist/previews/process-supervisor.js +38 -0
  72. package/dist/previews/proxy.js +387 -0
  73. package/dist/previews/store.js +750 -0
  74. package/dist/previews/types.js +1 -0
  75. package/dist/previews/web-app.js +446 -0
  76. package/dist/runs/registry.js +22 -0
  77. package/dist/runs/tools.js +46 -7
  78. package/dist/session-ui/terminalRows.js +187 -13
  79. package/dist/sessions/pibo-data-store.js +37 -0
  80. package/dist/sessions/runtime-binding-persistence.js +34 -0
  81. package/dist/sessions/runtime-binding.js +6 -0
  82. package/dist/sessions/sqlite-store.js +37 -0
  83. package/dist/shared/trace-engine.js +12 -64
  84. package/dist/shared/trace-event-projection.js +16 -0
  85. package/dist/shared/trace-history.js +684 -184
  86. package/dist/shared/trace-limits.js +7 -0
  87. package/dist/shared/trace-live-patch.js +61 -0
  88. package/dist/shared/trace-tool-identity.js +43 -0
  89. package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
  90. package/dist/speech/openai-codex.js +407 -0
  91. package/dist/speech/types.js +8 -0
  92. package/dist/subagents/context.js +5 -3
  93. package/dist/subagents/observations.js +94 -6
  94. package/dist/subagents/tool.js +103 -20
  95. package/dist/tools/codex-image-artifacts.js +24 -0
  96. package/dist/tools/codex-image-generation.js +4 -22
  97. package/dist/web/channel.js +88 -4
  98. package/dist/web/http.js +2 -1
  99. package/npm-shrinkwrap.json +2 -2
  100. package/package.json +1 -1
  101. package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
  102. package/dist/apps/chat-ui/assets/index-DT5jCQBP.js +0 -228
  103. package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
  104. package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.1.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";
@@ -52,6 +55,12 @@ export function resolvePiboSessionInitialFastMode(session) {
52
55
  const value = session.metadata?.initialFastMode;
53
56
  return typeof value === "boolean" ? value : undefined;
54
57
  }
58
+ export function resolvePiboSessionInitialRuntimeOptions(session) {
59
+ const value = session.metadata?.initialRuntimeOptions;
60
+ return value && typeof value === "object" && !Array.isArray(value)
61
+ ? structuredClone(value)
62
+ : undefined;
63
+ }
55
64
  function hasReachedSubagentMaxDepth(subagent, depth) {
56
65
  return depth >= (subagent.maxDepth ?? DEFAULT_SUBAGENT_MAX_DEPTH);
57
66
  }
@@ -72,12 +81,14 @@ function subagentAbortError() {
72
81
  function subagentRequestEventKey(piboSessionId, eventId) {
73
82
  return `${piboSessionId}\u0000${eventId}`;
74
83
  }
75
- function profileForSession(baseProfile, runtimeInstanceId, nativeSessionId, parentNativeSessionId, subagentDepth) {
84
+ function profileForSession(baseProfile, runtimeInstanceId, nativeSessionId, parentNativeSessionId, subagentDepth, runtimeOptionsOverride) {
76
85
  const usesProfileRuntime = baseProfile.runtimeInstanceId === runtimeInstanceId;
77
86
  const options = {
78
87
  profileName: baseProfile.profileName,
79
88
  runtimeInstanceId,
80
- runtimeOptions: usesProfileRuntime ? baseProfile.runtimeOptions : {},
89
+ runtimeOptions: usesProfileRuntime
90
+ ? { ...baseProfile.runtimeOptions, ...runtimeOptionsOverride }
91
+ : {},
81
92
  sessionId: nativeSessionId,
82
93
  parentSessionId: parentNativeSessionId,
83
94
  model: usesProfileRuntime ? baseProfile.model : undefined,
@@ -154,6 +165,23 @@ function formatRunReminderMessage(notification) {
154
165
  function isRunReminderServiceMessage(event) {
155
166
  return event.source === "service" && event.text.startsWith("<pibo_run_notification>");
156
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
+ }
157
185
  function yieldedRunOrigin(event) {
158
186
  if (!event?.id || event.provenance?.kind !== "loop-run")
159
187
  return undefined;
@@ -182,6 +210,58 @@ function isTerminalRunStatus(status) {
182
210
  function asJsonObject(value) {
183
211
  return value ?? {};
184
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
+ }
185
265
  function shouldResetSessionAfterAction(action, output) {
186
266
  if (action === "login.apikey" || action === "logout")
187
267
  return true;
@@ -265,10 +345,14 @@ export class PiboSessionRouter {
265
345
  agentObservationEvictedThroughByParent = new Map();
266
346
  nextAgentObservationSequence = 1;
267
347
  scheduledRunReminders = new Map();
348
+ runReminderDeliveries = new Map();
349
+ deferredRunReminders = new Map();
350
+ runReminderRecoveries = new Map();
268
351
  runReminderGenerations = new Map();
269
352
  runCancellationHandlers = new Map();
270
353
  activeRunExecutions = new Set();
271
354
  quiescingSessions = new Set();
355
+ unresolvedDerivedSessionTransitions = new Map();
272
356
  disposingSessions = new Map();
273
357
  idleSessionTimers = new Map();
274
358
  routedSessionIdleTimeoutMs;
@@ -348,6 +432,13 @@ export class PiboSessionRouter {
348
432
  async emit(event) {
349
433
  if (this.closing)
350
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
+ };
351
442
  const teardownAction = event.type === "execution" && (event.action === "dispose" || event.action === "kill" || event.action === "kill_all");
352
443
  const teardownIds = teardownAction
353
444
  ? [event.piboSessionId, ...this.descendantSessionIds(event.piboSessionId)]
@@ -362,7 +453,13 @@ export class PiboSessionRouter {
362
453
  const stored = this.sessionStore.get(event.piboSessionId);
363
454
  if (stored)
364
455
  this.signalRegistry.project({ type: "session_created", session: stored });
365
- 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
+ }
366
463
  }
367
464
  let session;
368
465
  try {
@@ -391,9 +488,13 @@ export class PiboSessionRouter {
391
488
  throw new Error(`Pibo session "${event.piboSessionId}" is quiescing.`);
392
489
  }
393
490
  if (event.type === "message") {
394
- return event.delivery === "steer"
395
- ? await session.steerMessage(event)
396
- : 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;
397
498
  }
398
499
  if (event.action === "abort") {
399
500
  this.signalRegistry.project({ type: "session_interrupted", piboSessionId: event.piboSessionId, reason: "abort action" });
@@ -700,7 +801,7 @@ export class PiboSessionRouter {
700
801
  && parentBinding.adapterId === binding.adapterId
701
802
  ? parentBinding.nativeSessionId
702
803
  : undefined;
703
- return profileForSession(createPiboProfileFromRegistryOrDefault(this.pluginRegistry, session.profile), binding.runtimeInstanceId, binding.nativeSessionId, parentNativeSessionId, this.getSubagentDepth(session.id));
804
+ return profileForSession(createPiboProfileFromRegistryOrDefault(this.pluginRegistry, session.profile), binding.runtimeInstanceId, binding.nativeSessionId, parentNativeSessionId, this.getSubagentDepth(session.id), resolvePiboSessionInitialRuntimeOptions(session));
704
805
  }
705
806
  async rebindSessionRuntime(piboSessionId, input) {
706
807
  if (this.quiescingSessions.has(piboSessionId)) {
@@ -748,7 +849,7 @@ export class PiboSessionRouter {
748
849
  }
749
850
  const workspace = session.workspace ?? this.options.cwd ?? getDefaultPiboWorkspace();
750
851
  const baseProfile = createPiboProfileFromRegistryOrDefault(this.pluginRegistry, session.profile);
751
- const targetProfile = profileForSession(baseProfile, input.runtimeInstanceId, startsNewNativeSession ? undefined : input.nativeSessionId, undefined, this.getSubagentDepth(session.id));
852
+ const targetProfile = profileForSession(baseProfile, input.runtimeInstanceId, startsNewNativeSession ? undefined : input.nativeSessionId, undefined, this.getSubagentDepth(session.id), resolvePiboSessionInitialRuntimeOptions(session));
752
853
  const targetDiagnostics = await adapter.diagnose();
753
854
  const unavailableTarget = targetDiagnostics.find((diagnostic) => diagnostic.severity === "error");
754
855
  if (unavailableTarget) {
@@ -832,6 +933,15 @@ export class PiboSessionRouter {
832
933
  this.scheduleIdleSessionEvictionIfIdle(piboSessionId);
833
934
  }
834
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
+ }
835
945
  async setLiveSessionActiveModel(piboSessionId, model) {
836
946
  const session = this.sessions.get(piboSessionId);
837
947
  if (!session)
@@ -1216,6 +1326,17 @@ export class PiboSessionRouter {
1216
1326
  this.portableToolSessions.delete(piboSession.id);
1217
1327
  throw error;
1218
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
+ });
1219
1340
  let runtimeSession;
1220
1341
  try {
1221
1342
  runtimeSession = await runtimeRegistry.openAgentRuntimeSession(binding.runtimeInstanceId, {
@@ -1241,6 +1362,7 @@ export class PiboSessionRouter {
1241
1362
  codeRuntimeToolController,
1242
1363
  portableTools,
1243
1364
  resources,
1365
+ ...(runtimeBindingPersistence ? { runtimeBindingPersistence } : {}),
1244
1366
  compatibility: {
1245
1367
  persistSession: this.options.persistSession,
1246
1368
  thinkingLevel: initialThinkingLevel ?? this.options.thinkingLevel,
@@ -1301,6 +1423,7 @@ export class PiboSessionRouter {
1301
1423
  binding = this.persistSessionRuntimeBinding(piboSession, openedBinding, {
1302
1424
  expectedRevision: binding.revision,
1303
1425
  });
1426
+ bindingSync.expectedRevision = binding.revision;
1304
1427
  }
1305
1428
  }
1306
1429
  catch (error) {
@@ -1323,6 +1446,7 @@ export class PiboSessionRouter {
1323
1446
  })
1324
1447
  : undefined,
1325
1448
  onSessionOperation: (result, event) => this.handleSessionOperation(result, event),
1449
+ onBeforeSessionIdentityOperation: (event) => this.retryUnresolvedDerivedSessionCompensation(event.piboSessionId),
1326
1450
  onKillChildren: (id, opts) => this.killChildSessions(id, opts),
1327
1451
  onStateChange: (state) => {
1328
1452
  this.signalRegistry.project({
@@ -1331,18 +1455,22 @@ export class PiboSessionRouter {
1331
1455
  processing: state.processing,
1332
1456
  queuedMessages: state.queuedMessages,
1333
1457
  });
1334
- if (!state.processing && state.queuedMessages === 0 && !state.disposed) {
1335
- 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);
1336
1461
  }
1337
- if (state.disposed || state.processing || state.queuedMessages > 0)
1462
+ if (state.disposed || state.processing || state.queuedMessages > 0 || state.sessionIdentityOperationInFlight)
1338
1463
  this.clearIdleSessionTimer(piboSession.id);
1339
1464
  else
1340
1465
  this.scheduleIdleSessionEvictionIfIdle(piboSession.id);
1341
1466
  },
1342
- onMessagesInterrupted: (messages, reason) => this.telemetryRecorder?.recordMessagesInterrupted(messages, {
1343
- session: this.sessionStore.get(piboSession.id),
1344
- status: this.sessions.get(piboSession.id)?.getStatus(),
1345
- }, 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
+ },
1346
1474
  messagePreflight: this.options.messagePreflight,
1347
1475
  getRuntimeAuthStatus: () => runtimeRegistry.getAgentRuntimeAuthStatus(binding.runtimeInstanceId),
1348
1476
  startRuntimeAuth: async (input) => {
@@ -1422,22 +1550,33 @@ export class PiboSessionRouter {
1422
1550
  updatedAt: now,
1423
1551
  };
1424
1552
  }
1425
- syncLiveSessionRuntimeBinding(piboSessionId, runtimeSession) {
1553
+ syncLiveSessionRuntimeBinding(piboSessionId, runtimeSession, state) {
1426
1554
  const session = this.sessionStore.get(piboSessionId);
1427
1555
  if (!session)
1428
1556
  return;
1429
1557
  try {
1430
1558
  const persisted = this.resolveSessionRuntimeBinding(session);
1431
- 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;
1432
1561
  }
1433
1562
  catch (error) {
1434
1563
  const message = error instanceof Error ? error.message : String(error);
1564
+ const reset = this.resetCachedSession(piboSessionId, "runtime binding synchronization failed");
1435
1565
  this.emitOutput({
1436
1566
  type: "session_error",
1437
1567
  piboSessionId,
1438
1568
  error: `Failed to persist the live runtime binding: ${message}`,
1439
1569
  errorDetails: runtimeSessionErrorDetails(message),
1440
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
+ });
1441
1580
  }
1442
1581
  }
1443
1582
  assertOpenableRuntimeBinding(binding) {
@@ -1486,21 +1625,44 @@ export class PiboSessionRouter {
1486
1625
  const source = this.resolvePiboSession(event.piboSessionId);
1487
1626
  const previousBinding = this.resolveSessionRuntimeBinding(source);
1488
1627
  const liveBinding = this.sessions.get(event.piboSessionId)?.getRuntimeBinding();
1628
+ const currentNativeSessionId = result.current.piSessionId || undefined;
1489
1629
  const currentBinding = {
1490
1630
  ...previousBinding,
1491
1631
  ...liveBinding,
1492
1632
  piboSessionId: source.id,
1493
- nativeSessionId: result.current.piSessionId,
1494
- state: "bound",
1495
- locator: result.current.sessionFile
1633
+ nativeSessionId: currentNativeSessionId,
1634
+ state: currentNativeSessionId ? "bound" : "unbound",
1635
+ locator: currentNativeSessionId && result.current.sessionFile
1496
1636
  ? { kind: "local-file", value: result.current.sessionFile }
1497
- : liveBinding?.locator ?? previousBinding.locator,
1637
+ : currentNativeSessionId
1638
+ ? liveBinding?.locator ?? previousBinding.locator
1639
+ : undefined,
1640
+ metadata: currentNativeSessionId ? liveBinding?.metadata ?? previousBinding.metadata : undefined,
1498
1641
  };
1499
1642
  if (event.action === "session.fork" || event.action === "session.clone") {
1500
1643
  const action = event.action;
1501
- const created = this.createDerivedSession(result, action, currentBinding);
1502
- result.piboSessionId = created.id;
1503
- 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;
1504
1666
  return;
1505
1667
  }
1506
1668
  this.persistSessionRuntimeBinding(source, currentBinding, {
@@ -1511,17 +1673,18 @@ export class PiboSessionRouter {
1511
1673
  }
1512
1674
  createDerivedSession(result, action, currentBinding) {
1513
1675
  const source = this.resolvePiboSession(result.piboSessionId);
1514
- return this.sessionStore.create({
1676
+ const input = {
1677
+ id: createPiboSessionId(),
1515
1678
  channel: source.channel,
1516
1679
  kind: "branch",
1517
1680
  profile: source.profile,
1518
- parentId: source.kind === "subagent" ? source.parentId : undefined,
1681
+ parentId: source.parentId,
1519
1682
  originId: source.id,
1520
1683
  runtimeBinding: {
1521
1684
  runtimeInstanceId: currentBinding.runtimeInstanceId,
1522
1685
  adapterId: currentBinding.adapterId,
1523
- nativeSessionId: currentBinding.nativeSessionId ?? result.current.piSessionId,
1524
- state: "bound",
1686
+ nativeSessionId: currentBinding.nativeSessionId,
1687
+ state: currentBinding.state,
1525
1688
  protocol: currentBinding.protocol,
1526
1689
  protocolVersion: currentBinding.protocolVersion,
1527
1690
  adapterVersion: currentBinding.adapterVersion,
@@ -1532,12 +1695,116 @@ export class PiboSessionRouter {
1532
1695
  title: source.title,
1533
1696
  activeModel: source.activeModel,
1534
1697
  metadata: {
1535
- ...asJsonObject(source.metadata),
1698
+ ...derivedSessionMetadata(source.metadata),
1536
1699
  originAction: action,
1537
1700
  originRuntimeNativeSessionId: result.previous.piSessionId,
1538
1701
  ...(currentBinding.adapterId === "pi" ? { originPiSessionId: result.previous.piSessionId } : {}),
1539
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
+ },
1540
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;
1541
1808
  }
1542
1809
  runtimeAuthAffectedInstanceIds(runtimeInstanceId) {
1543
1810
  const registry = this.resolveAgentRuntimeRegistry(runtimeInstanceId);
@@ -1653,13 +1920,14 @@ export class PiboSessionRouter {
1653
1920
  }
1654
1921
  createAgentsController(parentPiboSessionId) {
1655
1922
  return {
1656
- sendMessage: async ({ subagent, message, threadKey, toolCallId, requestId, parentProvenance, signal }) => {
1923
+ sendMessage: async ({ subagent, sessionName, message, threadKey, toolCallId, requestId, parentProvenance, signal }) => {
1657
1924
  if (signal?.aborted)
1658
1925
  throw subagentAbortError();
1659
1926
  if (typeof requestId !== "string" || !requestId.trim())
1660
1927
  throw new Error("Delegated agent requestId is required.");
1661
1928
  this.assertSubagentDepth(parentPiboSessionId, subagent);
1662
- const child = this.resolveSubagentSession(parentPiboSessionId, subagent, threadKey);
1929
+ const normalizedSessionName = normalizePiboAgentSessionName(sessionName);
1930
+ const child = this.resolveSubagentSession(parentPiboSessionId, subagent, normalizedSessionName, threadKey);
1663
1931
  const resolvedThreadKey = typeof child.metadata?.threadKey === "string" ? child.metadata.threadKey : "";
1664
1932
  const loopJobId = parentProvenance?.kind === "loop-run"
1665
1933
  ? parentProvenance.jobId
@@ -1754,6 +2022,7 @@ export class PiboSessionRouter {
1754
2022
  agentId: session.id,
1755
2023
  name: typeof session.metadata?.subagentName === "string" ? session.metadata.subagentName : session.profile,
1756
2024
  profile: session.profile,
2025
+ ...(session.title ? { sessionName: session.title } : {}),
1757
2026
  ...(typeof session.metadata?.threadKey === "string" ? { threadKey: session.metadata.threadKey } : {}),
1758
2027
  status: killed ? "killed" : running ? "running" : "idle",
1759
2028
  createdAt: session.createdAt,
@@ -1775,6 +2044,7 @@ export class PiboSessionRouter {
1775
2044
  observeManagedAgents(parentPiboSessionId, input) {
1776
2045
  const order = normalizePiboAgentObservationOrder(input.order);
1777
2046
  const limit = normalizePiboAgentObservationLimit(input.limit);
2047
+ const toolDetail = normalizePiboAgentObservationToolDetail(input.toolDetail);
1778
2048
  const afterSequence = normalizePiboAgentObservationCursor(input.afterSequence);
1779
2049
  const since = parsePiboAgentObservationTimestamp(input.since, "since");
1780
2050
  const until = parsePiboAgentObservationTimestamp(input.until, "until");
@@ -1793,6 +2063,14 @@ export class PiboSessionRouter {
1793
2063
  const kinds = input.kinds ? new Set(input.kinds) : undefined;
1794
2064
  const roles = input.roles ? new Set(input.roles) : undefined;
1795
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);
1796
2074
  const matches = this.agentObservations.filter((observation) => {
1797
2075
  if (observation.managingParentId !== parentPiboSessionId)
1798
2076
  return false;
@@ -1804,6 +2082,10 @@ export class PiboSessionRouter {
1804
2082
  return false;
1805
2083
  if (threadKeys && (!observation.threadKey || !threadKeys.has(observation.threadKey)))
1806
2084
  return false;
2085
+ if (observation.kind === "tool" && !includeTools)
2086
+ return false;
2087
+ if (defaultMessageView && !defaultEventTypeSet.has(observation.eventType))
2088
+ return false;
1807
2089
  if (eventTypes && !eventTypes.has(observation.eventType))
1808
2090
  return false;
1809
2091
  if (kinds && !kinds.has(observation.kind))
@@ -1829,7 +2111,10 @@ export class PiboSessionRouter {
1829
2111
  ordered.reverse();
1830
2112
  const selected = ordered.map((observation) => {
1831
2113
  const { managingParentId: _managingParentId, details, ...visible } = observation;
1832
- 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;
1833
2118
  });
1834
2119
  const evictedThrough = this.agentObservationEvictedThroughByParent.get(parentPiboSessionId) ?? 0;
1835
2120
  const retentionTruncated = afterSequence === undefined ? evictedThrough > 0 : afterSequence < evictedThrough;
@@ -1837,9 +2122,12 @@ export class PiboSessionRouter {
1837
2122
  return {
1838
2123
  filters: {
1839
2124
  ...input,
2125
+ ...(defaultMessageView ? { eventTypes: defaultEventTypes } : {}),
1840
2126
  ...(afterSequence !== undefined ? { afterSequence } : {}),
1841
2127
  order,
1842
2128
  limit,
2129
+ includeTools,
2130
+ toolDetail,
1843
2131
  includeDetails: input.includeDetails === true,
1844
2132
  },
1845
2133
  observations: selected,
@@ -2042,7 +2330,7 @@ export class PiboSessionRouter {
2042
2330
  }
2043
2331
  return depth;
2044
2332
  }
2045
- resolveSubagentSession(parentPiboSessionId, subagent, threadKey) {
2333
+ resolveSubagentSession(parentPiboSessionId, subagent, sessionName, threadKey) {
2046
2334
  const targetProfile = resolvePiboProfileNameFromRegistryOrDefault(this.pluginRegistry, subagent.targetProfile);
2047
2335
  const parent = this.resolvePiboSession(parentPiboSessionId);
2048
2336
  const resolvedThreadKey = resolveSubagentThreadKey(threadKey);
@@ -2058,9 +2346,13 @@ export class PiboSessionRouter {
2058
2346
  const parentChatRoomId = typeof parent.metadata?.chatRoomId === "string" ? parent.metadata.chatRoomId : undefined;
2059
2347
  if (parentChatRoomId)
2060
2348
  metadata.chatRoomId = parentChatRoomId;
2061
- const newSessionMetadata = subagent.thinkingLevel
2062
- ? { ...metadata, initialThinkingLevel: subagent.thinkingLevel }
2063
- : metadata;
2349
+ const newSessionMetadata = {
2350
+ ...metadata,
2351
+ ...(subagent.thinkingLevel ? { initialThinkingLevel: subagent.thinkingLevel } : {}),
2352
+ ...(subagent.runtimeOptions && Object.keys(subagent.runtimeOptions).length > 0
2353
+ ? { initialRuntimeOptions: structuredClone(subagent.runtimeOptions) }
2354
+ : {}),
2355
+ };
2064
2356
  const existing = this.sessionStore.find({
2065
2357
  channel: "pibo.subagents",
2066
2358
  kind: "subagent",
@@ -2075,8 +2367,8 @@ export class PiboSessionRouter {
2075
2367
  agentStatus: "active",
2076
2368
  ...(parentChatRoomId ? { chatRoomId: parentChatRoomId } : {}),
2077
2369
  }, "subagent");
2078
- if (JSON.stringify(updatedMetadata) !== JSON.stringify(existing.metadata ?? {})) {
2079
- 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;
2080
2372
  }
2081
2373
  return existing;
2082
2374
  }
@@ -2088,6 +2380,7 @@ export class PiboSessionRouter {
2088
2380
  parentId: parent.id,
2089
2381
  runtimeBinding: this.createRuntimeBindingInput(childProfile),
2090
2382
  workspace: parent.workspace,
2383
+ title: sessionName,
2091
2384
  metadata: newSessionMetadata,
2092
2385
  activeModel: subagent.model,
2093
2386
  });
@@ -2150,10 +2443,130 @@ export class PiboSessionRouter {
2150
2443
  for (const listener of this.listeners) {
2151
2444
  listener(event);
2152
2445
  }
2446
+ this.handleRunReminderOutput(event);
2153
2447
  if (event.type === "message_finished" && event.source !== "service") {
2154
2448
  this.scheduleRunReminder(event.piboSessionId, true);
2155
2449
  }
2156
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 (!isRunReminderContextPressureError(event)
2471
+ || this.closing
2472
+ || this.quiescingSessions.has(delivery.piboSessionId)
2473
+ || delivery.generation !== this.runReminderGeneration(delivery.piboSessionId)) {
2474
+ this.runRegistry.releaseNotification(delivery.piboSessionId, delivery.notification);
2475
+ return;
2476
+ }
2477
+ if (this.hasRunReminderRecovery(delivery)) {
2478
+ const released = this.runRegistry.releaseNotification(delivery.piboSessionId, delivery.notification);
2479
+ for (const run of released)
2480
+ this.runRegistry.suppressNotification(delivery.piboSessionId, run.runId);
2481
+ this.clearRunReminderRecovery(delivery);
2482
+ this.deferredRunReminders.delete(delivery.piboSessionId);
2483
+ this.sessions.get(delivery.piboSessionId)?.removeQueuedMessages(isRunReminderServiceMessage);
2484
+ this.scheduleRunReminder(delivery.piboSessionId, false, delivery.generation);
2485
+ return;
2486
+ }
2487
+ this.runRegistry.releaseNotification(delivery.piboSessionId, delivery.notification);
2488
+ if (!this.runRegistry.hasPendingNotification(delivery.piboSessionId))
2489
+ return;
2490
+ this.recordRunReminderRecovery(delivery);
2491
+ const alreadyDeferred = this.deferredRunReminders.get(delivery.piboSessionId) === delivery.generation;
2492
+ this.deferredRunReminders.set(delivery.piboSessionId, delivery.generation);
2493
+ this.sessions.get(delivery.piboSessionId)?.removeQueuedMessages(isRunReminderServiceMessage);
2494
+ if (!alreadyDeferred)
2495
+ this.queueRunReminderRecoveryCompaction(delivery.piboSessionId, delivery.generation);
2496
+ return;
2497
+ }
2498
+ if (event.type !== "compaction_end" || event.aborted || event.errorMessage)
2499
+ return;
2500
+ const generation = this.deferredRunReminders.get(event.piboSessionId);
2501
+ if (generation === undefined)
2502
+ return;
2503
+ this.deferredRunReminders.delete(event.piboSessionId);
2504
+ if (this.closing || this.quiescingSessions.has(event.piboSessionId))
2505
+ return;
2506
+ if (generation !== this.runReminderGeneration(event.piboSessionId))
2507
+ return;
2508
+ this.scheduleRunReminder(event.piboSessionId, true, generation);
2509
+ }
2510
+ hasRunReminderRecovery(delivery) {
2511
+ const recovery = this.runReminderRecoveries.get(delivery.piboSessionId);
2512
+ return recovery?.generation === delivery.generation
2513
+ && recovery.groups.has(runReminderRecoveryGroupKey(delivery.notification));
2514
+ }
2515
+ recordRunReminderRecovery(delivery) {
2516
+ let recovery = this.runReminderRecoveries.get(delivery.piboSessionId);
2517
+ if (!recovery || recovery.generation !== delivery.generation) {
2518
+ recovery = { generation: delivery.generation, groups: new Set() };
2519
+ this.runReminderRecoveries.set(delivery.piboSessionId, recovery);
2520
+ }
2521
+ recovery.groups.add(runReminderRecoveryGroupKey(delivery.notification));
2522
+ }
2523
+ clearRunReminderRecovery(delivery) {
2524
+ const recovery = this.runReminderRecoveries.get(delivery.piboSessionId);
2525
+ if (!recovery || recovery.generation !== delivery.generation)
2526
+ return;
2527
+ recovery.groups.delete(runReminderRecoveryGroupKey(delivery.notification));
2528
+ if (recovery.groups.size === 0)
2529
+ this.runReminderRecoveries.delete(delivery.piboSessionId);
2530
+ }
2531
+ queueRunReminderRecoveryCompaction(piboSessionId, generation) {
2532
+ const session = this.sessions.get(piboSessionId);
2533
+ if (!session)
2534
+ return;
2535
+ const eventId = randomUUID();
2536
+ void session.executeAction({
2537
+ type: "execution",
2538
+ piboSessionId,
2539
+ id: eventId,
2540
+ action: "compact",
2541
+ params: {
2542
+ customInstructions: "Preserve the current task and pending yielded-run lifecycle, and leave enough context for the deferred run notification.",
2543
+ },
2544
+ }).catch((error) => {
2545
+ if (this.closing || this.quiescingSessions.has(piboSessionId))
2546
+ return;
2547
+ if (generation !== this.runReminderGeneration(piboSessionId))
2548
+ return;
2549
+ const message = error instanceof Error ? error.message : String(error);
2550
+ this.emitOutput({
2551
+ type: "session_error",
2552
+ piboSessionId,
2553
+ eventId,
2554
+ error: message,
2555
+ errorDetails: runtimeSessionErrorDetails(message),
2556
+ });
2557
+ });
2558
+ }
2559
+ handleInterruptedRunReminders(messages) {
2560
+ for (const message of messages) {
2561
+ if (!isRunReminderServiceMessage(message) || !message.id)
2562
+ continue;
2563
+ const delivery = this.runReminderDeliveries.get(message.id);
2564
+ if (!delivery)
2565
+ continue;
2566
+ this.runReminderDeliveries.delete(message.id);
2567
+ this.runRegistry.releaseNotification(delivery.piboSessionId, delivery.notification);
2568
+ }
2569
+ }
2157
2570
  projectKnownSessionSignals() {
2158
2571
  for (const session of this.sessionStore.list?.() ?? []) {
2159
2572
  this.signalRegistry.project({ type: "session_created", session });
@@ -2173,6 +2586,12 @@ export class PiboSessionRouter {
2173
2586
  for (const piboSessionId of piboSessionIds) {
2174
2587
  this.runReminderGenerations.set(piboSessionId, this.runReminderGeneration(piboSessionId) + 1);
2175
2588
  this.scheduledRunReminders.delete(piboSessionId);
2589
+ this.deferredRunReminders.delete(piboSessionId);
2590
+ this.runReminderRecoveries.delete(piboSessionId);
2591
+ for (const [eventId, delivery] of this.runReminderDeliveries) {
2592
+ if (delivery.piboSessionId === piboSessionId)
2593
+ this.runReminderDeliveries.delete(eventId);
2594
+ }
2176
2595
  try {
2177
2596
  this.sessions.get(piboSessionId)?.removeQueuedMessages(isRunReminderServiceMessage);
2178
2597
  }
@@ -2201,6 +2620,11 @@ export class PiboSessionRouter {
2201
2620
  return;
2202
2621
  if (expectedGeneration !== this.runReminderGeneration(piboSessionId))
2203
2622
  return;
2623
+ const deferredGeneration = this.deferredRunReminders.get(piboSessionId);
2624
+ if (deferredGeneration === expectedGeneration)
2625
+ return;
2626
+ if (deferredGeneration !== undefined)
2627
+ this.deferredRunReminders.delete(piboSessionId);
2204
2628
  if (!this.runRegistry.hasPendingNotification(piboSessionId, { includeAlreadyNotified }))
2205
2629
  return;
2206
2630
  const previous = this.scheduledRunReminders.get(piboSessionId);
@@ -2228,23 +2652,31 @@ export class PiboSessionRouter {
2228
2652
  this.scheduledRunReminders.delete(piboSessionId);
2229
2653
  if (this.closing || this.quiescingSessions.has(piboSessionId) || expectedGeneration !== this.runReminderGeneration(piboSessionId))
2230
2654
  return;
2231
- const notification = this.runRegistry.createNotification(piboSessionId, { includeAlreadyNotified: scheduled.includeAlreadyNotified });
2232
- if (!notification)
2233
- return;
2655
+ let notification;
2656
+ let eventId;
2234
2657
  try {
2235
2658
  const session = await this.getOrCreateSession(piboSessionId);
2236
2659
  if (this.closing || this.quiescingSessions.has(piboSessionId) || expectedGeneration !== this.runReminderGeneration(piboSessionId))
2237
2660
  return;
2661
+ notification = this.runRegistry.createNotification(piboSessionId, { includeAlreadyNotified: scheduled.includeAlreadyNotified });
2662
+ if (!notification)
2663
+ return;
2664
+ eventId = randomUUID();
2665
+ this.runReminderDeliveries.set(eventId, { piboSessionId, generation: expectedGeneration, notification });
2238
2666
  session.enqueueMessage({
2239
2667
  type: "message",
2240
2668
  piboSessionId,
2241
2669
  text: formatRunReminderMessage(notification),
2242
2670
  source: "service",
2243
- id: randomUUID(),
2671
+ id: eventId,
2244
2672
  provenance: runReminderProvenance(notification),
2245
2673
  });
2246
2674
  }
2247
2675
  catch (error) {
2676
+ if (eventId)
2677
+ this.runReminderDeliveries.delete(eventId);
2678
+ if (notification)
2679
+ this.runRegistry.releaseNotification(piboSessionId, notification);
2248
2680
  if (this.closing || this.quiescingSessions.has(piboSessionId) || expectedGeneration !== this.runReminderGeneration(piboSessionId))
2249
2681
  return;
2250
2682
  const message = error instanceof Error ? error.message : String(error);