@ouro.bot/cli 0.1.0-alpha.765 → 0.1.0-alpha.766

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,14 @@
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.766",
6
+ "changes": [
7
+ "Keep Sanctuary status summaries grounded in successful current reads and stale-care-safe evidence.",
8
+ "Render Butler Telegram replies without literal bold markers while preserving commands and data.",
9
+ "Keep the packaged visibility contract clear of retired module markers."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.765",
6
14
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.765",
2
+ "runtimeVersion": "0.1.0-alpha.766",
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.765</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.766</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -569,6 +569,11 @@ function toolResultIndicatesFailure(content) {
569
569
  || normalized.startsWith("blocked:")
570
570
  || normalized.startsWith("rejected:");
571
571
  }
572
+ function requiredToolResultSucceeded(name, content, validate) {
573
+ if (toolResultIndicatesFailure(content))
574
+ return false;
575
+ return validate?.(name, content) ?? true;
576
+ }
572
577
  function effectFingerprint(name, rawArguments) {
573
578
  let args;
574
579
  try {
@@ -1128,6 +1133,12 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1128
1133
  ? { missing, message: `${options.requiredToolCalls.retryMessage} Missing required tool calls: ${missing.join(", ")}.` }
1129
1134
  : null;
1130
1135
  };
1136
+ const queueRequiredCorrection = (message, limitContext) => {
1137
+ if (providerIterations >= exports.MAX_PROVIDER_ITERATIONS)
1138
+ throw new Error(`provider iteration limit exhausted at response ${exports.MAX_PROVIDER_ITERATIONS} ${limitContext}`);
1139
+ messages.push({ role: "user", content: message });
1140
+ providerRuntime.resetTurnState(messages);
1141
+ };
1131
1142
  const toolLoopState = (0, tool_loop_1.createToolLoopState)();
1132
1143
  const toolFrictionLedger = (0, tool_friction_1.createToolFrictionLedger)();
1133
1144
  const finishTerminalProviderError = (error, classification) => {
@@ -1530,11 +1541,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1530
1541
  if (requiredToolCallsGate) {
1531
1542
  streamCallbackBuffer?.discard();
1532
1543
  callbacks.onClearText?.();
1533
- if (providerIterations >= exports.MAX_PROVIDER_ITERATIONS) {
1534
- throw new Error(`provider iteration limit exhausted at response ${exports.MAX_PROVIDER_ITERATIONS} before required tool calls completed`);
1535
- }
1536
- pushGenerated(msg);
1537
- messages.push({ role: "user", content: requiredToolCallsGate.message });
1544
+ queueRequiredCorrection(requiredToolCallsGate.message, "before required tool calls completed");
1538
1545
  (0, runtime_1.emitNervesEvent)({
1539
1546
  level: "warn",
1540
1547
  component: "engine",
@@ -1544,6 +1551,14 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1544
1551
  });
1545
1552
  continue;
1546
1553
  }
1554
+ const requiredAnswerRejection = options?.requiredToolCalls?.validateTerminalAnswer?.(String(msg.content ?? ""));
1555
+ if (requiredAnswerRejection) {
1556
+ streamCallbackBuffer?.discard();
1557
+ callbacks.onClearText?.();
1558
+ queueRequiredCorrection(requiredAnswerRejection, "before required terminal answer validation completed");
1559
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "engine", event: "engine.required_tool_answer_rejected", message: "unsupported terminal answer rejected after required reads", meta: { answerLength: String(msg.content ?? "").length } });
1560
+ continue;
1561
+ }
1547
1562
  if (privateReturnTextAckRetryError) {
1548
1563
  streamCallbackBuffer?.discard();
1549
1564
  callbacks.onClearText?.();
@@ -1751,9 +1766,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1751
1766
  streamCallbackBuffer?.discard();
1752
1767
  callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1753
1768
  callbacks.onClearText?.();
1754
- pushGenerated(msg);
1755
- pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: requiredToolCallsGate.message });
1756
- providerRuntime.appendToolOutput(result.toolCalls[0].id, requiredToolCallsGate.message);
1769
+ queueRequiredCorrection(requiredToolCallsGate.message, "before required tool calls completed");
1757
1770
  (0, runtime_1.emitNervesEvent)({
1758
1771
  level: "warn",
1759
1772
  component: "engine",
@@ -1778,6 +1791,15 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1778
1791
  // Extract answer from the tool call arguments.
1779
1792
  // Supports: {"answer":"text","intent":"..."} or "text" (JSON string).
1780
1793
  const { answer, intent } = parseSettlePayload(result.toolCalls[0].arguments);
1794
+ const requiredAnswerRejection = options?.requiredToolCalls?.validateTerminalAnswer?.(answer);
1795
+ if (requiredAnswerRejection) {
1796
+ streamCallbackBuffer?.discard();
1797
+ callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1798
+ callbacks.onClearText?.();
1799
+ queueRequiredCorrection(requiredAnswerRejection, "before required terminal answer validation completed");
1800
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "engine", event: "engine.required_tool_answer_rejected", message: "unsupported settle answer rejected after required reads", meta: { answerLength: answer.length } });
1801
+ continue;
1802
+ }
1781
1803
  // Private-runtime settle: no CompletionMetadata, "(settled)" ack
1782
1804
  if (isPrivateRuntimeChannel) {
1783
1805
  streamCallbackBuffer?.discard();
@@ -2289,7 +2311,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
2289
2311
  const execToolFn = options?.execTool ?? tools_1.execTool;
2290
2312
  const routineActionSelection = approvalCalls.find((entry) => entry.call.id === tc.id)?.routineActionSelection;
2291
2313
  const executionToolContext = routineActionSelection && augmentedToolContext ? { ...augmentedToolContext, routineActionSelection } : augmentedToolContext;
2292
- if (requiredToolCallNames.includes(tc.name))
2314
+ if (requiredToolCallNames.includes(tc.name) && !options?.requiredToolCalls?.requireSuccessfulResults)
2293
2315
  dispatchedRequiredToolCalls.add(tc.name);
2294
2316
  toolResult = await execToolFn(tc.name, args, executionToolContext);
2295
2317
  success = true;
@@ -2299,6 +2321,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
2299
2321
  success = false;
2300
2322
  augmentedToolContext?.habitSession?.recordError?.(toolResult);
2301
2323
  }
2324
+ if (success && requiredToolCallNames.includes(tc.name) && options?.requiredToolCalls?.requireSuccessfulResults
2325
+ && requiredToolResultSucceeded(tc.name, toolResult, options.requiredToolCalls.validateRequiredToolResult))
2326
+ dispatchedRequiredToolCalls.add(tc.name);
2302
2327
  if (success && currentEffectFingerprint && !toolResultIndicatesFailure(toolResult)) {
2303
2328
  unresolvedHistoricalEffects = unresolvedHistoricalEffects.filter((effect) => effect.fingerprint !== currentEffectFingerprint);
2304
2329
  }
