@ouro.bot/cli 0.1.0-alpha.768 → 0.1.0-alpha.769
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 +9 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/arc/cares.js +91 -9
- package/dist/heart/core.js +50 -5
- package/dist/heart/daemon/daemon.js +47 -0
- package/dist/heart/external-events/router.js +88 -4
- package/dist/heart/private-runtime/decision-renderer.js +2 -0
- package/dist/heart/private-runtime/ledger.js +4 -1
- package/dist/heart/private-runtime/policy.js +22 -8
- package/dist/heart/start-of-turn-packet.js +7 -3
- package/dist/repertoire/tools-continuity.js +23 -28
- package/dist/senses/pipeline.js +11 -4
- package/dist/senses/sanctuary-full-visibility-contract.js +199 -9
- package/dist/senses/sanctuary-storage-optimization-contract.js +2 -3
- package/dist/senses/telegram.js +34 -11
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
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.769",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Keep stale Sanctuary Care evidence out of current context and require provenance-qualified same-turn Docker verification before reporting or mutation.",
|
|
8
|
+
"Recognize natural whole-Sanctuary current-status requests and require all seven current reads, including notifications, before a terminal reply.",
|
|
9
|
+
"Revive provider-lane and managed-runtime external-event dead letters exactly once per generation after typed, newer recovery evidence.",
|
|
10
|
+
"Let verified fresh Docker alerts be reported while preserving unrelated Care display and policy metadata during partial incident refreshes and constraining durable status values."
|
|
11
|
+
]
|
|
12
|
+
},
|
|
4
13
|
{
|
|
5
14
|
"version": "0.1.0-alpha.768",
|
|
6
15
|
"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.769</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
package/dist/arc/cares.js
CHANGED
|
@@ -33,6 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CARE_INCIDENT_RECOVERY_REVIEW_RISK = void 0;
|
|
37
|
+
exports.isManagedDockerCareIncidentBinding = isManagedDockerCareIncidentBinding;
|
|
38
|
+
exports.projectCareEvidence = projectCareEvidence;
|
|
36
39
|
exports.createCare = createCare;
|
|
37
40
|
exports.readCares = readCares;
|
|
38
41
|
exports.readActiveCares = readActiveCares;
|
|
@@ -48,6 +51,32 @@ const session_events_1 = require("../heart/session-events");
|
|
|
48
51
|
const runtime_1 = require("../nerves/runtime");
|
|
49
52
|
const session_transaction_1 = require("../mind/session-transaction");
|
|
50
53
|
const json_store_1 = require("./json-store");
|
|
54
|
+
exports.CARE_INCIDENT_RECOVERY_REVIEW_RISK = "The verified incident recovered, but this Care still has unresolved context that needs review.";
|
|
55
|
+
function isManagedDockerCareIncidentBinding(binding) {
|
|
56
|
+
return binding.source === "sanctuary-health::Docker_critical_image_disk_utilization"
|
|
57
|
+
&& /^docker-image-disk-(?:100|[1-9]?[0-9])pct-[0-9]{8}T[0-9]{4}Z$/u.test(binding.incidentKey)
|
|
58
|
+
&& /^[a-f0-9]{64}$/u.test(binding.classifiedRevision)
|
|
59
|
+
&& binding.correlationKey === undefined
|
|
60
|
+
&& binding.resolvedAt === undefined;
|
|
61
|
+
}
|
|
62
|
+
function projectCareEvidence(care, now = Date.now()) {
|
|
63
|
+
if (care.kind !== "system" || (care.status !== "active" && care.status !== "watching") || care.nextCheckAt === null)
|
|
64
|
+
return care;
|
|
65
|
+
const staleAt = Date.parse(care.nextCheckAt);
|
|
66
|
+
if (Number.isFinite(staleAt) && staleAt > now)
|
|
67
|
+
return care;
|
|
68
|
+
return {
|
|
69
|
+
id: care.id,
|
|
70
|
+
kind: care.kind,
|
|
71
|
+
status: care.status,
|
|
72
|
+
salience: care.salience,
|
|
73
|
+
steward: care.steward,
|
|
74
|
+
evidenceStatus: "stale",
|
|
75
|
+
recheckRequired: true,
|
|
76
|
+
staleAt: care.nextCheckAt,
|
|
77
|
+
lastAssessedAt: care.updatedAt,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
51
80
|
function caresDir(agentRoot) {
|
|
52
81
|
return path.join(agentRoot, "arc", "cares");
|
|
53
82
|
}
|
|
@@ -208,11 +237,21 @@ function bindCareIncident(agentRoot, id, binding, options) {
|
|
|
208
237
|
function upsertCareForIncident(agentRoot, input) {
|
|
209
238
|
return withCareMutationLock(agentRoot, () => {
|
|
210
239
|
const incident = canonicalIncidentBinding(input.incident);
|
|
211
|
-
const existing = (0, json_store_1.readJsonDir)(caresDir(agentRoot)).find((care) => care.incidentBindings?.some((binding) => binding.source === incident.source && binding.incidentKey === incident.incidentKey));
|
|
240
|
+
const existing = (0, json_store_1.readJsonDir)(caresDir(agentRoot)).find((care) => (!input.id || care.id === input.id) && care.incidentBindings?.some((binding) => binding.source === incident.source && binding.incidentKey === incident.incidentKey));
|
|
212
241
|
if (existing) {
|
|
213
242
|
const index = existing.incidentBindings.findIndex((binding) => binding.source === incident.source && binding.incidentKey === incident.incidentKey);
|
|
214
|
-
const
|
|
215
|
-
|
|
243
|
+
const requestedRisk = input.currentRisk === undefined ? existing.currentRisk : input.currentRisk === null ? null : (0, session_events_1.capStructuredRecordString)(input.currentRisk);
|
|
244
|
+
const requestedNextCheckAt = input.nextCheckAt === undefined ? existing.nextCheckAt : input.nextCheckAt;
|
|
245
|
+
const incidentStateUnchanged = JSON.stringify(existing.incidentBindings[index]) === JSON.stringify(incident)
|
|
246
|
+
&& existing.currentRisk === requestedRisk
|
|
247
|
+
&& existing.nextCheckAt === requestedNextCheckAt;
|
|
248
|
+
const displayUnchanged = !input.id || ((input.label === undefined || existing.label === (0, session_events_1.capStructuredRecordString)(input.label))
|
|
249
|
+
&& (input.why === undefined || existing.why === (0, session_events_1.capStructuredRecordString)(input.why))
|
|
250
|
+
&& (input.kind === undefined || existing.kind === input.kind)
|
|
251
|
+
&& (input.status === undefined || existing.status === input.status)
|
|
252
|
+
&& (input.salience === undefined || existing.salience === input.salience)
|
|
253
|
+
&& (input.steward === undefined || existing.steward === input.steward));
|
|
254
|
+
const unchanged = incidentStateUnchanged && displayUnchanged;
|
|
216
255
|
if (unchanged)
|
|
217
256
|
return existing;
|
|
218
257
|
if (!input.expectedUpdatedAt || input.expectedUpdatedAt !== existing.updatedAt)
|
|
@@ -221,15 +260,29 @@ function upsertCareForIncident(agentRoot, input) {
|
|
|
221
260
|
incidentBindings[index] = incident;
|
|
222
261
|
const updated = {
|
|
223
262
|
...existing,
|
|
224
|
-
|
|
225
|
-
|
|
263
|
+
...(input.id && input.label !== undefined ? { label: (0, session_events_1.capStructuredRecordString)(input.label) } : {}),
|
|
264
|
+
...(input.id && input.why !== undefined ? { why: (0, session_events_1.capStructuredRecordString)(input.why) } : {}),
|
|
265
|
+
...(input.id && input.kind !== undefined ? { kind: input.kind } : {}),
|
|
266
|
+
...(input.id && input.status !== undefined ? { status: input.status } : {}),
|
|
267
|
+
...(input.id && input.salience !== undefined ? { salience: input.salience } : {}),
|
|
268
|
+
...(input.id && input.steward !== undefined ? { steward: input.steward } : {}),
|
|
269
|
+
currentRisk: requestedRisk,
|
|
270
|
+
nextCheckAt: requestedNextCheckAt,
|
|
226
271
|
incidentBindings,
|
|
227
272
|
updatedAt: nextUpdatedAt(existing.updatedAt),
|
|
228
273
|
};
|
|
229
274
|
writeCareFile(agentRoot, updated);
|
|
230
275
|
return updated;
|
|
231
276
|
}
|
|
232
|
-
|
|
277
|
+
if (input.id)
|
|
278
|
+
throw new Error("Care incident upsert target not found");
|
|
279
|
+
const { id: _id, incident: _incident, expectedUpdatedAt: _expectedUpdatedAt, ...careInput } = input;
|
|
280
|
+
return createCareUnlocked(agentRoot, {
|
|
281
|
+
label: careInput.label ?? "untitled", why: careInput.why ?? "", kind: careInput.kind ?? "system", status: careInput.status ?? "active",
|
|
282
|
+
salience: careInput.salience ?? "medium", steward: careInput.steward ?? "mine", currentRisk: careInput.currentRisk ?? null,
|
|
283
|
+
nextCheckAt: careInput.nextCheckAt ?? null, relatedFriendIds: careInput.relatedFriendIds, relatedAgentIds: careInput.relatedAgentIds,
|
|
284
|
+
relatedObligationIds: careInput.relatedObligationIds, relatedEpisodeIds: careInput.relatedEpisodeIds, incidentBindings: [incident],
|
|
285
|
+
});
|
|
233
286
|
});
|
|
234
287
|
}
|
|
235
288
|
function resolveCareIncident(agentRoot, id, input) {
|
|
@@ -241,11 +294,40 @@ function resolveCareIncident(agentRoot, id, input) {
|
|
|
241
294
|
const index = bindings.findIndex((binding) => binding.source === input.source && binding.incidentKey === input.incidentKey);
|
|
242
295
|
if (index < 0)
|
|
243
296
|
throw new Error("Care incident binding not found");
|
|
244
|
-
|
|
297
|
+
const display = input.display;
|
|
298
|
+
const requestedDisplay = display ? {
|
|
299
|
+
label: display.label === undefined ? care.label : (0, session_events_1.capStructuredRecordString)(display.label),
|
|
300
|
+
why: display.why === undefined ? care.why : (0, session_events_1.capStructuredRecordString)(display.why),
|
|
301
|
+
currentRisk: display.currentRisk === undefined ? care.currentRisk : display.currentRisk === null ? null : (0, session_events_1.capStructuredRecordString)(display.currentRisk),
|
|
302
|
+
nextCheckAt: display.nextCheckAt === undefined ? care.nextCheckAt : display.nextCheckAt,
|
|
303
|
+
} : undefined;
|
|
304
|
+
const displayUnchanged = !requestedDisplay || JSON.stringify({
|
|
305
|
+
label: care.label,
|
|
306
|
+
why: care.why,
|
|
307
|
+
currentRisk: care.currentRisk,
|
|
308
|
+
nextCheckAt: care.nextCheckAt,
|
|
309
|
+
}) === JSON.stringify(requestedDisplay);
|
|
310
|
+
if (bindings[index].resolvedAt && displayUnchanged)
|
|
245
311
|
return care;
|
|
246
312
|
const now = nextUpdatedAt(care.updatedAt);
|
|
247
|
-
|
|
248
|
-
|
|
313
|
+
if (!bindings[index].resolvedAt)
|
|
314
|
+
bindings[index] = { ...bindings[index], resolvedAt: now };
|
|
315
|
+
const noUnresolvedBindings = bindings.every((binding) => binding.resolvedAt);
|
|
316
|
+
const unresolvedBindingsBeforeResolution = care.incidentBindings.filter((binding) => !binding.resolvedAt);
|
|
317
|
+
const soleBindingOwnsRisk = care.kind === "system" && unresolvedBindingsBeforeResolution.length === 1 && isManagedDockerCareIncidentBinding(unresolvedBindingsBeforeResolution[0]);
|
|
318
|
+
const unresolvedContextRemains = !noUnresolvedBindings || (care.currentRisk !== null && !soleBindingOwnsRisk);
|
|
319
|
+
const canonicalDisplay = requestedDisplay && requestedDisplay.currentRisk === null && unresolvedContextRemains
|
|
320
|
+
? { ...requestedDisplay, currentRisk: exports.CARE_INCIDENT_RECOVERY_REVIEW_RISK, nextCheckAt: requestedDisplay.nextCheckAt ?? new Date(Date.parse(now) + 15 * 60_000).toISOString() }
|
|
321
|
+
: requestedDisplay;
|
|
322
|
+
const resultingRisk = canonicalDisplay ? canonicalDisplay.currentRisk : care.currentRisk;
|
|
323
|
+
const shouldResolveCare = noUnresolvedBindings && (care.currentRisk === null || soleBindingOwnsRisk) && resultingRisk === null;
|
|
324
|
+
const updated = {
|
|
325
|
+
...care,
|
|
326
|
+
...canonicalDisplay,
|
|
327
|
+
incidentBindings: bindings,
|
|
328
|
+
...(shouldResolveCare ? { status: "resolved", resolvedAt: care.resolvedAt ?? now } : {}),
|
|
329
|
+
updatedAt: now,
|
|
330
|
+
};
|
|
249
331
|
writeCareFile(agentRoot, updated);
|
|
250
332
|
(0, runtime_1.emitNervesEvent)({
|
|
251
333
|
component: "heart",
|
package/dist/heart/core.js
CHANGED
|
@@ -569,10 +569,10 @@ function toolResultIndicatesFailure(content) {
|
|
|
569
569
|
|| normalized.startsWith("blocked:")
|
|
570
570
|
|| normalized.startsWith("rejected:");
|
|
571
571
|
}
|
|
572
|
-
function requiredToolResultSucceeded(name, content, validate) {
|
|
572
|
+
function requiredToolResultSucceeded(name, content, args, validate) {
|
|
573
573
|
if (toolResultIndicatesFailure(content))
|
|
574
574
|
return false;
|
|
575
|
-
return validate?.(name, content) ?? true;
|
|
575
|
+
return validate?.(name, content, args) ?? true;
|
|
576
576
|
}
|
|
577
577
|
function effectFingerprint(name, rawArguments) {
|
|
578
578
|
let args;
|
|
@@ -1252,6 +1252,11 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1252
1252
|
})()
|
|
1253
1253
|
: ordinaryActiveTools;
|
|
1254
1254
|
const candidateToolNames = new Set(candidateActiveTools.map((tool) => tool.function.name));
|
|
1255
|
+
const unadvertisedRequiredTool = requiredToolCallNames.find((name) => !candidateToolNames.has(name));
|
|
1256
|
+
if (unadvertisedRequiredTool) {
|
|
1257
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", component: "engine", event: "engine.required_tool_unadvertised", message: "required tool is not advertised for the active channel", meta: { toolName: unadvertisedRequiredTool, channel: String(channel) } });
|
|
1258
|
+
throw new Error(`required tool is not advertised for this channel: ${unadvertisedRequiredTool}`);
|
|
1259
|
+
}
|
|
1255
1260
|
const forcedHistoricalToolNames = new Set(unresolvedHistoricalEffects
|
|
1256
1261
|
.map((effect) => effect.name)
|
|
1257
1262
|
.filter((name) => candidateToolNames.has(name)));
|
|
@@ -1942,7 +1947,14 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1942
1947
|
done = true;
|
|
1943
1948
|
continue;
|
|
1944
1949
|
}
|
|
1945
|
-
const
|
|
1950
|
+
const requiredDispatchRejections = new Map();
|
|
1951
|
+
for (const entry of validCalls) {
|
|
1952
|
+
const requiredArgs = entry.validated.arguments;
|
|
1953
|
+
const rejection = options?.requiredToolCalls?.validateToolCallBeforeDispatch?.(entry.call.name, requiredArgs);
|
|
1954
|
+
if (rejection)
|
|
1955
|
+
requiredDispatchRejections.set(entry.call.id, { name: entry.call.name, args: requiredArgs, message: rejection });
|
|
1956
|
+
}
|
|
1957
|
+
const approvalCalls = await Promise.all(validCalls.filter((entry) => !requiredDispatchRejections.has(entry.call.id)).map(async (entry) => {
|
|
1946
1958
|
const classification = await (0, tools_1.classifyApprovalForInvocation)(entry.call.name, entry.validated.arguments, augmentedToolContext);
|
|
1947
1959
|
return {
|
|
1948
1960
|
...entry,
|
|
@@ -2039,6 +2051,28 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
2039
2051
|
for (const tc of result.toolCalls) {
|
|
2040
2052
|
if (signal?.aborted)
|
|
2041
2053
|
break;
|
|
2054
|
+
const requiredDispatchRejection = requiredDispatchRejections.get(tc.id);
|
|
2055
|
+
if (requiredDispatchRejection) {
|
|
2056
|
+
callbacks.onToolStart(tc.name, requiredDispatchRejection.args);
|
|
2057
|
+
callbacks.onToolEnd(tc.name, (0, tools_1.summarizeArgs)(tc.name, requiredDispatchRejection.args), false);
|
|
2058
|
+
pushGenerated({ role: "tool", tool_call_id: tc.id, content: requiredDispatchRejection.message });
|
|
2059
|
+
providerRuntime.appendToolOutput(tc.id, requiredDispatchRejection.message);
|
|
2060
|
+
options?.toolBoundaryObserver?.({
|
|
2061
|
+
name: tc.name,
|
|
2062
|
+
reason: "dependency_rejected",
|
|
2063
|
+
globallyResolvable: typeof (0, tools_1.resolveToolDefinition)(tc.name)?.handler === "function",
|
|
2064
|
+
invoked: false,
|
|
2065
|
+
sideEffect: false,
|
|
2066
|
+
});
|
|
2067
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2068
|
+
level: "warn",
|
|
2069
|
+
component: "engine",
|
|
2070
|
+
event: "engine.required_tool_dispatch_rejected",
|
|
2071
|
+
message: "required tool dependency rejected before approval and handler dispatch",
|
|
2072
|
+
meta: { toolName: tc.name },
|
|
2073
|
+
});
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2042
2076
|
// Reject sole-call tools when mixed with other tool calls
|
|
2043
2077
|
const terminalProjection = (0, tools_1.resolveToolDefinition)(tc.name)?.terminalProjection;
|
|
2044
2078
|
const soleCallRejection = SOLE_CALL_REJECTION[tc.name]
|
|
@@ -2321,9 +2355,20 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
2321
2355
|
success = false;
|
|
2322
2356
|
augmentedToolContext?.habitSession?.recordError?.(toolResult);
|
|
2323
2357
|
}
|
|
2324
|
-
|
|
2325
|
-
|
|
2358
|
+
const validatedRequiredResult = success && requiredToolCallNames.includes(tc.name) && options?.requiredToolCalls?.requireSuccessfulResults
|
|
2359
|
+
? requiredToolResultSucceeded(tc.name, toolResult, args, options.requiredToolCalls.validateRequiredToolResult)
|
|
2360
|
+
: false;
|
|
2361
|
+
if (validatedRequiredResult) {
|
|
2326
2362
|
dispatchedRequiredToolCalls.add(tc.name);
|
|
2363
|
+
for (const requiredName of options?.requiredToolCalls?.requiredToolCallsAfterResult?.(tc.name, args, toolResult) ?? []) {
|
|
2364
|
+
if (!candidateToolNames.has(requiredName)) {
|
|
2365
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", component: "engine", event: "engine.required_tool_unadvertised", message: "dependent required tool is not advertised for the active channel", meta: { toolName: requiredName, channel: String(channel) } });
|
|
2366
|
+
throw new Error(`dependent required tool is not advertised for this channel: ${requiredName}`);
|
|
2367
|
+
}
|
|
2368
|
+
if (!requiredToolCallNames.includes(requiredName))
|
|
2369
|
+
requiredToolCallNames.push(requiredName);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2327
2372
|
if (success && currentEffectFingerprint && !toolResultIndicatesFailure(toolResult)) {
|
|
2328
2373
|
unresolvedHistoricalEffects = unresolvedHistoricalEffects.filter((effect) => effect.fingerprint !== currentEffectFingerprint);
|
|
2329
2374
|
}
|
|
@@ -70,6 +70,7 @@ const mailbox_types_1 = require("../mailbox/mailbox-types");
|
|
|
70
70
|
const mailbox_read_1 = require("../mailbox/mailbox-read");
|
|
71
71
|
const mailbox_view_1 = require("../mailbox/mailbox-view");
|
|
72
72
|
const provider_visibility_1 = require("../provider-visibility");
|
|
73
|
+
const provider_binding_resolver_1 = require("../provider-binding-resolver");
|
|
73
74
|
const private_runtime_1 = require("../private-runtime");
|
|
74
75
|
const socket_client_1 = require("./socket-client");
|
|
75
76
|
const flight_recorder_1 = require("../../arc/flight-recorder");
|
|
@@ -1555,6 +1556,7 @@ class OuroDaemon {
|
|
|
1555
1556
|
}
|
|
1556
1557
|
async dispatchExternalEvents(records) {
|
|
1557
1558
|
const claimed = [];
|
|
1559
|
+
let failureClass;
|
|
1558
1560
|
try {
|
|
1559
1561
|
for (const record of records) {
|
|
1560
1562
|
const owner = `external-event:${record.agent}:${record.source}:${record.eventId}:generation:${record.generation}:attempt:${record.attemptCount + 1}`;
|
|
@@ -1575,6 +1577,11 @@ class OuroDaemon {
|
|
|
1575
1577
|
eventId: primary.eventId,
|
|
1576
1578
|
}, receipt.id, primary.generation, primary.attemptCount), () => { for (const record of claimed)
|
|
1577
1579
|
this.queueExternalEventForPrivateRuntime(record); }, lease);
|
|
1580
|
+
failureClass = wake.denialCode === "managed_runtime_unavailable"
|
|
1581
|
+
? "managed_runtime_unavailable"
|
|
1582
|
+
: wake.data?.decision?.denialCode === "provider_lane_unavailable"
|
|
1583
|
+
? "provider_lane_unavailable"
|
|
1584
|
+
: undefined;
|
|
1578
1585
|
const denied = !wake.ok || wake.data?.decision?.executable === false;
|
|
1579
1586
|
if (denied)
|
|
1580
1587
|
throw new Error(wake.error ?? wake.message ?? /* v8 ignore next -- daemon wake responses always carry an error or message @preserve */ "external-event private turn was denied");
|
|
@@ -1587,6 +1594,7 @@ class OuroDaemon {
|
|
|
1587
1594
|
(0, router_1.failExternalEventAttempt)(latest.recordPath, {
|
|
1588
1595
|
owner: record.claimOwner, expectedVersion: latest.version, expectedGeneration: latest.generation,
|
|
1589
1596
|
error: error instanceof Error ? error.message : String(error),
|
|
1597
|
+
...(failureClass ? { failureClass } : {}),
|
|
1590
1598
|
});
|
|
1591
1599
|
}
|
|
1592
1600
|
}
|
|
@@ -1614,6 +1622,43 @@ class OuroDaemon {
|
|
|
1614
1622
|
let record = (0, router_1.readExternalEventRecord)(status.recordPath);
|
|
1615
1623
|
if (record.dispatchEnabled === false)
|
|
1616
1624
|
continue;
|
|
1625
|
+
if (record.executionState === "dead_letter") {
|
|
1626
|
+
const failure = (0, router_1.externalEventRecoveryFailure)(record);
|
|
1627
|
+
if (!failure || record.recoveryGrant?.generation === record.generation)
|
|
1628
|
+
continue;
|
|
1629
|
+
let evidence = null;
|
|
1630
|
+
try {
|
|
1631
|
+
if (failure.class === "provider_lane_unavailable") {
|
|
1632
|
+
const binding = (0, provider_binding_resolver_1.resolveEffectiveProviderBinding)({
|
|
1633
|
+
agentName: record.agent,
|
|
1634
|
+
agentRoot: path.join(this.bundlesRoot, `${record.agent}.ouro`),
|
|
1635
|
+
lane: "inner",
|
|
1636
|
+
});
|
|
1637
|
+
if (binding.ok && binding.binding.credential.status === "present"
|
|
1638
|
+
&& binding.binding.readiness.status === "ready" && binding.binding.readiness.checkedAt
|
|
1639
|
+
&& Date.parse(binding.binding.readiness.checkedAt) > Date.parse(failure.failedAt)) {
|
|
1640
|
+
evidence = { class: failure.class, observedAt: binding.binding.readiness.checkedAt };
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
else if (this.hasManagedPrivateRuntime(record.agent) && Date.parse(now) > Date.parse(failure.failedAt)) {
|
|
1644
|
+
evidence = { class: failure.class, observedAt: now };
|
|
1645
|
+
}
|
|
1646
|
+
if (!evidence)
|
|
1647
|
+
continue;
|
|
1648
|
+
const revival = (0, router_1.reviveExternalEventAfterRecovery)(record.recordPath, { expectedVersion: record.version, expectedGeneration: record.generation, evidence, now: () => now });
|
|
1649
|
+
record = revival.record;
|
|
1650
|
+
}
|
|
1651
|
+
catch (error) {
|
|
1652
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1653
|
+
level: "warn",
|
|
1654
|
+
component: "daemon",
|
|
1655
|
+
event: "daemon.external_event_recovery_error",
|
|
1656
|
+
message: "external event dead-letter recovery check failed",
|
|
1657
|
+
meta: { agent: record.agent, source: record.source, eventId: record.eventId, generation: record.generation, error: error instanceof Error ? error.message : String(error) },
|
|
1658
|
+
});
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1617
1662
|
if (record.executionState === "running" && record.claimExpiresAt && Date.parse(record.claimExpiresAt) <= Date.parse(now)) {
|
|
1618
1663
|
record = (0, router_1.reconcileExternalEvent)(record.recordPath);
|
|
1619
1664
|
}
|
|
@@ -1657,6 +1702,7 @@ class OuroDaemon {
|
|
|
1657
1702
|
return {
|
|
1658
1703
|
ok: false,
|
|
1659
1704
|
error: `No managed agent '${command.agent}' is registered with daemon-managed private runtime.`,
|
|
1705
|
+
denialCode: "managed_runtime_unavailable",
|
|
1660
1706
|
};
|
|
1661
1707
|
}
|
|
1662
1708
|
const decision = await (0, private_runtime_1.requestPrivateTurnDecision)(this.buildPrivateRuntimeWakeRequest(command), {
|
|
@@ -1667,6 +1713,7 @@ class OuroDaemon {
|
|
|
1667
1713
|
return {
|
|
1668
1714
|
ok: true,
|
|
1669
1715
|
message: `private-runtime wake denied for ${command.agent}: ${decision.deniedReason}`,
|
|
1716
|
+
...(decision.denialCode ? { denialCode: decision.denialCode } : {}),
|
|
1670
1717
|
data: { decision },
|
|
1671
1718
|
};
|
|
1672
1719
|
}
|
|
@@ -45,6 +45,8 @@ exports.claimExternalEvent = claimExternalEvent;
|
|
|
45
45
|
exports.renewExternalEventClaim = renewExternalEventClaim;
|
|
46
46
|
exports.commitExternalEventDisposition = commitExternalEventDisposition;
|
|
47
47
|
exports.failExternalEventAttempt = failExternalEventAttempt;
|
|
48
|
+
exports.externalEventRecoveryFailure = externalEventRecoveryFailure;
|
|
49
|
+
exports.reviveExternalEventAfterRecovery = reviveExternalEventAfterRecovery;
|
|
48
50
|
exports.reconcileExternalEvent = reconcileExternalEvent;
|
|
49
51
|
exports.advanceExternalEventFromAwait = advanceExternalEventFromAwait;
|
|
50
52
|
exports.advanceExternalEventsFromAwait = advanceExternalEventsFromAwait;
|
|
@@ -305,11 +307,19 @@ function isRecord(value) {
|
|
|
305
307
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
306
308
|
return false;
|
|
307
309
|
const candidate = value;
|
|
310
|
+
const failure = candidate.failureProvenance;
|
|
311
|
+
const grant = candidate.recoveryGrant;
|
|
312
|
+
const validFailure = failure === undefined || (failure !== null && typeof failure === "object"
|
|
313
|
+
&& (failure.class === "provider_lane_unavailable" || failure.class === "managed_runtime_unavailable") && canonicalIso(failure.failedAt));
|
|
314
|
+
const validGrant = grant === undefined || (grant !== null && typeof grant === "object"
|
|
315
|
+
&& Number.isSafeInteger(grant.generation) && grant.generation === candidate.generation && canonicalIso(grant.consumedAt));
|
|
308
316
|
return candidate.schemaVersion === 2
|
|
309
317
|
&& typeof candidate.recordPath === "string"
|
|
310
318
|
&& Number.isSafeInteger(candidate.version)
|
|
311
319
|
&& Number.isSafeInteger(candidate.generation)
|
|
312
|
-
&& typeof candidate.observationRevision === "string"
|
|
320
|
+
&& typeof candidate.observationRevision === "string"
|
|
321
|
+
&& validFailure
|
|
322
|
+
&& validGrant;
|
|
313
323
|
}
|
|
314
324
|
function readExternalEventRecord(recordPath) {
|
|
315
325
|
const parsed = JSON.parse(fs.readFileSync(recordPath, "utf8"));
|
|
@@ -359,6 +369,8 @@ function listExternalEventStatus(root) {
|
|
|
359
369
|
careId: record.disposition?.careId ?? null,
|
|
360
370
|
awaitId: record.disposition?.awaitId ?? null,
|
|
361
371
|
lastError: record.lastError,
|
|
372
|
+
failureProvenance: record.failureProvenance ?? null,
|
|
373
|
+
recoveryGrant: record.recoveryGrant ?? null,
|
|
362
374
|
nextAttemptAt: record.nextAttemptAt,
|
|
363
375
|
claimOwner: record.claimOwner,
|
|
364
376
|
claimExpiresAt: record.claimExpiresAt,
|
|
@@ -390,6 +402,8 @@ function listExternalEventStatus(root) {
|
|
|
390
402
|
careId: null,
|
|
391
403
|
awaitId: null,
|
|
392
404
|
lastError: `invalid receipt: ${error instanceof Error ? error.message : /* v8 ignore next -- filesystem and JSON parsers throw Error objects @preserve */ String(error)}`,
|
|
405
|
+
failureProvenance: null,
|
|
406
|
+
recoveryGrant: null,
|
|
393
407
|
nextAttemptAt: null,
|
|
394
408
|
claimOwner: null,
|
|
395
409
|
claimExpiresAt: null,
|
|
@@ -533,6 +547,8 @@ function recordExternalEventInternal(input, options = {}) {
|
|
|
533
547
|
claimExpiresAt: shouldWake || quietInitialReceipt ? null : existing.claimExpiresAt,
|
|
534
548
|
nextAttemptAt: shouldWake || quietInitialReceipt ? null : existing.nextAttemptAt,
|
|
535
549
|
lastError: shouldWake || quietInitialReceipt ? null : existing.lastError,
|
|
550
|
+
...(!shouldWake && !quietInitialReceipt && existing?.failureProvenance ? { failureProvenance: existing.failureProvenance } : {}),
|
|
551
|
+
...(!shouldWake && !quietInitialReceipt && existing?.recoveryGrant ? { recoveryGrant: existing.recoveryGrant } : {}),
|
|
536
552
|
disposition: shouldWake || quietInitialReceipt ? null : existing.disposition,
|
|
537
553
|
pendingObservation: shouldWake || quietInitialReceipt ? null : existing.pendingObservation,
|
|
538
554
|
dispatchEnabled: options.dispatchEnabled ?? existing?.dispatchEnabled ?? true,
|
|
@@ -1032,12 +1048,13 @@ function commitExternalEventDisposition(recordPath, input) {
|
|
|
1032
1048
|
disposition: wakePending ? null : input.disposition,
|
|
1033
1049
|
pendingObservation: null,
|
|
1034
1050
|
pendingPrivilegedProtectiveAction: undefined,
|
|
1051
|
+
...(wakePending ? { failureProvenance: undefined, recoveryGrant: undefined } : {}),
|
|
1035
1052
|
shouldWake: wakePending,
|
|
1036
1053
|
}, now);
|
|
1037
1054
|
});
|
|
1038
1055
|
}
|
|
1039
|
-
function retryState(record, now, maxAttempts, baseDelayMs, error) {
|
|
1040
|
-
const dead = record.attemptCount >= maxAttempts;
|
|
1056
|
+
function retryState(record, now, maxAttempts, baseDelayMs, error, failureClass) {
|
|
1057
|
+
const dead = record.recoveryGrant?.generation === record.generation || record.attemptCount >= maxAttempts;
|
|
1041
1058
|
return {
|
|
1042
1059
|
...record,
|
|
1043
1060
|
executionState: dead ? "dead_letter" : "retry_wait",
|
|
@@ -1045,6 +1062,7 @@ function retryState(record, now, maxAttempts, baseDelayMs, error) {
|
|
|
1045
1062
|
claimExpiresAt: null,
|
|
1046
1063
|
nextAttemptAt: dead ? null : new Date(Date.parse(now) + baseDelayMs * 2 ** Math.max(0, record.attemptCount - 1)).toISOString(),
|
|
1047
1064
|
lastError: error.slice(0, 1_000),
|
|
1065
|
+
...(dead && failureClass && !record.failureProvenance ? { failureProvenance: { class: failureClass, failedAt: now } } : {}),
|
|
1048
1066
|
shouldWake: false,
|
|
1049
1067
|
};
|
|
1050
1068
|
}
|
|
@@ -1055,9 +1073,73 @@ function failExternalEventAttempt(recordPath, input) {
|
|
|
1055
1073
|
const now = input.now?.() ?? new Date().toISOString();
|
|
1056
1074
|
const maxAttempts = input.maxAttempts ?? 5;
|
|
1057
1075
|
const baseDelayMs = input.baseDelayMs ?? 1_000;
|
|
1076
|
+
if (input.failureClass !== undefined && input.failureClass !== "provider_lane_unavailable" && input.failureClass !== "managed_runtime_unavailable")
|
|
1077
|
+
throw new Error("External event failure class is invalid");
|
|
1058
1078
|
if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1 || !Number.isSafeInteger(baseDelayMs) || baseDelayMs < 1)
|
|
1059
1079
|
throw new Error("External event retry policy is invalid");
|
|
1060
|
-
return commitMutation(recordPath, retryState(record, now, maxAttempts, baseDelayMs, input.error), now);
|
|
1080
|
+
return commitMutation(recordPath, retryState(record, now, maxAttempts, baseDelayMs, input.error, input.failureClass), now);
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
function exactLegacyProviderFailure(record) {
|
|
1084
|
+
const escapedAgent = record.agent.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
1085
|
+
const canonical = new RegExp(`^private-runtime wake denied for ${escapedAgent}: provider lane resolution failed$`, "u");
|
|
1086
|
+
return record.lastError && canonical.test(record.lastError)
|
|
1087
|
+
? { class: "provider_lane_unavailable", failedAt: record.updatedAt }
|
|
1088
|
+
: null;
|
|
1089
|
+
}
|
|
1090
|
+
function externalEventRecoveryFailure(record) {
|
|
1091
|
+
return record.failureProvenance ?? exactLegacyProviderFailure(record);
|
|
1092
|
+
}
|
|
1093
|
+
function recoveryIneligible(record, reason) {
|
|
1094
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1095
|
+
component: "daemon",
|
|
1096
|
+
event: "daemon.external_event_recovery_ineligible",
|
|
1097
|
+
message: "external event dead-letter recovery was ineligible or exhausted",
|
|
1098
|
+
meta: { agent: record.agent, source: record.source, eventId: record.eventId, generation: record.generation, reason },
|
|
1099
|
+
});
|
|
1100
|
+
return { revived: false, reason, record };
|
|
1101
|
+
}
|
|
1102
|
+
function reviveExternalEventAfterRecovery(recordPath, input) {
|
|
1103
|
+
return withRecordLock(recordPath, () => {
|
|
1104
|
+
const record = readExternalEventRecord(recordPath);
|
|
1105
|
+
assertCas(record, input);
|
|
1106
|
+
if (input.evidence.class !== "provider_lane_unavailable" && input.evidence.class !== "managed_runtime_unavailable")
|
|
1107
|
+
throw new Error("External event recovery evidence class is invalid");
|
|
1108
|
+
if (!canonicalIso(input.evidence.observedAt))
|
|
1109
|
+
throw new Error("External event recovery evidence time is invalid");
|
|
1110
|
+
if (record.executionState !== "dead_letter")
|
|
1111
|
+
return recoveryIneligible(record, "not_dead_letter");
|
|
1112
|
+
if (record.dispatchEnabled === false)
|
|
1113
|
+
return recoveryIneligible(record, "dispatch_disabled");
|
|
1114
|
+
if (record.recoveryGrant?.generation === record.generation)
|
|
1115
|
+
return recoveryIneligible(record, "grant_consumed");
|
|
1116
|
+
const failure = externalEventRecoveryFailure(record);
|
|
1117
|
+
if (!failure)
|
|
1118
|
+
return recoveryIneligible(record, "ineligible_failure");
|
|
1119
|
+
if (failure.class !== input.evidence.class)
|
|
1120
|
+
return recoveryIneligible(record, "evidence_mismatch");
|
|
1121
|
+
if (Date.parse(input.evidence.observedAt) <= Date.parse(failure.failedAt))
|
|
1122
|
+
return recoveryIneligible(record, "stale_evidence");
|
|
1123
|
+
const now = input.now?.() ?? new Date().toISOString();
|
|
1124
|
+
if (!canonicalIso(now))
|
|
1125
|
+
throw new Error("External event recovery grant consumed time is invalid");
|
|
1126
|
+
const revived = commitMutation(recordPath, {
|
|
1127
|
+
...record,
|
|
1128
|
+
executionState: "queued",
|
|
1129
|
+
claimOwner: null,
|
|
1130
|
+
claimExpiresAt: null,
|
|
1131
|
+
nextAttemptAt: null,
|
|
1132
|
+
shouldWake: true,
|
|
1133
|
+
failureProvenance: failure,
|
|
1134
|
+
recoveryGrant: { generation: record.generation, consumedAt: now },
|
|
1135
|
+
}, now);
|
|
1136
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1137
|
+
component: "daemon",
|
|
1138
|
+
event: "daemon.external_event_requeued",
|
|
1139
|
+
message: "requeued external event after infrastructure recovery",
|
|
1140
|
+
meta: { agent: revived.agent, source: revived.source, eventId: revived.eventId, generation: revived.generation, failureClass: failure.class },
|
|
1141
|
+
});
|
|
1142
|
+
return { revived: true, record: revived };
|
|
1061
1143
|
});
|
|
1062
1144
|
}
|
|
1063
1145
|
function reconcileExternalEvent(recordPath, options = {}) {
|
|
@@ -1084,6 +1166,8 @@ function advanceExternalEventFromAwait(recordPath, input) {
|
|
|
1084
1166
|
generation: record.generation + 1,
|
|
1085
1167
|
executionState: "queued",
|
|
1086
1168
|
attemptCount: 0,
|
|
1169
|
+
failureProvenance: undefined,
|
|
1170
|
+
recoveryGrant: undefined,
|
|
1087
1171
|
disposition: null,
|
|
1088
1172
|
shouldWake: true,
|
|
1089
1173
|
}, now);
|
|
@@ -57,6 +57,7 @@ function sanitizePrivateDecision(row, ledgerPath) {
|
|
|
57
57
|
? row
|
|
58
58
|
: {};
|
|
59
59
|
const deniedReason = stringField(record.deniedReason);
|
|
60
|
+
const denialCode = record.denialCode === "provider_lane_unavailable" ? record.denialCode : undefined;
|
|
60
61
|
const duplicateOf = stringField(record.duplicateOf);
|
|
61
62
|
const error = stringField(record.error);
|
|
62
63
|
return {
|
|
@@ -76,6 +77,7 @@ function sanitizePrivateDecision(row, ledgerPath) {
|
|
|
76
77
|
decidedAt: stringField(record.decidedAt),
|
|
77
78
|
ledgerLocator: ledgerLocatorField(record.ledgerLocator, ledgerPath),
|
|
78
79
|
...(deniedReason ? { deniedReason } : {}),
|
|
80
|
+
...(denialCode ? { denialCode } : {}),
|
|
79
81
|
...(duplicateOf ? { duplicateOf } : {}),
|
|
80
82
|
...(error ? { error } : {}),
|
|
81
83
|
};
|
|
@@ -137,6 +137,7 @@ function mismatchDecision(candidate, existing) {
|
|
|
137
137
|
result: "deny",
|
|
138
138
|
executable: false,
|
|
139
139
|
deniedReason: "idempotency-key fingerprint mismatch",
|
|
140
|
+
denialCode: undefined,
|
|
140
141
|
duplicateOf: existing.receiptId,
|
|
141
142
|
};
|
|
142
143
|
}
|
|
@@ -159,6 +160,7 @@ function ledgerWriteFailedDecision(candidate, ledgerPath, error) {
|
|
|
159
160
|
result: "deny",
|
|
160
161
|
executable: false,
|
|
161
162
|
deniedReason: "ledger write failed",
|
|
163
|
+
denialCode: undefined,
|
|
162
164
|
ledgerLocator: { path: ledgerPath },
|
|
163
165
|
error: String(error),
|
|
164
166
|
};
|
|
@@ -194,7 +196,8 @@ function recordPrivateTurnDecision(decision, deps = {}) {
|
|
|
194
196
|
if (latestSameFingerprint
|
|
195
197
|
&& latestSameFingerprint.result === candidate.result
|
|
196
198
|
&& latestSameFingerprint.executable === candidate.executable
|
|
197
|
-
&& latestSameFingerprint.deniedReason === candidate.deniedReason
|
|
199
|
+
&& latestSameFingerprint.deniedReason === candidate.deniedReason
|
|
200
|
+
&& latestSameFingerprint.denialCode === candidate.denialCode) {
|
|
198
201
|
return latestSameFingerprint;
|
|
199
202
|
}
|
|
200
203
|
if (priorExecutable && !candidate.executable) {
|
|
@@ -167,12 +167,6 @@ async function requestPrivateTurnDecision(request, deps = {}) {
|
|
|
167
167
|
let evaluation;
|
|
168
168
|
try {
|
|
169
169
|
providerLane = await resolveProviderLaneMetadata(normalizedRequest, deps);
|
|
170
|
-
requestFingerprint = createPrivateTurnRequestFingerprint(normalizedRequest, providerLane);
|
|
171
|
-
evaluation = await evaluatePolicy(normalizedRequest, {
|
|
172
|
-
requestFingerprint,
|
|
173
|
-
idempotencyKey,
|
|
174
|
-
providerLane,
|
|
175
|
-
}, deps);
|
|
176
170
|
}
|
|
177
171
|
catch (error) {
|
|
178
172
|
providerLane = {
|
|
@@ -181,13 +175,30 @@ async function requestPrivateTurnDecision(request, deps = {}) {
|
|
|
181
175
|
model: "-",
|
|
182
176
|
source: "agent.json",
|
|
183
177
|
};
|
|
184
|
-
requestFingerprint = createPrivateTurnRequestFingerprint(normalizedRequest, providerLane);
|
|
185
178
|
evaluation = {
|
|
186
179
|
result: "deny",
|
|
187
180
|
reason: error instanceof Error ? error.message : String(error),
|
|
188
181
|
deniedReason: "provider lane resolution failed",
|
|
182
|
+
denialCode: "provider_lane_unavailable",
|
|
189
183
|
};
|
|
190
184
|
}
|
|
185
|
+
requestFingerprint = createPrivateTurnRequestFingerprint(normalizedRequest, providerLane);
|
|
186
|
+
if (evaluation === undefined) {
|
|
187
|
+
try {
|
|
188
|
+
evaluation = await evaluatePolicy(normalizedRequest, {
|
|
189
|
+
requestFingerprint,
|
|
190
|
+
idempotencyKey,
|
|
191
|
+
providerLane,
|
|
192
|
+
}, deps);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
evaluation = {
|
|
196
|
+
result: "deny",
|
|
197
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
198
|
+
deniedReason: "private runtime policy evaluation failed",
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
191
202
|
const result = evaluation.result;
|
|
192
203
|
emitPolicyEvaluated(deps, { request: normalizedRequest, result, requestFingerprint, idempotencyKey });
|
|
193
204
|
const reason = evaluation.reason ?? normalizedRequest.reason;
|
|
@@ -208,7 +219,10 @@ async function requestPrivateTurnDecision(request, deps = {}) {
|
|
|
208
219
|
executable: result === "allow",
|
|
209
220
|
decidedAt: nowIso(deps),
|
|
210
221
|
ledgerLocator: { path: deps.ledgerPath ?? "" },
|
|
211
|
-
...(result === "deny" ? {
|
|
222
|
+
...(result === "deny" ? {
|
|
223
|
+
deniedReason: evaluation.deniedReason ?? reason,
|
|
224
|
+
...(evaluation.denialCode ? { denialCode: evaluation.denialCode } : {}),
|
|
225
|
+
} : {}),
|
|
212
226
|
};
|
|
213
227
|
return (0, ledger_1.recordPrivateTurnDecision)(decision, deps);
|
|
214
228
|
}
|
|
@@ -42,6 +42,7 @@ const path = __importStar(require("path"));
|
|
|
42
42
|
const runtime_1 = require("../nerves/runtime");
|
|
43
43
|
const bundle_state_1 = require("./bundle-state");
|
|
44
44
|
const tempo_1 = require("./tempo");
|
|
45
|
+
const cares_1 = require("../arc/cares");
|
|
45
46
|
const flight_recorder_1 = require("../arc/flight-recorder");
|
|
46
47
|
const context_loss_sentinel_1 = require("./context-loss-sentinel");
|
|
47
48
|
const orientation_frame_1 = require("./orientation-frame");
|
|
@@ -82,11 +83,14 @@ function buildObligationsSection(obligations) {
|
|
|
82
83
|
})
|
|
83
84
|
.join("\n");
|
|
84
85
|
}
|
|
85
|
-
function buildCaresSection(cares) {
|
|
86
|
+
function buildCaresSection(cares, now) {
|
|
86
87
|
if (cares.length === 0)
|
|
87
88
|
return "";
|
|
88
89
|
return cares
|
|
89
|
-
.map((
|
|
90
|
+
.map((care) => {
|
|
91
|
+
const c = (0, cares_1.projectCareEvidence)(care, now);
|
|
92
|
+
if ("recheckRequired" in c)
|
|
93
|
+
return `- system care [${c.salience}] evidence stale; recheck required`;
|
|
90
94
|
const parts = [`- ${c.label}`];
|
|
91
95
|
if (c.salience !== "low") {
|
|
92
96
|
parts.push(` [${c.salience}]`);
|
|
@@ -192,7 +196,7 @@ function buildStartOfTurnPacket(view, opts) {
|
|
|
192
196
|
const packet = {
|
|
193
197
|
plotLine: buildPlotLine(view.recentEpisodes, tempo),
|
|
194
198
|
obligations: buildObligationsSection(effectiveObligations),
|
|
195
|
-
cares: buildCaresSection(view.activeCares),
|
|
199
|
+
cares: buildCaresSection(view.activeCares, opts?.careEvidenceNow ?? Date.now()),
|
|
196
200
|
presence: buildPresenceSection(view.peerPresence),
|
|
197
201
|
arcResume: opts?.flightRecorderResume ? (0, flight_recorder_1.formatFlightRecorderResume)(opts.flightRecorderResume) : undefined,
|
|
198
202
|
recoverySentinel: opts?.recoverySentinel,
|
|
@@ -45,24 +45,6 @@ const presence_1 = require("../arc/presence");
|
|
|
45
45
|
const intentions_1 = require("../arc/intentions");
|
|
46
46
|
const steward_policy_1 = require("../heart/steward-policy");
|
|
47
47
|
const await_parser_1 = require("../heart/awaiting/await-parser");
|
|
48
|
-
function presentCare(care) {
|
|
49
|
-
const staleAt = care.nextCheckAt ? Date.parse(care.nextCheckAt) : Number.NaN;
|
|
50
|
-
if (care.kind !== "system" || !["active", "watching"].includes(care.status) || care.nextCheckAt === null)
|
|
51
|
-
return care;
|
|
52
|
-
if (Number.isFinite(staleAt) && staleAt >= Date.now())
|
|
53
|
-
return care;
|
|
54
|
-
return {
|
|
55
|
-
id: care.id,
|
|
56
|
-
kind: care.kind,
|
|
57
|
-
status: care.status,
|
|
58
|
-
salience: care.salience,
|
|
59
|
-
steward: care.steward,
|
|
60
|
-
evidenceStatus: "stale",
|
|
61
|
-
recheckRequired: true,
|
|
62
|
-
staleAt: care.nextCheckAt,
|
|
63
|
-
lastAssessedAt: care.updatedAt,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
48
|
exports.continuityToolDefinitions = [
|
|
67
49
|
// ── Continuity tools ──────────────────────────────────────────────
|
|
68
50
|
{
|
|
@@ -326,7 +308,8 @@ exports.continuityToolDefinitions = [
|
|
|
326
308
|
},
|
|
327
309
|
handler: (a) => {
|
|
328
310
|
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
329
|
-
const
|
|
311
|
+
const now = Date.now();
|
|
312
|
+
const cares = (a.status === "all" ? (0, cares_1.readCares)(agentRoot) : (0, cares_1.readActiveCares)(agentRoot)).map((care) => (0, cares_1.projectCareEvidence)(care, now));
|
|
330
313
|
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.query_cares", message: `queried ${cares.length} cares`, meta: { count: cares.length } });
|
|
331
314
|
return JSON.stringify(cares, null, 2);
|
|
332
315
|
},
|
|
@@ -346,6 +329,7 @@ exports.continuityToolDefinitions = [
|
|
|
346
329
|
why: { type: "string", description: "Why this matters" },
|
|
347
330
|
salience: { type: "string", description: "low, medium, high, or critical" },
|
|
348
331
|
kind: { type: "string", description: "person, agent, project, mission, or system" },
|
|
332
|
+
status: { type: "string", enum: ["active", "watching", "resolved", "dormant"], description: "active, watching, resolved, or dormant" },
|
|
349
333
|
stewardship: { type: "string", description: "mine, shared, or delegated" },
|
|
350
334
|
source: { type: "string", description: "Machine evidence source for an incident binding" },
|
|
351
335
|
incidentKey: { type: "string", description: "Stable incident key within the source" },
|
|
@@ -367,7 +351,7 @@ exports.continuityToolDefinitions = [
|
|
|
367
351
|
label: a.label ?? "untitled",
|
|
368
352
|
why: a.why ?? "",
|
|
369
353
|
kind: a.kind ?? "project",
|
|
370
|
-
status: "active",
|
|
354
|
+
status: a.status ?? "active",
|
|
371
355
|
salience: a.salience ?? "medium",
|
|
372
356
|
steward: a.stewardship ?? "mine",
|
|
373
357
|
relatedFriendIds: [],
|
|
@@ -412,22 +396,33 @@ exports.continuityToolDefinitions = [
|
|
|
412
396
|
source: a.source,
|
|
413
397
|
incidentKey: a.incidentKey,
|
|
414
398
|
expectedUpdatedAt: a.expectedUpdatedAt,
|
|
399
|
+
...((a.label !== undefined || a.why !== undefined || a.currentRisk !== undefined || a.nextCheckAt !== undefined) ? {
|
|
400
|
+
display: {
|
|
401
|
+
...(a.label !== undefined ? { label: String(a.label) } : {}),
|
|
402
|
+
...(a.why !== undefined ? { why: String(a.why) } : {}),
|
|
403
|
+
...(a.currentRisk !== undefined ? { currentRisk: a.currentRisk ? String(a.currentRisk) : null } : {}),
|
|
404
|
+
...(a.nextCheckAt !== undefined ? { nextCheckAt: a.nextCheckAt ? String(a.nextCheckAt) : null } : {}),
|
|
405
|
+
},
|
|
406
|
+
} : {}),
|
|
415
407
|
});
|
|
416
408
|
}
|
|
417
409
|
else if (a.action === "upsert_incident") {
|
|
418
410
|
result = (0, cares_1.upsertCareForIncident)(agentRoot, {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
411
|
+
...(a.id ? { id: String(a.id) } : {}),
|
|
412
|
+
...(!a.id ? {
|
|
413
|
+
label: a.label ?? "untitled", why: a.why ?? "", kind: a.kind ?? "system", status: a.status ?? "active",
|
|
414
|
+
salience: a.salience ?? "medium", steward: a.stewardship ?? "mine",
|
|
415
|
+
} : {
|
|
416
|
+
...(a.label !== undefined ? { label: String(a.label) } : {}), ...(a.why !== undefined ? { why: String(a.why) } : {}),
|
|
417
|
+
...(a.kind !== undefined ? { kind: a.kind } : {}), ...(a.status !== undefined ? { status: a.status } : {}),
|
|
418
|
+
...(a.salience !== undefined ? { salience: a.salience } : {}), ...(a.stewardship !== undefined ? { steward: a.stewardship } : {}),
|
|
419
|
+
}),
|
|
425
420
|
relatedFriendIds: [],
|
|
426
421
|
relatedAgentIds: [],
|
|
427
422
|
relatedObligationIds: [],
|
|
428
423
|
relatedEpisodeIds: [],
|
|
429
|
-
currentRisk: a.currentRisk ? String(a.currentRisk) : null,
|
|
430
|
-
nextCheckAt: a.nextCheckAt ? String(a.nextCheckAt) : null,
|
|
424
|
+
...(!a.id || a.currentRisk !== undefined ? { currentRisk: a.currentRisk ? String(a.currentRisk) : null } : {}),
|
|
425
|
+
...(!a.id || a.nextCheckAt !== undefined ? { nextCheckAt: a.nextCheckAt ? String(a.nextCheckAt) : null } : {}),
|
|
431
426
|
...(a.expectedUpdatedAt ? { expectedUpdatedAt: String(a.expectedUpdatedAt) } : {}),
|
|
432
427
|
incident: {
|
|
433
428
|
source: a.source,
|
package/dist/senses/pipeline.js
CHANGED
|
@@ -72,6 +72,7 @@ const provider_visibility_1 = require("../heart/provider-visibility");
|
|
|
72
72
|
const orientation_frame_1 = require("../heart/orientation-frame");
|
|
73
73
|
const flight_recorder_1 = require("../arc/flight-recorder");
|
|
74
74
|
const context_loss_sentinel_1 = require("../heart/context-loss-sentinel");
|
|
75
|
+
const cares_1 = require("../arc/cares");
|
|
75
76
|
const VOICE_PENDING_MAX_AGE_MS = 15 * 60 * 1_000;
|
|
76
77
|
function pendingExpirationReason(channel, message, now) {
|
|
77
78
|
/* v8 ignore start -- pending expiry edge permutations are covered by the stale voice queue tests; this helper keeps defensive non-voice fallbacks @preserve */
|
|
@@ -809,11 +810,14 @@ async function handleInboundTurn(input) {
|
|
|
809
810
|
: undefined);
|
|
810
811
|
// Step 4b: Continuity pipeline — derive tempo, build start-of-turn packet, snapshot obligations
|
|
811
812
|
let renderedStartOfTurnPacket;
|
|
813
|
+
const careEvidenceNow = Date.now();
|
|
814
|
+
const activeCaresSnapshot = ctx.activeCares;
|
|
812
815
|
const preTurnObligationIds = new Set(pendingObligations.map((ob) => `${ob.id}:${ob.status}`));
|
|
813
816
|
try {
|
|
814
817
|
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
815
818
|
const agentName = (0, identity_1.getAgentName)();
|
|
816
|
-
const { recentEpisodes
|
|
819
|
+
const { recentEpisodes } = ctx;
|
|
820
|
+
const projectedCares = activeCaresSnapshot.map((care) => (0, cares_1.projectCareEvidence)(care, careEvidenceNow));
|
|
817
821
|
const tempoState = (0, tempo_1.deriveTempo)({
|
|
818
822
|
activeSessions: sessionActivity.length + 1,
|
|
819
823
|
openObligations: pendingObligations.length,
|
|
@@ -823,15 +827,15 @@ async function handleInboundTurn(input) {
|
|
|
823
827
|
: 0,
|
|
824
828
|
hasBlockers: false, // obligations use specific statuses, not "blocked"
|
|
825
829
|
highSalienceEpisodes: recentEpisodes.filter((ep) => ep.salience === "high" || ep.salience === "critical").length,
|
|
826
|
-
activeCareCount:
|
|
827
|
-
atRiskCareCount:
|
|
830
|
+
activeCareCount: activeCaresSnapshot.length,
|
|
831
|
+
atRiskCareCount: projectedCares.filter((care) => !("recheckRequired" in care) && care.currentRisk != null).length,
|
|
828
832
|
});
|
|
829
833
|
const temporalView = (0, temporal_view_1.buildTemporalView)(agentRoot, {
|
|
830
834
|
tempo: tempoState.mode,
|
|
831
835
|
preloaded: {
|
|
832
836
|
recentEpisodes,
|
|
833
837
|
activeObligations: pendingObligations,
|
|
834
|
-
activeCares,
|
|
838
|
+
activeCares: activeCaresSnapshot,
|
|
835
839
|
},
|
|
836
840
|
});
|
|
837
841
|
const startOfTurnPacket = (0, start_of_turn_packet_1.buildStartOfTurnPacket)(temporalView, {
|
|
@@ -843,6 +847,7 @@ async function handleInboundTurn(input) {
|
|
|
843
847
|
friendContactTiming,
|
|
844
848
|
flightRecorderResume: ctx.flightRecorderResume,
|
|
845
849
|
recoverySentinel: ctx.recoverySentinel,
|
|
850
|
+
careEvidenceNow,
|
|
846
851
|
});
|
|
847
852
|
/* v8 ignore next 3 -- syncFailure propagation tested in sync.test.ts @preserve */
|
|
848
853
|
if (syncFailure) {
|
|
@@ -928,6 +933,8 @@ async function handleInboundTurn(input) {
|
|
|
928
933
|
currentUserMessages,
|
|
929
934
|
resolvedContext,
|
|
930
935
|
runAgentOptions,
|
|
936
|
+
activeCares: activeCaresSnapshot,
|
|
937
|
+
careEvidenceNow,
|
|
931
938
|
}));
|
|
932
939
|
const checkpointCurrentAsk = selectCheckpointCurrentAsk({
|
|
933
940
|
currentUserMessage: runAgentOptions.toolContext?.currentUserMessage ?? currentUserMessage,
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSanctuaryCurrentStatusIntent = isSanctuaryCurrentStatusIntent;
|
|
3
4
|
exports.sanctuaryFullVisibilityEmptyResponse = sanctuaryFullVisibilityEmptyResponse;
|
|
4
5
|
exports.sanctuaryFullVisibilityRequiredToolCalls = sanctuaryFullVisibilityRequiredToolCalls;
|
|
6
|
+
exports.sanctuaryStaleDockerCareRequiredToolCalls = sanctuaryStaleDockerCareRequiredToolCalls;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const cares_1 = require("../arc/cares");
|
|
5
9
|
const runtime_1 = require("../nerves/runtime");
|
|
6
|
-
const REQUIRED_TOOL_NAMES = ["query_active_work", "query_cares", "unraid_get_system", "unraid_list_containers", "unraid_get_storage", "sanctuary_get_download_queue"];
|
|
10
|
+
const REQUIRED_TOOL_NAMES = ["query_active_work", "query_cares", "unraid_get_system", "unraid_list_containers", "unraid_get_storage", "unraid_get_notifications", "sanctuary_get_download_queue"];
|
|
7
11
|
const WHOLE_STATUS_REQUESTS = new Set(["what are you working on", "what's going on with sanctuary"]);
|
|
8
|
-
function unsupportedCurrentClaim(answer, queueUnavailable) {
|
|
12
|
+
function unsupportedCurrentClaim(answer, queueUnavailable, authorizedDockerClaims) {
|
|
9
13
|
if (queueUnavailable && answer.trim().length === 0)
|
|
10
14
|
return "Give Ari the current results that did complete and say plainly that the download queue is currently unavailable; do not return an empty answer.";
|
|
11
15
|
if (queueUnavailable) {
|
|
@@ -21,6 +25,9 @@ function unsupportedCurrentClaim(answer, queueUnavailable) {
|
|
|
21
25
|
const unsupported = answer.replace(/docker\.img/giu, "docker image").split(/[,;!?\n]|(?<!\d)\.(?!\d)/u).some((sentence) => {
|
|
22
26
|
if (!/docker image(?: disk)?/iu.test(sentence))
|
|
23
27
|
return false;
|
|
28
|
+
const normalizedSentence = sentence.normalize("NFKC").trim().toLocaleLowerCase("en-US");
|
|
29
|
+
if (authorizedDockerClaims.some((claim) => claim.normalize("NFKC").replace(/[.!?]+$/u, "").trim().toLocaleLowerCase("en-US") === normalizedSentence))
|
|
30
|
+
return false;
|
|
24
31
|
const uncertaintyOnly = /\b(?:cannot|can't|unable to) (?:currently )?(?:verify|measure)\b|\b(?:unknown|unverified)\b|\bneeds? (?:a )?(?:fresh |authoritative )*(?:check|measurement)\b/iu.test(sentence);
|
|
25
32
|
const stateClaim = /\b\d+(?:\.\d+)?\s*%|\bfull\b|\b(?:no|out of) space\b|\bwrites? (?:will |may )?fail\b|\b(?:healthy|unhealthy|running|stopped)\b/iu.test(sentence);
|
|
26
33
|
return stateClaim || !uncertaintyOnly;
|
|
@@ -59,6 +66,19 @@ function successfulCurrentResult(name, result) {
|
|
|
59
66
|
const parsed = JSON.parse(result);
|
|
60
67
|
if (name === "query_cares")
|
|
61
68
|
return { valid: Array.isArray(parsed), queueUnavailable: false };
|
|
69
|
+
if (name === "unraid_get_notifications") {
|
|
70
|
+
const root = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
71
|
+
const error = root?.error && typeof root.error === "object" && !Array.isArray(root.error) ? root.error : null;
|
|
72
|
+
const unavailable = root?.ok === false
|
|
73
|
+
&& JSON.stringify(Object.keys(root).sort()) === JSON.stringify(["error", "ok"])
|
|
74
|
+
&& !!error && JSON.stringify(Object.keys(error).sort()) === JSON.stringify(["code", "degraded", "message"])
|
|
75
|
+
&& ["unauthorized", "forbidden", "timeout", "transport", "graphql", "invalid_response"].includes(String(error.code))
|
|
76
|
+
&& typeof error.message === "string" && Buffer.byteLength(error.message, "utf8") <= 512 && error.degraded === true;
|
|
77
|
+
if (unavailable)
|
|
78
|
+
return { valid: true, queueUnavailable: false };
|
|
79
|
+
const data = root?.data && typeof root.data === "object" && !Array.isArray(root.data) ? root.data : null;
|
|
80
|
+
return { valid: root?.ok === true && Array.isArray(data?.unacknowledged) && typeof data.truncated === "boolean", queueUnavailable: false };
|
|
81
|
+
}
|
|
62
82
|
if (name === "sanctuary_get_download_queue") {
|
|
63
83
|
if (exactQueueUnavailableResult(parsed))
|
|
64
84
|
return { valid: true, queueUnavailable: true };
|
|
@@ -80,23 +100,37 @@ function successfulCurrentResult(name, result) {
|
|
|
80
100
|
}
|
|
81
101
|
}
|
|
82
102
|
function normalizedRequest(request) {
|
|
83
|
-
return request.normalize("NFKC").trim().toLocaleLowerCase("en-US").
|
|
103
|
+
return request.normalize("NFKC").trim().toLocaleLowerCase("en-US").replace(/[‘’]/gu, "'").replace(/[^a-z0-9']+/gu, " ").trim();
|
|
104
|
+
}
|
|
105
|
+
function isSanctuaryCurrentStatusIntent(request) {
|
|
106
|
+
const normalized = normalizedRequest(request);
|
|
107
|
+
if (WHOLE_STATUS_REQUESTS.has(normalized))
|
|
108
|
+
return true;
|
|
109
|
+
if (["what's up", "everything good", "anything wrong"].includes(normalized))
|
|
110
|
+
return true;
|
|
111
|
+
if (!normalized || /\b(?:yesterday|tomorrow|document|movie request)\b/u.test(normalized))
|
|
112
|
+
return false;
|
|
113
|
+
if (/^do you care about\b/u.test(normalized) || /\b(?:plex|jellyfin|sonarr|radarr|docker labels?)\b/u.test(normalized))
|
|
114
|
+
return false;
|
|
115
|
+
const wholeScope = /\b(?:anything|everything|what(?:'s| is)|things?)\b/u.test(normalized);
|
|
116
|
+
const current = /\b(?:now|right now|currently|going on|status)\b/u.test(normalized);
|
|
117
|
+
const concern = /\b(?:care about|should (?:i|we) know about|working on|going on|status)\b/u.test(normalized);
|
|
118
|
+
return wholeScope && current && concern;
|
|
84
119
|
}
|
|
85
120
|
function sanctuaryFullVisibilityEmptyResponse(request) {
|
|
86
|
-
return
|
|
121
|
+
return isSanctuaryCurrentStatusIntent(request)
|
|
87
122
|
? "I couldn't finish a trustworthy Sanctuary status check because a current check was unavailable. I won't guess or reuse old alerts; please try again shortly."
|
|
88
123
|
: undefined;
|
|
89
124
|
}
|
|
90
|
-
function sanctuaryFullVisibilityRequiredToolCalls(request,
|
|
91
|
-
|
|
92
|
-
if (!WHOLE_STATUS_REQUESTS.has(normalized) || !REQUIRED_TOOL_NAMES.every((name) => advertisedToolNames.includes(name)))
|
|
125
|
+
function sanctuaryFullVisibilityRequiredToolCalls(request, _advertisedToolNames, authorizedDockerClaims = () => []) {
|
|
126
|
+
if (!isSanctuaryCurrentStatusIntent(request))
|
|
93
127
|
return undefined;
|
|
94
128
|
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.sanctuary_full_visibility_reads_required", message: "required current Sanctuary visibility reads", meta: { toolCount: REQUIRED_TOOL_NAMES.length } });
|
|
95
129
|
let queueUnavailable = false;
|
|
96
130
|
const completed = new Set();
|
|
97
131
|
return {
|
|
98
132
|
names: REQUIRED_TOOL_NAMES,
|
|
99
|
-
retryMessage: "Before answering, read current active work, cares, system health, service state, storage, and the download queue. Current tool facts outrank care history; a stale care is a recheck item, not a present-tense fact. Then give Ari one compact household summary; do not ask him to choose a status slice.",
|
|
133
|
+
retryMessage: "Before answering, read current active work, cares, system health, service state, storage, notifications, and the download queue. Current tool facts outrank care history; a stale care is a recheck item, not a present-tense fact. Then give Ari one compact household summary; do not ask him to choose a status slice.",
|
|
100
134
|
requireSuccessfulResults: true,
|
|
101
135
|
validateRequiredToolResult: (name, result) => {
|
|
102
136
|
const validation = successfulCurrentResult(name, result);
|
|
@@ -106,7 +140,163 @@ function sanctuaryFullVisibilityRequiredToolCalls(request, advertisedToolNames)
|
|
|
106
140
|
completed.add(name);
|
|
107
141
|
return validation.valid;
|
|
108
142
|
},
|
|
109
|
-
validateTerminalAnswer: (answer) => unsupportedCurrentClaim(answer, queueUnavailable),
|
|
143
|
+
validateTerminalAnswer: (answer) => unsupportedCurrentClaim(answer, queueUnavailable, authorizedDockerClaims()),
|
|
110
144
|
emptyResponseFallback: () => REQUIRED_TOOL_NAMES.every((name) => completed.has(name)) ? sanctuaryFullVisibilityEmptyResponse(request) : undefined,
|
|
111
145
|
};
|
|
112
146
|
}
|
|
147
|
+
const SAFE_LABEL = "Docker image disk utilization";
|
|
148
|
+
const INCONCLUSIVE_RISK = "Docker image disk utilization verification is inconclusive.";
|
|
149
|
+
const ACTIVE_RISK = "A fresh Unraid notification reports high Docker image disk utilization.";
|
|
150
|
+
function qualifyingDockerCare(care, now) {
|
|
151
|
+
if ((0, cares_1.projectCareEvidence)(care, now) === care)
|
|
152
|
+
return undefined;
|
|
153
|
+
const binding = care.incidentBindings?.find(cares_1.isManagedDockerCareIncidentBinding);
|
|
154
|
+
return binding ? {
|
|
155
|
+
id: care.id, updatedAt: care.updatedAt, currentRisk: care.currentRisk, nextCheckAt: care.nextCheckAt, salience: care.salience, steward: care.steward, binding: { ...binding },
|
|
156
|
+
hasOtherUnresolvedBindings: care.incidentBindings.some((candidate) => candidate !== binding && !candidate.resolvedAt),
|
|
157
|
+
} : undefined;
|
|
158
|
+
}
|
|
159
|
+
function canonicalNotification(value) {
|
|
160
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
161
|
+
return undefined;
|
|
162
|
+
const notification = value;
|
|
163
|
+
return typeof notification.id === "string"
|
|
164
|
+
&& typeof notification.createdAt === "string"
|
|
165
|
+
&& Number.isFinite(Date.parse(notification.createdAt))
|
|
166
|
+
&& new Date(notification.createdAt).toISOString() === notification.createdAt
|
|
167
|
+
&& typeof notification.title === "string"
|
|
168
|
+
&& typeof notification.summary === "string"
|
|
169
|
+
&& typeof notification.severity === "string"
|
|
170
|
+
&& typeof notification.degraded === "boolean"
|
|
171
|
+
? notification
|
|
172
|
+
: undefined;
|
|
173
|
+
}
|
|
174
|
+
function notificationRevision(notification) {
|
|
175
|
+
return (0, node_crypto_1.createHash)("sha256").update(JSON.stringify({
|
|
176
|
+
id: notification.id,
|
|
177
|
+
createdAt: notification.createdAt,
|
|
178
|
+
severity: notification.severity,
|
|
179
|
+
title: notification.title,
|
|
180
|
+
summary: notification.summary,
|
|
181
|
+
degraded: notification.degraded,
|
|
182
|
+
})).digest("hex");
|
|
183
|
+
}
|
|
184
|
+
function dockerObservation(result, care) {
|
|
185
|
+
try {
|
|
186
|
+
const parsed = JSON.parse(result);
|
|
187
|
+
const data = parsed?.ok === true && parsed.data && typeof parsed.data === "object" && !Array.isArray(parsed.data) ? parsed.data : undefined;
|
|
188
|
+
if (!data || data.truncated !== false || !Array.isArray(data.unacknowledged))
|
|
189
|
+
return { outcome: "inconclusive" };
|
|
190
|
+
const boundary = Math.max(Date.parse(care.updatedAt), Date.parse(care.nextCheckAt));
|
|
191
|
+
const related = data.unacknowledged.map(canonicalNotification).filter((item) => !!item).filter((item) => {
|
|
192
|
+
const text = `${item.title} ${item.summary}`;
|
|
193
|
+
return item.degraded === false && Date.parse(String(item.createdAt)) > boundary && /docker/iu.test(text) && /image/iu.test(text) && /(?:disk|utilization)/iu.test(text);
|
|
194
|
+
});
|
|
195
|
+
if (related.length !== 1)
|
|
196
|
+
return { outcome: "inconclusive" };
|
|
197
|
+
const notification = related[0];
|
|
198
|
+
const text = `${notification.title} ${notification.summary}`;
|
|
199
|
+
const revision = notificationRevision(notification);
|
|
200
|
+
if (/\b(?:recover(?:ed|y)?|resolved|normal|healthy|cleared)\b/iu.test(text))
|
|
201
|
+
return { outcome: "fresh_recovered", revision };
|
|
202
|
+
if (/\b(?:critical|warning|error|full|high|9[0-9]%|100%)\b/iu.test(`${notification.severity} ${text}`)) {
|
|
203
|
+
return { outcome: "fresh_active", revision, risk: ACTIVE_RISK };
|
|
204
|
+
}
|
|
205
|
+
return { outcome: "inconclusive" };
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return { outcome: "inconclusive" };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function mutationFor(care, observation, now) {
|
|
212
|
+
const common = {
|
|
213
|
+
id: care.id,
|
|
214
|
+
source: care.binding.source,
|
|
215
|
+
incidentKey: care.binding.incidentKey,
|
|
216
|
+
expectedUpdatedAt: care.updatedAt,
|
|
217
|
+
label: SAFE_LABEL,
|
|
218
|
+
why: observation.outcome === "inconclusive" ? "Current Unraid notification evidence was inconclusive." : "Current Unraid notification evidence was checked.",
|
|
219
|
+
};
|
|
220
|
+
if (observation.outcome === "fresh_recovered") {
|
|
221
|
+
const unresolvedContextRemains = care.hasOtherUnresolvedBindings;
|
|
222
|
+
return {
|
|
223
|
+
...common,
|
|
224
|
+
action: "resolve_incident",
|
|
225
|
+
currentRisk: unresolvedContextRemains ? cares_1.CARE_INCIDENT_RECOVERY_REVIEW_RISK : "",
|
|
226
|
+
nextCheckAt: unresolvedContextRemains ? new Date(now + 15 * 60_000).toISOString() : "",
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
...common,
|
|
231
|
+
action: "upsert_incident",
|
|
232
|
+
kind: "system",
|
|
233
|
+
status: observation.outcome === "fresh_active" ? "active" : "watching",
|
|
234
|
+
salience: care.salience,
|
|
235
|
+
stewardship: care.steward,
|
|
236
|
+
classifiedRevision: observation.outcome === "fresh_active" ? observation.revision : care.binding.classifiedRevision,
|
|
237
|
+
currentRisk: observation.outcome === "fresh_active" ? observation.risk : INCONCLUSIVE_RISK,
|
|
238
|
+
nextCheckAt: new Date(now + 15 * 60_000).toISOString(),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function exactMutation(expected, actual) {
|
|
242
|
+
return Object.keys(expected).length === Object.keys(actual).length
|
|
243
|
+
&& Object.entries(expected).every(([key, value]) => actual[key] === value);
|
|
244
|
+
}
|
|
245
|
+
function sanctuaryStaleDockerCareRequiredToolCalls(activeCares, now, _advertisedToolNames) {
|
|
246
|
+
const cares = activeCares.map((care) => qualifyingDockerCare(care, now)).filter((care) => !!care);
|
|
247
|
+
if (cares.length === 0)
|
|
248
|
+
return undefined;
|
|
249
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.sanctuary_stale_docker_care_verification_required", message: "stale Docker Care requires current notification verification", meta: { careCount: cares.length } });
|
|
250
|
+
let verifierComplete = false;
|
|
251
|
+
let mutations = [];
|
|
252
|
+
const completed = new Set();
|
|
253
|
+
return {
|
|
254
|
+
names: ["unraid_get_notifications"],
|
|
255
|
+
get retryMessage() {
|
|
256
|
+
return mutations.length === 0
|
|
257
|
+
? "Recheck the stale Docker image disk Care from current notifications, then apply every exact evidence-bounded Care update before answering."
|
|
258
|
+
: `Apply these exact evidence-bounded care_manage arguments before answering: ${JSON.stringify(mutations)}.`;
|
|
259
|
+
},
|
|
260
|
+
requireSuccessfulResults: true,
|
|
261
|
+
validateRequiredToolResult: (name, result, args) => {
|
|
262
|
+
if (name === "unraid_get_notifications")
|
|
263
|
+
return Buffer.byteLength(result, "utf8") <= 1_000_000;
|
|
264
|
+
if (name !== "care_manage")
|
|
265
|
+
return false;
|
|
266
|
+
const expected = mutations.find((candidate) => exactMutation(candidate, args));
|
|
267
|
+
if (!expected)
|
|
268
|
+
return false;
|
|
269
|
+
try {
|
|
270
|
+
const care = JSON.parse(result);
|
|
271
|
+
const binding = care.incidentBindings?.find((candidate) => candidate.source === expected.source && candidate.incidentKey === expected.incidentKey);
|
|
272
|
+
const resolved = expected.action === "resolve_incident" ? !!binding?.resolvedAt : binding?.classifiedRevision === expected.classifiedRevision;
|
|
273
|
+
const updatedUnderCas = typeof care.updatedAt === "string" && Date.parse(care.updatedAt) > Date.parse(expected.expectedUpdatedAt);
|
|
274
|
+
const wholeCareResolutionValid = expected.action !== "resolve_incident" || expected.currentRisk !== "" || care.incidentBindings?.some((candidate) => !candidate.resolvedAt) || care.status === "resolved";
|
|
275
|
+
if (care.id !== expected.id || care.label !== SAFE_LABEL || care.why !== expected.why || care.currentRisk !== (expected.currentRisk || null) || care.nextCheckAt !== (expected.nextCheckAt || null) || (expected.status && care.status !== expected.status) || !resolved || !updatedUnderCas || !wholeCareResolutionValid)
|
|
276
|
+
return false;
|
|
277
|
+
completed.add(care.id);
|
|
278
|
+
return completed.size === mutations.length;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
validateToolCallBeforeDispatch: (name, args) => {
|
|
285
|
+
if (name !== "care_manage")
|
|
286
|
+
return undefined;
|
|
287
|
+
if (!verifierComplete)
|
|
288
|
+
return "Read current Unraid notifications before mutating stale Docker Care.";
|
|
289
|
+
return mutations.some((candidate) => exactMutation(candidate, args)) ? undefined : `Only these exact evidence-bounded stale Docker Care mutations are authorized in this turn: ${JSON.stringify(mutations)}.`;
|
|
290
|
+
},
|
|
291
|
+
requiredToolCallsAfterResult: (name, _args, result) => {
|
|
292
|
+
if (name !== "unraid_get_notifications")
|
|
293
|
+
return [];
|
|
294
|
+
mutations = cares.map((care) => mutationFor(care, dockerObservation(result, care), now));
|
|
295
|
+
verifierComplete = true;
|
|
296
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.sanctuary_stale_docker_care_verification_completed", message: "current notification verification produced bounded Care outcomes", meta: { careCount: cares.length, mutationCount: mutations.length } });
|
|
297
|
+
return ["care_manage"];
|
|
298
|
+
},
|
|
299
|
+
expectedMutations: () => mutations.map((mutation) => ({ ...mutation })),
|
|
300
|
+
currentRiskClaims: () => mutations.filter((mutation) => mutation.currentRisk === ACTIVE_RISK).map((mutation) => mutation.currentRisk),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
@@ -11,13 +11,12 @@ function normalizedRequest(request) {
|
|
|
11
11
|
.replace(/[^a-z0-9']+/gu, " ")
|
|
12
12
|
.trim();
|
|
13
13
|
}
|
|
14
|
-
function sanctuaryStorageOptimizationRequiredToolCalls(request,
|
|
14
|
+
function sanctuaryStorageOptimizationRequiredToolCalls(request, _advertisedToolNames) {
|
|
15
15
|
const normalized = normalizedRequest(request);
|
|
16
16
|
const hasStorageSubject = /\b(?:space|storage)\b/u.test(normalized);
|
|
17
17
|
const hasUsageDiagnosis = /\b(?:using|taking up|find what|diagnos(?:e|is))\b/u.test(normalized);
|
|
18
18
|
const hasShrinkIntent = /\b(?:make it smaller|shrink|reclaim|free up|reduce|optimi[sz])\b/u.test(normalized);
|
|
19
|
-
|
|
20
|
-
if (!hasStorageSubject || !hasUsageDiagnosis || !hasShrinkIntent || !REQUIRED_TOOL_NAMES.every((name) => advertised.has(name)))
|
|
19
|
+
if (!hasStorageSubject || !hasUsageDiagnosis || !hasShrinkIntent)
|
|
21
20
|
return undefined;
|
|
22
21
|
const names = [...REQUIRED_TOOL_NAMES];
|
|
23
22
|
(0, runtime_1.emitNervesEvent)({
|
package/dist/senses/telegram.js
CHANGED
|
@@ -1078,23 +1078,46 @@ function createTelegramSenseApp(options) {
|
|
|
1078
1078
|
authorizeTool: async (name, args) => (await options.resolveRelationshipAuthorization(relationshipCoordinates)).authorizeTool(name, args),
|
|
1079
1079
|
};
|
|
1080
1080
|
};
|
|
1081
|
-
return async ({ runAgentOptions }) => {
|
|
1081
|
+
return async ({ runAgentOptions, activeCares = [], careEvidenceNow = Date.now() }) => {
|
|
1082
1082
|
const relationshipAuthorization = await resolveLiveRelationshipAuthorization();
|
|
1083
|
-
const
|
|
1083
|
+
const isSanctuaryOwner = options.agentName === "sanctuary" && relationshipAuthorization.profileId === "sanctuary-owner";
|
|
1084
|
+
const storageOptimization = isSanctuaryOwner
|
|
1084
1085
|
? (0, sanctuary_storage_optimization_contract_1.sanctuaryStorageOptimizationRequiredToolCalls)(input.userMessage, relationshipAuthorization.advertisedToolNames)
|
|
1085
1086
|
: undefined;
|
|
1086
|
-
const
|
|
1087
|
-
? (0, sanctuary_full_visibility_contract_1.
|
|
1087
|
+
const staleDockerCare = isSanctuaryOwner
|
|
1088
|
+
? (0, sanctuary_full_visibility_contract_1.sanctuaryStaleDockerCareRequiredToolCalls)(activeCares, careEvidenceNow, relationshipAuthorization.advertisedToolNames)
|
|
1089
|
+
: undefined;
|
|
1090
|
+
const fullVisibility = isSanctuaryOwner
|
|
1091
|
+
? (0, sanctuary_full_visibility_contract_1.sanctuaryFullVisibilityRequiredToolCalls)(input.userMessage, relationshipAuthorization.advertisedToolNames, () => staleDockerCare?.currentRiskClaims() ?? [])
|
|
1088
1092
|
: undefined;
|
|
1089
1093
|
if (input.fullVisibilityProgress && fullVisibility)
|
|
1090
1094
|
input.fullVisibilityProgress.fallback = fullVisibility.emptyResponseFallback;
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1095
|
+
const contracts = [storageOptimization, fullVisibility, staleDockerCare].filter(Boolean);
|
|
1096
|
+
const ownedNames = contracts.map((contract) => new Set(contract.names));
|
|
1097
|
+
const requiredToolCalls = contracts.length === 0 ? undefined : {
|
|
1098
|
+
names: [...new Set(contracts.flatMap((contract) => [...contract.names]))],
|
|
1099
|
+
get retryMessage() { return contracts.map((contract) => contract.retryMessage).join(" "); },
|
|
1100
|
+
requireSuccessfulResults: true,
|
|
1101
|
+
validateRequiredToolResult: (name, result, args) => contracts.every((contract, index) => {
|
|
1102
|
+
if (!ownedNames[index].has(name))
|
|
1103
|
+
return true;
|
|
1104
|
+
return contract.validateRequiredToolResult?.(name, result, args) ?? true;
|
|
1105
|
+
}),
|
|
1106
|
+
validateToolCallBeforeDispatch: (name, args) => contracts.map((contract) => contract.validateToolCallBeforeDispatch?.(name, args)).find((rejection) => rejection !== undefined),
|
|
1107
|
+
requiredToolCallsAfterResult: (name, args, result) => {
|
|
1108
|
+
const added = new Set();
|
|
1109
|
+
contracts.forEach((contract, index) => {
|
|
1110
|
+
if (!ownedNames[index].has(name) || !contract.requiredToolCallsAfterResult)
|
|
1111
|
+
return;
|
|
1112
|
+
for (const requiredName of contract.requiredToolCallsAfterResult(name, args, result)) {
|
|
1113
|
+
ownedNames[index].add(requiredName);
|
|
1114
|
+
added.add(requiredName);
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
return [...added];
|
|
1118
|
+
},
|
|
1119
|
+
validateTerminalAnswer: (answer) => contracts.map((contract) => contract.validateTerminalAnswer?.(answer)).find((rejection) => rejection !== undefined),
|
|
1120
|
+
};
|
|
1098
1121
|
return {
|
|
1099
1122
|
...runAgentOptions,
|
|
1100
1123
|
...(requiredToolCalls ? { requiredToolCalls } : {}),
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.769",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.769",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|