@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.
- package/changelog.json +12 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +2 -2
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/agent-entry.js +69 -6
- package/dist/heart/context-loss-gauntlet.js +2 -1
- package/dist/heart/daemon/daemon.js +54 -9
- package/dist/heart/daemon/process-manager.js +113 -4
- package/dist/heart/external-events/router.js +14 -0
- package/dist/heart/tool-loop.js +1 -1
- package/dist/repertoire/tools-continuity.js +199 -146
- package/dist/senses/private-runtime-worker.js +287 -208
- package/dist/senses/private-runtime.js +4 -1
- package/dist/senses/sanctuary-runtime.js +43 -0
- package/npm-shrinkwrap.json +40 -25
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
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.778",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Treat rested terminal turns waiting for new input as safe context-loss Sentinel idle states, matching settled turns and preventing false critical health alerts after normal private-runtime rests."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.777",
|
|
12
|
+
"changes": [
|
|
13
|
+
"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."
|
|
14
|
+
]
|
|
15
|
+
},
|
|
4
16
|
{
|
|
5
17
|
"version": "0.1.0-alpha.776",
|
|
6
18
|
"changes": [
|
|
@@ -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.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.778</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
|
|
249
|
+
void handleWorkerMessage(message);
|
|
187
250
|
};
|
|
188
|
-
const
|
|
189
|
-
for (const message of
|
|
251
|
+
const bufferedMessages = ipcState.bufferedMessages.splice(0);
|
|
252
|
+
for (const message of bufferedMessages) {
|
|
190
253
|
ipcState.workerMessageHandler(message);
|
|
191
254
|
}
|
|
192
255
|
})
|
|
@@ -85,7 +85,8 @@ function isSettledTurnWait(resume) {
|
|
|
85
85
|
return resume.hasCompleteState
|
|
86
86
|
&& resume.recorderHealth.status === "ok"
|
|
87
87
|
&& resume.blockedBecause.length === 1
|
|
88
|
-
&& resume.blockedBecause[0] === "turn outcome settled; wait for new input before acting"
|
|
88
|
+
&& (resume.blockedBecause[0] === "turn outcome settled; wait for new input before acting"
|
|
89
|
+
|| resume.blockedBecause[0] === "turn outcome rested; wait for new input before acting")
|
|
89
90
|
&& resume.nextSafeAction.value === "inspect the latest session and wait for new input before acting";
|
|
90
91
|
}
|
|
91
92
|
function nextSafeActionCheck(card, resume) {
|
|
@@ -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
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
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
|
-
|
|
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
|
|
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");
|
package/dist/heart/tool-loop.js
CHANGED
|
@@ -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.`);
|