@@ -45,6 +45,24 @@ 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
+ }
48
66
  exports.continuityToolDefinitions = [
49
67
  // ── Continuity tools ──────────────────────────────────────────────
50
68
  {
@@ -308,7 +326,7 @@ exports.continuityToolDefinitions = [
308
326
  },
309
327
  handler: (a) => {
310
328
  const agentRoot = (0, identity_1.getAgentRoot)();
311
- const cares = a.status === "all" ? (0, cares_1.readCares)(agentRoot) : (0, cares_1.readActiveCares)(agentRoot);
329
+ const cares = (a.status === "all" ? (0, cares_1.readCares)(agentRoot) : (0, cares_1.readActiveCares)(agentRoot)).map(presentCare);
312
330
  (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.query_cares", message: `queried ${cares.length} cares`, meta: { count: cares.length } });
313
331
  return JSON.stringify(cares, null, 2);
314
332
  },
@@ -2,8 +2,55 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sanctuaryFullVisibilityRequiredToolCalls = sanctuaryFullVisibilityRequiredToolCalls;
4
4
  const runtime_1 = require("../nerves/runtime");
5
- const REQUIRED_TOOL_NAMES = ["query_active_work", "query_cares", "unraid_get_system", "unraid_list_containers"];
5
+ const REQUIRED_TOOL_NAMES = ["query_active_work", "query_cares", "unraid_get_system", "unraid_list_containers", "unraid_get_storage", "sanctuary_get_download_queue"];
6
6
  const WHOLE_STATUS_REQUESTS = new Set(["what are you working on", "what's going on with sanctuary"]);
7
+ function unsupportedCurrentClaim(answer) {
8
+ const unsupported = answer.replace(/docker\.img/giu, "docker image").split(/[,;!?\n]|(?<!\d)\.(?!\d)/u).some((sentence) => {
9
+ if (!/docker image(?: disk)?/iu.test(sentence))
10
+ return false;
11
+ 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);
12
+ 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);
13
+ return stateClaim || !uncertaintyOnly;
14
+ });
15
+ if (unsupported)
16
+ return "No current Butler tool measures Docker image utilization. Do not report a Docker image percentage or full state as current; say that the stale care needs a fresh authoritative check.";
17
+ const unsupportedProviderClaim = answer.split(/[,;!?\n]|(?<!\d)\.(?!\d)/u).some((clause) => {
18
+ const explicitlyUnverified = /\b(?:cannot|can't|unable to) (?:currently )?(?:verify|confirm)|\b(?:unknown|unverified|stale|historical|previous|prior)\b|\bneeds? (?:a )?(?:fresh |authoritative )*(?:check|verification)\b/iu.test(clause);
19
+ if (explicitlyUnverified)
20
+ return false;
21
+ if (/\b(?:Astraweb|Usenet provider|download provider|(?:block|prepaid|download|provider) credit)\b/iu.test(clause))
22
+ return true;
23
+ return /\bSABnzbd\b/iu.test(clause) && /\b(?:auth(?:entication)?|authenticated|credentials?)\b/iu.test(clause);
24
+ });
25
+ return unsupportedProviderClaim ? "The current queue read does not prove provider credit or authentication status. Do not report Astraweb, block-credit, or authentication failure as current; say that the provider needs a fresh authoritative check." : undefined;
26
+ }
27
+ function successfulCurrentResult(name, result) {
28
+ if (Buffer.byteLength(result, "utf8") > 1_000_000)
29
+ return false;
30
+ if (name === "query_active_work")
31
+ return result.trimStart().startsWith("this is my current top-level live world-state.");
32
+ try {
33
+ const parsed = JSON.parse(result);
34
+ if (name === "query_cares")
35
+ return Array.isArray(parsed);
36
+ if (name === "sanctuary_get_download_queue") {
37
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
38
+ return false;
39
+ const queue = parsed;
40
+ const exactKeys = ["observedAt", "paused", "queuedJobs", "stateDigest", "status"];
41
+ return JSON.stringify(Object.keys(queue).sort()) === JSON.stringify(exactKeys)
42
+ && typeof queue.paused === "boolean"
43
+ && typeof queue.status === "string" && Buffer.byteLength(queue.status, "utf8") <= 64
44
+ && Number.isSafeInteger(queue.queuedJobs) && Number(queue.queuedJobs) >= 0 && Number(queue.queuedJobs) <= 1_000_000
45
+ && typeof queue.observedAt === "string" && Number.isFinite(Date.parse(queue.observedAt)) && new Date(queue.observedAt).toISOString() === queue.observedAt
46
+ && typeof queue.stateDigest === "string" && /^[a-f0-9]{64}$/u.test(queue.stateDigest);
47
+ }
48
+ return !!parsed && typeof parsed === "object" && !Array.isArray(parsed) && parsed.ok === true;
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
7
54
  function sanctuaryFullVisibilityRequiredToolCalls(request, advertisedToolNames) {
8
55
  const normalized = request.normalize("NFKC").trim().toLocaleLowerCase("en-US").replaceAll("’", "'").replace(/[?!.\s]+$/gu, "");
9
56
  if (!WHOLE_STATUS_REQUESTS.has(normalized) || !REQUIRED_TOOL_NAMES.every((name) => advertisedToolNames.includes(name)))
@@ -11,6 +58,9 @@ function sanctuaryFullVisibilityRequiredToolCalls(request, advertisedToolNames)
11
58
  (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 } });
12
59
  return {
13
60
  names: REQUIRED_TOOL_NAMES,
14
- retryMessage: "Before answering, read current active work, cares, system health, and service state. Then give Ari one compact household summary; do not ask him to choose a status slice.",
61
+ 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.",
62
+ requireSuccessfulResults: true,
63
+ validateRequiredToolResult: successfulCurrentResult,
64
+ validateTerminalAnswer: unsupportedCurrentClaim,
15
65
  };
16
66
  }
@@ -76,6 +76,12 @@ function preparedTexts(effect) {
76
76
  return [requireText(effect.text, "Telegram text")];
77
77
  return [effect.text?.trim() || null];
78
78
  }
79
+ function telegramPlainText(text) {
80
+ return text.replace(/(^|[\s([{])\*\*([\p{L}\p{N}][^*\n]*?)\*\*(?=$|[\s.,;!?)\]}])/gmu, "$1$2");
81
+ }
82
+ function presentTelegramEffect(authorClass, effect) {
83
+ return authorClass === "butler" && effect.kind === "text" ? { ...effect, text: telegramPlainText(effect.text) } : effect;
84
+ }
79
85
  function assertEffectTarget(target, effect, idempotencyKey) {
80
86
  if (target.kind === "admission_gate") {
81
87
  if (effect.kind !== "admission_ack" || effect.text !== exports.FIXED_ADMISSION_ACKNOWLEDGEMENT || idempotencyKey !== `ack:${target.admissionId}`) {
@@ -425,11 +431,12 @@ function createTelegramAuthorizedEffectExecutor(options) {
425
431
  if (input.signal?.aborted)
426
432
  throw input.signal.reason;
427
433
  barrier();
428
- const authorization = await options.authorize({ phase: "prepare", ...input });
434
+ const presentedInput = { ...input, effect: presentTelegramEffect(input.authorClass, input.effect) };
435
+ const authorization = await options.authorize({ phase: "prepare", ...presentedInput });
429
436
  if (!authorization.allowed)
430
437
  throw new Error(`Telegram effect authorization denied: ${authorization.reason}`);
431
438
  const store = getStore();
432
- const prepared = prepareTelegramEffect(store, { ...input, authorization });
439
+ const prepared = prepareTelegramEffect(store, { ...presentedInput, authorization });
433
440
  try {
434
441
  const executed = await executeTelegramEffect(store, prepared.id, options.api, async (artifact) => {
435
442
  if (input.signal?.aborted)
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.765",
3
+ "version": "0.1.0-alpha.766",
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.765",
9
+ "version": "0.1.0-alpha.766",
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.765",
3
+ "version": "0.1.0-alpha.766",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },