@ouro.bot/cli 0.1.0-alpha.960 → 0.1.0-alpha.962

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,20 @@
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.962",
6
+ "changes": [
7
+ "The upgrade check that asks the Butler to change his personality now asks for a change for everyone, so his saved per-person preference can no longer make the check pass or fail at random.",
8
+ "When the Butler finishes watching something for you, he now tells you in his own plain words what he checked, instead of forwarding his internal notes.",
9
+ "If the Butler's first try at telling you a watch finished does not go through, he resends the same words instead of getting stuck."
10
+ ]
11
+ },
12
+ {
13
+ "version": "0.1.0-alpha.961",
14
+ "changes": [
15
+ "When Butler finishes a watch you asked for in Telegram, he now actually tells you and closes it, instead of silently re-checking every few hours."
16
+ ]
17
+ },
4
18
  {
5
19
  "version": "0.1.0-alpha.960",
6
20
  "changes": [
@@ -428,7 +428,8 @@ export const CASES = [
428
428
  {
429
429
  id: "psyche-via-pr",
430
430
  // A concrete ask. Given only "can you change your personality?" the Butler describes the pull-request path and offers to file it once told what to change (alpha.878 session bf112be9), which is reasonable, so he never calls a filing tool.
431
- words: "change your personality: drop the jokes and be formal and flat with me from now on",
431
+ // It asks for everyone, not "with me": the replay principal already has a saved formal, flat preference, so "with me" is already met and offering to file was an equally right answer, which failed this case in about half of runs (alpha.961 sessions 09585e4a, c9d1ee4d, b196f170).
432
+ words: "change your personality for everyone you talk to, not just me: drop the jokes and be formal and flat from now on",
432
433
  sender: "principal",
433
434
  delegated: false,
434
435
  readback: ({ trace, reply, said, before, after }) => {
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.960",
2
+ "runtimeVersion": "0.1.0-alpha.962",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-09-03T00:00:00.000Z"
5
5
  }
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0"?>
2
2
  <Container version="2">
3
3
  <Name>ouro-butler</Name>
4
- <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.960</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.962</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -38,6 +38,9 @@ exports.applyAwaitRuntimeState = applyAwaitRuntimeState;
38
38
  exports.writeAwaitRuntimeState = writeAwaitRuntimeState;
39
39
  exports.recordAwaitDispatch = recordAwaitDispatch;
40
40
  exports.recordAwaitCheck = recordAwaitCheck;
41
+ exports.pinAwaitAlertContent = pinAwaitAlertContent;
42
+ exports.clearAwaitAlertContent = clearAwaitAlertContent;
43
+ const fs = __importStar(require("fs"));
41
44
  const path = __importStar(require("path"));
42
45
  const json_store_1 = require("../../arc/json-store");
43
46
  const session_events_1 = require("../session-events");
@@ -109,3 +112,19 @@ function recordAwaitCheck(agentRoot, name, observation, now) {
109
112
  checked_count: nextCount,
110
113
  });
111
114
  }
115
+ // A request-bound alert's Telegram effect is keyed by the await name alone, so a
116
+ // retry with different words is refused as a reused key and the watch never
117
+ // closes. The first text tried is pinned and every retry resends it.
118
+ function awaitAlertDir(agentRoot) {
119
+ return path.join(agentRoot, "state", "await-alerts");
120
+ }
121
+ function pinAwaitAlertContent(agentRoot, name, content) {
122
+ const pinned = (0, json_store_1.readJsonFile)(awaitAlertDir(agentRoot), name);
123
+ if (typeof pinned?.content === "string")
124
+ return pinned.content;
125
+ (0, json_store_1.writeJsonFile)(awaitAlertDir(agentRoot), name, { schemaVersion: 1, name, content });
126
+ return content;
127
+ }
128
+ function clearAwaitAlertContent(agentRoot, name) {
129
+ fs.rmSync(path.join(awaitAlertDir(agentRoot), `${name}.json`), { force: true });
130
+ }
@@ -55,6 +55,7 @@ const session_events_1 = require("../heart/session-events");
55
55
  const runtime_1 = require("../nerves/runtime");
56
56
  const await_parser_1 = require("../heart/awaiting/await-parser");
57
57
  const await_alert_1 = require("../heart/awaiting/await-alert");
58
+ const await_runtime_state_1 = require("../heart/awaiting/await-runtime-state");
58
59
  const friends_1 = require("@ouro.bot/friends");
59
60
  const a2a_await_delivery_1 = require("../heart/awaiting/a2a-await-delivery");
60
61
  const pending_1 = require("../mind/pending");
@@ -143,9 +144,28 @@ function setAwaitToolDeps(deps) {
143
144
  function resetAwaitToolDeps() {
144
145
  injected = {};
145
146
  }
147
+ /**
148
+ * A tick runs in the private runtime, where no resident Telegram sense registered a deliverer. Without this one-shot
149
+ * sender a Telegram-bound watch queued its alert in the inner pending folder, never reached the owner, and never closed.
150
+ * A resident sense's own deliverer still wins when one is registered.
151
+ */
152
+ async function sendTelegramAwaitAlert(agentName, request) {
153
+ // Only a request-bound watch has an exact chat to answer; any other alert keeps queueing for the agent's next turn.
154
+ if (!request.requestId)
155
+ return { status: "unavailable", detail: "no request-bound Telegram chat to answer" };
156
+ const { sendTelegramAwaitFollowUp } = await Promise.resolve().then(() => __importStar(require("../senses/telegram")));
157
+ return sendTelegramAwaitFollowUp(agentName, request);
158
+ }
146
159
  function resolveDeliveryDeps(agentName) {
147
160
  const deps = injected.buildDeliveryDeps ? injected.buildDeliveryDeps(agentName) : defaultDeliveryDeps(agentName);
148
- return { ...deps, deliverers: { a2a: (0, a2a_await_delivery_1.createA2AAwaitOwnerDeliverer)(agentName), ...deps.deliverers } };
161
+ return {
162
+ ...deps,
163
+ deliverers: {
164
+ a2a: (0, a2a_await_delivery_1.createA2AAwaitOwnerDeliverer)(agentName),
165
+ telegram: (request) => sendTelegramAwaitAlert(agentName, request),
166
+ ...deps.deliverers,
167
+ },
168
+ };
149
169
  }
150
170
  const ASK_OWNER_MIN_CHOICES = 2;
151
171
  const ASK_OWNER_MAX_CHOICES = 4;
@@ -462,7 +482,9 @@ function fulfillAwaitObligation(agentRoot, awaitFile) {
462
482
  /** The package-managed daily sweep must stay a byte-exact pending file in awaiting/, so it can only ever be resolved 'no'. */
463
483
  exports.HOUSE_CARE_AWAIT = "house-care-sweep";
464
484
  const isHouseCareAwait = (name) => String(name ?? "").replace(/\.md$/u, "") === exports.HOUSE_CARE_AWAIT;
465
- async function resolveAwaitTool(name, verdict, observation, agentRoot, agentName, ask) {
485
+ async function resolveAwaitTool(name, verdict, observation, agentRoot, agentName, ask, message) {
486
+ // The words for whoever asked; without them they get the condition and observation, which read as unsourced notes.
487
+ const content = typeof message === "string" && message.trim() ? message.trim() : undefined;
466
488
  const nameError = validateName(name);
467
489
  if (nameError)
468
490
  return JSON.stringify({ error: nameError });
@@ -500,7 +522,7 @@ async function resolveAwaitTool(name, verdict, observation, agentRoot, agentName
500
522
  let alert = null;
501
523
  if (existing.request_id && existing.filed_from !== "external-event") {
502
524
  try {
503
- alert = await (0, await_alert_1.deliverAwaitAlert)({ awaitFile: existing, reason: "resolved", observation: observation.trim(), agentRoot, agentName, deliveryDeps: resolveDeliveryDeps(agentName) });
525
+ alert = await (0, await_alert_1.deliverAwaitAlert)({ awaitFile: existing, reason: "resolved", observation: observation.trim(), content: (0, await_runtime_state_1.pinAwaitAlertContent)(agentRoot, name, content ?? (0, await_alert_1.buildAlertContent)(existing, "resolved", observation.trim())), agentRoot, agentName, deliveryDeps: resolveDeliveryDeps(agentName) });
504
526
  }
505
527
  catch (error) {
506
528
  (0, runtime_1.emitNervesEvent)({ level: "error", component: "repertoire", event: "repertoire.await_alert_error", message: "await alert delivery threw", meta: { agent: agentName, name, error: error instanceof Error ? error.message : String(error) } });
@@ -530,6 +552,8 @@ async function resolveAwaitTool(name, verdict, observation, agentRoot, agentName
530
552
  if (!archive.ok)
531
553
  return JSON.stringify({ error: archive.error });
532
554
  fulfillAwaitObligation(agentRoot, archive.file);
555
+ if (existing.request_id)
556
+ (0, await_runtime_state_1.clearAwaitAlertContent)(agentRoot, name);
533
557
  (0, runtime_1.emitNervesEvent)({
534
558
  component: "repertoire",
535
559
  event: "repertoire.await_resolved",
@@ -538,7 +562,7 @@ async function resolveAwaitTool(name, verdict, observation, agentRoot, agentName
538
562
  });
539
563
  if (!existing.request_id) {
540
564
  try {
541
- alert = await (0, await_alert_1.deliverAwaitAlert)({ awaitFile: archive.file, reason: "resolved", observation: observation.trim(), agentRoot, agentName, deliveryDeps: resolveDeliveryDeps(agentName) });
565
+ alert = await (0, await_alert_1.deliverAwaitAlert)({ awaitFile: archive.file, reason: "resolved", observation: observation.trim(), content, agentRoot, agentName, deliveryDeps: resolveDeliveryDeps(agentName) });
542
566
  }
543
567
  catch (error) {
544
568
  (0, runtime_1.emitNervesEvent)({ level: "error", component: "repertoire", event: "repertoire.await_alert_error", message: "await alert delivery threw", meta: { agent: agentName, name, error: error instanceof Error ? error.message : String(error) } });
@@ -783,6 +807,7 @@ exports.awaitingToolDefinitions = [
783
807
  name: { type: "string", description: "Await name (filename stem)." },
784
808
  verdict: { type: "string", enum: ["yes", "no", "ask_owner"], description: "Exactly 'yes' if the condition is met. Exactly 'no' for anything else, including still pending, not yet, or unable to tell: send 'no' with an observation and polling continues. 'ask_owner' only when the condition cannot be met without the owner deciding; it needs question and choices." },
785
809
  observation: { type: "string", description: "One-line summary of what I saw this tick." },
810
+ message: { type: "string", description: "Only for 'yes': what I tell whoever asked, written to them in plain words and sent as is. Say what I just checked and what it means for them." },
786
811
  question: { type: "string", description: "Only for ask_owner: the decision I need from my owner, in plain words." },
787
812
  choices: { type: "array", items: { type: "string" }, minItems: 2, maxItems: 4, description: "Only for ask_owner: 2 to 4 short options the owner can pick from." },
788
813
  },
@@ -825,7 +850,7 @@ exports.awaitingToolDefinitions = [
825
850
  if (!binding?.active)
826
851
  return JSON.stringify({ error: "resolve_await is limited to the current relationship request or bound external event" });
827
852
  }
828
- return resolveAwaitTool(a.name, a.verdict, a.observation, agentRoot, agentName, { question: a.question, choices: a.choices });
853
+ return resolveAwaitTool(a.name, a.verdict, a.observation, agentRoot, agentName, { question: a.question, choices: a.choices }, a.message);
829
854
  },
830
855
  riskProfile: {
831
856
  mutates: ["durable_state_write", "external_side_effect"],
@@ -59,7 +59,7 @@ function buildAwaitTurnMessage(options) {
59
59
  lines.push(`last checkpoint: ${options.checkpoint}`);
60
60
  }
61
61
  lines.push("");
62
- lines.push("look around and decide. if the condition is met, call resolve_await with verdict='yes' and a one-line observation. otherwise call resolve_await with verdict='no' and a one-line observation of what i saw this tick.");
62
+ lines.push("look around and decide. if the condition is met, call resolve_await with verdict='yes', a one-line observation, and a message: what i tell whoever asked, in plain words to them, saying what i just checked. otherwise call resolve_await with verdict='no' and a one-line observation of what i saw this tick.");
63
63
  lines.push("verdicts: 'yes' (condition met), 'no' (not yet or unsure), 'ask_owner' (only when the condition cannot be met without my owner deciding, e.g. a confirmed dead download, or a stall the tool marks owner_decision_needed; give a plain question and 2-4 choices).");
64
64
  return lines.join("\n");
65
65
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.960",
3
+ "version": "0.1.0-alpha.962",
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.960",
9
+ "version": "0.1.0-alpha.962",
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.960",
3
+ "version": "0.1.0-alpha.962",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },