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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/changelog.json CHANGED
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.777",
6
+ "changes": [
7
+ "Hold Sanctuary external-event ownership until source-specific machine credentials are ready, acknowledge exact private-runtime turns over existing IPC, complete 32-member dispositions under existing tool limits, and immediately settle failed owned claims."
8
+ ]
9
+ },
4
10
  {
5
11
  "version": "0.1.0-alpha.776",
6
12
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.776",
2
+ "runtimeVersion": "0.1.0-alpha.777",
3
3
  "bundleSchemaVersion": 3,
4
- "lastUpdated": "2026-08-30T00:00:00.000Z"
4
+ "lastUpdated": "2026-09-03T00:00:00.000Z"
5
5
  }
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0"?>
2
2
  <Container version="2">
3
3
  <Name>Mendelow Cloud Butler</Name>
4
- <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.776</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.777</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -90,7 +90,18 @@ function isPrivateRuntimeWorkMessage(message) {
90
90
  || type === "shutdown"
91
91
  || type === "poke"
92
92
  || type === "chat"
93
- || type === "message";
93
+ || type === "message"
94
+ || type === "ouro.privateRuntimeDispatchCancel";
95
+ }
96
+ function safeDispatchError(error) {
97
+ const message = error instanceof Error ? error.message : String(error);
98
+ if (message.startsWith("[agent-runnable]"))
99
+ return "[agent-runnable] Private-runtime work failed; inspect the agent's redacted runtime diagnostics and run the indicated repair.";
100
+ if (message.startsWith("[human-required]"))
101
+ return "[human-required] Private-runtime work needs human repair; inspect the agent's redacted runtime diagnostics.";
102
+ if (message.startsWith("[human-choice]"))
103
+ return "[human-choice] Private-runtime work needs an explicit human decision; inspect the agent's redacted runtime diagnostics.";
104
+ return "private-runtime worker failed; inspect the worker's redacted runtime diagnostics";
94
105
  }
95
106
  function forwardOrBufferRuntimeMessage(message) {
96
107
  if (isRuntimeCredentialBootstrapMessage(message)) {
@@ -177,16 +188,68 @@ Promise.resolve().then(() => __importStar(require("./runtime-credentials"))).the
177
188
  .catch(() => undefined);
178
189
  }
179
190
  const { startPrivateRuntimeWorker } = await Promise.resolve().then(() => __importStar(require("../senses/private-runtime-worker")));
180
- const bufferedMessages = ipcState.bufferedMessages.splice(0);
181
191
  const worker = await startPrivateRuntimeWorker({
182
192
  attachProcessListeners: false,
183
- bufferedMessages,
193
+ bufferedMessages: [],
184
194
  });
195
+ const { ensureSanctuarySourceRuntimeReady } = await Promise.resolve().then(() => __importStar(require("../senses/sanctuary-runtime")));
196
+ const readinessPendingDispatchIds = new Set();
197
+ const cancelledReadinessDispatchIds = new Set();
198
+ const handleWorkerMessage = async (message) => {
199
+ /* v8 ignore next -- sole caller is gated by isPrivateRuntimeWorkMessage, which rejects falsy payloads @preserve */
200
+ const envelope = message && typeof message === "object" ? message : null;
201
+ if (envelope?.type === "ouro.privateRuntimeDispatchCancel" && typeof envelope.dispatchId === "string") {
202
+ if (readinessPendingDispatchIds.has(envelope.dispatchId))
203
+ cancelledReadinessDispatchIds.add(envelope.dispatchId);
204
+ else
205
+ worker.cancelMessage(envelope.dispatchId);
206
+ return;
207
+ }
208
+ const dispatchId = typeof envelope?.dispatchId === "string" ? envelope.dispatchId : null;
209
+ try {
210
+ if (typeof envelope?.externalEvent?.source === "string") {
211
+ if (dispatchId)
212
+ readinessPendingDispatchIds.add(dispatchId);
213
+ await ensureSanctuarySourceRuntimeReady(agentName, envelope.externalEvent.source);
214
+ if (dispatchId)
215
+ readinessPendingDispatchIds.delete(dispatchId);
216
+ if (dispatchId && cancelledReadinessDispatchIds.delete(dispatchId))
217
+ return;
218
+ }
219
+ await worker.handleMessage(message);
220
+ if (dispatchId)
221
+ process.send?.({ type: "ouro.privateRuntimeDispatchResult", dispatchId, ok: true });
222
+ }
223
+ catch (error) {
224
+ if (dispatchId) {
225
+ process.send?.({
226
+ type: "ouro.privateRuntimeDispatchResult",
227
+ dispatchId,
228
+ ok: false,
229
+ error: safeDispatchError(error),
230
+ });
231
+ return;
232
+ }
233
+ (0, runtime_1.emitNervesEvent)({
234
+ level: "error",
235
+ component: "senses",
236
+ event: "senses.private_runtime_dispatch_error",
237
+ message: "private-runtime work message failed",
238
+ meta: { agentName, error: error instanceof Error ? error.message : String(error) },
239
+ });
240
+ }
241
+ finally {
242
+ if (dispatchId) {
243
+ readinessPendingDispatchIds.delete(dispatchId);
244
+ cancelledReadinessDispatchIds.delete(dispatchId);
245
+ }
246
+ }
247
+ };
185
248
  ipcState.workerMessageHandler = (message) => {
186
- void worker.handleMessage(message);
249
+ void handleWorkerMessage(message);
187
250
  };
188
- const messagesBufferedDuringStart = ipcState.bufferedMessages.splice(0);
189
- for (const message of messagesBufferedDuringStart) {
251
+ const bufferedMessages = ipcState.bufferedMessages.splice(0);
252
+ for (const message of bufferedMessages) {
190
253
  ipcState.workerMessageHandler(message);
191
254
  }
192
255
  })
@@ -633,6 +633,7 @@ class OuroDaemon {
633
633
  schedulerFireVerifier;
634
634
  schedulerFireConsumer;
635
635
  orphanStartupDrain;
636
+ externalEventSourceReadiness;
636
637
  constructor(options) {
637
638
  this.socketPath = options.socketPath;
638
639
  this.processManager = options.processManager;
@@ -654,6 +655,10 @@ class OuroDaemon {
654
655
  this.schedulerFireVerifier = options.schedulerFireVerifier;
655
656
  this.schedulerFireConsumer = options.schedulerFireConsumer;
656
657
  this.orphanStartupDrain = options.orphanStartupDrain ?? drainOrphanProcessesBeforeStartup;
658
+ this.externalEventSourceReadiness = options.externalEventSourceReadiness ?? (async (agent, source) => {
659
+ const { ensureSanctuarySourceRuntimeReady } = await Promise.resolve().then(() => __importStar(require("../../senses/sanctuary-runtime")));
660
+ await ensureSanctuarySourceRuntimeReady(agent, source);
661
+ });
657
662
  }
658
663
  /* v8 ignore start -- default mailbox server wiring: production-only path, tests inject mailboxServerFactory stub instead. startMailboxHttpServer itself has full coverage in mailbox-http.test.ts @preserve */
659
664
  createDefaultMailboxServer() {
@@ -1561,6 +1566,10 @@ class OuroDaemon {
1561
1566
  const claimed = [];
1562
1567
  let failureClass;
1563
1568
  try {
1569
+ for (const key of new Set(records.map((record) => `${record.agent}\0${record.source}`))) {
1570
+ const [agent, source] = key.split("\0");
1571
+ await this.externalEventSourceReadiness(agent, source);
1572
+ }
1564
1573
  for (const record of records) {
1565
1574
  const owner = `external-event:${record.agent}:${record.source}:${record.eventId}:generation:${record.generation}:attempt:${record.attemptCount + 1}`;
1566
1575
  claimed.push((0, router_1.claimExternalEvent)(record.recordPath, { owner, expectedVersion: record.version, expectedGeneration: record.generation }));
@@ -1603,14 +1612,11 @@ class OuroDaemon {
1603
1612
  }
1604
1613
  catch (error) {
1605
1614
  for (const record of claimed) {
1606
- const latest = (0, router_1.readExternalEventRecord)(record.recordPath);
1607
- if (latest.executionState === "running" && latest.claimOwner === record.claimOwner) {
1608
- (0, router_1.failExternalEventAttempt)(latest.recordPath, {
1609
- owner: record.claimOwner, expectedVersion: latest.version, expectedGeneration: latest.generation,
1610
- error: error instanceof Error ? error.message : String(error),
1611
- ...(failureClass ? { failureClass } : {}),
1612
- });
1613
- }
1615
+ (0, router_1.settleExternalEventFailureIfOwned)(record.recordPath, {
1616
+ owner: record.claimOwner, expectedGeneration: record.generation,
1617
+ error: failureClass ? `external-event private-runtime dispatch failed: ${failureClass}` : "external-event private-runtime dispatch failed",
1618
+ ...(failureClass ? { failureClass } : {}),
1619
+ });
1614
1620
  }
1615
1621
  throw error;
1616
1622
  }
@@ -1629,6 +1635,39 @@ class OuroDaemon {
1629
1635
  this.privilegedEventScanner({ spoolRoot: this.privilegedEventSpoolRoot, eventRoot: this.externalEventRootPath() });
1630
1636
  }
1631
1637
  const statuses = (0, router_1.listExternalEventStatus)(this.externalEventRootPath());
1638
+ const readinessKeys = new Set();
1639
+ for (const status of statuses) {
1640
+ if (status.corrupt)
1641
+ continue;
1642
+ const candidate = (0, router_1.readExternalEventRecord)(status.recordPath);
1643
+ if (candidate.dispatchEnabled === false)
1644
+ continue;
1645
+ const retryDue = candidate.executionState === "retry_wait" && candidate.nextAttemptAt !== null && Date.parse(candidate.nextAttemptAt) <= Date.parse(now);
1646
+ const leaseExpired = candidate.executionState === "running" && candidate.claimExpiresAt !== null && Date.parse(candidate.claimExpiresAt) <= Date.parse(now);
1647
+ const recoverableDeadLetter = candidate.executionState === "dead_letter"
1648
+ && (0, router_1.externalEventRecoveryFailure)(candidate) !== null
1649
+ && candidate.recoveryGrant?.generation !== candidate.generation;
1650
+ const dispatchable = candidate.executionState === "received" || candidate.executionState === "queued" || retryDue || leaseExpired || recoverableDeadLetter;
1651
+ if (dispatchable && (candidate.source === "sanctuary-health" || candidate.source === "sanctuary-usenet"))
1652
+ readinessKeys.add(`${candidate.agent}\0${candidate.source}`);
1653
+ }
1654
+ const blockedReadinessKeys = new Set();
1655
+ for (const key of readinessKeys) {
1656
+ const [agent, source] = key.split("\0");
1657
+ try {
1658
+ await this.externalEventSourceReadiness(agent, source);
1659
+ }
1660
+ catch (error) {
1661
+ blockedReadinessKeys.add(key);
1662
+ (0, runtime_1.emitNervesEvent)({
1663
+ level: "warn",
1664
+ component: "daemon",
1665
+ event: "daemon.external_event_readiness_blocked",
1666
+ message: "external-event reconciliation is waiting for source runtime readiness",
1667
+ meta: { agent, source, error: (error instanceof Error ? error.message : String(error)).slice(0, 1_000) },
1668
+ });
1669
+ }
1670
+ }
1632
1671
  const dueRecords = [];
1633
1672
  const providerEvidenceByAgent = new Map();
1634
1673
  const runtimeEvidenceByAgent = new Map();
@@ -1639,6 +1678,8 @@ class OuroDaemon {
1639
1678
  let record = (0, router_1.readExternalEventRecord)(status.recordPath);
1640
1679
  if (record.dispatchEnabled === false)
1641
1680
  continue;
1681
+ if (blockedReadinessKeys.has(`${record.agent}\0${record.source}`))
1682
+ continue;
1642
1683
  if (record.executionState === "dead_letter") {
1643
1684
  const failure = (0, router_1.externalEventRecoveryFailure)(record);
1644
1685
  if (!failure || record.recoveryGrant?.generation === record.generation)
@@ -1765,7 +1806,11 @@ class OuroDaemon {
1765
1806
  }
1766
1807
  await beforeDispatch?.(decision);
1767
1808
  await this.processManager.startAgent(command.agent);
1768
- this.processManager.sendToAgent?.(command.agent, this.buildPrivateRuntimeWorkerWakeMessage(command, decision, externalEvent));
1809
+ const message = this.buildPrivateRuntimeWorkerWakeMessage(command, decision, externalEvent);
1810
+ if (this.processManager.dispatchToAgent)
1811
+ await this.processManager.dispatchToAgent(command.agent, message);
1812
+ else
1813
+ this.processManager.sendToAgent?.(command.agent, message);
1769
1814
  return {
1770
1815
  ok: true,
1771
1816
  message: `woke private runtime for ${command.agent}`,
@@ -33,8 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.DaemonProcessManager = exports.RESPAWN_GUARD_WINDOW_MS = exports.RESPAWN_GUARD_MAX_RESTARTS = void 0;
36
+ exports.DaemonProcessManager = exports.PRIVATE_RUNTIME_DISPATCH_ACK_TIMEOUT_MS = exports.RESPAWN_GUARD_WINDOW_MS = exports.RESPAWN_GUARD_MAX_RESTARTS = void 0;
37
37
  const child_process_1 = require("child_process");
38
+ const crypto_1 = require("crypto");
38
39
  const path = __importStar(require("path"));
39
40
  const identity_1 = require("../identity");
40
41
  const runtime_1 = require("../../nerves/runtime");
@@ -54,6 +55,7 @@ function startOfHour(ms) {
54
55
  exports.RESPAWN_GUARD_MAX_RESTARTS = 5;
55
56
  exports.RESPAWN_GUARD_WINDOW_MS = 10 * 60_000;
56
57
  const MAX_PENDING_IPC_MESSAGES = 20;
58
+ exports.PRIVATE_RUNTIME_DISPATCH_ACK_TIMEOUT_MS = 8 * 60_000;
57
59
  class DaemonProcessManager {
58
60
  agents = new Map();
59
61
  /** `stopAll()` is terminal for a manager instance. Once set, it fences
@@ -166,6 +168,7 @@ class DaemonProcessManager {
166
168
  startAttemptedAtMs: null,
167
169
  startAttemptId: 0,
168
170
  pendingIpcMessages: [],
171
+ pendingDispatches: new Map(),
169
172
  restartTimer: null,
170
173
  crashTimestamps: [],
171
174
  orchestratedRestartTimestamps: [],
@@ -328,6 +331,27 @@ class DaemonProcessManager {
328
331
  state.snapshot.status = "running";
329
332
  state.snapshot.pid = child.pid ?? null;
330
333
  state.snapshot.startedAt = new Date(this.currentTimeMs()).toISOString();
334
+ child.on("message", (message) => {
335
+ if (state.process !== child || !message || typeof message !== "object")
336
+ return;
337
+ const result = message;
338
+ if (result.type !== "ouro.privateRuntimeDispatchResult" || typeof result.dispatchId !== "string")
339
+ return;
340
+ const pending = state.pendingDispatches.get(result.dispatchId);
341
+ if (!pending || pending.child !== child)
342
+ return;
343
+ state.pendingDispatches.delete(result.dispatchId);
344
+ this.clearTimeoutFn(pending.timer);
345
+ if (result.ok === true)
346
+ pending.resolve();
347
+ else
348
+ pending.reject(new Error(typeof result.error === "string" && result.error.trim() ? result.error.slice(0, 1_000) : "private-runtime dispatch failed"));
349
+ });
350
+ child.once("disconnect", () => {
351
+ if (state.process !== child)
352
+ return;
353
+ this.rejectPendingDispatchesForChild(state, child, new Error(`Managed agent '${agent}' disconnected before acknowledged dispatch completed.`));
354
+ });
331
355
  const bootstrap = state.config.getRuntimeCredentialBootstrap?.() ?? null;
332
356
  if (bootstrap) {
333
357
  const message = {
@@ -367,7 +391,16 @@ class DaemonProcessManager {
367
391
  }
368
392
  }
369
393
  const pendingIpcMessages = state.pendingIpcMessages.splice(0);
370
- for (const message of pendingIpcMessages) {
394
+ for (const pendingMessage of pendingIpcMessages) {
395
+ const { message, dispatchId } = pendingMessage;
396
+ if (dispatchId) {
397
+ const pending = state.pendingDispatches.get(dispatchId);
398
+ if (!pending)
399
+ continue;
400
+ pending.child = child;
401
+ this.sendAcknowledgedMessage(state, child, dispatchId, message);
402
+ continue;
403
+ }
371
404
  try {
372
405
  child.send?.(message);
373
406
  (0, runtime_1.emitNervesEvent)({
@@ -413,6 +446,9 @@ class DaemonProcessManager {
413
446
  if (this.isStartAttemptCurrent(state, attemptId)) {
414
447
  state.startInFlight = false;
415
448
  state.startAttemptedAtMs = null;
449
+ if (!state.process) {
450
+ this.rejectPendingDispatchesForChild(state, null, new Error(`Managed agent '${agent}' startup ended before acknowledged dispatch completed.`));
451
+ }
416
452
  }
417
453
  }
418
454
  }
@@ -421,6 +457,9 @@ class DaemonProcessManager {
421
457
  this.clearRestartTimer(state);
422
458
  this.clearCooldownTimer(state);
423
459
  state.stopRequested = true;
460
+ for (const [dispatchId] of state.pendingDispatches) {
461
+ this.rejectPendingDispatch(state, dispatchId, new Error(`Managed agent '${agent}' stopped before acknowledged dispatch completed.`));
462
+ }
424
463
  state.pendingIpcMessages = [];
425
464
  // NOTE: do not touch state.respawnLoopTripped / orchestratedRestartTimestamps
426
465
  // here. restartAgent calls stopAgent internally; clearing the guard here
@@ -626,9 +665,11 @@ class DaemonProcessManager {
626
665
  if (!state.process) {
627
666
  if (state.startInFlight) {
628
667
  if (state.pendingIpcMessages.length >= MAX_PENDING_IPC_MESSAGES) {
629
- state.pendingIpcMessages.shift();
668
+ const evicted = state.pendingIpcMessages.shift();
669
+ if (evicted?.dispatchId)
670
+ this.rejectPendingDispatch(state, evicted.dispatchId, new Error(`Managed agent '${agent}' startup IPC queue evicted acknowledged dispatch.`));
630
671
  }
631
- state.pendingIpcMessages.push(message);
672
+ state.pendingIpcMessages.push({ message });
632
673
  (0, runtime_1.emitNervesEvent)({
633
674
  component: "daemon",
634
675
  event: "daemon.agent_ipc_queued_during_startup",
@@ -651,6 +692,73 @@ class DaemonProcessManager {
651
692
  });
652
693
  }
653
694
  }
695
+ dispatchToAgent(agent, message, options = {}) {
696
+ const state = this.requireAgent(agent);
697
+ const dispatchId = (0, crypto_1.randomUUID)();
698
+ const timeoutMs = options.timeoutMs ?? exports.PRIVATE_RUNTIME_DISPATCH_ACK_TIMEOUT_MS;
699
+ return new Promise((resolve, reject) => {
700
+ const timer = this.setTimeoutFn(() => {
701
+ const pending = state.pendingDispatches.get(dispatchId);
702
+ if (!pending)
703
+ return;
704
+ state.pendingDispatches.delete(dispatchId);
705
+ if (pending.child && state.process === pending.child) {
706
+ try {
707
+ pending.child.send?.({ type: "ouro.privateRuntimeDispatchCancel", dispatchId });
708
+ }
709
+ catch { /* cancellation is best effort */ }
710
+ }
711
+ reject(new Error(`Private-runtime dispatch timed out after ${timeoutMs}ms.`));
712
+ }, timeoutMs);
713
+ const child = state.process?.connected === false ? null : state.process;
714
+ state.pendingDispatches.set(dispatchId, { child, timer, resolve, reject });
715
+ if (child) {
716
+ this.sendAcknowledgedMessage(state, child, dispatchId, message);
717
+ return;
718
+ }
719
+ if (state.startInFlight) {
720
+ if (state.pendingIpcMessages.length >= MAX_PENDING_IPC_MESSAGES) {
721
+ const evicted = state.pendingIpcMessages.shift();
722
+ if (evicted?.dispatchId)
723
+ this.rejectPendingDispatch(state, evicted.dispatchId, new Error(`Managed agent '${agent}' startup IPC queue evicted acknowledged dispatch.`));
724
+ }
725
+ state.pendingIpcMessages.push({ message, dispatchId });
726
+ return;
727
+ }
728
+ this.rejectPendingDispatch(state, dispatchId, new Error(`Managed agent '${agent}' is unavailable for acknowledged dispatch.`));
729
+ });
730
+ }
731
+ rejectPendingDispatch(state, dispatchId, error) {
732
+ const pending = state.pendingDispatches.get(dispatchId);
733
+ if (!pending)
734
+ return;
735
+ state.pendingDispatches.delete(dispatchId);
736
+ this.clearTimeoutFn(pending.timer);
737
+ pending.reject(error);
738
+ }
739
+ rejectPendingDispatchesForChild(state, child, error) {
740
+ for (const [dispatchId, pending] of state.pendingDispatches) {
741
+ if (pending.child === child)
742
+ this.rejectPendingDispatch(state, dispatchId, error);
743
+ }
744
+ if (child === null)
745
+ state.pendingIpcMessages = state.pendingIpcMessages.filter((pending) => pending.dispatchId === undefined);
746
+ }
747
+ sendAcknowledgedMessage(state, child, dispatchId, message) {
748
+ try {
749
+ child.send?.({ ...message, dispatchId }, (error) => {
750
+ if (!error)
751
+ return;
752
+ const pending = state.pendingDispatches.get(dispatchId);
753
+ if (!pending || pending.child !== child)
754
+ return;
755
+ this.rejectPendingDispatch(state, dispatchId, error);
756
+ });
757
+ }
758
+ catch (error) {
759
+ this.rejectPendingDispatch(state, dispatchId, error instanceof Error ? error : new Error(String(error)));
760
+ }
761
+ }
654
762
  getAgentSnapshot(agent) {
655
763
  return this.agents.get(agent)?.snapshot;
656
764
  }
@@ -661,6 +769,7 @@ class DaemonProcessManager {
661
769
  /* v8 ignore next -- defensive: replacement cannot start before this child's one-shot exit listener drains @preserve */
662
770
  if (state.process !== child)
663
771
  return;
772
+ this.rejectPendingDispatchesForChild(state, child, new Error(`Managed agent '${state.config.name}' exited before acknowledged dispatch completed.`));
664
773
  state.process = null;
665
774
  state.startInFlight = false;
666
775
  state.startAttemptedAtMs = null;
@@ -46,6 +46,7 @@ exports.claimExternalEvent = claimExternalEvent;
46
46
  exports.renewExternalEventClaim = renewExternalEventClaim;
47
47
  exports.commitExternalEventDisposition = commitExternalEventDisposition;
48
48
  exports.failExternalEventAttempt = failExternalEventAttempt;
49
+ exports.settleExternalEventFailureIfOwned = settleExternalEventFailureIfOwned;
49
50
  exports.isExactLegacyProviderRecoveryFailure = isExactLegacyProviderRecoveryFailure;
50
51
  exports.externalEventRecoveryFailure = externalEventRecoveryFailure;
51
52
  exports.reviveExternalEventAfterRecovery = reviveExternalEventAfterRecovery;
@@ -1280,6 +1281,19 @@ function failExternalEventAttempt(recordPath, input) {
1280
1281
  return commitMutation(recordPath, retryState(record, now, maxAttempts, baseDelayMs, input.error, input.failureClass), now);
1281
1282
  });
1282
1283
  }
1284
+ function settleExternalEventFailureIfOwned(recordPath, input) {
1285
+ return withRecordLock(recordPath, () => {
1286
+ const record = readExternalEventRecord(recordPath);
1287
+ const now = input.now?.() ?? new Date().toISOString();
1288
+ if (record.executionState !== "running"
1289
+ || record.claimOwner !== input.owner
1290
+ || record.generation !== input.expectedGeneration) {
1291
+ return { settled: false, record };
1292
+ }
1293
+ const settled = commitMutation(recordPath, retryState(record, now, 5, 1_000, input.error, input.failureClass), now);
1294
+ return { settled: true, record: settled };
1295
+ });
1296
+ }
1283
1297
  function exactLegacyProviderFailure(record) {
1284
1298
  const escapedAgent = record.agent.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
1285
1299
  const canonical = new RegExp(`^private-runtime wake denied for ${escapedAgent}: provider lane resolution failed$`, "u");
@@ -183,7 +183,7 @@ function recordToolOutcome(state, toolName, args, result, success) {
183
183
  // ponder = continue thinking (private runtime) or hand off to private runtime (outer).
184
184
  // rest = end private runtime turn (added in Unit 8b).
185
185
  // Blocking these traps the agent: it can think all it wants but can never speak or stop.
186
- const CIRCUIT_BREAKER_EXEMPT = new Set(["settle", "surface", "ponder", "rest"]);
186
+ const CIRCUIT_BREAKER_EXEMPT = new Set(["settle", "surface", "ponder", "rest", "external_event_disposition"]);
187
187
  function detectToolLoop(state, toolName, args) {
188
188
  if (state.history.length >= exports.GLOBAL_CIRCUIT_BREAKER_LIMIT && !CIRCUIT_BREAKER_EXEMPT.has(toolName)) {
189
189
  return emitDetection("global_circuit_breaker", toolName, state.history.length, `this turn has already made ${state.history.length} tool calls. stop thrashing, use the current evidence, and either change approach or answer truthfully with the best grounded status.`);