@ouro.bot/cli 0.1.0-alpha.790 → 0.1.0-alpha.792

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,26 @@
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.792",
6
+ "changes": [
7
+ "Catch asynchronous bundle watcher errors so transient pending-message processing files on live filesystems cannot tombstone the daemon or interrupt private-runtime wakes.",
8
+ "Warm runtime credentials before private-runtime bootstrap so Telegram-capable workers start with runtime config instead of racing the managed Telegram sense.",
9
+ "Treat simple Sanctuary library visibility asks like 'see the lib' as catalog reads, and block replies that send Ari to inspect Jellyfin, Unmanic, dashboards, or logs while safe evidence tools are available.",
10
+ "Degrade optional Unmanic file-size metrics, including failed detail reads, without hiding available media catalog evidence."
11
+ ]
12
+ },
13
+ {
14
+ "version": "0.1.0-alpha.791",
15
+ "changes": [
16
+ "Promote operator CLI pending messages into the active inner-turn user message so private-runtime wakes answer the text that woke them instead of stale heartbeat context.",
17
+ "Keep non-operator pending work in the background pending section so automated noise does not become the live user ask.",
18
+ "Deliver private-runtime Telegram surface and send_message returns through the existing Telegram effect journal instead of queuing ghost messages for the next chat.",
19
+ "Canonicalize Telegram session filename keys before live delivery so production sanitized session paths still authorize against the real Telegram binding.",
20
+ "Prefer the authoritative settle tool answer over incidental streamed prose for shared-turn delivery, preventing concatenated Telegram replies.",
21
+ "Expire stale generic Telegram outreach from the pending queue while preserving request-bound returns and trust/admission notices."
22
+ ]
23
+ },
4
24
  {
5
25
  "version": "0.1.0-alpha.790",
6
26
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.790",
2
+ "runtimeVersion": "0.1.0-alpha.792",
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>Mendelow Cloud Butler</Name>
4
- <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.790</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.792</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -172,7 +172,7 @@ Promise.resolve().then(() => __importStar(require("./runtime-credentials"))).the
172
172
  drainBufferedRuntimeCredentialBootstrap(applyRuntimeCredentialBootstrapMessage);
173
173
  }
174
174
  if (!readRuntimeCredentialConfig(agentName).ok) {
175
- void refreshRuntimeCredentialConfig(agentName, { preserveCachedOnFailure: true }).catch(() => undefined);
175
+ await refreshRuntimeCredentialConfig(agentName, { preserveCachedOnFailure: true }).catch(() => undefined);
176
176
  }
177
177
  const providerPool = readProviderCredentialPool(agentName);
178
178
  const providerTargets = await selectedProviderTargets(agentName);
@@ -57,6 +57,7 @@ exports.getSyncConfig = getSyncConfig;
57
57
  exports.getOpenAIEmbeddingsApiKey = getOpenAIEmbeddingsApiKey;
58
58
  exports.getLogsDir = getLogsDir;
59
59
  exports.sanitizeKey = sanitizeKey;
60
+ exports.canonicalizeTelegramSessionKey = canonicalizeTelegramSessionKey;
60
61
  exports.slugify = slugify;
61
62
  exports.resolveSessionPath = resolveSessionPath;
62
63
  exports.sessionPath = sessionPath;
@@ -351,6 +352,19 @@ function getLogsDir() {
351
352
  function sanitizeKey(key) {
352
353
  return key.replace(/[/:]/g, "_");
353
354
  }
355
+ function canonicalizeTelegramSessionKey(key) {
356
+ if (key.startsWith("telegram:"))
357
+ return key;
358
+ if (!key.startsWith("telegram_"))
359
+ return key;
360
+ const suffix = key.slice("telegram_".length);
361
+ if (suffix.startsWith("tg_"))
362
+ return `telegram:${suffix}`;
363
+ const numericSession = suffix.match(/^([0-9]+)_(-?[0-9]+)$/u);
364
+ if (numericSession)
365
+ return `telegram:${numericSession[1]}:${numericSession[2]}`;
366
+ return key;
367
+ }
354
368
  function slugify(value) {
355
369
  return value
356
370
  .trim()
@@ -591,6 +591,7 @@ async function prepareProviderRuntime() {
591
591
  const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
592
592
  const readiness = await Promise.all(managedAgents.map(async (agent) => {
593
593
  try {
594
+ await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agent, { preserveCachedOnFailure: true });
594
595
  const result = await (0, agent_config_check_1.checkAgentConfigWithProviderHealth)(agent, bundlesRoot);
595
596
  if (result.ok)
596
597
  return null;
@@ -92,7 +92,20 @@ function createBundleWatcher(bundlesRoot, onChange, deps = DEFAULT_BUNDLE_WATCHE
92
92
  }
93
93
  try {
94
94
  if (deps.existsSync(bundlesRoot)) {
95
- watchers.push(deps.watch(bundlesRoot, { recursive: true }, debouncedOnChange));
95
+ const watcher = deps.watch(bundlesRoot, { recursive: true }, debouncedOnChange);
96
+ watcher.on?.("error", () => {
97
+ try {
98
+ watcher.close();
99
+ }
100
+ catch {
101
+ // Already closed.
102
+ }
103
+ const index = watchers.indexOf(watcher);
104
+ if (index >= 0)
105
+ watchers.splice(index, 1);
106
+ debouncedOnChange();
107
+ });
108
+ watchers.push(watcher);
96
109
  }
97
110
  }
98
111
  catch {
@@ -39,6 +39,7 @@ exports.formatButlerOperationalVisibility = formatButlerOperationalVisibility;
39
39
  exports.renderInnerProgressStatus = renderInnerProgressStatus;
40
40
  const fs = __importStar(require("fs"));
41
41
  const path = __importStar(require("path"));
42
+ const crypto_1 = require("crypto");
42
43
  const config_1 = require("../heart/config");
43
44
  const identity_1 = require("../heart/identity");
44
45
  const session_events_1 = require("../heart/session-events");
@@ -1050,6 +1051,17 @@ exports.sessionToolDefinitions = [
1050
1051
  detail: "live delivery unavailable right now; queued for the next active turn",
1051
1052
  };
1052
1053
  },
1054
+ telegram: async (request) => {
1055
+ const key = (0, config_1.canonicalizeTelegramSessionKey)(request.key);
1056
+ const hash = (0, crypto_1.createHash)("sha256").update(`${request.friendId}\0${key}\0${request.content}`).digest("hex");
1057
+ const { sendTelegramAwaitFollowUp } = await Promise.resolve().then(() => __importStar(require("../senses/telegram")));
1058
+ return sendTelegramAwaitFollowUp(agentName, {
1059
+ ...request,
1060
+ key,
1061
+ requestId: request.requestId ?? `send-message:${hash}`,
1062
+ deliveryId: request.deliveryId ?? `send-message:${hash}`,
1063
+ });
1064
+ },
1053
1065
  voice: async (request) => deliverVoiceChannelMessage(request, agentName, voiceInitialAudio),
1054
1066
  },
1055
1067
  });
@@ -35,6 +35,8 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.surfaceToolDefinition = exports.surfaceToolDef = void 0;
37
37
  const fs = __importStar(require("fs"));
38
+ const crypto_1 = require("crypto");
39
+ const config_1 = require("../heart/config");
38
40
  const identity_1 = require("../heart/identity");
39
41
  const surface_tool_1 = require("../senses/surface-tool");
40
42
  const obligations_1 = require("../arc/obligations");
@@ -173,6 +175,38 @@ exports.surfaceToolDefinition = {
173
175
  const agentRoot = (0, identity_1.getAgentRoot)();
174
176
  const sessionsDir = path.join(agentRoot, "state", "sessions");
175
177
  const friendsDir = path.join(agentRoot, "friends");
178
+ const queueToSession = async (target, detail) => {
179
+ const { queuePendingMessage, getPendingDir } = await Promise.resolve().then(() => __importStar(require("../mind/pending")));
180
+ const pendingDir = getPendingDir(agentName, target.friendId, target.channel, target.key);
181
+ queuePendingMessage(pendingDir, {
182
+ from: agentName,
183
+ friendId: target.friendId,
184
+ channel: target.channel,
185
+ key: target.key,
186
+ content,
187
+ timestamp: Date.now(),
188
+ });
189
+ return { status: "queued", detail };
190
+ };
191
+ const deliverTelegramOrQueue = async (target, detail) => {
192
+ const telegramKey = (0, config_1.canonicalizeTelegramSessionKey)(target.key);
193
+ const hash = (0, crypto_1.createHash)("sha256").update(`${target.friendId}\0${telegramKey}\0${content}`).digest("hex");
194
+ const requestId = queueItem?.obligationId ?? `surface:${hash}`;
195
+ const deliveryId = queueItem?.obligationId ? `surface:${queueItem.obligationId}:${hash}` : `surface:${hash}`;
196
+ const { sendTelegramAwaitFollowUp } = await Promise.resolve().then(() => __importStar(require("../senses/telegram")));
197
+ const delivered = await sendTelegramAwaitFollowUp(agentName, {
198
+ friendId: target.friendId,
199
+ channel: "telegram",
200
+ key: telegramKey,
201
+ content,
202
+ requestId,
203
+ deliveryId,
204
+ intent: "generic_outreach",
205
+ });
206
+ return delivered.status === "delivered_now"
207
+ ? { status: "delivered", detail: "sent to Telegram now" }
208
+ : queueToSession(target, detail);
209
+ };
176
210
  // Resolve friend name → UUID if needed (agents may pass name instead of UUID)
177
211
  let resolvedFriendId = friendId;
178
212
  if (!fs.existsSync(path.join(sessionsDir, friendId))) {
@@ -213,17 +247,10 @@ exports.surfaceToolDefinition = {
213
247
  && activity.channel !== "inner"
214
248
  && bridge.attachedSessions.some((s) => s.friendId === activity.friendId && s.channel === activity.channel && s.key === activity.key));
215
249
  if (bridgeTarget) {
216
- const { queuePendingMessage, getPendingDir } = await Promise.resolve().then(() => __importStar(require("../mind/pending")));
217
- const pendingDir = getPendingDir(agentName, bridgeTarget.friendId, bridgeTarget.channel, bridgeTarget.key);
218
- queuePendingMessage(pendingDir, {
219
- from: agentName,
220
- friendId: bridgeTarget.friendId,
221
- channel: bridgeTarget.channel,
222
- key: bridgeTarget.key,
223
- content,
224
- timestamp: Date.now(),
225
- });
226
- return { status: "queued", detail: `for next interaction via ${bridgeTarget.channel}` };
250
+ const detail = `for next interaction via ${bridgeTarget.channel}`;
251
+ return bridgeTarget.channel === "telegram"
252
+ ? deliverTelegramOrQueue(bridgeTarget, detail)
253
+ : queueToSession(bridgeTarget, detail);
227
254
  }
228
255
  }
229
256
  }
@@ -232,17 +259,11 @@ exports.surfaceToolDefinition = {
232
259
  // address before considering fresher sessions for the same friend,
233
260
  // especially MCP sessions where check_response drains this path.
234
261
  if (queueItem?.channel && queueItem.key && queueItem.channel !== "inner") {
235
- const { queuePendingMessage, getPendingDir } = await Promise.resolve().then(() => __importStar(require("../mind/pending")));
236
- const pendingDir = getPendingDir(agentName, friendId, queueItem.channel, queueItem.key);
237
- queuePendingMessage(pendingDir, {
238
- from: agentName,
239
- friendId,
240
- channel: queueItem.channel,
241
- key: queueItem.key,
242
- content,
243
- timestamp: Date.now(),
244
- });
245
- return { status: "queued", detail: `for originating ${queueItem.channel} session` };
262
+ const target = { friendId, channel: queueItem.channel, key: queueItem.key };
263
+ const detail = `for originating ${queueItem.channel} session`;
264
+ return queueItem.channel === "telegram"
265
+ ? deliverTelegramOrQueue(target, detail)
266
+ : queueToSession(target, detail);
246
267
  }
247
268
  // Priority 2: Try proactive delivery first, then queue to freshest session
248
269
  const allFriendSessions = (0, session_activity_1.listSessionActivity)({ sessionsDir, friendsDir, agentName, activeThresholdMs: Number.MAX_SAFE_INTEGER })
@@ -250,17 +271,10 @@ exports.surfaceToolDefinition = {
250
271
  // Priority 2: Queue to freshest non-private-runtime session
251
272
  const freshest = allFriendSessions[0];
252
273
  if (freshest) {
253
- const { queuePendingMessage, getPendingDir } = await Promise.resolve().then(() => __importStar(require("../mind/pending")));
254
- const pendingDir = getPendingDir(agentName, freshest.friendId, freshest.channel, freshest.key);
255
- queuePendingMessage(pendingDir, {
256
- from: agentName,
257
- friendId: freshest.friendId,
258
- channel: freshest.channel,
259
- key: freshest.key,
260
- content,
261
- timestamp: Date.now(),
262
- });
263
- return { status: "queued", detail: `for next interaction via ${freshest.channel}` };
274
+ const detail = `for next interaction via ${freshest.channel}`;
275
+ return freshest.channel === "telegram"
276
+ ? deliverTelegramOrQueue(freshest, detail)
277
+ : queueToSession(freshest, detail);
264
278
  }
265
279
  // Priority 3: Deferred — no active session found
266
280
  const { getDeferredReturnDir } = await Promise.resolve().then(() => __importStar(require("../mind/pending")));
@@ -74,10 +74,21 @@ const flight_recorder_1 = require("../arc/flight-recorder");
74
74
  const context_loss_sentinel_1 = require("../heart/context-loss-sentinel");
75
75
  const cares_1 = require("../arc/cares");
76
76
  const VOICE_PENDING_MAX_AGE_MS = 15 * 60 * 1_000;
77
- function pendingExpirationReason(channel, message, now) {
77
+ const TELEGRAM_GENERIC_OUTREACH_PENDING_MAX_AGE_MS = 15 * 60 * 1_000;
78
+ function pendingExpirationReason(channel, message, now, agentName) {
78
79
  /* v8 ignore start -- pending expiry edge permutations are covered by the stale voice queue tests; this helper keeps defensive non-voice fallbacks @preserve */
79
80
  if (Number.isFinite(message.expiresAt) && Number(message.expiresAt) <= now)
80
81
  return "explicit_expiry";
82
+ if (channel === "telegram"
83
+ && agentName
84
+ && message.from === agentName
85
+ && !message.delegatedFrom
86
+ && !message.obligationId
87
+ && !message.requestId
88
+ && Number.isFinite(message.timestamp)
89
+ && now - message.timestamp > TELEGRAM_GENERIC_OUTREACH_PENDING_MAX_AGE_MS) {
90
+ return "telegram_generic_outreach_freshness_window";
91
+ }
81
92
  if (channel !== "voice")
82
93
  return null;
83
94
  if (!Number.isFinite(message.timestamp))
@@ -139,10 +150,18 @@ function filterDeliverablePendingMessages(input) {
139
150
  if (input.messages.length === 0)
140
151
  return input.messages;
141
152
  const now = input.now ?? Date.now();
153
+ const agentName = (() => {
154
+ try {
155
+ return (0, identity_1.getAgentName)();
156
+ }
157
+ catch {
158
+ return undefined;
159
+ }
160
+ })();
142
161
  const deliverable = [];
143
162
  const expired = [];
144
163
  for (const message of input.messages) {
145
- const reason = pendingExpirationReason(input.channel, message, now);
164
+ const reason = pendingExpirationReason(input.channel, message, now, agentName);
146
165
  if (reason) {
147
166
  expired.push({ message, reason });
148
167
  }
@@ -786,6 +805,14 @@ async function handleInboundTurn(input) {
786
805
  channel: input.channel,
787
806
  messages: [...deferredReturns, ...sessionPending],
788
807
  });
808
+ const promotedPendingUserMessages = input.channel === "inner"
809
+ ? pending
810
+ .filter((message) => message.from === "ouro-cli" && message.content.trim().length > 0)
811
+ .map((message) => ({ role: "user", content: message.content }))
812
+ : [];
813
+ const backgroundPendingMessages = promotedPendingUserMessages.length > 0
814
+ ? pending.filter((message) => message.from !== "ouro-cli")
815
+ : pending;
789
816
  // Assemble messages: session messages + pending + inbound user messages
790
817
  // NOTE: live world-state checkpoint and pending messages are rendered via buildSystem (system prompt sections)
791
818
  const extraPrefixSections = input.onPendingDrained?.(pending) ?? [];
@@ -794,11 +821,11 @@ async function handleInboundTurn(input) {
794
821
  input.messages[0] = prependTurnSections(input.messages[0], extraPrefixSections);
795
822
  }
796
823
  // Append user messages from the inbound turn
797
- for (const msg of input.messages) {
824
+ for (const msg of [...input.messages, ...promotedPendingUserMessages]) {
798
825
  (0, session_events_1.stampIngressTime)(msg);
799
826
  sessionMessages.push(msg);
800
827
  }
801
- const currentUserMessages = input.messages.filter((message) => message.role === "user");
828
+ const currentUserMessages = [...input.messages, ...promotedPendingUserMessages].filter((message) => message.role === "user");
802
829
  const orientationFrame = input.runAgentOptions?.orientationFrame
803
830
  ?? (currentUserMessages.length > 0
804
831
  ? (0, orientation_frame_1.buildOrientationFrame)({
@@ -891,8 +918,10 @@ async function handleInboundTurn(input) {
891
918
  }
892
919
  // Step 5: runAgent
893
920
  const existingToolContext = input.runAgentOptions?.toolContext;
894
- const currentUserMessage = existingToolContext?.currentUserMessage
895
- ?? latestUserAuthoredText(input.messages, input.continuityIngressTexts);
921
+ const promotedCurrentUserMessage = latestUserAuthoredText(promotedPendingUserMessages, undefined);
922
+ const currentUserMessage = promotedCurrentUserMessage
923
+ ?? existingToolContext?.currentUserMessage
924
+ ?? latestUserAuthoredText([...input.messages, ...promotedPendingUserMessages], input.continuityIngressTexts);
896
925
  let runAgentOptions = {
897
926
  ...input.runAgentOptions,
898
927
  resumePriorWork,
@@ -902,7 +931,7 @@ async function handleInboundTurn(input) {
902
931
  activeWorkFrame,
903
932
  delegationDecision,
904
933
  startOfTurnPacket: renderedStartOfTurnPacket,
905
- pendingMessages: pending.length > 0 ? pending.map((msg) => ({ from: msg.from, content: msg.content })) : undefined,
934
+ pendingMessages: backgroundPendingMessages.length > 0 ? backgroundPendingMessages.map((msg) => ({ from: msg.from, content: msg.content })) : undefined,
906
935
  currentSessionKey: currentSession.key,
907
936
  currentObligation,
908
937
  mustResolveBeforeHandoff,
@@ -15,8 +15,8 @@ function sanctuaryMediaCatalogRequiredToolCalls(request, advertisedToolNames) {
15
15
  if (!advertisedToolNames.includes("sanctuary_search_media_catalog"))
16
16
  return undefined;
17
17
  const normalized = normalizedRequest(request);
18
- const mentionsMedia = /\b(?:film|films|movie|movies|show|shows|tv|jellyfin|watch|shelf|stock|catalog|library)\b/u.test(normalized);
19
- const asksCatalog = /\b(?:have|got|stock|catalog|library|favorite|favourite|recommend|suggest|pick|watch)\b/u.test(normalized);
18
+ const mentionsMedia = /\b(?:film|films|movie|movies|show|shows|tv|jellyfin|watch|shelf|stock|catalog|library|lib)\b/u.test(normalized);
19
+ const asksCatalog = /\b(?:have|got|stock|catalog|library|lib|favorite|favourite|recommend|suggest|pick|watch|see)\b/u.test(normalized);
20
20
  const titleInventoryQuestion = /\b(?:do|did|can)\s+(?:we|you)\s+(?:have|got|stock)\s+[a-z0-9'][a-z0-9' ]*\??$/u.test(normalized);
21
21
  if ((!mentionsMedia && !titleInventoryQuestion) || !asksCatalog)
22
22
  return undefined;
@@ -29,10 +29,13 @@ function sanctuaryMediaCatalogRequiredToolCalls(request, advertisedToolNames) {
29
29
  });
30
30
  return {
31
31
  names,
32
- retryMessage: "Use sanctuary_search_media_catalog before answering. If asked for taste or a favorite, form a light recommendation from returned catalog evidence instead of claiming you cannot have preferences. Keep it honest: say you cannot watch, but you can pick from the household shelf.",
32
+ retryMessage: "Use sanctuary_search_media_catalog before answering. If a broader media-optimization read fails or degrades, treat that as a diagnostic note and still use the catalog tool for ordinary library visibility questions. If asked for taste or a favorite, form a light recommendation from returned catalog evidence instead of claiming you cannot have preferences. Keep it honest: say you cannot watch, but you can pick from the household shelf.",
33
33
  validateTerminalAnswer(answer) {
34
- return /\b(?:just|only|actually|merely)\s+(?:a\s+)?(?:bot|ai|assistant)\b|\bi don'?t actually watch\b/iu.test(answer)
35
- ? "Answer from the catalog evidence with a truthful but personable recommendation; do not retreat into 'I am just a bot' framing."
34
+ if (/\b(?:just|only|actually|merely)\s+(?:a\s+)?(?:bot|ai|assistant)\b|\bi don'?t actually watch\b/iu.test(answer)) {
35
+ return "Answer from the catalog evidence with a truthful but personable recommendation; do not retreat into 'I am just a bot' framing.";
36
+ }
37
+ return /\b(?:you'?d|you\s+would|you\s+need\s+to|please)\b.*\b(?:check|look at|poke|nudge)\b.*\b(?:log|logs|dashboard|jellyfin|unmanic)\b/iu.test(answer)
38
+ ? "Do not send Ari to check Jellyfin, Unmanic, dashboards, or logs for ordinary library visibility while safe catalog tools are available. Use the catalog evidence, or if the catalog read itself fails, say exactly what I could and could not verify."
36
39
  : undefined;
37
40
  },
38
41
  };
@@ -314,9 +314,8 @@ function createSanctuaryMediaOptimizationClient(options) {
314
314
  };
315
315
  }
316
316
  catch (error) {
317
- if (!(error instanceof ReadFailure) || error.code !== "invalid_response")
318
- throw error;
319
- history = { available: false, reason: "file_size_metrics panel returned invalid data" };
317
+ const code = error.code;
318
+ history = { available: false, reason: code === "invalid_response" ? "file_size_metrics panel returned invalid data" : "file_size_metrics panel is unavailable" };
320
319
  }
321
320
  return {
322
321
  data: {
@@ -27,6 +27,6 @@ function sanctuaryStorageOptimizationRequiredToolCalls(request, _advertisedToolN
27
27
  });
28
28
  return {
29
29
  names,
30
- 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.",
30
+ 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. If one read returns a degraded or partial result, continue with the other safe reads and bounded container/log tools before answering. Do not ask permission or send Ari to a shell, dashboard, logs, or QDirStat while these typed reads are available.",
31
31
  };
32
32
  }
@@ -418,7 +418,13 @@ async function runSenseTurn(options) {
418
418
  };
419
419
  }
420
420
  const persistedEvents = persistPromise ? await persistPromise : [];
421
- await deliverPending(terminalDeliveryKind, { throwOnError: false });
421
+ const finalDeliveryKind = terminalDeliveryKind;
422
+ if (finalDeliveryKind === "settle" && Array.isArray(turnResult.messages)) {
423
+ const settledText = extractOutwardSenseDeliveryText(turnResult.messages);
424
+ if (settledText)
425
+ pendingResponseText = settledText;
426
+ }
427
+ await deliverPending(finalDeliveryKind, { throwOnError: false });
422
428
  const ponderDeferred = false;
423
429
  // Build response
424
430
  let finalResponse;
@@ -67,6 +67,7 @@ const sanctuary_runtime_1 = require("./sanctuary-runtime");
67
67
  const sanctuary_sab_1 = require("./sanctuary-sab");
68
68
  const sanctuary_download_credit_presentation_1 = require("./sanctuary-download-credit-presentation");
69
69
  const shared_turn_1 = require("./shared-turn");
70
+ const config_1 = require("../heart/config");
70
71
  const telegram_client_1 = require("./telegram-client");
71
72
  const sanctuary_runtime_2 = require("./sanctuary-runtime");
72
73
  const sanctuary_full_visibility_contract_1 = require("./sanctuary-full-visibility-contract");
@@ -1015,19 +1016,20 @@ function createTelegramSenseApp(options) {
1015
1016
  const deliverAwaitFollowUp = async (request) => {
1016
1017
  if (!request.requestId)
1017
1018
  return { status: "blocked", detail: "Telegram follow-up is missing its request binding" };
1019
+ const sessionKey = (0, config_1.canonicalizeTelegramSessionKey)(request.key);
1018
1020
  try {
1019
- const obligation = (0, obligations_1.findPendingObligationForRequest)(agentRoot, { requestId: request.requestId, owedTo: { friendId: request.friendId, channel: "telegram", key: request.key } });
1021
+ const obligation = (0, obligations_1.findPendingObligationForRequest)(agentRoot, { requestId: request.requestId, owedTo: { friendId: request.friendId, channel: "telegram", key: sessionKey } });
1020
1022
  if (obligation && !obligation.returnReadyAt)
1021
1023
  (0, obligations_1.markObligationReturnReady)(agentRoot, obligation.id, request.deliveryId ?? `telegram-return:${request.requestId}`);
1022
1024
  const expiry = request.deliveryId?.endsWith(":expired") === true;
1023
1025
  const artifact = await executeAuthorizedEffect({
1024
1026
  idempotencyKey: `await-${expiry ? "expiry-" : ""}follow-up:${request.requestId}:${(0, node_crypto_1.createHash)("sha256").update(request.deliveryId ?? request.content).digest("hex")}`,
1025
- target: { kind: "approved_relationship", friendId: request.friendId, sessionKey: request.key, requestId: request.requestId },
1027
+ target: { kind: "approved_relationship", friendId: request.friendId, sessionKey, requestId: request.requestId },
1026
1028
  authorClass: "butler",
1027
1029
  effect: { kind: "text", text: request.content },
1028
1030
  ...(obligation ? { obligationReturnId: obligation.id } : {}),
1029
1031
  });
1030
- await recordAcceptedEffects((0, shared_turn_1.getSenseSessionPath)(options.agentName, request.friendId, "telegram", request.key, agentRoot), [artifact]);
1032
+ await recordAcceptedEffects((0, shared_turn_1.getSenseSessionPath)(options.agentName, request.friendId, "telegram", sessionKey, agentRoot), [artifact]);
1031
1033
  return { status: "delivered_now", detail: "sent to the exact request-bound Telegram chat" };
1032
1034
  }
1033
1035
  catch (error) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.790",
3
+ "version": "0.1.0-alpha.792",
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.790",
9
+ "version": "0.1.0-alpha.792",
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.790",
3
+ "version": "0.1.0-alpha.792",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },