@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 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,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.768",
2
+ "runtimeVersion": "0.1.0-alpha.769",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-08-30T00:00:00.000Z"
5
5
  }
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0"?>
2
2
  <Container version="2">
3
3
  <Name>Mendelow Cloud Butler</Name>
4
- <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.768</Repository>
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 unchanged = JSON.stringify(existing.incidentBindings[index]) === JSON.stringify(incident)
215
- && existing.currentRisk === input.currentRisk && existing.nextCheckAt === input.nextCheckAt;
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
- currentRisk: input.currentRisk === null ? null : (0, session_events_1.capStructuredRecordString)(input.currentRisk),
225
- nextCheckAt: input.nextCheckAt,
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
- return createCareUnlocked(agentRoot, { ...input, incidentBindings: [incident] });
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
- if (bindings[index].resolvedAt)
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
- bindings[index] = { ...bindings[index], resolvedAt: now };
248
- const updated = { ...care, incidentBindings: bindings, updatedAt: now };
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",
@@ -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 approvalCalls = await Promise.all(validCalls.map(async (entry) => {
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
- if (success && requiredToolCallNames.includes(tc.name) && options?.requiredToolCalls?.requireSuccessfulResults
2325
- && requiredToolResultSucceeded(tc.name, toolResult, options.requiredToolCalls.validateRequiredToolResult))
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" ? { deniedReason: evaluation.deniedReason ?? reason } : {}),
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((c) => {
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 cares = (a.status === "all" ? (0, cares_1.readCares)(agentRoot) : (0, cares_1.readActiveCares)(agentRoot)).map(presentCare);
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
- label: a.label ?? "untitled",
420
- why: a.why ?? "",
421
- kind: a.kind ?? "system",
422
- status: "active",
423
- salience: a.salience ?? "medium",
424
- steward: a.stewardship ?? "mine",
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,
@@ -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, activeCares } = ctx;
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: activeCares.length,
827
- atRiskCareCount: activeCares.filter((c) => c.currentRisk != null).length,
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").replaceAll("’", "'").replace(/[?!.\s]+$/gu, "");
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 WHOLE_STATUS_REQUESTS.has(normalizedRequest(request))
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, advertisedToolNames) {
91
- const normalized = normalizedRequest(request);
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, advertisedToolNames) {
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
- const advertised = new Set(advertisedToolNames);
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)({
@@ -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 storageOptimization = options.agentName === "sanctuary" && relationshipAuthorization.profileId === "sanctuary-owner"
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 fullVisibility = options.agentName === "sanctuary" && relationshipAuthorization.profileId === "sanctuary-owner" && !storageOptimization
1087
- ? (0, sanctuary_full_visibility_contract_1.sanctuaryFullVisibilityRequiredToolCalls)(input.userMessage, relationshipAuthorization.advertisedToolNames)
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 requiredToolCalls = storageOptimization ?? (fullVisibility ? {
1092
- names: fullVisibility.names,
1093
- retryMessage: fullVisibility.retryMessage,
1094
- requireSuccessfulResults: fullVisibility.requireSuccessfulResults,
1095
- validateRequiredToolResult: fullVisibility.validateRequiredToolResult,
1096
- validateTerminalAnswer: fullVisibility.validateTerminalAnswer,
1097
- } : undefined);
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 } : {}),
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.768",
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.768",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.768",
3
+ "version": "0.1.0-alpha.769",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },