@ouro.bot/cli 0.1.0-alpha.776 → 0.1.0-alpha.778

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.
@@ -57,6 +57,15 @@ const spend_ledger_1 = require("../rsvp/spend-ledger");
57
57
  const run_ledger_1 = require("../heart/run-ledger");
58
58
  const autonomy_budget_1 = require("../heart/autonomy-budget");
59
59
  const habit_lifecycle_guard_1 = require("./habit-lifecycle-guard");
60
+ function createQueueCompletion(rejectOnError) {
61
+ let resolve;
62
+ let reject;
63
+ const promise = new Promise((resolvePromise, rejectPromise) => {
64
+ resolve = resolvePromise;
65
+ reject = rejectPromise;
66
+ });
67
+ return { promise, resolve, reject, errors: [], rejectOnError };
68
+ }
60
69
  function isUsageData(value) {
61
70
  const usage = value;
62
71
  return !!usage
@@ -492,226 +501,287 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
492
501
  timer.unref?.();
493
502
  return () => clearInterval(timer);
494
503
  };
495
- async function run(reason, taskId, habitName, awaitName, trigger, privateTurnDecision, noSend, externalEvent) {
504
+ async function run(reason, taskId, habitName, awaitName, trigger, privateTurnDecision, noSend, externalEvent, dispatchId) {
505
+ const completion = createQueueCompletion(dispatchId !== undefined);
506
+ let activeCompletion = completion;
496
507
  if (running) {
497
- queue.push({ reason, taskId, habitName, awaitName, trigger, privateTurnDecision, noSend, externalEvent, ...(externalEvent ? { stopExternalEventHeartbeat: startExternalEventHeartbeat(externalEvent) } : {}) });
498
- return;
508
+ queue.push({ reason, taskId, habitName, awaitName, trigger, privateTurnDecision, noSend, externalEvent, dispatchId, completion, ...(externalEvent ? { stopExternalEventHeartbeat: startExternalEventHeartbeat(externalEvent) } : {}) });
509
+ return completion.promise;
499
510
  }
500
511
  running = true;
501
- try {
502
- let nextReason = reason;
503
- let nextTaskId = taskId;
504
- let nextHabitName = habitName;
505
- let nextAwaitName = awaitName;
506
- let nextTrigger = trigger;
507
- let nextPrivateTurnDecision = privateTurnDecision;
508
- let nextExternalEvent = externalEvent;
509
- let nextExternalEventHeartbeat = externalEvent ? startExternalEventHeartbeat(externalEvent) : undefined;
510
- let nextNoSend = reason === "habit" ? noSend : undefined;
511
- let nextHabitRun = null;
512
- let consecutiveInstinctTurns = reason === "instinct" ? 1 : 0;
513
- runLoop: do {
514
- const currentReason = nextReason;
515
- const currentHabitName = nextHabitName;
516
- const currentTrigger = nextTrigger ?? "overdue";
517
- const currentNoSend = nextNoSend;
518
- const currentExternalEvent = nextExternalEvent;
519
- const currentExternalEventHeartbeat = nextExternalEventHeartbeat;
520
- nextExternalEventHeartbeat = undefined;
521
- const currentHabitRun = currentReason === "habit" && currentHabitName
522
- ? nextHabitRun && nextHabitRun.habit.name === currentHabitName
523
- ? nextHabitRun
524
- : await prepareHabitRun(currentHabitName, currentTrigger, new Date(nowSource()).toISOString())
525
- : null;
526
- nextHabitRun = null;
527
- let currentHabitRunFinalized = false;
528
- const finalizeCurrentHabitRun = () => {
529
- if (!currentHabitRun || currentHabitRunFinalized)
530
- return;
531
- recordHabitCompletion(currentHabitRun, new Date(nowSource()).toISOString());
532
- currentHabitRunFinalized = true;
533
- };
534
- const turnErrors = [];
535
- if (!(currentReason === "habit" && currentHabitName === "heartbeat")) {
536
- clearHeartbeatRestShield();
537
- }
538
- let turnResult;
539
- let blockedAutonomyTurn = false;
540
- try {
541
- const turnStartedAt = new Date(nowSource()).toISOString();
542
- const autonomyDecision = currentHabitRun && !currentHabitRun.blockedReason
543
- ? reserveHabitAutonomyBudget(currentHabitRun, turnStartedAt)
512
+ void (async () => {
513
+ try {
514
+ let nextReason = reason;
515
+ let nextTaskId = taskId;
516
+ let nextHabitName = habitName;
517
+ let nextAwaitName = awaitName;
518
+ let nextTrigger = trigger;
519
+ let nextPrivateTurnDecision = privateTurnDecision;
520
+ let nextExternalEvent = externalEvent;
521
+ let nextExternalEventHeartbeat = externalEvent ? startExternalEventHeartbeat(externalEvent) : undefined;
522
+ let nextCompletion = completion;
523
+ let nextNoSend = reason === "habit" ? noSend : undefined;
524
+ let nextHabitRun = null;
525
+ let consecutiveInstinctTurns = reason === "instinct" ? 1 : 0;
526
+ runLoop: do {
527
+ const currentReason = nextReason;
528
+ const currentHabitName = nextHabitName;
529
+ const currentTrigger = nextTrigger ?? "overdue";
530
+ const currentNoSend = nextNoSend;
531
+ const currentExternalEvent = nextExternalEvent;
532
+ const currentExternalEventHeartbeat = nextExternalEventHeartbeat;
533
+ const currentCompletion = nextCompletion;
534
+ activeCompletion = currentCompletion;
535
+ nextExternalEventHeartbeat = undefined;
536
+ nextCompletion = undefined;
537
+ const currentHabitRun = currentReason === "habit" && currentHabitName
538
+ ? nextHabitRun && nextHabitRun.habit.name === currentHabitName
539
+ ? nextHabitRun
540
+ : await prepareHabitRun(currentHabitName, currentTrigger, new Date(nowSource()).toISOString())
544
541
  : null;
545
- if (currentHabitRun)
546
- recordHabitStartIfNeeded(currentHabitRun);
547
- if (currentHabitRun?.blockedReason) {
548
- blockedAutonomyTurn = true;
549
- turnResult = {
550
- turnOutcome: "blocked",
551
- reason: currentHabitRun.blockedReason,
552
- };
542
+ nextHabitRun = null;
543
+ let currentHabitRunFinalized = false;
544
+ const finalizeCurrentHabitRun = () => {
545
+ if (!currentHabitRun || currentHabitRunFinalized)
546
+ return;
547
+ recordHabitCompletion(currentHabitRun, new Date(nowSource()).toISOString());
548
+ currentHabitRunFinalized = true;
549
+ };
550
+ const turnErrors = [];
551
+ if (!(currentReason === "habit" && currentHabitName === "heartbeat")) {
553
552
  clearHeartbeatRestShield();
554
553
  }
555
- else if (autonomyDecision && !autonomyDecision.allowed) {
556
- blockedAutonomyTurn = true;
557
- const reason = `autonomy budget blocked: ${autonomyDecision.reason}`;
558
- turnErrors.push(reason);
559
- turnResult = {
560
- turnOutcome: "blocked",
561
- reason,
562
- autonomyReceiptId: autonomyDecision.receiptId,
563
- };
564
- clearHeartbeatRestShield();
565
- }
566
- else {
567
- const turnOptions = {
568
- reason: nextReason,
569
- taskId: nextTaskId,
570
- habitName: nextHabitName,
571
- awaitName: nextAwaitName,
572
- ...(currentNoSend ? { noSend: true } : {}),
573
- ...(nextPrivateTurnDecision ? { privateTurnDecision: nextPrivateTurnDecision } : {}),
574
- ...(nextExternalEvent ? { externalEvent: nextExternalEvent } : {}),
575
- ...(currentHabitRun
576
- ? {
577
- trigger: currentHabitRun.trigger,
578
- preparedHabit: {
579
- runId: currentHabitRun.runId,
554
+ let turnResult;
555
+ let blockedAutonomyTurn = false;
556
+ try {
557
+ const turnStartedAt = new Date(nowSource()).toISOString();
558
+ const autonomyDecision = currentHabitRun && !currentHabitRun.blockedReason
559
+ ? reserveHabitAutonomyBudget(currentHabitRun, turnStartedAt)
560
+ : null;
561
+ if (currentHabitRun)
562
+ recordHabitStartIfNeeded(currentHabitRun);
563
+ if (currentHabitRun?.blockedReason) {
564
+ blockedAutonomyTurn = true;
565
+ turnResult = {
566
+ turnOutcome: "blocked",
567
+ reason: currentHabitRun.blockedReason,
568
+ };
569
+ clearHeartbeatRestShield();
570
+ }
571
+ else if (autonomyDecision && !autonomyDecision.allowed) {
572
+ blockedAutonomyTurn = true;
573
+ const reason = `autonomy budget blocked: ${autonomyDecision.reason}`;
574
+ turnErrors.push(reason);
575
+ turnResult = {
576
+ turnOutcome: "blocked",
577
+ reason,
578
+ autonomyReceiptId: autonomyDecision.receiptId,
579
+ };
580
+ clearHeartbeatRestShield();
581
+ }
582
+ else {
583
+ const turnOptions = {
584
+ reason: nextReason,
585
+ taskId: nextTaskId,
586
+ habitName: nextHabitName,
587
+ awaitName: nextAwaitName,
588
+ ...(currentNoSend ? { noSend: true } : {}),
589
+ ...(nextPrivateTurnDecision ? { privateTurnDecision: nextPrivateTurnDecision } : {}),
590
+ ...(nextExternalEvent ? { externalEvent: nextExternalEvent } : {}),
591
+ ...(currentHabitRun
592
+ ? {
580
593
  trigger: currentHabitRun.trigger,
581
- operationId: currentHabitRun.operationId,
582
- habit: currentHabitRun.habit,
583
- priorSessionSummary: currentHabitRun.priorSessionSummary,
584
- },
585
- habitSession: {
586
- runId: currentHabitRun.runId,
587
- sessionPath: currentHabitRun.paths.sessionPath,
588
- pendingDir: currentHabitRun.paths.pendingDir,
589
- permissionEnvelope: currentHabitRun.permissionEnvelope,
590
- toolPolicy: currentHabitRun.toolPolicy,
591
- ...(currentHabitRun.rsvpPolicy ? { rsvpPolicy: currentHabitRun.rsvpPolicy } : {}),
592
- friendStore: currentHabitRun.friendStore,
593
- recordProducedRef: (ref) => { currentHabitRun.producedRefs.push(ref); },
594
- recordSurfaceAttempt: (attempt) => { currentHabitRun.surfaceAttempts.push(attempt); },
595
- recordError: (error) => { currentHabitRun.errors.push(error); },
596
- },
597
- }
598
- : {}),
599
- };
600
- turnResult = await runTurn(turnOptions);
594
+ preparedHabit: {
595
+ runId: currentHabitRun.runId,
596
+ trigger: currentHabitRun.trigger,
597
+ operationId: currentHabitRun.operationId,
598
+ habit: currentHabitRun.habit,
599
+ priorSessionSummary: currentHabitRun.priorSessionSummary,
600
+ },
601
+ habitSession: {
602
+ runId: currentHabitRun.runId,
603
+ sessionPath: currentHabitRun.paths.sessionPath,
604
+ pendingDir: currentHabitRun.paths.pendingDir,
605
+ permissionEnvelope: currentHabitRun.permissionEnvelope,
606
+ toolPolicy: currentHabitRun.toolPolicy,
607
+ ...(currentHabitRun.rsvpPolicy ? { rsvpPolicy: currentHabitRun.rsvpPolicy } : {}),
608
+ friendStore: currentHabitRun.friendStore,
609
+ recordProducedRef: (ref) => { currentHabitRun.producedRefs.push(ref); },
610
+ recordSurfaceAttempt: (attempt) => { currentHabitRun.surfaceAttempts.push(attempt); },
611
+ recordError: (error) => { currentHabitRun.errors.push(error); },
612
+ },
613
+ }
614
+ : {}),
615
+ };
616
+ turnResult = await runTurn(turnOptions);
617
+ }
601
618
  }
602
- }
603
- catch (error) {
604
- clearHeartbeatRestShield();
605
- turnErrors.push(error instanceof Error ? error.message : String(error));
606
- (0, runtime_1.emitNervesEvent)({
607
- level: "error",
608
- component: "senses",
609
- event: "senses.private_runtime_worker_error",
610
- message: "private-runtime worker turn failed",
611
- meta: {
612
- reason: nextReason,
613
- error: error instanceof Error ? error.message : String(error),
614
- },
615
- });
616
- }
617
- currentExternalEventHeartbeat?.();
618
- if (currentReason === "habit" && currentHabitName === "heartbeat") {
619
- heartbeatOkRestedAt = isHeartbeatOkRestResult(turnResult) ? nowSource() : null;
620
- }
621
- if (currentHabitRun) {
622
- currentHabitRun.results.push(turnResult);
623
- currentHabitRun.errors.push(...turnErrors);
624
- }
625
- // Drain queue first. Externally-queued work resets the instinct cap
626
- // because a real outside trigger arrived between turns.
627
- while (queue.length > 0) {
628
- const next = queue.shift();
629
- if (next.reason === "habit" && next.habitName === "heartbeat" && shouldReuseHeartbeatOkRest(next.habitName)) {
630
- finalizeCurrentHabitRun();
631
- await reuseHeartbeatOkRest(next.habitName);
632
- continue;
633
- }
634
- if (!(next.reason === "habit" && next.habitName === "heartbeat")) {
619
+ catch (error) {
635
620
  clearHeartbeatRestShield();
636
- }
637
- finalizeCurrentHabitRun();
638
- nextReason = next.reason;
639
- nextTaskId = next.taskId;
640
- nextHabitName = next.habitName;
641
- nextAwaitName = next.awaitName;
642
- nextTrigger = next.trigger;
643
- nextPrivateTurnDecision = next.privateTurnDecision;
644
- nextExternalEvent = next.externalEvent;
645
- nextExternalEventHeartbeat = next.stopExternalEventHeartbeat;
646
- nextNoSend = next.reason === "habit" ? next.noSend : undefined;
647
- consecutiveInstinctTurns = nextReason === "instinct" ? consecutiveInstinctTurns + 1 : 0;
648
- continue runLoop;
649
- }
650
- if (blockedAutonomyTurn) {
651
- finalizeCurrentHabitRun();
652
- break;
653
- }
654
- // Then check hasPendingWork fallback. This is the loop site: any
655
- // tool that writes to the private-runtime pending dir during a turn
656
- // would cause hasPendingWork() to be true here, producing a
657
- // self-sustaining "instinct" loop with no external input. Cap it.
658
- if (hasPendingWork(currentHabitRun?.paths.pendingDir)) {
659
- if (currentExternalEvent && (turnErrors.length > 0
660
- || (turnResult && typeof turnResult === "object" && "turnOutcome" in turnResult && turnResult.turnOutcome === "errored"))) {
661
- finalizeCurrentHabitRun();
662
- break;
663
- }
664
- clearHeartbeatRestShield();
665
- if (consecutiveInstinctTurns >= exports.MAX_CONSECUTIVE_INSTINCT_TURNS) {
621
+ turnErrors.push(error instanceof Error ? error.message : String(error));
666
622
  (0, runtime_1.emitNervesEvent)({
667
- level: "warn",
623
+ level: "error",
668
624
  component: "senses",
669
- event: "senses.private_runtime_worker_instinct_loop_capped",
670
- message: "private-runtime worker stopped chaining instinct turns; pending work remains for next external trigger",
625
+ event: "senses.private_runtime_worker_error",
626
+ message: "private-runtime worker turn failed",
671
627
  meta: {
672
- consecutiveInstinctTurns,
673
- cap: exports.MAX_CONSECUTIVE_INSTINCT_TURNS,
674
- lastReason: nextReason,
628
+ reason: nextReason,
629
+ error: error instanceof Error ? error.message : String(error),
675
630
  },
676
631
  });
677
- finalizeCurrentHabitRun();
678
- break;
679
632
  }
680
- if (currentReason === "habit" && currentHabitName && currentHabitRun) {
681
- consecutiveInstinctTurns += 1;
682
- nextReason = "habit";
683
- nextTaskId = undefined;
684
- nextHabitName = currentHabitName;
685
- nextAwaitName = undefined;
686
- nextTrigger = currentTrigger;
687
- nextPrivateTurnDecision = undefined;
688
- nextExternalEvent = undefined;
689
- nextExternalEventHeartbeat = undefined;
690
- nextNoSend = currentNoSend;
691
- nextHabitRun = currentHabitRun;
633
+ currentExternalEventHeartbeat?.();
634
+ if (currentExternalEvent) {
635
+ const turnOutcome = turnResult && typeof turnResult === "object" && "turnOutcome" in turnResult
636
+ ? turnResult.turnOutcome
637
+ : undefined;
638
+ const failureReason = turnErrors.length > 0 || turnOutcome === "errored"
639
+ ? "private-runtime external-event turn failed"
640
+ : blockedAutonomyTurn || turnOutcome === "blocked" || turnOutcome === "suspended"
641
+ ? "external-event turn was blocked"
642
+ : "external-event turn completed without a terminal disposition";
643
+ let settledIncomplete = false;
644
+ for (const member of [currentExternalEvent, ...(currentExternalEvent.relatedEvents ?? [])]) {
645
+ const result = (0, router_1.settleExternalEventFailureIfOwned)(member.recordPath, {
646
+ owner: member.claimOwner,
647
+ expectedGeneration: member.generation,
648
+ error: failureReason,
649
+ });
650
+ settledIncomplete = result.settled || settledIncomplete;
651
+ }
652
+ if (settledIncomplete && turnErrors.length === 0)
653
+ turnErrors.push(failureReason);
654
+ }
655
+ if (currentReason === "habit" && currentHabitName === "heartbeat") {
656
+ heartbeatOkRestedAt = isHeartbeatOkRestResult(turnResult) ? nowSource() : null;
657
+ }
658
+ if (currentHabitRun) {
659
+ currentHabitRun.results.push(turnResult);
660
+ currentHabitRun.errors.push(...turnErrors);
692
661
  }
693
- else {
662
+ currentCompletion?.errors.push(...turnErrors);
663
+ const settleCurrentCompletion = () => {
664
+ /* v8 ignore next -- every run and queued entry is constructed with a completion @preserve */
665
+ if (!currentCompletion)
666
+ return;
667
+ if (currentCompletion.rejectOnError && currentCompletion.errors.length > 0)
668
+ currentCompletion.reject(new Error(currentCompletion.errors.join("; ")));
669
+ else
670
+ currentCompletion.resolve();
671
+ if (activeCompletion === currentCompletion)
672
+ activeCompletion = undefined;
673
+ };
674
+ // Drain queue first. Externally-queued work resets the instinct cap
675
+ // because a real outside trigger arrived between turns.
676
+ if (queue.length > 0)
677
+ settleCurrentCompletion();
678
+ while (queue.length > 0) {
679
+ const next = queue.shift();
680
+ activeCompletion = next.completion;
681
+ if (next.reason === "habit" && next.habitName === "heartbeat" && shouldReuseHeartbeatOkRest(next.habitName)) {
682
+ finalizeCurrentHabitRun();
683
+ await reuseHeartbeatOkRest(next.habitName);
684
+ next.completion.resolve();
685
+ /* v8 ignore next -- activeCompletion was assigned from this exact queue entry immediately above @preserve */
686
+ if (activeCompletion === next.completion)
687
+ activeCompletion = undefined;
688
+ continue;
689
+ }
690
+ if (!(next.reason === "habit" && next.habitName === "heartbeat")) {
691
+ clearHeartbeatRestShield();
692
+ }
694
693
  finalizeCurrentHabitRun();
695
- consecutiveInstinctTurns += 1;
696
- nextReason = "instinct";
697
- nextTaskId = undefined;
698
- nextHabitName = undefined;
699
- nextAwaitName = undefined;
700
- nextTrigger = undefined;
701
- nextPrivateTurnDecision = undefined;
702
- nextExternalEvent = undefined;
703
- nextExternalEventHeartbeat = undefined;
704
- nextNoSend = undefined;
694
+ nextReason = next.reason;
695
+ nextTaskId = next.taskId;
696
+ nextHabitName = next.habitName;
697
+ nextAwaitName = next.awaitName;
698
+ nextTrigger = next.trigger;
699
+ nextPrivateTurnDecision = next.privateTurnDecision;
700
+ nextExternalEvent = next.externalEvent;
701
+ nextExternalEventHeartbeat = next.stopExternalEventHeartbeat;
702
+ nextCompletion = next.completion;
703
+ nextNoSend = next.reason === "habit" ? next.noSend : undefined;
704
+ consecutiveInstinctTurns = nextReason === "instinct" ? consecutiveInstinctTurns + 1 : 0;
705
+ continue runLoop;
705
706
  }
706
- continue;
707
- }
708
- finalizeCurrentHabitRun();
709
- break;
710
- } while (true);
711
- }
712
- finally {
713
- running = false;
714
- }
707
+ if (blockedAutonomyTurn) {
708
+ finalizeCurrentHabitRun();
709
+ settleCurrentCompletion();
710
+ break;
711
+ }
712
+ // Then check hasPendingWork fallback. This is the loop site: any
713
+ // tool that writes to the private-runtime pending dir during a turn
714
+ // would cause hasPendingWork() to be true here, producing a
715
+ // self-sustaining "instinct" loop with no external input. Cap it.
716
+ if (hasPendingWork(currentHabitRun?.paths.pendingDir)) {
717
+ if (currentExternalEvent && (turnErrors.length > 0
718
+ || (turnResult && typeof turnResult === "object" && "turnOutcome" in turnResult && turnResult.turnOutcome === "errored"))) {
719
+ finalizeCurrentHabitRun();
720
+ settleCurrentCompletion();
721
+ break;
722
+ }
723
+ clearHeartbeatRestShield();
724
+ if (consecutiveInstinctTurns >= exports.MAX_CONSECUTIVE_INSTINCT_TURNS) {
725
+ (0, runtime_1.emitNervesEvent)({
726
+ level: "warn",
727
+ component: "senses",
728
+ event: "senses.private_runtime_worker_instinct_loop_capped",
729
+ message: "private-runtime worker stopped chaining instinct turns; pending work remains for next external trigger",
730
+ meta: {
731
+ consecutiveInstinctTurns,
732
+ cap: exports.MAX_CONSECUTIVE_INSTINCT_TURNS,
733
+ lastReason: nextReason,
734
+ },
735
+ });
736
+ finalizeCurrentHabitRun();
737
+ settleCurrentCompletion();
738
+ break;
739
+ }
740
+ if (currentReason === "habit" && currentHabitName && currentHabitRun) {
741
+ consecutiveInstinctTurns += 1;
742
+ nextReason = "habit";
743
+ nextTaskId = undefined;
744
+ nextHabitName = currentHabitName;
745
+ nextAwaitName = undefined;
746
+ nextTrigger = currentTrigger;
747
+ nextPrivateTurnDecision = undefined;
748
+ nextExternalEvent = undefined;
749
+ nextExternalEventHeartbeat = undefined;
750
+ nextNoSend = currentNoSend;
751
+ nextHabitRun = currentHabitRun;
752
+ }
753
+ else {
754
+ finalizeCurrentHabitRun();
755
+ consecutiveInstinctTurns += 1;
756
+ nextReason = "instinct";
757
+ nextTaskId = undefined;
758
+ nextHabitName = undefined;
759
+ nextAwaitName = undefined;
760
+ nextTrigger = undefined;
761
+ nextPrivateTurnDecision = undefined;
762
+ nextExternalEvent = undefined;
763
+ nextExternalEventHeartbeat = undefined;
764
+ nextNoSend = undefined;
765
+ }
766
+ nextCompletion = currentCompletion;
767
+ continue;
768
+ }
769
+ finalizeCurrentHabitRun();
770
+ settleCurrentCompletion();
771
+ break;
772
+ } while (true);
773
+ }
774
+ finally {
775
+ running = false;
776
+ }
777
+ })().catch((error) => {
778
+ activeCompletion?.reject(error);
779
+ for (const queued of queue.splice(0)) {
780
+ queued.stopExternalEventHeartbeat?.();
781
+ queued.completion.reject(error);
782
+ }
783
+ });
784
+ return completion.promise;
715
785
  }
716
786
  async function handleMessage(message) {
717
787
  if (!message || typeof message !== "object")
@@ -725,7 +795,7 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
725
795
  return;
726
796
  }
727
797
  recordHabitFireForRecursion(habitName);
728
- await run("habit", undefined, maybeMessage.habitName, undefined, maybeMessage.trigger ?? "overdue", maybeMessage.privateTurnDecision, maybeMessage.noSend);
798
+ await run("habit", undefined, maybeMessage.habitName, undefined, maybeMessage.trigger ?? "overdue", maybeMessage.privateTurnDecision, maybeMessage.noSend, undefined, maybeMessage.dispatchId);
729
799
  return;
730
800
  }
731
801
  if (maybeMessage.type === "await") {
@@ -733,7 +803,7 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
733
803
  /* v8 ignore next -- defensive fallback: live await dispatch always sets awaitName @preserve */
734
804
  const awaitName = maybeMessage.awaitName ?? "(unnamed)";
735
805
  recordHabitFireForRecursion(`await:${awaitName}`);
736
- await run("await", undefined, undefined, maybeMessage.awaitName, undefined, maybeMessage.privateTurnDecision);
806
+ await run("await", undefined, undefined, maybeMessage.awaitName, undefined, maybeMessage.privateTurnDecision, undefined, undefined, maybeMessage.dispatchId);
737
807
  return;
738
808
  }
739
809
  if (maybeMessage.type === "heartbeat") {
@@ -743,25 +813,34 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
743
813
  return;
744
814
  }
745
815
  recordHabitFireForRecursion("heartbeat");
746
- await run("habit", undefined, "heartbeat", undefined, "overdue", maybeMessage.privateTurnDecision);
816
+ await run("habit", undefined, "heartbeat", undefined, "overdue", maybeMessage.privateTurnDecision, undefined, undefined, maybeMessage.dispatchId);
747
817
  return;
748
818
  }
749
819
  if (maybeMessage.type === "poke") {
750
820
  clearHeartbeatRestShield();
751
- await run("instinct", maybeMessage.taskId, undefined, undefined, undefined, maybeMessage.privateTurnDecision);
821
+ await run("instinct", maybeMessage.taskId, undefined, undefined, undefined, maybeMessage.privateTurnDecision, undefined, undefined, maybeMessage.dispatchId);
752
822
  return;
753
823
  }
754
824
  if (maybeMessage.type === "chat" ||
755
825
  maybeMessage.type === "message") {
756
826
  clearHeartbeatRestShield();
757
- await run("instinct", undefined, undefined, undefined, undefined, maybeMessage.privateTurnDecision, undefined, maybeMessage.externalEvent);
827
+ await run("instinct", undefined, undefined, undefined, undefined, maybeMessage.privateTurnDecision, undefined, maybeMessage.externalEvent, maybeMessage.dispatchId);
758
828
  return;
759
829
  }
760
830
  if (maybeMessage.type === "shutdown") {
761
831
  process.exit(0);
762
832
  }
763
833
  }
764
- return { run, handleMessage };
834
+ function cancelMessage(dispatchId) {
835
+ const index = queue.findIndex((entry) => entry.dispatchId === dispatchId);
836
+ if (index < 0)
837
+ return false;
838
+ const [cancelled] = queue.splice(index, 1);
839
+ cancelled?.stopExternalEventHeartbeat?.();
840
+ cancelled?.completion.reject(new Error(`Private-runtime dispatch ${dispatchId} was cancelled.`));
841
+ return true;
842
+ }
843
+ return { run, handleMessage, cancelMessage };
765
844
  }
766
845
  async function startPrivateRuntimeWorker(options = {}) {
767
846
  const worker = createPrivateRuntimeWorker();
@@ -837,10 +837,13 @@ function buildHeldReturnWakeMessage() {
837
837
  }
838
838
  function buildExternalEventLeaseMessage(event) {
839
839
  const members = [event, ...(event.relatedEvents ?? [])];
840
+ const dispositionInstruction = members.length === 1
841
+ ? "Investigate the current observation, then call external_event_disposition once for this lease before settling."
842
+ : "Investigate the current observations, then call external_event_disposition once with one bounded batch containing every listed lease before settling.";
840
843
  return [
841
844
  "[current external-event disposition contract]",
842
845
  "This exact lease frame is authoritative for this turn. Ignore older receipts, failed tool arguments, held-work summaries, and checkpoints when choosing disposition arguments.",
843
- "Investigate the current observation, then call external_event_disposition once for every listed lease before settling.",
846
+ dispositionInstruction,
844
847
  ...members.flatMap((member, index) => [
845
848
  "",
846
849
  `lease ${index + 1}:`,