@pasko70/pibo 2.4.1 → 2.4.2

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.
@@ -9,6 +9,20 @@ const RUN_REMINDER_CAPABILITY_TOOLS = new Set([
9
9
  "pibo_run_cancel",
10
10
  "pibo_run_ack",
11
11
  ]);
12
+ // Run reminders are autonomous service wakeups, so their provider/tool loop needs a deterministic boundary.
13
+ const RUN_REMINDER_MAX_TOOL_EXECUTIONS = 64;
14
+ const RUN_REMINDER_MAX_PROVIDER_ROUNDS = 64;
15
+ const RUN_REMINDER_MAX_TOTAL_TOKENS = 2_000_000;
16
+ const RUN_REMINDER_MAX_DURATION_MS = 15 * 60 * 1000;
17
+ const RUN_REMINDER_MAX_REPEATED_TOOL_CALLS = 12;
18
+ function serializedToolArgs(value) {
19
+ try {
20
+ return JSON.stringify(value) ?? String(value);
21
+ }
22
+ catch {
23
+ return String(value);
24
+ }
25
+ }
12
26
  function errorMessage(error) {
13
27
  return error instanceof Error ? error.message : String(error);
14
28
  }
@@ -82,9 +96,11 @@ export class RuntimeRoutedSession {
82
96
  runtimeDisposePromise;
83
97
  forceDisposalStarted = false;
84
98
  drainPromise;
99
+ inFlightMessage;
85
100
  activeMessage;
86
101
  activeExecutionEvent;
87
102
  activeMessageFailed = false;
103
+ runReminderTurnGuard;
88
104
  activeAssistantIndex;
89
105
  nextAssistantIndex = 0;
90
106
  activeThinkingIndex;
@@ -409,6 +425,28 @@ export class RuntimeRoutedSession {
409
425
  this.notifyState();
410
426
  return true;
411
427
  }
428
+ const inFlight = this.inFlightMessage;
429
+ if (inFlight?.event.id === eventId) {
430
+ if (!inFlight.cancellation) {
431
+ inFlight.cancelled = true;
432
+ this.notifyMessagesInterrupted([inFlight.event], "message cancelled");
433
+ const active = this.activeMessage?.id === eventId;
434
+ inFlight.cancellation = (async () => {
435
+ try {
436
+ if (active)
437
+ await this.runtimeSession.abort();
438
+ await inFlight.settled;
439
+ }
440
+ catch (error) {
441
+ inFlight.cancelled = false;
442
+ inFlight.cancellation = undefined;
443
+ throw error;
444
+ }
445
+ })();
446
+ }
447
+ await inFlight.cancellation;
448
+ return true;
449
+ }
412
450
  if (this.activeMessage?.id === eventId) {
413
451
  this.notifyMessagesInterrupted([this.activeMessage], "message cancelled");
414
452
  await this.runtimeSession.abort();
@@ -462,6 +500,7 @@ export class RuntimeRoutedSession {
462
500
  return;
463
501
  case "tool_execution_started":
464
502
  this.emit(this.withActiveMessage({ ...event, type: "tool_execution_started", piboSessionId: this.piboSessionId }));
503
+ this.trackRunReminderTurnGuard("tool_execution_started", { toolName: event.toolName, args: event.args });
465
504
  return;
466
505
  case "tool_execution_updated":
467
506
  this.emit(this.withActiveMessage({ ...event, type: "tool_execution_updated", piboSessionId: this.piboSessionId }));
@@ -479,7 +518,9 @@ export class RuntimeRoutedSession {
479
518
  cacheWriteTokens: event.usage.cacheWriteTokens,
480
519
  reasoningTokens: event.usage.reasoningTokens,
481
520
  totalTokens: event.usage.totalTokens,
521
+ provenance: this.activeMessage?.provenance,
482
522
  }));
523
+ this.trackRunReminderTurnGuard("usage", { totalTokens: event.usage.totalTokens });
483
524
  return;
484
525
  case "compaction_start":
485
526
  this.emit(this.withActiveMessage({
@@ -593,9 +634,10 @@ export class RuntimeRoutedSession {
593
634
  }
594
635
  }
595
636
  async processQueuedMessage(event) {
637
+ const inFlight = this.beginInFlightMessage(event);
596
638
  try {
597
639
  const preflight = await this.options.messagePreflight?.(event);
598
- if (this.disposed)
640
+ if (this.disposed || inFlight.cancelled)
599
641
  return;
600
642
  if (preflight && !preflight.allowed) {
601
643
  this.emit({
@@ -614,6 +656,10 @@ export class RuntimeRoutedSession {
614
656
  });
615
657
  return;
616
658
  }
659
+ this.activeMessage = event;
660
+ this.activeMessageFailed = false;
661
+ this.beginRunReminderTurnGuard(event);
662
+ this.resetContentIndices();
617
663
  this.emit({
618
664
  type: "message_started",
619
665
  piboSessionId: this.piboSessionId,
@@ -622,15 +668,14 @@ export class RuntimeRoutedSession {
622
668
  source: event.source,
623
669
  provenance: event.provenance,
624
670
  });
625
- this.activeMessage = event;
626
- this.activeMessageFailed = false;
627
- this.resetContentIndices();
671
+ if (inFlight.cancelled)
672
+ return;
628
673
  await this.runtimeSession.prompt({
629
674
  text: event.text,
630
675
  source: promptSource(event.source),
631
676
  capabilityScope: event.capabilityScope,
632
677
  });
633
- if (this.disposed)
678
+ if (this.disposed || inFlight.cancelled)
634
679
  return;
635
680
  if (!this.activeMessageFailed) {
636
681
  this.emit({
@@ -643,7 +688,7 @@ export class RuntimeRoutedSession {
643
688
  }
644
689
  }
645
690
  catch (error) {
646
- if (this.disposed)
691
+ if (this.disposed || inFlight.cancelled)
647
692
  return;
648
693
  if (!this.activeMessageFailed) {
649
694
  const message = errorMessage(error);
@@ -661,7 +706,102 @@ export class RuntimeRoutedSession {
661
706
  this.activeMessage = undefined;
662
707
  this.activeMessageFailed = false;
663
708
  this.resetContentIndices();
709
+ this.clearRunReminderTurnGuard();
710
+ if (this.inFlightMessage === inFlight)
711
+ this.inFlightMessage = undefined;
712
+ inFlight.resolveSettled();
713
+ }
714
+ }
715
+ beginInFlightMessage(event) {
716
+ let resolveSettled;
717
+ const inFlight = {
718
+ event,
719
+ cancelled: false,
720
+ settled: new Promise((resolve) => { resolveSettled = resolve; }),
721
+ resolveSettled: () => { resolveSettled?.(); },
722
+ };
723
+ this.inFlightMessage = inFlight;
724
+ return inFlight;
725
+ }
726
+ beginRunReminderTurnGuard(event) {
727
+ this.clearRunReminderTurnGuard();
728
+ if (event.source !== "service" || !event.text.startsWith("<pibo_run_notification>"))
729
+ return;
730
+ const guard = {
731
+ eventId: event.id,
732
+ toolExecutions: 0,
733
+ providerRounds: 0,
734
+ totalTokens: 0,
735
+ toolSignatures: new Map(),
736
+ tripped: false,
737
+ };
738
+ guard.timer = setTimeout(() => {
739
+ this.tripRunReminderTurnGuard(guard, `exceeded ${RUN_REMINDER_MAX_DURATION_MS / 60_000} minutes`);
740
+ }, RUN_REMINDER_MAX_DURATION_MS);
741
+ guard.timer.unref?.();
742
+ this.runReminderTurnGuard = guard;
743
+ }
744
+ clearRunReminderTurnGuard() {
745
+ const guard = this.runReminderTurnGuard;
746
+ if (!guard)
747
+ return;
748
+ if (guard.timer)
749
+ clearTimeout(guard.timer);
750
+ this.runReminderTurnGuard = undefined;
751
+ }
752
+ trackRunReminderTurnGuard(type, payload) {
753
+ const guard = this.runReminderTurnGuard;
754
+ if (!guard || guard.tripped || guard.eventId !== this.activeMessage?.id)
755
+ return;
756
+ if (type === "usage") {
757
+ const totalTokens = payload.totalTokens ?? 0;
758
+ guard.totalTokens += totalTokens;
759
+ guard.providerRounds += 1;
760
+ if (guard.totalTokens > RUN_REMINDER_MAX_TOTAL_TOKENS) {
761
+ this.tripRunReminderTurnGuard(guard, `exceeded ${RUN_REMINDER_MAX_TOTAL_TOKENS} total tokens`);
762
+ return;
763
+ }
764
+ if (guard.providerRounds > RUN_REMINDER_MAX_PROVIDER_ROUNDS) {
765
+ this.tripRunReminderTurnGuard(guard, `exceeded ${RUN_REMINDER_MAX_PROVIDER_ROUNDS} provider rounds`);
766
+ }
767
+ return;
664
768
  }
769
+ const toolName = payload.toolName;
770
+ const args = payload.args;
771
+ guard.toolExecutions += 1;
772
+ if (guard.toolExecutions > RUN_REMINDER_MAX_TOOL_EXECUTIONS) {
773
+ this.tripRunReminderTurnGuard(guard, `exceeded ${RUN_REMINDER_MAX_TOOL_EXECUTIONS} tool executions`);
774
+ return;
775
+ }
776
+ const signature = `${String(toolName ?? "unknown")}:${serializedToolArgs(args)}`;
777
+ const repeated = (guard.toolSignatures.get(signature) ?? 0) + 1;
778
+ guard.toolSignatures.set(signature, repeated);
779
+ if (repeated > RUN_REMINDER_MAX_REPEATED_TOOL_CALLS) {
780
+ this.tripRunReminderTurnGuard(guard, `repeated the same tool call more than ${RUN_REMINDER_MAX_REPEATED_TOOL_CALLS} times`);
781
+ }
782
+ }
783
+ tripRunReminderTurnGuard(guard, reason) {
784
+ if (guard.tripped || this.runReminderTurnGuard !== guard)
785
+ return;
786
+ guard.tripped = true;
787
+ this.activeMessageFailed = true;
788
+ const error = `Run-reminder turn stopped because it ${reason}.`;
789
+ this.emit({
790
+ type: "session_error",
791
+ piboSessionId: this.piboSessionId,
792
+ eventId: guard.eventId,
793
+ error,
794
+ errorDetails: {
795
+ category: "runtime_abort",
796
+ errorClass: "runtime_abort",
797
+ code: "run_reminder_limit_exceeded",
798
+ origin: "runtime",
799
+ retryable: false,
800
+ userMessage: error,
801
+ },
802
+ provenance: this.activeMessage?.provenance,
803
+ });
804
+ void Promise.resolve(this.runtimeSession.abort()).catch(() => undefined);
665
805
  }
666
806
  async processQueuedCompact(event) {
667
807
  this.activeExecutionEvent = event;
@@ -851,7 +991,10 @@ export class RuntimeRoutedSession {
851
991
  }
852
992
  activeAndQueuedMessages() {
853
993
  const messages = this.queue.flatMap((item) => item.kind === "message" ? [item.event] : []);
854
- return this.activeMessage ? [this.activeMessage, ...messages] : messages;
994
+ const inFlight = this.inFlightMessage?.event;
995
+ if (this.activeMessage)
996
+ return [this.activeMessage, ...messages];
997
+ return inFlight ? [inFlight, ...messages] : messages;
855
998
  }
856
999
  notifyMessagesInterrupted(messages, reason) {
857
1000
  if (messages.length === 0)
@@ -451,6 +451,7 @@ export class RoutedSession {
451
451
  runtimeDisposePromise;
452
452
  forceDisposalStarted = false;
453
453
  drainPromise;
454
+ inFlightMessage;
454
455
  fastMode = false;
455
456
  fastModePatchedAgents = new WeakSet();
456
457
  activeMessage;
@@ -1101,6 +1102,31 @@ export class RoutedSession {
1101
1102
  this.onStateChange?.({ processing: this.processing, queuedMessages: this.queue.length, disposed: this.disposed });
1102
1103
  return true;
1103
1104
  }
1105
+ const inFlight = this.inFlightMessage;
1106
+ if (inFlight?.event.id === eventId) {
1107
+ if (!inFlight.cancellation) {
1108
+ inFlight.cancelled = true;
1109
+ this.notifyMessagesInterrupted([inFlight.event], "message cancelled");
1110
+ const active = this.activeMessage?.id === eventId;
1111
+ inFlight.cancellation = (async () => {
1112
+ try {
1113
+ if (active) {
1114
+ this.cancelProviderRecovery();
1115
+ this.cancelContextGuardRecovery("Context guard recovery cancelled with the active message");
1116
+ await this.runtime.session.abort();
1117
+ }
1118
+ await inFlight.settled;
1119
+ }
1120
+ catch (error) {
1121
+ inFlight.cancelled = false;
1122
+ inFlight.cancellation = undefined;
1123
+ throw error;
1124
+ }
1125
+ })();
1126
+ }
1127
+ await inFlight.cancellation;
1128
+ return true;
1129
+ }
1104
1130
  if (this.activeMessage?.id === eventId) {
1105
1131
  this.notifyMessagesInterrupted([this.activeMessage], "message cancelled");
1106
1132
  this.cancelProviderRecovery();
@@ -1143,9 +1169,10 @@ export class RoutedSession {
1143
1169
  }
1144
1170
  }
1145
1171
  async processQueuedMessage(event) {
1172
+ const inFlight = this.beginInFlightMessage(event);
1146
1173
  try {
1147
1174
  const preflight = await this.messagePreflight?.(event);
1148
- if (this.disposed)
1175
+ if (this.disposed || inFlight.cancelled)
1149
1176
  return;
1150
1177
  if (preflight && !preflight.allowed) {
1151
1178
  this.emit({
@@ -1166,16 +1193,8 @@ export class RoutedSession {
1166
1193
  }
1167
1194
  const session = this.runtime.session;
1168
1195
  await this.resumeTranscriptIntegrityRecovery(session);
1169
- if (this.disposed)
1196
+ if (this.disposed || inFlight.cancelled)
1170
1197
  return;
1171
- this.emit({
1172
- type: "message_started",
1173
- piboSessionId: this.piboSessionId,
1174
- eventId: event.id,
1175
- text: event.text,
1176
- source: event.source,
1177
- provenance: event.provenance,
1178
- });
1179
1198
  this.activeMessage = event;
1180
1199
  this.providerRecoveryCancelled = false;
1181
1200
  this.pendingAssistantError = undefined;
@@ -1185,19 +1204,29 @@ export class RoutedSession {
1185
1204
  this.nextAssistantIndex = 0;
1186
1205
  this.activeThinkingIndex = undefined;
1187
1206
  this.nextThinkingIndex = 0;
1207
+ this.emit({
1208
+ type: "message_started",
1209
+ piboSessionId: this.piboSessionId,
1210
+ eventId: event.id,
1211
+ text: event.text,
1212
+ source: event.source,
1213
+ provenance: event.provenance,
1214
+ });
1215
+ if (inFlight.cancelled)
1216
+ return;
1188
1217
  this.applyMessageCapabilityScope(event, session);
1189
1218
  const expandedText = expandInlineSkills(event.text, session.resourceLoader.getSkills().skills);
1190
1219
  await session.prompt(expandedText, { source: promptSource(event.source) });
1191
- if (this.disposed)
1220
+ if (this.disposed || inFlight.cancelled)
1192
1221
  return;
1193
1222
  await this.waitForPiAgentSettlement(session);
1194
- if (this.disposed)
1223
+ if (this.disposed || inFlight.cancelled)
1195
1224
  return;
1196
1225
  await this.resumeContextGuardRecovery(session);
1197
- if (this.disposed)
1226
+ if (this.disposed || inFlight.cancelled)
1198
1227
  return;
1199
1228
  await this.recoverTransientProviderErrors(session);
1200
- if (this.disposed)
1229
+ if (this.disposed || inFlight.cancelled)
1201
1230
  return;
1202
1231
  this.flushPendingAssistantError();
1203
1232
  if (!this.activeMessageFailed) {
@@ -1211,7 +1240,7 @@ export class RoutedSession {
1211
1240
  }
1212
1241
  }
1213
1242
  catch (error) {
1214
- if (error instanceof PiboProviderRecoveryCancelledError || this.disposed)
1243
+ if (error instanceof PiboProviderRecoveryCancelledError || this.disposed || inFlight.cancelled)
1215
1244
  return;
1216
1245
  const message = errorMessage(error);
1217
1246
  this.emit({
@@ -1234,8 +1263,22 @@ export class RoutedSession {
1234
1263
  this.nextAssistantIndex = 0;
1235
1264
  this.activeThinkingIndex = undefined;
1236
1265
  this.nextThinkingIndex = 0;
1266
+ if (this.inFlightMessage === inFlight)
1267
+ this.inFlightMessage = undefined;
1268
+ inFlight.resolveSettled();
1237
1269
  }
1238
1270
  }
1271
+ beginInFlightMessage(event) {
1272
+ let resolveSettled;
1273
+ const inFlight = {
1274
+ event,
1275
+ cancelled: false,
1276
+ settled: new Promise((resolve) => { resolveSettled = resolve; }),
1277
+ resolveSettled: () => { resolveSettled?.(); },
1278
+ };
1279
+ this.inFlightMessage = inFlight;
1280
+ return inFlight;
1281
+ }
1239
1282
  applyMessageCapabilityScope(event, session) {
1240
1283
  if (event.capabilityScope !== "run-reminder")
1241
1284
  return;
@@ -1395,7 +1438,10 @@ export class RoutedSession {
1395
1438
  }
1396
1439
  activeAndQueuedMessages() {
1397
1440
  const messages = this.queue.flatMap((item) => item.kind === "message" ? [item.event] : []);
1398
- return this.activeMessage ? [this.activeMessage, ...messages] : messages;
1441
+ const inFlight = this.inFlightMessage?.event;
1442
+ if (this.activeMessage)
1443
+ return [this.activeMessage, ...messages];
1444
+ return inFlight ? [inFlight, ...messages] : messages;
1399
1445
  }
1400
1446
  notifyMessagesInterrupted(messages, reason) {
1401
1447
  if (messages.length === 0)
@@ -5,7 +5,7 @@ import { RuntimeRoutedSession as RoutedSession, } from "../agent-runtime/routed-
5
5
  import { runtimeSessionErrorDetails } from "./session-errors.js";
6
6
  import { normalizePiboAgentObservationCursor, normalizePiboAgentObservationLimit, normalizePiboAgentObservationOrder, parsePiboAgentObservationTimestamp, piboAgentObservationDetails, piboAgentObservationKind, piboAgentObservationRole, piboAgentObservationSourceFromEvent, piboAgentObservationText, } from "../subagents/observations.js";
7
7
  import { PiboRunRegistry } from "../runs/registry.js";
8
- import { PiboRunExecutionTimeoutError } from "../runs/lifecycle.js";
8
+ import { PiboRunCancellationError, PiboRunCancelledError, PiboRunExecutionTimeoutError } from "../runs/lifecycle.js";
9
9
  import { PiboRunResourceLimitError } from "../runs/resource-isolation.js";
10
10
  import { createPiboSignalRegistry } from "../signals/registry.js";
11
11
  import { createDefaultPiboReliabilityStore } from "../reliability/store.js";
@@ -150,7 +150,29 @@ function formatRunReminderMessage(notification) {
150
150
  ].join("\n");
151
151
  }
152
152
  function isRunReminderServiceMessage(event) {
153
- return event.source === "service" && event.capabilityScope === "run-reminder";
153
+ return event.source === "service" && event.text.startsWith("<pibo_run_notification>");
154
+ }
155
+ function yieldedRunOrigin(event) {
156
+ if (!event?.id || event.provenance?.kind !== "loop-run")
157
+ return undefined;
158
+ return {
159
+ eventId: event.provenance.rootEventId ?? event.id,
160
+ provenance: {
161
+ kind: event.provenance.kind,
162
+ jobId: event.provenance.jobId,
163
+ runId: event.provenance.runId,
164
+ },
165
+ };
166
+ }
167
+ function runReminderProvenance(notification) {
168
+ const origin = notification.origin;
169
+ if (!origin || origin.provenance.kind !== "loop-run")
170
+ return undefined;
171
+ return {
172
+ ...origin.provenance,
173
+ cause: "run-reminder",
174
+ rootEventId: origin.eventId,
175
+ };
154
176
  }
155
177
  function isTerminalRunStatus(status) {
156
178
  return status === "completed" || status === "failed" || status === "timed_out" || status === "cancelled";
@@ -235,7 +257,7 @@ export class PiboSessionRouter {
235
257
  portableHistoryProvider;
236
258
  runtimeResourceSessions = new Map();
237
259
  runtimeAuthFingerprints = new Map();
238
- activeSubagentChildren = new Map();
260
+ activeSubagentRequests = new Map();
239
261
  agentObservations = [];
240
262
  agentObservationEvictedThroughByParent = new Map();
241
263
  nextAgentObservationSequence = 1;
@@ -388,11 +410,24 @@ export class PiboSessionRouter {
388
410
  teardownCompleted = true;
389
411
  return output;
390
412
  }
391
- const childAbort = event.action === "abort"
392
- ? this.abortActiveSubagentSessions(event.piboSessionId)
393
- : undefined;
394
- const output = await session.executeAction(event);
395
- await childAbort;
413
+ let output;
414
+ if (event.action === "abort") {
415
+ const [sessionAbort, childAbort] = await Promise.allSettled([
416
+ session.executeAction(event),
417
+ this.abortActiveSubagentSessions(event.piboSessionId),
418
+ ]);
419
+ if (sessionAbort.status === "rejected" && childAbort.status === "rejected") {
420
+ throw new AggregateError([sessionAbort.reason, childAbort.reason], "Failed to abort the session and its active subagent requests.");
421
+ }
422
+ if (sessionAbort.status === "rejected")
423
+ throw sessionAbort.reason;
424
+ if (childAbort.status === "rejected")
425
+ throw childAbort.reason;
426
+ output = sessionAbort.value;
427
+ }
428
+ else {
429
+ output = await session.executeAction(event);
430
+ }
396
431
  if (event.action === "kill" || event.action === "kill_all") {
397
432
  await this.disposeSessionSubtree(event.piboSessionId, `${event.action} action`, { cancelRuns: event.action === "kill_all" });
398
433
  teardownCompleted = true;
@@ -837,20 +872,10 @@ export class PiboSessionRouter {
837
872
  const eventWithId = { ...event, id: event.id ?? randomUUID() };
838
873
  return await new Promise((resolve, reject) => {
839
874
  let settled = false;
840
- let messageDispatched = false;
875
+ let dispatchPromise;
841
876
  let lastAssistantMessage;
842
877
  let timeout;
843
- const abortChild = () => {
844
- if (!messageDispatched)
845
- return;
846
- void this.emit({
847
- type: "execution",
848
- piboSessionId: eventWithId.piboSessionId,
849
- action: "abort",
850
- id: randomUUID(),
851
- }).catch(() => { });
852
- };
853
- const finish = (result) => {
878
+ const claimSettlement = () => {
854
879
  if (settled)
855
880
  return false;
856
881
  settled = true;
@@ -858,16 +883,33 @@ export class PiboSessionRouter {
858
883
  clearTimeout(timeout);
859
884
  signal?.removeEventListener("abort", onAbort);
860
885
  unsubscribe();
886
+ return true;
887
+ };
888
+ const finish = (result) => {
889
+ if (!claimSettlement())
890
+ return;
861
891
  if (result instanceof Error)
862
892
  reject(result);
863
893
  else
864
894
  resolve(result);
865
- return true;
866
895
  };
867
- const onAbort = () => {
868
- if (!finish(subagentAbortError()))
896
+ const rejectAfterMessageCancellation = (error) => {
897
+ if (!claimSettlement())
869
898
  return;
870
- abortChild();
899
+ void (async () => {
900
+ try {
901
+ await dispatchPromise;
902
+ await this.cancelSessionMessage(eventWithId.piboSessionId, eventWithId.id);
903
+ }
904
+ catch (cancellationError) {
905
+ reject(new PiboRunCancellationError(`Failed to cancel subagent request "${eventWithId.id}" in Pibo session "${eventWithId.piboSessionId}".`, { cause: cancellationError }));
906
+ return;
907
+ }
908
+ reject(error);
909
+ })();
910
+ };
911
+ const onAbort = () => {
912
+ rejectAfterMessageCancellation(subagentAbortError());
871
913
  };
872
914
  const unsubscribe = this.subscribe((output) => {
873
915
  if (output.piboSessionId !== eventWithId.piboSessionId ||
@@ -886,20 +928,25 @@ export class PiboSessionRouter {
886
928
  }
887
929
  });
888
930
  if (signal?.aborted) {
889
- onAbort();
931
+ finish(subagentAbortError());
890
932
  return;
891
933
  }
892
934
  signal?.addEventListener("abort", onAbort, { once: true });
893
935
  timeout = setTimeout(() => {
894
- const timeoutError = new Error(`Timed out waiting for assistant reply from Pibo session "${eventWithId.piboSessionId}"`);
895
- if (!finish(timeoutError))
896
- return;
897
- abortChild();
936
+ rejectAfterMessageCancellation(new PiboRunExecutionTimeoutError(`Timed out waiting for assistant reply from Pibo session "${eventWithId.piboSessionId}"`, "lifetime"));
898
937
  }, timeoutMs);
899
- messageDispatched = true;
900
- this.emit(eventWithId).catch(finish);
938
+ dispatchPromise = this.emit(eventWithId);
939
+ dispatchPromise.catch((error) => {
940
+ finish(error instanceof Error ? error : new Error(String(error)));
941
+ });
901
942
  });
902
943
  }
944
+ async cancelSessionMessage(piboSessionId, eventId) {
945
+ const session = this.sessions.get(piboSessionId);
946
+ if (!session || !await session.cancelMessage(eventId)) {
947
+ throw new Error(`Pibo session "${piboSessionId}" no longer owns message "${eventId}".`);
948
+ }
949
+ }
903
950
  async disposeAll() {
904
951
  if (this.disposePromise)
905
952
  return this.disposePromise;
@@ -946,7 +993,7 @@ export class PiboSessionRouter {
946
993
  await this.portableToolService.dispose();
947
994
  await this.runtimeResourceService.dispose();
948
995
  this.runtimeResourceSessions.clear();
949
- this.activeSubagentChildren.clear();
996
+ this.activeSubagentRequests.clear();
950
997
  this.agentObservations.length = 0;
951
998
  this.agentObservationEvictedThroughByParent.clear();
952
999
  await this.telemetryWriter?.dispose();
@@ -1562,44 +1609,40 @@ export class PiboSessionRouter {
1562
1609
  this.signalRegistry.project({ type: "session_created", session: created });
1563
1610
  return created;
1564
1611
  }
1565
- trackActiveSubagent(parentPiboSessionId, childPiboSessionId) {
1566
- let children = this.activeSubagentChildren.get(parentPiboSessionId);
1567
- if (!children) {
1568
- children = new Map();
1569
- this.activeSubagentChildren.set(parentPiboSessionId, children);
1612
+ trackActiveSubagent(parentPiboSessionId, request) {
1613
+ let requests = this.activeSubagentRequests.get(parentPiboSessionId);
1614
+ if (!requests) {
1615
+ requests = new Set();
1616
+ this.activeSubagentRequests.set(parentPiboSessionId, requests);
1570
1617
  }
1571
- children.set(childPiboSessionId, (children.get(childPiboSessionId) ?? 0) + 1);
1618
+ requests.add(request);
1572
1619
  let active = true;
1573
1620
  return () => {
1574
1621
  if (!active)
1575
1622
  return;
1576
1623
  active = false;
1577
- const current = this.activeSubagentChildren.get(parentPiboSessionId);
1624
+ const current = this.activeSubagentRequests.get(parentPiboSessionId);
1578
1625
  if (!current)
1579
1626
  return;
1580
- const remaining = (current.get(childPiboSessionId) ?? 1) - 1;
1581
- if (remaining > 0)
1582
- current.set(childPiboSessionId, remaining);
1583
- else
1584
- current.delete(childPiboSessionId);
1627
+ current.delete(request);
1585
1628
  if (current.size === 0)
1586
- this.activeSubagentChildren.delete(parentPiboSessionId);
1629
+ this.activeSubagentRequests.delete(parentPiboSessionId);
1587
1630
  };
1588
1631
  }
1589
1632
  async abortActiveSubagentSessions(parentPiboSessionId) {
1590
- const childIds = [...(this.activeSubagentChildren.get(parentPiboSessionId)?.keys() ?? [])];
1591
- if (childIds.length === 0)
1592
- return;
1593
- await Promise.allSettled(childIds.map(async (childPiboSessionId) => await this.emit({
1594
- type: "execution",
1595
- piboSessionId: childPiboSessionId,
1596
- action: "abort",
1597
- id: randomUUID(),
1598
- })));
1633
+ const requests = [...(this.activeSubagentRequests.get(parentPiboSessionId) ?? [])];
1634
+ for (const request of requests)
1635
+ request.abortController.abort();
1636
+ const settlements = await Promise.all(requests.map(async (request) => await request.settled));
1637
+ const failures = settlements.flatMap((settlement) => settlement.status === "rejected" ? [settlement.reason] : []);
1638
+ if (failures.length > 0)
1639
+ throw new AggregateError(failures, "Failed to cancel active subagent requests.");
1599
1640
  }
1600
1641
  createAgentsController(parentPiboSessionId) {
1601
1642
  return {
1602
1643
  sendMessage: async ({ subagent, message, threadKey, toolCallId, signal }) => {
1644
+ if (signal?.aborted)
1645
+ throw subagentAbortError();
1603
1646
  this.assertSubagentDepth(parentPiboSessionId, subagent);
1604
1647
  const child = this.resolveSubagentSession(parentPiboSessionId, subagent, threadKey);
1605
1648
  const resolvedThreadKey = typeof child.metadata?.threadKey === "string" ? child.metadata.threadKey : "";
@@ -1619,9 +1662,21 @@ export class PiboSessionRouter {
1619
1662
  childPiboSessionId: child.id,
1620
1663
  threadKey: resolvedThreadKey,
1621
1664
  });
1622
- const untrack = this.trackActiveSubagent(parentPiboSessionId, child.id);
1665
+ const parentAbortController = new AbortController();
1666
+ const requestSignal = signal
1667
+ ? AbortSignal.any([signal, parentAbortController.signal])
1668
+ : parentAbortController.signal;
1669
+ let resolveSettled;
1670
+ const settled = new Promise((resolve) => {
1671
+ resolveSettled = resolve;
1672
+ });
1673
+ const untrack = this.trackActiveSubagent(parentPiboSessionId, {
1674
+ abortController: parentAbortController,
1675
+ settled,
1676
+ });
1677
+ let settlement = { status: "fulfilled" };
1623
1678
  try {
1624
- const reply = await this.emitMessageAndWaitForReply(event, subagent.timeoutMs ?? DEFAULT_SUBAGENT_REPLY_TIMEOUT_MS, signal);
1679
+ const reply = await this.emitMessageAndWaitForReply(event, subagent.timeoutMs ?? DEFAULT_SUBAGENT_REPLY_TIMEOUT_MS, requestSignal);
1625
1680
  return {
1626
1681
  agentId: child.id,
1627
1682
  name: subagent.name,
@@ -1631,8 +1686,17 @@ export class PiboSessionRouter {
1631
1686
  reply,
1632
1687
  };
1633
1688
  }
1689
+ catch (error) {
1690
+ const confirmedParentCancellation = parentAbortController.signal.aborted
1691
+ && error instanceof Error
1692
+ && error.name === "AbortError";
1693
+ if (!confirmedParentCancellation)
1694
+ settlement = { status: "rejected", reason: error };
1695
+ throw error;
1696
+ }
1634
1697
  finally {
1635
1698
  untrack();
1699
+ resolveSettled?.(settlement);
1636
1700
  }
1637
1701
  },
1638
1702
  listAgents: () => this.listManagedAgents(parentPiboSessionId),
@@ -1767,8 +1831,9 @@ export class PiboSessionRouter {
1767
1831
  const cancel = this.runCancellationHandlers.get(run.runId);
1768
1832
  if (!cancel)
1769
1833
  return;
1770
- this.runCancellationHandlers.delete(run.runId);
1771
1834
  await cancel();
1835
+ if (this.runCancellationHandlers.get(run.runId) === cancel)
1836
+ this.runCancellationHandlers.delete(run.runId);
1772
1837
  }));
1773
1838
  const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
1774
1839
  if (failures.length > 0)
@@ -1796,14 +1861,32 @@ export class PiboSessionRouter {
1796
1861
  timeoutMs,
1797
1862
  serviceWarning,
1798
1863
  resources,
1864
+ origin: yieldedRunOrigin(this.sessions.get(parentPiboSessionId)?.getActiveMessage?.()),
1799
1865
  });
1800
1866
  }
1801
1867
  catch (error) {
1802
1868
  admission.release();
1803
1869
  throw error;
1804
1870
  }
1805
- if (cancel)
1806
- this.runCancellationHandlers.set(run.runId, cancel);
1871
+ const cancellation = { state: "none" };
1872
+ if (cancel) {
1873
+ this.runCancellationHandlers.set(run.runId, async () => {
1874
+ cancellation.state = "pending";
1875
+ let resolveDecision;
1876
+ cancellation.decision = new Promise((resolve) => { resolveDecision = resolve; });
1877
+ try {
1878
+ await cancel();
1879
+ cancellation.state = "confirmed";
1880
+ }
1881
+ catch (error) {
1882
+ cancellation.state = "failed";
1883
+ throw error;
1884
+ }
1885
+ finally {
1886
+ resolveDecision?.();
1887
+ }
1888
+ });
1889
+ }
1807
1890
  void (async () => {
1808
1891
  try {
1809
1892
  const result = await execute();
@@ -1814,6 +1897,12 @@ export class PiboSessionRouter {
1814
1897
  this.handleTerminalRunReminder(parentPiboSessionId, completed.runId, reminderGeneration);
1815
1898
  }
1816
1899
  catch (error) {
1900
+ if (error instanceof PiboRunCancelledError) {
1901
+ if (cancellation.state === "pending")
1902
+ await cancellation.decision;
1903
+ if (cancellation.state === "confirmed")
1904
+ return;
1905
+ }
1817
1906
  const message = error instanceof Error ? error.message : String(error);
1818
1907
  if (resources)
1819
1908
  this.runRegistry.updateResources(run.runId, resources);
@@ -1842,14 +1931,15 @@ export class PiboSessionRouter {
1842
1931
  return run;
1843
1932
  },
1844
1933
  cancelRun: async (runId) => {
1845
- const cancelled = this.runRegistry.cancel(parentPiboSessionId, runId);
1934
+ const current = this.runRegistry.status(parentPiboSessionId, runId);
1846
1935
  try {
1847
- await this.invokeRunCancellationHandlers([cancelled]);
1936
+ if (!isTerminalRunStatus(current.status))
1937
+ await this.invokeRunCancellationHandlers([current]);
1938
+ return this.runRegistry.cancel(parentPiboSessionId, runId);
1848
1939
  }
1849
1940
  finally {
1850
1941
  this.refreshQueuedRunReminders(parentPiboSessionId);
1851
1942
  }
1852
- return cancelled;
1853
1943
  },
1854
1944
  ackRun: (runId) => {
1855
1945
  const run = this.runRegistry.ack(parentPiboSessionId, runId);
@@ -2067,8 +2157,8 @@ export class PiboSessionRouter {
2067
2157
  piboSessionId,
2068
2158
  text: formatRunReminderMessage(notification),
2069
2159
  source: "service",
2070
- capabilityScope: "run-reminder",
2071
2160
  id: randomUUID(),
2161
+ provenance: runReminderProvenance(notification),
2072
2162
  });
2073
2163
  }
2074
2164
  catch (error) {
@@ -531,9 +531,13 @@ export class PiboLoopService {
531
531
  this.store.updateRunMessageState(eventId, 'invalidated');
532
532
  if (event.type !== 'assistant_usage')
533
533
  return;
534
- const run = this.store.getRunByMessageEventId(eventId);
534
+ const provenance = event.provenance?.kind === 'loop-run' ? event.provenance : undefined;
535
+ const run = this.store.getRunByMessageEventId(eventId) ?? (provenance ? this.store.getRun(provenance.runId) : undefined);
535
536
  if (!run || run.piboSessionId !== event.piboSessionId)
536
537
  return;
538
+ if (provenance && (run.jobId !== provenance.jobId
539
+ || (provenance.cause === 'run-reminder' && run.messageEventId !== provenance.rootEventId)))
540
+ return;
537
541
  const job = this.store.getJob(run.jobId);
538
542
  if (!job || job.mode !== 'goal')
539
543
  return;
@@ -636,7 +636,11 @@ export class PiboLoopStore {
636
636
  const completedIterations = (job.state.completedIterations ?? 0) + 1;
637
637
  const reachedMaxIterations = job.maxIterations !== undefined && completedIterations >= job.maxIterations;
638
638
  const currentGoalStatus = goalStatus(job);
639
- const nextGoalStatus = job.mode === 'goal' ? isTerminalGoalStatus(currentGoalStatus) ? currentGoalStatus : input.goalStatus ?? currentGoalStatus : undefined;
639
+ const nextGoalStatus = job.mode === 'goal'
640
+ ? isTerminalGoalStatus(currentGoalStatus) || currentGoalStatus === 'paused'
641
+ ? currentGoalStatus
642
+ : input.goalStatus ?? currentGoalStatus
643
+ : undefined;
640
644
  const terminalGoalStatus = job.mode === 'goal' && isTerminalGoalStatus(nextGoalStatus);
641
645
  const shouldDisable = terminalGoalStatus || reachedMaxIterations || input.stopAfterRun === true || input.stopEvaluation?.finalAction === 'stop-after-run' || input.stopEvaluation?.finalAction === 'cancel-current-run';
642
646
  const state = {
@@ -833,15 +837,23 @@ export function createLoopMessagePreflight(options = {}) {
833
837
  const job = store.getJob(jobId);
834
838
  const run = store.getRun(runId);
835
839
  const status = job?.mode === 'goal' ? goalStatus(job) ?? (job.enabled ? 'active' : 'paused') : undefined;
840
+ const causalReminder = event.provenance.cause === 'run-reminder';
841
+ const validMessageBinding = causalReminder
842
+ ? event.source === 'service'
843
+ && event.text.startsWith('<pibo_run_notification>')
844
+ && typeof event.provenance.rootEventId === 'string'
845
+ && run?.messageEventId === event.provenance.rootEventId
846
+ : run?.messageEventId === event.id;
847
+ const validRunState = causalReminder
848
+ ? true
849
+ : run?.status === 'running' && Boolean(job?.state.runningAt) && job?.state.lastRunId === runId;
836
850
  const allowed = Boolean(job
837
851
  && run
838
852
  && run.jobId === jobId
839
- && run.status === 'running'
840
- && run.messageEventId === event.id
853
+ && validMessageBinding
854
+ && validRunState
841
855
  && (!run.piboSessionId || run.piboSessionId === event.piboSessionId)
842
856
  && job.enabled
843
- && job.state.runningAt
844
- && job.state.lastRunId === runId
845
857
  && (job.mode !== 'goal' || status === 'active'));
846
858
  if (allowed)
847
859
  return { allowed: true };
@@ -34,7 +34,9 @@ function resolveGoalForTurn(store, context, piboSessionId) {
34
34
  if (provenance?.kind !== 'loop-run')
35
35
  return store.getSessionGoalOwner(piboSessionId) ?? store.getLatestGoalForSession(piboSessionId);
36
36
  const run = store.getRun(provenance.runId);
37
- if (!run || run.jobId !== provenance.jobId || run.piboSessionId !== piboSessionId || run.messageEventId !== activeMessage?.id) {
37
+ const expectedEventId = provenance.cause === 'run-reminder' ? provenance.rootEventId : activeMessage?.id;
38
+ const validReminder = provenance.cause !== 'run-reminder' || (activeMessage?.source === 'service' && typeof provenance.rootEventId === 'string');
39
+ if (!run || !validReminder || run.jobId !== provenance.jobId || run.piboSessionId !== piboSessionId || run.messageEventId !== expectedEventId) {
38
40
  throw new Error('cannot resolve goal because this turn has stale or invalid Loop provenance');
39
41
  }
40
42
  const job = store.getJob(provenance.jobId);
@@ -149,7 +149,8 @@ export class PiboReliabilityStore {
149
149
  timeout_at TEXT,
150
150
  timeout_phase TEXT,
151
151
  service_warning TEXT,
152
- resource_json TEXT
152
+ resource_json TEXT,
153
+ origin_json TEXT
153
154
  );
154
155
  CREATE INDEX IF NOT EXISTS idx_pibo_runs_controller_updated
155
156
  ON pibo_runs(controller_pibo_session_id, updated_at);
@@ -161,6 +162,7 @@ export class PiboReliabilityStore {
161
162
  ensurePiboRunColumn(this.db, "timeout_phase", "TEXT");
162
163
  ensurePiboRunColumn(this.db, "service_warning", "TEXT");
163
164
  ensurePiboRunColumn(this.db, "resource_json", "TEXT");
165
+ ensurePiboRunColumn(this.db, "origin_json", "TEXT");
164
166
  this.appendEventStatement = this.db.prepare(`
165
167
  INSERT INTO pibo_event_stream (topic, key, event_id, idempotency_key, created_at, retention_class, payload_json)
166
168
  VALUES (?, ?, ?, ?, ?, ?, ?)
@@ -502,10 +504,10 @@ export class PiboReliabilityStore {
502
504
  INSERT INTO pibo_runs (
503
505
  run_id, kind, controller_pibo_session_id, status, completion_policy, consumed, tool_name,
504
506
  summary, result_json, error, notified_status, acknowledged_status, created_at, updated_at,
505
- completed_at, job_id, retryable, max_attempts, timeout_ms, timeout_at, timeout_phase, service_warning, resource_json
506
- ) VALUES (?, 'tool', ?, 'running', ?, 0, ?, ?, NULL, NULL, NULL, NULL, ?, ?, NULL, ?, ?, ?, ?, ?, NULL, ?, ?)
507
+ completed_at, job_id, retryable, max_attempts, timeout_ms, timeout_at, timeout_phase, service_warning, resource_json, origin_json
508
+ ) VALUES (?, 'tool', ?, 'running', ?, 0, ?, ?, NULL, NULL, NULL, NULL, ?, ?, NULL, ?, ?, ?, ?, ?, NULL, ?, ?, ?)
507
509
  `)
508
- .run(runId, input.controllerPiboSessionId, input.completionPolicy, input.toolName, `${input.toolName} run is running.`, timestamp, timestamp, job.jobId, input.retryable ? 1 : 0, maxAttempts, input.timeoutMs ?? null, timeoutAt ?? null, input.serviceWarning ?? null, input.resources ? JSON.stringify(input.resources) : null);
510
+ .run(runId, input.controllerPiboSessionId, input.completionPolicy, input.toolName, `${input.toolName} run is running.`, timestamp, timestamp, job.jobId, input.retryable ? 1 : 0, maxAttempts, input.timeoutMs ?? null, timeoutAt ?? null, input.serviceWarning ?? null, input.resources ? JSON.stringify(input.resources) : null, input.origin ? JSON.stringify(input.origin) : null);
509
511
  this.claimJob(job.jobId, input.workerId ?? `run-registry:${process.pid}`, 24 * 60 * 60 * 1000);
510
512
  return this.requireRun(runId);
511
513
  }
@@ -534,10 +536,11 @@ export class PiboReliabilityStore {
534
536
  timeout_at = ?,
535
537
  timeout_phase = ?,
536
538
  service_warning = ?,
537
- resource_json = ?
539
+ resource_json = ?,
540
+ origin_json = ?
538
541
  WHERE run_id = ?
539
542
  `)
540
- .run(next.status, next.completionPolicy, next.consumed ? 1 : 0, next.summary ?? null, next.result ? JSON.stringify(next.result) : null, next.error ?? null, next.notifiedStatus ?? null, next.acknowledgedStatus ?? null, next.updatedAt, next.completedAt ?? null, next.jobId ?? null, next.retryable ? 1 : 0, next.maxAttempts, next.timeoutMs ?? null, next.timeoutAt ?? null, next.timeoutPhase ?? null, next.serviceWarning ?? null, next.resources ? JSON.stringify(next.resources) : null, runId);
543
+ .run(next.status, next.completionPolicy, next.consumed ? 1 : 0, next.summary ?? null, next.result ? JSON.stringify(next.result) : null, next.error ?? null, next.notifiedStatus ?? null, next.acknowledgedStatus ?? null, next.updatedAt, next.completedAt ?? null, next.jobId ?? null, next.retryable ? 1 : 0, next.maxAttempts, next.timeoutMs ?? null, next.timeoutAt ?? null, next.timeoutPhase ?? null, next.serviceWarning ?? null, next.resources ? JSON.stringify(next.resources) : null, next.origin ? JSON.stringify(next.origin) : null, runId);
541
544
  return this.requireRun(runId);
542
545
  }
543
546
  getRun(runId) {
@@ -810,6 +813,8 @@ function runFromRow(row) {
810
813
  output.serviceWarning = row.service_warning;
811
814
  if (row.resource_json)
812
815
  output.resources = JSON.parse(row.resource_json);
816
+ if (row.origin_json)
817
+ output.origin = JSON.parse(row.origin_json);
813
818
  return output;
814
819
  }
815
820
  function retryDelayMs(attempts, input) {
@@ -6,6 +6,18 @@ export class PiboRunExecutionTimeoutError extends Error {
6
6
  this.name = "PiboRunExecutionTimeoutError";
7
7
  }
8
8
  }
9
+ export class PiboRunCancellationError extends Error {
10
+ constructor(message, options) {
11
+ super(message, options);
12
+ this.name = "PiboRunCancellationError";
13
+ }
14
+ }
15
+ export class PiboRunCancelledError extends Error {
16
+ constructor(message = "Yielded run was cancelled.", options) {
17
+ super(message, options);
18
+ this.name = "PiboRunCancelledError";
19
+ }
20
+ }
9
21
  export function resolveRunTimeoutMs(toolName, params) {
10
22
  if (!params || typeof params !== "object" || Array.isArray(params))
11
23
  return undefined;
@@ -7,6 +7,16 @@ function now() {
7
7
  function runTimeoutAt(createdAt, timeoutMs) {
8
8
  return timeoutMs === undefined ? undefined : new Date(Date.parse(createdAt) + timeoutMs).toISOString();
9
9
  }
10
+ function sameOrigin(left, right) {
11
+ if (!left || !right)
12
+ return left === right;
13
+ return left.eventId === right.eventId
14
+ && left.provenance.kind === right.provenance.kind
15
+ && left.provenance.jobId === right.provenance.jobId
16
+ && left.provenance.runId === right.provenance.runId
17
+ && left.provenance.cause === right.provenance.cause
18
+ && left.provenance.rootEventId === right.provenance.rootEventId;
19
+ }
10
20
  function formatTimeout(timeoutMs) {
11
21
  if (timeoutMs === undefined)
12
22
  return "its configured timeout";
@@ -83,6 +93,7 @@ export class PiboRunRegistry {
83
93
  serviceWarning: input.serviceWarning,
84
94
  resources: input.resources,
85
95
  workerId: this.workerId,
96
+ origin: input.origin,
86
97
  });
87
98
  const record = recordFromStored(stored);
88
99
  this.runs.set(record.runId, record);
@@ -108,6 +119,7 @@ export class PiboRunRegistry {
108
119
  ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs, timeoutAt: runTimeoutAt(timestamp, input.timeoutMs) } : {}),
109
120
  ...(input.serviceWarning ? { serviceWarning: input.serviceWarning } : {}),
110
121
  ...(input.resources ? { resources: structuredClone(input.resources) } : {}),
122
+ ...(input.origin ? { origin: structuredClone(input.origin) } : {}),
111
123
  };
112
124
  this.runs.set(runId, record);
113
125
  const output = snapshot(record);
@@ -245,7 +257,7 @@ export class PiboRunRegistry {
245
257
  }
246
258
  read(controllerPiboSessionId, runId) {
247
259
  const record = this.requireRunForController(controllerPiboSessionId, runId);
248
- if (terminal(record.status)) {
260
+ if (terminal(record.status) && !record.consumed) {
249
261
  record.consumed = true;
250
262
  record.updatedAt = now();
251
263
  this.options.store?.updateRun(runId, record);
@@ -277,6 +289,9 @@ export class PiboRunRegistry {
277
289
  }
278
290
  ack(controllerPiboSessionId, runId) {
279
291
  const record = this.requireRunForController(controllerPiboSessionId, runId);
292
+ const consumesTerminalRun = terminal(record.status) && !record.consumed;
293
+ if (record.acknowledgedStatus === record.status && !consumesTerminalRun)
294
+ return { ...snapshot(record), changed: false };
280
295
  record.acknowledgedStatus = record.status;
281
296
  if (terminal(record.status))
282
297
  record.consumed = true;
@@ -284,7 +299,7 @@ export class PiboRunRegistry {
284
299
  this.options.store?.updateRun(runId, record);
285
300
  const output = snapshot(record);
286
301
  this.notify({ type: "run_acknowledged", run: output });
287
- return output;
302
+ return { ...output, changed: true };
288
303
  }
289
304
  suppressNotification(controllerPiboSessionId, runId) {
290
305
  const record = this.requireRunForController(controllerPiboSessionId, runId);
@@ -306,14 +321,17 @@ export class PiboRunRegistry {
306
321
  return suppressed;
307
322
  }
308
323
  createNotification(controllerPiboSessionId, options = {}) {
309
- const records = [...this.runs.values()].filter((record) => this.needsNotification(record, controllerPiboSessionId, options));
310
- if (records.length === 0)
324
+ const pendingRecords = [...this.runs.values()].filter((record) => this.needsNotification(record, controllerPiboSessionId, options));
325
+ if (pendingRecords.length === 0)
311
326
  return undefined;
327
+ const origin = pendingRecords[0].origin;
328
+ const records = pendingRecords.filter((record) => sameOrigin(record.origin, origin));
312
329
  for (const record of records) {
313
330
  record.notifiedStatus = record.status;
314
331
  this.options.store?.updateRun(record.runId, record);
315
332
  }
316
333
  const notification = {
334
+ ...(origin ? { origin: structuredClone(origin) } : {}),
317
335
  completed: [],
318
336
  failed: [],
319
337
  timedOut: [],
@@ -461,5 +479,6 @@ function recordFromStored(record) {
461
479
  timeoutPhase: record.timeoutPhase,
462
480
  serviceWarning: record.serviceWarning,
463
481
  resources: record.resources,
482
+ origin: record.origin,
464
483
  };
465
484
  }
@@ -1,7 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import { piboStringEnum } from "../tools/schema.js";
3
3
  import { definePiboTool } from "../tools/contract.js";
4
- import { foregroundServiceWarning, hasMeaningfulTimeoutOutput, isConfiguredTimeoutError, PiboRunExecutionTimeoutError, resolveRunTimeoutMs } from "./lifecycle.js";
4
+ import { foregroundServiceWarning, hasMeaningfulTimeoutOutput, isConfiguredTimeoutError, PiboRunCancellationError, PiboRunCancelledError, PiboRunExecutionTimeoutError, resolveRunTimeoutMs } from "./lifecycle.js";
5
5
  import { PiboRunResourceLimitError, prepareYieldedRunExecution } from "./resource-isolation.js";
6
6
  function resultText(prefix, value) {
7
7
  return `${prefix}\n${JSON.stringify(value, null, 2)}`;
@@ -73,6 +73,7 @@ export function createRunToolDefinitions(yieldableTools, controller) {
73
73
  resolveExecutionSettled = resolve;
74
74
  });
75
75
  let observedOutput = false;
76
+ let cancellationFailure;
76
77
  const run = controller.startToolRun({
77
78
  toolName: tool.name,
78
79
  params: params.arguments,
@@ -95,6 +96,8 @@ export function createRunToolDefinitions(yieldableTools, controller) {
95
96
  throw processCancellationError;
96
97
  if (executionStarted)
97
98
  await waitForRunCancellationSettlement(executionSettled);
99
+ if (cancellationFailure)
100
+ throw cancellationFailure;
98
101
  },
99
102
  async execute() {
100
103
  executionStarted = true;
@@ -113,8 +116,13 @@ export function createRunToolDefinitions(yieldableTools, controller) {
113
116
  return { text, details: resultObject.details ?? result };
114
117
  }
115
118
  catch (error) {
116
- if (error instanceof PiboRunExecutionTimeoutError || error instanceof PiboRunResourceLimitError)
119
+ if (error instanceof PiboRunCancellationError)
120
+ cancellationFailure = error;
121
+ if (error instanceof PiboRunExecutionTimeoutError || error instanceof PiboRunResourceLimitError || error instanceof PiboRunCancellationError)
117
122
  throw error;
123
+ if (runAbortController.signal.aborted) {
124
+ throw new PiboRunCancelledError("Yielded run was cancelled; execution ended after cancellation.", { cause: error });
125
+ }
118
126
  if (timeoutMs !== undefined && isConfiguredTimeoutError(error))
119
127
  throw new PiboRunExecutionTimeoutError(error instanceof Error ? error.message : String(error), observedOutput ? "lifetime" : "startup");
120
128
  throw error;
@@ -240,8 +248,9 @@ export function createRunToolDefinitions(yieldableTools, controller) {
240
248
  }),
241
249
  async execute(_toolCallId, params) {
242
250
  const run = controller.ackRun(params.runId);
251
+ const prefix = run.changed ? `Acknowledged run ${run.runId}.` : `Run ${run.runId} was already acknowledged in state ${run.status}; no state changed.`;
243
252
  return {
244
- content: [{ type: "text", text: resultText(`Acknowledged run ${run.runId}.`, run) }],
253
+ content: [{ type: "text", text: resultText(prefix, run) }],
245
254
  details: run,
246
255
  };
247
256
  },
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "2.4.1",
3
+ "version": "2.4.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@pasko70/pibo",
9
- "version": "2.4.1",
9
+ "version": "2.4.2",
10
10
  "workspaces": [
11
11
  "packages/workflows"
12
12
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "2.4.1",
3
+ "version": "2.4.2",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  "packages/workflows"