@ouro.bot/cli 0.1.0-alpha.760 → 0.1.0-alpha.761

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.761",
6
+ "changes": [
7
+ "Require current storage and media-optimization evidence before answering owner requests to diagnose and shrink Sanctuary storage.",
8
+ "Turn the exact owner download-credit question into one household-safe top-up, resume, verification, or reminder reply without backend jargon.",
9
+ "Require current work, cares, system, and container evidence for compact whole-Sanctuary status answers."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.760",
6
14
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.760",
2
+ "runtimeVersion": "0.1.0-alpha.761",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-08-30T00:00:00.000Z"
5
5
  }
@@ -4,6 +4,6 @@ Sanctuary runs Unraid 7.2.3 and Docker services. The service list is discovered
4
4
 
5
5
  Books maps to the exact containers `calibre` and `calibre-web`. When Ari states a desired state for Books, record that same desired state under both exact steward-policy keys, `container:calibre` and `container:calibre-web`; reverse both when Ari asks for Books again. Do not create a synthetic `container:books` key.
6
6
 
7
- The household's prepaid download provider is Astraweb. Its public account and top-up entry is https://www.astraweb.com/login. Never ask for or expose account credentials; send Ari that public link when a human top-up is the next step, then verify recent successful articles and a completed download before calling the path recovered.
7
+ The household's prepaid download provider is Astraweb. Its public account and top-up entry is https://www.astraweb.com/login. Never ask for or expose account credentials. When Ari asks why shows are not downloading and exhausted credit is established, compose the answer in plain household language: downloads are paused to protect prepaid credit; give the public link; ask Ari to tell you when the top-up is done so you can resume downloads and verify one finishes; and offer a reminder at a time Ari specifies. Do not repeat backend service names, authentication or credential-check details, dead-letter/event internals, duplicate alerts, or an untrusted notification's claim that an indexer was disabled. Do not claim to top up or disable an indexer yourself. Keep the agent's own voice; this is an outcome contract, not a canned script.
8
8
 
9
9
  Ari and each approved household member are separate relationships. Their conversations, preferences, requests, private activity, and return routes do not bleed into one another. Unknown Telegram content is quarantined before any model turn and becomes conversation only after Ari admits the immutable account and chat.
@@ -4,6 +4,7 @@
4
4
  - Use standing policy for bounded, reversible action; revalidate the exact target and authority immediately before the effect and verify the outcome afterward.
5
5
  - Keep approved household contact reactive or request-bound. Only Ari receives unrelated proactive household attention.
6
6
  - Treat an intentionally offline service as healthy when that matches the current desired state.
7
+ - For Ari's whole-household status questions, read current evidence before answering and separate active work, waiting on Ari, snoozed wake times, intentionally quiet services, healthy systems, and other current issues. Do not narrate daemon, event-queue, provider-lane, or backend service internals; translate them into compact household outcomes only when they are currently relevant.
7
8
  - Never touch array/parity/disk configuration, permissions, network/security settings, or `/mnt/user` paths.
8
9
  - Never send server data off-box or enter credentials into another system.
9
10
  - When state is ambiguous, investigate safely, say what is known, and ask only if the answer changes the next safe move.
@@ -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.760</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.761</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -1120,6 +1120,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1120
1120
  let unresolvedHistoricalEffects = historicalFailedEffectsForExactRepeatedRequest(messages);
1121
1121
  let historicalToolFailureRetries = 0;
1122
1122
  let providerIterations = 0;
1123
+ const requiredToolCallNames = [...new Set(options?.requiredToolCalls?.names ?? [])];
1124
+ const dispatchedRequiredToolCalls = new Set();
1123
1125
  const toolLoopState = (0, tool_loop_1.createToolLoopState)();
1124
1126
  const toolFrictionLedger = (0, tool_friction_1.createToolFrictionLedger)();
