@ouro.bot/cli 0.1.0-alpha.726 → 0.1.0-alpha.728
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/README.md +14 -1
- package/assets/bluebubbles-host +264 -0
- package/changelog.json +18 -0
- package/dist/heart/config.js +1 -2
- package/dist/heart/core.js +181 -209
- package/dist/heart/daemon/bluebubbles-cli-integration.js +71 -0
- package/dist/heart/daemon/bluebubbles-host-protocol.js +433 -0
- package/dist/heart/daemon/bluebubbles-host.js +273 -0
- package/dist/heart/daemon/cli-defaults.js +8 -1
- package/dist/heart/daemon/cli-exec.js +239 -34
- package/dist/heart/daemon/cli-help.js +17 -6
- package/dist/heart/daemon/cli-parse.js +69 -1
- package/dist/heart/daemon/cli-render.js +7 -0
- package/dist/heart/daemon/daemon.js +79 -14
- package/dist/heart/daemon/doctor.js +21 -1
- package/dist/heart/daemon/mcp-canary.js +160 -29
- package/dist/heart/daemon/process-manager.js +100 -18
- package/dist/heart/daemon/socket-client.js +28 -19
- package/dist/heart/providers/anthropic.js +17 -12
- package/dist/heart/streaming.js +6 -6
- package/dist/heart/versioning/ouro-path-installer.js +30 -24
- package/dist/heart/versioning/ouro-recovery-launcher.js +76 -0
- package/dist/heart/versioning/version-intent.js +114 -0
- package/dist/mind/prompt-budget.js +104 -5
- package/dist/repertoire/tools.js +0 -10
- package/dist/senses/bluebubbles/client.js +43 -57
- package/dist/senses/bluebubbles/context-packet.js +104 -19
- package/dist/senses/bluebubbles/entry.js +14 -1
- package/dist/senses/bluebubbles/index.js +1398 -719
- package/dist/senses/bluebubbles/latest-turn.js +311 -0
- package/dist/senses/bluebubbles/outbound-state.js +4 -0
- package/dist/senses/bluebubbles/reaction-policy.js +4 -12
- package/dist/senses/bluebubbles/semantic-receipts.js +178 -10
- package/dist/senses/bluebubbles/webhook-registration.js +228 -0
- package/package.json +4 -2
|
@@ -33,7 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.DEFAULT_CANARY_TIMEOUT_MS = void 0;
|
|
36
|
+
exports.SETUP_CANARY_TIMEOUT_MS = exports.DEFAULT_CANARY_TIMEOUT_MS = void 0;
|
|
37
|
+
exports.classifyMcpBoundary = classifyMcpBoundary;
|
|
38
|
+
exports.sanitizeMcpCanaryText = sanitizeMcpCanaryText;
|
|
39
|
+
exports.sanitizeMcpCanaryArgs = sanitizeMcpCanaryArgs;
|
|
37
40
|
exports.buildMcpBridgeRepairGuidance = buildMcpBridgeRepairGuidance;
|
|
38
41
|
exports.formatMcpBridgeRepairDetails = formatMcpBridgeRepairDetails;
|
|
39
42
|
exports.buildMcpDoctorNextSteps = buildMcpDoctorNextSteps;
|
|
@@ -47,7 +50,40 @@ const child_process_1 = require("child_process");
|
|
|
47
50
|
const path = __importStar(require("path"));
|
|
48
51
|
const runtime_1 = require("../../nerves/runtime");
|
|
49
52
|
exports.DEFAULT_CANARY_TIMEOUT_MS = 60_000;
|
|
53
|
+
exports.SETUP_CANARY_TIMEOUT_MS = 10_000;
|
|
50
54
|
const MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
55
|
+
function classifyMcpBoundary(input) {
|
|
56
|
+
if (!input.bridgeHealthy)
|
|
57
|
+
return "ouro-bridge-failed";
|
|
58
|
+
return input.hostStallObserved ? "host-stall-unexplained" : "ouro-bridge-healthy-at-capture";
|
|
59
|
+
}
|
|
60
|
+
function sanitizeMcpCanaryText(text) {
|
|
61
|
+
return text
|
|
62
|
+
.replace(/(https?:\/\/[^\s?]+)\?[^\s]*/g, "$1?[redacted]")
|
|
63
|
+
.replace(/"(?:password|token|secret|api[_-]?key)"\s*:\s*"[^"]*"/gi, '"credential":"[redacted]"')
|
|
64
|
+
.replace(/\bBearer\s+[^\s]+/gi, "Bearer [redacted]")
|
|
65
|
+
.replace(/--(?:password|token|secret|api[_-]?key)(\s+|=)[^\s]+/gi, "--credential$1[redacted]")
|
|
66
|
+
.replace(/\b(?:password|token|secret|api[_-]?key)=([^\s]+)/gi, "credential=[redacted]")
|
|
67
|
+
.trim();
|
|
68
|
+
}
|
|
69
|
+
function sanitizeMcpCanaryArgs(args) {
|
|
70
|
+
const sanitized = [];
|
|
71
|
+
let redactNext = false;
|
|
72
|
+
for (const arg of args) {
|
|
73
|
+
if (redactNext) {
|
|
74
|
+
sanitized.push("[redacted]");
|
|
75
|
+
redactNext = false;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (/^--(?:password|token|secret|api[_-]?key)$/i.test(arg)) {
|
|
79
|
+
sanitized.push("--credential");
|
|
80
|
+
redactNext = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
sanitized.push(sanitizeMcpCanaryText(arg));
|
|
84
|
+
}
|
|
85
|
+
return sanitized;
|
|
86
|
+
}
|
|
51
87
|
function defaultCommandArgs(agent, socketPath) {
|
|
52
88
|
const entryPath = path.join(__dirname, "ouro-bot-entry.js");
|
|
53
89
|
return [
|
|
@@ -58,18 +94,32 @@ function defaultCommandArgs(agent, socketPath) {
|
|
|
58
94
|
...(socketPath ? ["--socket", socketPath] : []),
|
|
59
95
|
];
|
|
60
96
|
}
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
97
|
+
function isRecord(value) {
|
|
98
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
99
|
+
}
|
|
100
|
+
function requireInitializeResult(response) {
|
|
101
|
+
if (!isRecord(response.result)) {
|
|
102
|
+
throw new Error("MCP canary received invalid initialize result");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function requireStatusText(response) {
|
|
106
|
+
if (!isRecord(response.result)) {
|
|
107
|
+
throw new Error("MCP canary received invalid status result");
|
|
108
|
+
}
|
|
109
|
+
if (response.result.isError === true) {
|
|
110
|
+
const errorContent = response.result.content;
|
|
111
|
+
const errorFirst = Array.isArray(errorContent) ? errorContent[0] : undefined;
|
|
112
|
+
const errorText = isRecord(errorFirst) && typeof errorFirst.text === "string"
|
|
113
|
+
? errorFirst.text
|
|
114
|
+
: "MCP status tool returned an error";
|
|
115
|
+
throw new Error(errorText);
|
|
116
|
+
}
|
|
117
|
+
const content = response.result.content;
|
|
118
|
+
const first = Array.isArray(content) ? content[0] : undefined;
|
|
119
|
+
if (!isRecord(first) || typeof first.text !== "string") {
|
|
120
|
+
throw new Error("MCP canary received invalid status result");
|
|
121
|
+
}
|
|
122
|
+
return first.text;
|
|
73
123
|
}
|
|
74
124
|
function parseFields(line) {
|
|
75
125
|
const parsed = {};
|
|
@@ -214,7 +264,23 @@ function validateMcpStatus(parsed, requiredSenses, options = {}) {
|
|
|
214
264
|
};
|
|
215
265
|
}
|
|
216
266
|
async function runMcpStatusCanary(options) {
|
|
267
|
+
const startedAt = Date.now();
|
|
217
268
|
const timeoutMs = options.timeoutMs ?? exports.DEFAULT_CANARY_TIMEOUT_MS;
|
|
269
|
+
const deadlineAt = startedAt + timeoutMs;
|
|
270
|
+
const evidence = {
|
|
271
|
+
capturedAt: new Date(startedAt).toISOString(),
|
|
272
|
+
durationMs: 0,
|
|
273
|
+
childPid: null,
|
|
274
|
+
phase: "spawn",
|
|
275
|
+
exitCode: null,
|
|
276
|
+
exitSignal: null,
|
|
277
|
+
stderr: "",
|
|
278
|
+
};
|
|
279
|
+
const decorate = (result, bridgeHealthy = false) => ({
|
|
280
|
+
...result,
|
|
281
|
+
classification: classifyMcpBoundary({ bridgeHealthy, hostStallObserved: options.hostStallObserved === true }),
|
|
282
|
+
evidence,
|
|
283
|
+
});
|
|
218
284
|
/* v8 ignore next -- default spawn is exercised by live canaries, while unit tests inject a fake child @preserve */
|
|
219
285
|
const spawnImpl = options.spawnImpl ?? child_process_1.spawn;
|
|
220
286
|
const command = options.command ?? process.execPath;
|
|
@@ -227,17 +293,38 @@ async function runMcpStatusCanary(options) {
|
|
|
227
293
|
meta: {
|
|
228
294
|
agent: options.agent,
|
|
229
295
|
command,
|
|
230
|
-
commandArgs,
|
|
296
|
+
commandArgs: sanitizeMcpCanaryArgs(commandArgs),
|
|
231
297
|
timeoutMs,
|
|
232
298
|
requiredSenses,
|
|
233
299
|
ignoreOverviewHealth: options.ignoreOverviewHealth === true,
|
|
234
300
|
ignoreSenseHealth: options.ignoreSenseHealth === true,
|
|
235
301
|
},
|
|
236
302
|
});
|
|
237
|
-
|
|
303
|
+
let child;
|
|
304
|
+
try {
|
|
305
|
+
child = spawnImpl(command, commandArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
306
|
+
evidence.childPid = child.pid ?? null;
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
const reason = sanitizeMcpCanaryText(error instanceof Error ? error.message : String(error));
|
|
310
|
+
evidence.durationMs = Date.now() - startedAt;
|
|
311
|
+
(0, runtime_1.emitNervesEvent)({
|
|
312
|
+
component: "daemon",
|
|
313
|
+
event: "daemon.mcp_canary_error",
|
|
314
|
+
level: "error",
|
|
315
|
+
message: "MCP status canary failed to spawn",
|
|
316
|
+
meta: { agent: options.agent, reason },
|
|
317
|
+
});
|
|
318
|
+
return decorate({ ok: false, summary: `mcp canary failed: ${reason}`, details: [reason] });
|
|
319
|
+
}
|
|
238
320
|
let buffer = "";
|
|
239
321
|
let stderr = "";
|
|
240
322
|
const pending = new Map();
|
|
323
|
+
let resolveExit;
|
|
324
|
+
const exitObserved = new Promise((resolve) => { resolveExit = resolve; });
|
|
325
|
+
function safeStderr() {
|
|
326
|
+
return sanitizeMcpCanaryText(stderr);
|
|
327
|
+
}
|
|
241
328
|
function cleanup() {
|
|
242
329
|
for (const [, request] of pending) {
|
|
243
330
|
clearTimeout(request.timer);
|
|
@@ -268,14 +355,19 @@ async function runMcpStatusCanary(options) {
|
|
|
268
355
|
buffer = buffer.slice(idx + 1);
|
|
269
356
|
if (!line)
|
|
270
357
|
continue;
|
|
271
|
-
let
|
|
358
|
+
let parsedResponse;
|
|
272
359
|
try {
|
|
273
|
-
|
|
360
|
+
parsedResponse = JSON.parse(line);
|
|
274
361
|
}
|
|
275
362
|
catch {
|
|
276
363
|
failAll(new Error(`MCP canary received malformed JSON: ${line}`));
|
|
277
364
|
return;
|
|
278
365
|
}
|
|
366
|
+
if (!isRecord(parsedResponse)) {
|
|
367
|
+
failAll(new Error("MCP canary received malformed JSON-RPC response"));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const response = parsedResponse;
|
|
279
371
|
const id = typeof response.id === "number" ? response.id : null;
|
|
280
372
|
if (id === null)
|
|
281
373
|
continue;
|
|
@@ -284,14 +376,27 @@ async function runMcpStatusCanary(options) {
|
|
|
284
376
|
continue;
|
|
285
377
|
pending.delete(id);
|
|
286
378
|
clearTimeout(request.timer);
|
|
379
|
+
const hasResult = Object.prototype.hasOwnProperty.call(response, "result");
|
|
380
|
+
const hasError = Object.prototype.hasOwnProperty.call(response, "error");
|
|
381
|
+
if (response.jsonrpc !== "2.0" || (!hasResult && !hasError)) {
|
|
382
|
+
request.reject(new Error("MCP canary received malformed JSON-RPC response"));
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (hasError) {
|
|
386
|
+
request.reject(new Error(`MCP canary received JSON-RPC error: ${JSON.stringify(response.error)}`));
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
287
389
|
request.resolve(response);
|
|
288
390
|
}
|
|
289
391
|
});
|
|
290
392
|
child.on("error", (error) => failAll(error));
|
|
291
393
|
child.on("close", (code, signal) => {
|
|
394
|
+
evidence.exitCode = code;
|
|
395
|
+
evidence.exitSignal = signal;
|
|
396
|
+
resolveExit();
|
|
292
397
|
if (pending.size === 0)
|
|
293
398
|
return;
|
|
294
|
-
failAll(new Error(`MCP canary process closed before response code=${code} signal=${signal ?? "none"} stderr=${
|
|
399
|
+
failAll(new Error(`MCP canary process closed before response code=${code} signal=${signal ?? "none"} stderr=${safeStderr()}`));
|
|
295
400
|
});
|
|
296
401
|
let nextId = 1;
|
|
297
402
|
function request(method, params) {
|
|
@@ -300,36 +405,43 @@ async function runMcpStatusCanary(options) {
|
|
|
300
405
|
reject(new Error("MCP canary stdin is not writable"));
|
|
301
406
|
return;
|
|
302
407
|
}
|
|
408
|
+
const remainingMs = deadlineAt - Date.now();
|
|
409
|
+
if (remainingMs <= 0) {
|
|
410
|
+
reject(new Error(`MCP canary timed out waiting for ${method}; stderr=${safeStderr()}`));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
303
413
|
const id = nextId++;
|
|
304
414
|
const timer = setTimeout(() => {
|
|
305
415
|
pending.delete(id);
|
|
306
|
-
reject(new Error(`MCP canary timed out waiting for ${method}; stderr=${
|
|
307
|
-
},
|
|
416
|
+
reject(new Error(`MCP canary timed out waiting for ${method}; stderr=${safeStderr()}`));
|
|
417
|
+
}, remainingMs);
|
|
308
418
|
pending.set(id, { resolve, reject, timer });
|
|
309
419
|
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
|
|
310
420
|
});
|
|
311
421
|
}
|
|
312
422
|
try {
|
|
313
|
-
|
|
423
|
+
evidence.phase = "initialize";
|
|
424
|
+
const initializeResponse = await request("initialize", {
|
|
314
425
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
315
426
|
capabilities: {},
|
|
316
427
|
clientInfo: { name: "ouro-mcp-canary", version: "1.0" },
|
|
317
428
|
});
|
|
429
|
+
requireInitializeResult(initializeResponse);
|
|
318
430
|
child.stdin?.write(JSON.stringify({ jsonrpc: "2.0", method: "initialized" }) + "\n");
|
|
431
|
+
evidence.phase = "status";
|
|
319
432
|
const statusResponse = await request("tools/call", {
|
|
320
433
|
name: "status",
|
|
321
434
|
arguments: {},
|
|
322
435
|
});
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
const parsed = parseMcpStatusText(responseText(statusResponse));
|
|
436
|
+
const statusText = requireStatusText(statusResponse);
|
|
437
|
+
const bridgeHealthy = true;
|
|
438
|
+
const parsed = parseMcpStatusText(sanitizeMcpCanaryText(statusText));
|
|
328
439
|
const canary = validateMcpStatus(parsed, requiredSenses, {
|
|
329
440
|
agent: options.agent,
|
|
330
441
|
ignoreOverviewHealth: options.ignoreOverviewHealth,
|
|
331
442
|
ignoreSenseHealth: options.ignoreSenseHealth,
|
|
332
443
|
});
|
|
444
|
+
evidence.phase = "complete";
|
|
333
445
|
(0, runtime_1.emitNervesEvent)({
|
|
334
446
|
component: "daemon",
|
|
335
447
|
event: canary.ok ? "daemon.mcp_canary_end" : "daemon.mcp_canary_error",
|
|
@@ -337,10 +449,10 @@ async function runMcpStatusCanary(options) {
|
|
|
337
449
|
message: canary.summary,
|
|
338
450
|
meta: { agent: options.agent, ok: canary.ok },
|
|
339
451
|
});
|
|
340
|
-
return canary;
|
|
452
|
+
return decorate(canary, bridgeHealthy);
|
|
341
453
|
}
|
|
342
454
|
catch (error) {
|
|
343
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
455
|
+
const reason = sanitizeMcpCanaryText(error instanceof Error ? error.message : String(error));
|
|
344
456
|
(0, runtime_1.emitNervesEvent)({
|
|
345
457
|
component: "daemon",
|
|
346
458
|
event: "daemon.mcp_canary_error",
|
|
@@ -348,16 +460,30 @@ async function runMcpStatusCanary(options) {
|
|
|
348
460
|
message: "MCP status canary failed",
|
|
349
461
|
meta: { agent: options.agent, reason },
|
|
350
462
|
});
|
|
351
|
-
return { ok: false, summary: `mcp canary failed: ${reason}`, details: [reason] };
|
|
463
|
+
return decorate({ ok: false, summary: `mcp canary failed: ${reason}`, details: [reason] });
|
|
352
464
|
}
|
|
353
465
|
finally {
|
|
354
466
|
child.stdin?.end();
|
|
355
467
|
cleanup();
|
|
468
|
+
let exitTimer;
|
|
469
|
+
const remainingExitWaitMs = Math.min(100, Math.max(0, deadlineAt - Date.now()));
|
|
470
|
+
await Promise.race([
|
|
471
|
+
exitObserved,
|
|
472
|
+
new Promise((resolve) => { exitTimer = setTimeout(resolve, remainingExitWaitMs); }),
|
|
473
|
+
]);
|
|
474
|
+
clearTimeout(exitTimer);
|
|
475
|
+
evidence.stderr = safeStderr();
|
|
476
|
+
evidence.durationMs = Date.now() - startedAt;
|
|
356
477
|
}
|
|
357
478
|
}
|
|
358
479
|
function formatMcpStatusCanaryResult(result) {
|
|
359
480
|
return [
|
|
360
481
|
result.ok ? "mcp canary: ok" : "mcp canary: failed",
|
|
482
|
+
...(result.classification ? [`classification: ${result.classification}`] : []),
|
|
483
|
+
...(result.evidence ? [
|
|
484
|
+
`captured=${result.evidence.capturedAt} durationMs=${result.evidence.durationMs} childPid=${result.evidence.childPid ?? "none"} phase=${result.evidence.phase} exitCode=${result.evidence.exitCode ?? "none"} exitSignal=${result.evidence.exitSignal ?? "none"}`,
|
|
485
|
+
...(result.evidence.stderr ? [`stderr=${result.evidence.stderr}`] : []),
|
|
486
|
+
] : []),
|
|
361
487
|
result.summary,
|
|
362
488
|
...result.details.map((line) => ` ${line}`),
|
|
363
489
|
].join("\n");
|
|
@@ -368,6 +494,11 @@ function formatMcpStatusDoctorResult(result, agent) {
|
|
|
368
494
|
&& !line.startsWith("verify command="));
|
|
369
495
|
const lines = [
|
|
370
496
|
result.ok ? "mcp doctor: ok" : "mcp doctor: failed",
|
|
497
|
+
...(result.classification ? [`classification: ${result.classification}`] : []),
|
|
498
|
+
...(result.evidence ? [
|
|
499
|
+
`captured=${result.evidence.capturedAt} durationMs=${result.evidence.durationMs} childPid=${result.evidence.childPid ?? "none"} phase=${result.evidence.phase} exitCode=${result.evidence.exitCode ?? "none"} exitSignal=${result.evidence.exitSignal ?? "none"}`,
|
|
500
|
+
...(result.evidence.stderr ? [`stderr=${result.evidence.stderr}`] : []),
|
|
501
|
+
] : []),
|
|
371
502
|
result.summary,
|
|
372
503
|
...rawDetails.map((line) => ` ${line}`),
|
|
373
504
|
];
|
|
@@ -56,6 +56,9 @@ exports.RESPAWN_GUARD_WINDOW_MS = 10 * 60_000;
|
|
|
56
56
|
const MAX_PENDING_IPC_MESSAGES = 20;
|
|
57
57
|
class DaemonProcessManager {
|
|
58
58
|
agents = new Map();
|
|
59
|
+
/** `stopAll()` is terminal for a manager instance. Once set, it fences
|
|
60
|
+
* detached restart/start continuations until the owning daemon exits. */
|
|
61
|
+
shutdownRequested = false;
|
|
59
62
|
maxRestartsPerHour;
|
|
60
63
|
stabilityThresholdMs;
|
|
61
64
|
initialBackoffMs;
|
|
@@ -67,6 +70,7 @@ class DaemonProcessManager {
|
|
|
67
70
|
cooldownRecoveryMs;
|
|
68
71
|
maxCooldownRetries;
|
|
69
72
|
startupStaleAfterMs;
|
|
73
|
+
stopTimeoutMs;
|
|
70
74
|
existsSyncFn;
|
|
71
75
|
configCheckFn;
|
|
72
76
|
statusWriterFn;
|
|
@@ -143,6 +147,7 @@ class DaemonProcessManager {
|
|
|
143
147
|
this.cooldownRecoveryMs = options.cooldownRecoveryMs ?? 5 * 60 * 1_000;
|
|
144
148
|
this.maxCooldownRetries = options.maxCooldownRetries ?? 3;
|
|
145
149
|
this.startupStaleAfterMs = options.startupStaleAfterMs ?? 45_000;
|
|
150
|
+
this.stopTimeoutMs = options.stopTimeoutMs ?? 4_000;
|
|
146
151
|
this.spawnFn = options.spawn ?? ((command, args, spawnOptions) => (0, child_process_1.spawn)(command, args, spawnOptions));
|
|
147
152
|
this.now = options.now ?? (() => Date.now());
|
|
148
153
|
this.setTimeoutFn = options.setTimeoutFn ?? ((cb, delay) => setTimeout(cb, delay));
|
|
@@ -169,6 +174,7 @@ class DaemonProcessManager {
|
|
|
169
174
|
cooldownTimer: null,
|
|
170
175
|
cooldownRetryCount: 0,
|
|
171
176
|
fastCrashCount: 0,
|
|
177
|
+
stopFailureActive: false,
|
|
172
178
|
snapshot: {
|
|
173
179
|
name: agent.name,
|
|
174
180
|
channel: agent.channel,
|
|
@@ -206,6 +212,8 @@ class DaemonProcessManager {
|
|
|
206
212
|
}
|
|
207
213
|
async startAgent(agent, options = {}) {
|
|
208
214
|
const state = this.requireAgent(agent);
|
|
215
|
+
if (this.shutdownRequested)
|
|
216
|
+
return;
|
|
209
217
|
if (state.process || state.startInFlight)
|
|
210
218
|
return;
|
|
211
219
|
const attemptId = state.startAttemptId + 1;
|
|
@@ -426,25 +434,87 @@ class DaemonProcessManager {
|
|
|
426
434
|
return;
|
|
427
435
|
}
|
|
428
436
|
const child = state.process;
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
437
|
+
const childPid = child.pid ?? null;
|
|
438
|
+
const childPidLabel = childPid === null ? "unknown" : String(childPid);
|
|
439
|
+
await new Promise((resolve, reject) => {
|
|
440
|
+
let settled = false;
|
|
441
|
+
let timeout = null;
|
|
442
|
+
const finish = (error) => {
|
|
443
|
+
/* v8 ignore next -- defensive idempotence if an injected timer fires after cancellation @preserve */
|
|
444
|
+
if (settled)
|
|
445
|
+
return;
|
|
446
|
+
settled = true;
|
|
447
|
+
child.removeListener("exit", onExit);
|
|
448
|
+
if (timeout !== null)
|
|
449
|
+
this.clearTimeoutFn(timeout);
|
|
450
|
+
if (error)
|
|
451
|
+
reject(error);
|
|
452
|
+
else
|
|
453
|
+
resolve();
|
|
454
|
+
};
|
|
455
|
+
const onExit = () => finish();
|
|
456
|
+
child.once("exit", onExit);
|
|
457
|
+
let signalAccepted = false;
|
|
458
|
+
try {
|
|
459
|
+
signalAccepted = child.kill("SIGTERM");
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
const errorReason = "failed to send SIGTERM to managed agent; worker remains tracked";
|
|
463
|
+
state.stopFailureActive = true;
|
|
464
|
+
state.snapshot.errorReason = errorReason;
|
|
465
|
+
state.snapshot.fixHint = `Inspect managed worker PID ${childPidLabel} before retrying the stop or restart.`;
|
|
466
|
+
(0, runtime_1.emitNervesEvent)({
|
|
467
|
+
level: "error",
|
|
468
|
+
component: "daemon",
|
|
469
|
+
event: "daemon.agent_stop_error",
|
|
470
|
+
message: errorReason,
|
|
471
|
+
meta: { agent, pid: childPid },
|
|
472
|
+
});
|
|
473
|
+
this.notifySnapshotChange(state.snapshot);
|
|
474
|
+
finish(new Error(errorReason));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (!signalAccepted) {
|
|
478
|
+
const errorReason = "managed agent rejected SIGTERM; worker remains tracked";
|
|
479
|
+
state.stopFailureActive = true;
|
|
480
|
+
state.snapshot.errorReason = errorReason;
|
|
481
|
+
state.snapshot.fixHint = `Inspect managed worker PID ${childPidLabel} before retrying the stop or restart.`;
|
|
482
|
+
(0, runtime_1.emitNervesEvent)({
|
|
483
|
+
level: "error",
|
|
484
|
+
component: "daemon",
|
|
485
|
+
event: "daemon.agent_stop_error",
|
|
486
|
+
message: errorReason,
|
|
487
|
+
meta: { agent, pid: childPid },
|
|
488
|
+
});
|
|
489
|
+
this.notifySnapshotChange(state.snapshot);
|
|
490
|
+
finish(new Error(errorReason));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (settled)
|
|
494
|
+
return;
|
|
495
|
+
timeout = this.setTimeoutFn(() => {
|
|
496
|
+
if (settled)
|
|
497
|
+
return;
|
|
498
|
+
const errorReason = `managed agent did not exit within ${this.stopTimeoutMs}ms; replacement was not started`;
|
|
499
|
+
state.stopFailureActive = true;
|
|
500
|
+
state.snapshot.errorReason = errorReason;
|
|
501
|
+
state.snapshot.fixHint = `Inspect managed worker PID ${childPidLabel}; retry only after that worker has exited.`;
|
|
502
|
+
(0, runtime_1.emitNervesEvent)({
|
|
503
|
+
level: "error",
|
|
504
|
+
component: "daemon",
|
|
505
|
+
event: "daemon.agent_stop_timeout",
|
|
506
|
+
message: errorReason,
|
|
507
|
+
meta: { agent, pid: childPid, timeoutMs: this.stopTimeoutMs },
|
|
508
|
+
});
|
|
509
|
+
this.notifySnapshotChange(state.snapshot);
|
|
510
|
+
finish(new Error(errorReason));
|
|
511
|
+
}, this.stopTimeoutMs);
|
|
512
|
+
});
|
|
445
513
|
}
|
|
446
514
|
async restartAgent(agent, options = {}) {
|
|
447
515
|
const state = this.requireAgent(agent);
|
|
516
|
+
if (this.shutdownRequested)
|
|
517
|
+
return;
|
|
448
518
|
// Respawn-loop guard: prune timestamps outside the window, then check
|
|
449
519
|
// whether we've already restarted this agent too many times in it.
|
|
450
520
|
const now = this.now();
|
|
@@ -528,8 +598,14 @@ class DaemonProcessManager {
|
|
|
528
598
|
await this.startAgent(agent, options);
|
|
529
599
|
}
|
|
530
600
|
async stopAll() {
|
|
531
|
-
|
|
532
|
-
|
|
601
|
+
this.shutdownRequested = true;
|
|
602
|
+
const states = [...this.agents.values()];
|
|
603
|
+
const results = await Promise.allSettled(states.map((state) => this.stopAgent(state.config.name)));
|
|
604
|
+
const failures = results.flatMap((result, index) => result.status === "rejected"
|
|
605
|
+
? [{ agent: states[index].config.name, reason: result.reason }]
|
|
606
|
+
: []);
|
|
607
|
+
if (failures.length > 0) {
|
|
608
|
+
throw new AggregateError(failures.map((failure) => failure.reason), `failed to stop ${failures.length} managed worker(s): ${failures.map((failure) => failure.agent).join(", ")}`);
|
|
533
609
|
}
|
|
534
610
|
}
|
|
535
611
|
resetAgentFailureState(agent) {
|
|
@@ -582,6 +658,7 @@ class DaemonProcessManager {
|
|
|
582
658
|
return [...this.agents.values()].map((state) => state.snapshot);
|
|
583
659
|
}
|
|
584
660
|
onExit(state, child, code, signal) {
|
|
661
|
+
/* v8 ignore next -- defensive: replacement cannot start before this child's one-shot exit listener drains @preserve */
|
|
585
662
|
if (state.process !== child)
|
|
586
663
|
return;
|
|
587
664
|
state.process = null;
|
|
@@ -590,6 +667,11 @@ class DaemonProcessManager {
|
|
|
590
667
|
state.snapshot.pid = null;
|
|
591
668
|
state.snapshot.lastExitCode = code;
|
|
592
669
|
state.snapshot.lastSignal = signal;
|
|
670
|
+
if (state.stopFailureActive) {
|
|
671
|
+
state.stopFailureActive = false;
|
|
672
|
+
state.snapshot.errorReason = null;
|
|
673
|
+
state.snapshot.fixHint = null;
|
|
674
|
+
}
|
|
593
675
|
const crashed = !state.stopRequested && code !== 0;
|
|
594
676
|
const now = this.currentTimeMs();
|
|
595
677
|
const startedAt = state.snapshot.startedAt ? Date.parse(state.snapshot.startedAt) : now;
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.DEFAULT_DAEMON_COMMAND_TIMEOUT_MS = exports.DEFAULT_DAEMON_SOCKET_PATH = void 0;
|
|
36
|
+
exports.DEFAULT_DAEMON_STATUS_TIMEOUT_MS = exports.DEFAULT_DAEMON_COMMAND_TIMEOUT_MS = exports.DEFAULT_DAEMON_SOCKET_PATH = void 0;
|
|
37
37
|
exports.__bypassVitestGuardForTests = __bypassVitestGuardForTests;
|
|
38
38
|
exports.sendDaemonCommand = sendDaemonCommand;
|
|
39
39
|
exports.checkDaemonSocketAlive = checkDaemonSocketAlive;
|
|
@@ -44,6 +44,7 @@ const net = __importStar(require("net"));
|
|
|
44
44
|
const runtime_1 = require("../../nerves/runtime");
|
|
45
45
|
exports.DEFAULT_DAEMON_SOCKET_PATH = "/tmp/ouroboros-daemon.sock";
|
|
46
46
|
exports.DEFAULT_DAEMON_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
|
|
47
|
+
exports.DEFAULT_DAEMON_STATUS_TIMEOUT_MS = 5_000;
|
|
47
48
|
/**
|
|
48
49
|
* Defense-in-depth: detect if we're running under vitest. Tests that forget
|
|
49
50
|
* to `vi.mock("../../heart/daemon/socket-client")` would otherwise leak real
|
|
@@ -130,11 +131,16 @@ function sendDaemonCommand(socketPath, command, options = {}) {
|
|
|
130
131
|
let raw = "";
|
|
131
132
|
let settled = false;
|
|
132
133
|
const timeoutMs = options.timeoutMs ?? exports.DEFAULT_DAEMON_COMMAND_TIMEOUT_MS;
|
|
134
|
+
let deadlineTimer;
|
|
135
|
+
const clearDeadline = () => {
|
|
136
|
+
clearTimeout(deadlineTimer);
|
|
137
|
+
};
|
|
133
138
|
const resolveOnce = (response) => {
|
|
134
139
|
/* v8 ignore next -- duplicate settlement guard; callers also guard event handlers before reaching this helper @preserve */
|
|
135
140
|
if (settled)
|
|
136
141
|
return;
|
|
137
142
|
settled = true;
|
|
143
|
+
clearDeadline();
|
|
138
144
|
resolve(response);
|
|
139
145
|
};
|
|
140
146
|
const rejectOnce = (error) => {
|
|
@@ -142,27 +148,30 @@ function sendDaemonCommand(socketPath, command, options = {}) {
|
|
|
142
148
|
if (settled)
|
|
143
149
|
return;
|
|
144
150
|
settled = true;
|
|
151
|
+
clearDeadline();
|
|
145
152
|
reject(error);
|
|
146
153
|
};
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
154
|
+
deadlineTimer = setTimeout(() => {
|
|
155
|
+
/* v8 ignore next -- terminal socket events clear this timer; guard protects a queued timer callback race @preserve */
|
|
156
|
+
if (settled)
|
|
157
|
+
return;
|
|
158
|
+
const error = new Error(`Daemon command ${command.kind} timed out after ${timeoutMs}ms waiting for a response.`);
|
|
159
|
+
error.code = "ETIMEDOUT";
|
|
160
|
+
(0, runtime_1.emitNervesEvent)({
|
|
161
|
+
level: "error",
|
|
162
|
+
component: "daemon",
|
|
163
|
+
event: "daemon.socket_command_timeout",
|
|
164
|
+
message: "daemon socket command timed out",
|
|
165
|
+
meta: {
|
|
166
|
+
socketPath,
|
|
167
|
+
kind: command.kind,
|
|
168
|
+
timeoutMs,
|
|
169
|
+
error: error.message,
|
|
170
|
+
},
|
|
164
171
|
});
|
|
165
|
-
|
|
172
|
+
client.destroy();
|
|
173
|
+
rejectOnce(error);
|
|
174
|
+
}, timeoutMs);
|
|
166
175
|
client.on("connect", () => {
|
|
167
176
|
// Write the command + newline delimiter. DO NOT call client.end()
|
|
168
177
|
// afterwards — the server closes the connection once it has written
|
|
@@ -104,14 +104,13 @@ function parseToolCallInput(argumentsJson) {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
function toAnthropicMessages(messages) {
|
|
107
|
-
|
|
107
|
+
const systemParts = [];
|
|
108
108
|
const converted = [];
|
|
109
109
|
for (const msg of messages) {
|
|
110
110
|
if (msg.role === "system") {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
111
|
+
const text = toAnthropicTextContent(msg.content).trim();
|
|
112
|
+
if (text)
|
|
113
|
+
systemParts.push(text);
|
|
115
114
|
continue;
|
|
116
115
|
}
|
|
117
116
|
if (msg.role === "user") {
|
|
@@ -173,7 +172,7 @@ function toAnthropicMessages(messages) {
|
|
|
173
172
|
});
|
|
174
173
|
}
|
|
175
174
|
}
|
|
176
|
-
return { system, messages: converted };
|
|
175
|
+
return { system: systemParts.length > 0 ? systemParts.join("\n\n") : undefined, messages: converted };
|
|
177
176
|
}
|
|
178
177
|
function toAnthropicTools(tools) {
|
|
179
178
|
return tools.map((tool) => ({
|
|
@@ -231,6 +230,11 @@ function isAnthropicAuthFailure(error) {
|
|
|
231
230
|
}
|
|
232
231
|
async function streamAnthropicMessages(client, model, request) {
|
|
233
232
|
const { system, messages } = toAnthropicMessages(request.messages);
|
|
233
|
+
const additionalSystemParts = request.messages
|
|
234
|
+
.filter((message) => message.role === "system")
|
|
235
|
+
.slice(1)
|
|
236
|
+
.map((message) => toAnthropicTextContent(message.content).trim())
|
|
237
|
+
.filter((text) => text.length > 0);
|
|
234
238
|
const anthropicTools = toAnthropicTools(request.activeTools);
|
|
235
239
|
const modelCaps = (0, model_capabilities_1.getModelCapabilities)(model);
|
|
236
240
|
const maxTokens = modelCaps.maxOutputTokens ?? 16384;
|
|
@@ -255,12 +259,13 @@ async function streamAnthropicMessages(client, model, request) {
|
|
|
255
259
|
text: preambleText + "\n\n" + request.systemPrompt.stable,
|
|
256
260
|
cache_control: { type: "ephemeral" },
|
|
257
261
|
};
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
params.system = [
|
|
263
|
+
stableBlock,
|
|
264
|
+
...(request.systemPrompt.volatile
|
|
265
|
+
? [{ type: "text", text: request.systemPrompt.volatile }]
|
|
266
|
+
: []),
|
|
267
|
+
...additionalSystemParts.map((text) => ({ type: "text", text })),
|
|
268
|
+
];
|
|
264
269
|
}
|
|
265
270
|
else if (system) {
|
|
266
271
|
// Fallback: no structured prompt, extract from messages (legacy path)
|