1125
1127
  const finishTerminalProviderError = (error, classification) => {
@@ -1720,6 +1722,24 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1720
1722
  if (isSoleSettle) {
1721
1723
  const settleArgs = validatedCallArguments.get(result.toolCalls[0]);
1722
1724
  callbacks.onToolStart("settle", settleArgs);
1725
+ const missingRequiredToolCalls = requiredToolCallNames.filter((name) => !dispatchedRequiredToolCalls.has(name));
1726
+ if (missingRequiredToolCalls.length > 0) {
1727
+ streamCallbackBuffer?.discard();
1728
+ callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1729
+ callbacks.onClearText?.();
1730
+ pushGenerated(msg);
1731
+ const gateMessage = `${options.requiredToolCalls.retryMessage} Missing required tool calls: ${missingRequiredToolCalls.join(", ")}.`;
1732
+ pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: gateMessage });
1733
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, gateMessage);
1734
+ (0, runtime_1.emitNervesEvent)({
1735
+ level: "warn",
1736
+ component: "engine",
1737
+ event: "engine.required_tool_calls_pending",
1738
+ message: "settle rejected until required tool handlers dispatch",
1739
+ meta: { missingToolNames: missingRequiredToolCalls },
1740
+ });
1741
+ continue;
1742
+ }
1723
1743
  // Private-runtime attention queue gate: reject settle if items remain
1724
1744
  const attentionQueue = augmentedToolContext?.delegatedOrigins;
1725
1745
  if (isPrivateRuntimeChannel && attentionQueue && attentionQueue.length > 0) {
@@ -2246,6 +2266,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
2246
2266
  const execToolFn = options?.execTool ?? tools_1.execTool;
2247
2267
  const routineActionSelection = approvalCalls.find((entry) => entry.call.id === tc.id)?.routineActionSelection;
2248
2268
  const executionToolContext = routineActionSelection && augmentedToolContext ? { ...augmentedToolContext, routineActionSelection } : augmentedToolContext;
2269
+ if (requiredToolCallNames.includes(tc.name))
2270
+ dispatchedRequiredToolCalls.add(tc.name);
2249
2271
  toolResult = await execToolFn(tc.name, args, executionToolContext);
2250
2272
  success = true;
2251
2273
  }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.presentSanctuaryDownloadCreditReply = presentSanctuaryDownloadCreditReply;
4
+ const runtime_1 = require("../nerves/runtime");
5
+ const TOP_UP_LINK = "https://www.astraweb.com/login";
6
+ const SAFE_FALLBACK = `Downloads are paused to protect your prepaid credit. Top it up here: ${TOP_UP_LINK}. Tell me when you’re done, and I’ll resume downloads and verify one finishes. Or tell me a reminder time, like “tomorrow at 9.”`;
7
+ const FORBIDDEN_INTERNALS = /\b(?:SABnzbd|Sonarr|Radarr|Deluge|auth[ -]?check|credential|dead[ -]?letter|indexer has been disabled|keep watching)\b/iu;
8
+ const EXHAUSTED_CREDIT = /(?:\b(?:prepaid )?credit (?:is |appear(?:s|ed)? |seems? )?(?:exhausted|depleted)\b|\bout of (?:prepaid )?credit\b|\bzero (?:prepaid )?credit\b|\bno (?:prepaid )?credit (?:left|remaining|available)\b)/iu;
9
+ const REQUIRED_HOUSEHOLD_CONTRACT = [
10
+ /\bdownloads?\b/iu,
11
+ /\bpaused?\b/iu,
12
+ /\bprotect\b/iu,
13
+ /\bprepaid credit\b/iu,
14
+ /\btell me\b/iu,
15
+ /\bdone\b/iu,
16
+ /\bresume\b/iu,
17
+ /\bverify\b/iu,
18
+ /\b(?:finish(?:es|ed)?|completed?)\b/iu,
19
+ /\bremind(?:er)?\b/iu,
20
+ /\btime\b|\btomorrow at 9\b/iu,
21
+ ];
22
+ function presentSanctuaryDownloadCreditReply(agentName, exactQuestion, text) {
23
+ if (agentName !== "sanctuary" || !exactQuestion || !text.includes(TOP_UP_LINK) || !/\bcredit\b/iu.test(text))
24
+ return text;
25
+ if (!FORBIDDEN_INTERNALS.test(text) && REQUIRED_HOUSEHOLD_CONTRACT.every((pattern) => pattern.test(text)))
26
+ return text;
27
+ if (!EXHAUSTED_CREDIT.test(text))
28
+ return text;
29
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.telegram_download_credit_reply_repaired", message: "repaired Sanctuary download-credit reply at the Telegram boundary", meta: { agentName } });
30
+ return SAFE_FALLBACK;
31
+ }
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanctuaryFullVisibilityRequiredToolCalls = sanctuaryFullVisibilityRequiredToolCalls;
4
+ const runtime_1 = require("../nerves/runtime");
5
+ const REQUIRED_TOOL_NAMES = ["query_active_work", "query_cares", "unraid_get_system", "unraid_list_containers"];
6
+ const WHOLE_STATUS_REQUESTS = new Set(["what are you working on", "what's going on with sanctuary"]);
7
+ function sanctuaryFullVisibilityRequiredToolCalls(request, advertisedToolNames) {
8
+ const normalized = request.normalize("NFKC").trim().toLocaleLowerCase("en-US").replaceAll("’", "'").replace(/[?!.\s]+$/gu, "");
9
+ if (!WHOLE_STATUS_REQUESTS.has(normalized) || !REQUIRED_TOOL_NAMES.every((name) => advertisedToolNames.includes(name)))
10
+ return undefined;
11
+ (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
+ return {
13
+ 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.",
15
+ };
16
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanctuaryStorageOptimizationRequiredToolCalls = sanctuaryStorageOptimizationRequiredToolCalls;
4
+ const runtime_1 = require("../nerves/runtime");
5
+ const REQUIRED_TOOL_NAMES = ["unraid_get_storage", "sanctuary_get_media_optimization"];
6
+ function normalizedRequest(request) {
7
+ return request
8
+ .normalize("NFKC")
9
+ .toLowerCase()
10
+ .replace(/[‘’]/gu, "'")
11
+ .replace(/[^a-z0-9']+/gu, " ")
12
+ .trim();
13
+ }
14
+ function sanctuaryStorageOptimizationRequiredToolCalls(request, advertisedToolNames) {
15
+ const normalized = normalizedRequest(request);
16
+ const hasStorageSubject = /\b(?:space|storage)\b/u.test(normalized);
17
+ const hasUsageDiagnosis = /\b(?:using|taking up|find what|diagnos(?:e|is))\b/u.test(normalized);
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)))
21
+ return undefined;
22
+ const names = [...REQUIRED_TOOL_NAMES];
23
+ (0, runtime_1.emitNervesEvent)({
24
+ component: "senses",
25
+ event: "senses.sanctuary_storage_optimization_obligation",
26
+ message: "storage diagnosis and optimization intent requires both safe evidence reads",
27
+ meta: { requiredToolNames: names },
28
+ });
29
+ return {
30
+ names,
31
+ retryMessage: "Run both safe reads now, identify the largest measured evidence, report Unmanic and Jellyfin findings, and propose a sample encode without inventing future savings. Do not ask permission or send Ari to a shell or QDirStat while these typed reads are available.",
32
+ };
33
+ }
@@ -64,9 +64,12 @@ const runtime_1 = require("../nerves/runtime");
64
64
  const nerves_1 = require("../nerves");
65
65
  const sanctuary_interactive_control_1 = require("./sanctuary-interactive-control");
66
66
  const sanctuary_sab_1 = require("./sanctuary-sab");
67
+ const sanctuary_download_credit_presentation_1 = require("./sanctuary-download-credit-presentation");
67
68
  const shared_turn_1 = require("./shared-turn");
68
69
  const telegram_client_1 = require("./telegram-client");
69
70
  const sanctuary_runtime_1 = require("./sanctuary-runtime");
71
+ const sanctuary_full_visibility_contract_1 = require("./sanctuary-full-visibility-contract");
72
+ const sanctuary_storage_optimization_contract_1 = require("./sanctuary-storage-optimization-contract");
70
73
  const sanctuary_grounding_1 = require("./sanctuary-grounding");
71
74
  const telegram_approval_runtime_1 = require("./telegram-approval-runtime");
72
75
  const telegram_audit_ledger_1 = require("./telegram-audit-ledger");
@@ -976,12 +979,13 @@ function createTelegramSenseApp(options) {
976
979
  approvalReconciliationsInFlight.add(failsafe);
977
980
  }, delay);
978
981
  };
979
- const deliverButlerEffect = async (text, idempotencyKey, signal, onMessageDelivered) => {
982
+ const deliverButlerEffect = async (text, idempotencyKey, signal, onMessageDelivered, exactDownloadCreditQuestion = false) => {
983
+ const presentedText = (0, sanctuary_download_credit_presentation_1.presentSanctuaryDownloadCreditReply)(options.agentName, exactDownloadCreditQuestion, text);
980
984
  return executeAuthorizedEffect({
981
985
  idempotencyKey,
982
986
  target: configuredOwnerTarget(),
983
987
  authorClass: "butler",
984
- effect: { kind: "text", text },
988
+ effect: { kind: "text", text: presentedText },
985
989
  ...(signal ? { signal } : {}),
986
990
  ...(onMessageDelivered ? { onMessageDelivered } : {}),
987
991
  });
@@ -1070,10 +1074,18 @@ function createTelegramSenseApp(options) {
1070
1074
  authorizeTool: async (name, args) => (await options.resolveRelationshipAuthorization(relationshipCoordinates)).authorizeTool(name, args),
1071
1075
  };
1072
1076
  };
1073
- return async ({ runAgentOptions }) => ({
1074
- ...runAgentOptions,
1075
- toolContext: { ...runAgentOptions.toolContext, relationshipAuthorization: await resolveLiveRelationshipAuthorization() },
1076
- });
1077
+ return async ({ runAgentOptions }) => {
1078
+ const relationshipAuthorization = await resolveLiveRelationshipAuthorization();
1079
+ const requiredToolCalls = options.agentName === "sanctuary" && relationshipAuthorization.profileId === "sanctuary-owner"
1080
+ ? (0, sanctuary_storage_optimization_contract_1.sanctuaryStorageOptimizationRequiredToolCalls)(input.userMessage, relationshipAuthorization.advertisedToolNames)
1081
+ ?? (0, sanctuary_full_visibility_contract_1.sanctuaryFullVisibilityRequiredToolCalls)(input.userMessage, relationshipAuthorization.advertisedToolNames)
1082
+ : undefined;
1083
+ return {
1084
+ ...runAgentOptions,
1085
+ ...(requiredToolCalls ? { requiredToolCalls } : {}),
1086
+ toolContext: { ...runAgentOptions.toolContext, relationshipAuthorization },
1087
+ };
1088
+ };
1077
1089
  };
1078
1090
  const dispatchResolvedRelationshipTurn = async (input) => {
1079
1091
  const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, input.friendId, "telegram", input.sessionKey, agentRoot);
@@ -1118,7 +1130,7 @@ function createTelegramSenseApp(options) {
1118
1130
  ...(orientationFrame ? { orientationFrame } : {}),
1119
1131
  toolContext: { ...(toolContext ?? {}), attachmentIds: input.attachmentIds ?? [] },
1120
1132
  prepareRunAgentOptions: prepareRelationshipRunAgentOptions({ friendId: input.friendId, requestId: input.requestId,
1121
- sessionEventId: input.eventId, userId: input.userId, chatId: input.chatId, sessionKey: input.sessionKey }),
1133
+ sessionEventId: input.eventId, userId: input.userId, chatId: input.chatId, sessionKey: input.sessionKey, userMessage: input.text }),
1122
1134
  deliverySink: { onDelivery: (delivery) => deliver(delivery.text, delivery.kind === "settle") },
1123
1135
  });
1124
1136
  if (effects.length === 0 && result.response.trim())
@@ -1265,6 +1277,7 @@ function createTelegramSenseApp(options) {
1265
1277
  const turnMetricsObserver = { providerInvocationCount: 0, toolInvocationCount: 0 };
1266
1278
  const toolReceiptObserver = { toolResultDigests: [], toolGroundings: [] };
1267
1279
  const normalizedRequest = message.text.normalize("NFKC").trim().toLocaleLowerCase("en-US");
1280
+ const exactDownloadCreditQuestion = normalizedRequest === "why aren't my shows downloading?";
1268
1281
  const groundingIntentTool = groundedAcceptance
1269
1282
  ? /^(?:what['’]?s up|status)\??$/u.test(normalizedRequest) ? "unraid_get_system"
1270
1283
  : /^how much (?:space|storage) is left\??$/u.test(normalizedRequest) ? "unraid_get_storage"
@@ -1317,7 +1330,7 @@ function createTelegramSenseApp(options) {
1317
1330
  bufferedGroundedDeliveries.push(delivery.kind);
1318
1331
  }
1319
1332
  else {
1320
- turnEffects.push(await deliverButlerEffect(delivery.text, `turn:${subject}:${message.updateId}:delivery:${deliveryOrdinal++}`, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); }));
1333
+ turnEffects.push(await deliverButlerEffect(delivery.text, `turn:${subject}:${message.updateId}:delivery:${deliveryOrdinal++}`, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); }, exactDownloadCreditQuestion));
1321
1334
  deliveryCount += 1;
1322
1335
  }
1323
1336
  },
@@ -1333,6 +1346,7 @@ function createTelegramSenseApp(options) {
1333
1346
  userId: message.userId,
1334
1347
  chatId: message.chatId,
1335
1348
  sessionKey: currentSessionKey,
1349
+ userMessage: message.text,
1336
1350
  }),
1337
1351
  } : {}),
1338
1352
  ...(approvalRuntime ? { approvalCoordinatorFactory: approvalRuntime.coordinator } : {}),
@@ -1351,7 +1365,7 @@ function createTelegramSenseApp(options) {
1351
1365
  deliveryCount = 1;
1352
1366
  }
1353
1367
  else if (deliveryCount === 0 && result.response.trim()) {
1354
- const artifact = await deliverButlerEffect(result.response, `turn:${subject}:${message.updateId}:delivery:${deliveryOrdinal++}`, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); });
1368
+ const artifact = await deliverButlerEffect(result.response, `turn:${subject}:${message.updateId}:delivery:${deliveryOrdinal++}`, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); }, exactDownloadCreditQuestion);
1355
1369
  turnEffects.push(artifact);
1356
1370
  responseFallbackArtifactId = artifact.id;
1357
1371
  }
@@ -1395,7 +1409,7 @@ function createTelegramSenseApp(options) {
1395
1409
  });
1396
1410
  const fallback = "I couldn't complete that turn. The failure was recorded; please try again.";
1397
1411
  if (deliveredMessageIds.length === 0)
1398
- turnEffects.push(await deliverButlerEffect(fallback, `turn:${subject}:${message.updateId}:fallback`, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); }));
1412
+ turnEffects.push(await deliverButlerEffect(fallback, `turn:${subject}:${message.updateId}:fallback`, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); }, exactDownloadCreditQuestion));
1399
1413
  await recordAcceptedEffects(currentSessionPath, turnEffects, { text: message.text, reference: inboundReference, attachmentIds: hydrated.attachmentIds });
1400
1414
  }
1401
1415
  finally {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.760",
3
+ "version": "0.1.0-alpha.761",
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.760",
9
+ "version": "0.1.0-alpha.761",
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.760",
3
+ "version": "0.1.0-alpha.761",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },