@ouro.bot/cli 0.1.0-alpha.750 → 0.1.0-alpha.752

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.
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.MAX_TELEGRAM_ATTACHMENT_BYTES = void 0;
37
+ exports.ingestTelegramAttachments = ingestTelegramAttachments;
38
+ const fs = __importStar(require("node:fs/promises"));
39
+ const path = __importStar(require("node:path"));
40
+ const node_crypto_1 = require("node:crypto");
41
+ const store_1 = require("../heart/attachments/store");
42
+ const store_2 = require("../heart/attachments/store");
43
+ const originals_1 = require("../heart/attachments/originals");
44
+ const originals_2 = require("../heart/attachments/originals");
45
+ const telegram_1 = require("../heart/attachments/sources/telegram");
46
+ const runtime_1 = require("../nerves/runtime");
47
+ exports.MAX_TELEGRAM_ATTACHMENT_BYTES = 20_000_000;
48
+ const MAX_TELEGRAM_ATTACHMENTS_PER_MESSAGE = 8;
49
+ const SAFE_FILE_PATH = /^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))[A-Za-z0-9._/-]{1,512}$/u;
50
+ async function readBounded(response, limit) {
51
+ const advertised = Number(response.headers.get("content-length"));
52
+ if (Number.isFinite(advertised) && advertised > limit)
53
+ throw new Error("advertised size exceeds limit");
54
+ if (!response.body)
55
+ throw new Error("empty response body");
56
+ const chunks = [];
57
+ let total = 0;
58
+ const reader = response.body.getReader();
59
+ try {
60
+ for (;;) {
61
+ const { done, value } = await reader.read();
62
+ if (done)
63
+ break;
64
+ total += value.byteLength;
65
+ if (total > limit)
66
+ throw new Error("download exceeds limit");
67
+ chunks.push(value);
68
+ }
69
+ }
70
+ finally {
71
+ await reader.cancel().catch(() => undefined);
72
+ }
73
+ return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), total);
74
+ }
75
+ async function atomicWrite(filePath, buffer) {
76
+ const directory = path.dirname(filePath);
77
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
78
+ const temporary = `${filePath}.${(0, node_crypto_1.randomUUID)()}.tmp`;
79
+ let handle;
80
+ try {
81
+ handle = await fs.open(temporary, "wx", 0o600);
82
+ await handle.writeFile(buffer);
83
+ await handle.sync();
84
+ await handle.close();
85
+ handle = undefined;
86
+ await fs.rename(temporary, filePath);
87
+ const directoryHandle = await fs.open(directory, "r");
88
+ try {
89
+ await directoryHandle.sync();
90
+ }
91
+ finally {
92
+ await directoryHandle.close();
93
+ }
94
+ }
95
+ finally {
96
+ await handle?.close().catch(() => undefined);
97
+ await fs.unlink(temporary).catch((error) => { if (error.code !== "ENOENT")
98
+ throw error; });
99
+ }
100
+ }
101
+ async function ingestTelegramAttachments(input) {
102
+ const fetchImpl = input.fetch ?? globalThis.fetch;
103
+ const unique = [...new Map(input.attachments.map((attachment) => [attachment.fileId, attachment])).values()]
104
+ .slice(0, MAX_TELEGRAM_ATTACHMENTS_PER_MESSAGE);
105
+ const attachments = [];
106
+ const notices = [];
107
+ for (const candidate of unique) {
108
+ try {
109
+ if (!/^[A-Za-z0-9_-]{1,512}$/u.test(candidate.fileId))
110
+ throw new Error("invalid file id");
111
+ if (candidate.byteCount !== undefined && (!Number.isSafeInteger(candidate.byteCount) || candidate.byteCount < 0 || candidate.byteCount > exports.MAX_TELEGRAM_ATTACHMENT_BYTES))
112
+ throw new Error("advertised size exceeds limit");
113
+ const cached = (0, store_2.getRecentAttachment)(input.agentName, `attachment:telegram:${candidate.fileId}`, input.agentRoot);
114
+ if (cached?.source === "telegram") {
115
+ const localPath = cached.sourceData.localPath;
116
+ if (typeof localPath === "string") {
117
+ await (0, originals_2.ensureReadableFile)(localPath);
118
+ attachments.push(cached);
119
+ continue;
120
+ }
121
+ }
122
+ const recoverable = (0, telegram_1.buildTelegramAttachmentRecord)({
123
+ fileId: candidate.fileId,
124
+ displayName: candidate.displayName,
125
+ mimeType: candidate.mimeType,
126
+ byteCount: candidate.byteCount,
127
+ localPath: path.resolve(input.agentRoot, "state", "attachments", ".pending", candidate.fileId),
128
+ });
129
+ const recoverablePath = (0, originals_1.originalStoragePath)(input.agentRoot, recoverable, recoverable.mimeType);
130
+ try {
131
+ const stat = await fs.lstat(recoverablePath);
132
+ if (!stat.isFile() || stat.size > exports.MAX_TELEGRAM_ATTACHMENT_BYTES)
133
+ throw new Error("persisted attachment is invalid");
134
+ const recovered = (0, store_1.cacheRecentAttachment)(input.agentName, (0, telegram_1.buildTelegramAttachmentRecord)({
135
+ fileId: candidate.fileId,
136
+ displayName: candidate.displayName,
137
+ mimeType: candidate.mimeType,
138
+ byteCount: stat.size,
139
+ localPath: recoverablePath,
140
+ }), input.agentRoot);
141
+ attachments.push(recovered);
142
+ continue;
143
+ }
144
+ catch (error) {
145
+ if (error.code !== "ENOENT")
146
+ throw error;
147
+ }
148
+ const remote = await input.api.request("getFile", { file_id: candidate.fileId });
149
+ if (typeof remote.file_path !== "string" || !SAFE_FILE_PATH.test(remote.file_path))
150
+ throw new Error("invalid remote path");
151
+ if (remote.file_size !== undefined && (!Number.isSafeInteger(remote.file_size) || remote.file_size < 0 || remote.file_size > exports.MAX_TELEGRAM_ATTACHMENT_BYTES))
152
+ throw new Error("advertised size exceeds limit");
153
+ const response = await fetchImpl(`https://api.telegram.org/file/bot${input.botToken}/${remote.file_path}`, { signal: AbortSignal.timeout(30_000) });
154
+ if (!response.ok)
155
+ throw new Error(`download failed with HTTP ${response.status}`);
156
+ const buffer = await readBounded(response, exports.MAX_TELEGRAM_ATTACHMENT_BYTES);
157
+ const provisional = (0, telegram_1.buildTelegramAttachmentRecord)({
158
+ fileId: candidate.fileId,
159
+ displayName: candidate.displayName,
160
+ mimeType: candidate.mimeType ?? response.headers.get("content-type") ?? undefined,
161
+ byteCount: buffer.length,
162
+ localPath: path.resolve(input.agentRoot, "state", "attachments", ".pending", candidate.fileId),
163
+ });
164
+ const localPath = (0, originals_1.originalStoragePath)(input.agentRoot, provisional, provisional.mimeType);
165
+ await atomicWrite(localPath, buffer);
166
+ const record = (0, store_1.cacheRecentAttachment)(input.agentName, (0, telegram_1.buildTelegramAttachmentRecord)({
167
+ fileId: candidate.fileId,
168
+ displayName: candidate.displayName,
169
+ mimeType: provisional.mimeType,
170
+ byteCount: buffer.length,
171
+ localPath,
172
+ }), input.agentRoot);
173
+ attachments.push(record);
174
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.telegram_attachment_ingested", message: "Telegram attachment entered the shared attachment store", meta: { kind: record.kind, byteCount: buffer.length } });
175
+ }
176
+ catch (error) {
177
+ notices.push(`attachment unavailable: ${candidate.displayName}`);
178
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.telegram_attachment_ingest_error", message: "Telegram attachment ingestion failed", meta: { kind: candidate.kind, reason: error instanceof Error ? error.name : "unknown" } });
179
+ }
180
+ }
181
+ return { attachments, notices };
182
+ }
@@ -479,18 +479,56 @@ function createTelegramLongPoll(options) {
479
479
  reject(signal.reason);
480
480
  }, { once: true });
481
481
  });
482
+ const inboundAttachments = (message) => {
483
+ const result = [];
484
+ const push = (media, kind, fallbackName, fallbackMimeType) => {
485
+ if (typeof media?.file_id !== "string" || !media.file_id.trim())
486
+ return;
487
+ result.push({
488
+ fileId: media.file_id,
489
+ kind,
490
+ displayName: media.file_name?.trim() || fallbackName,
491
+ ...((media.mime_type?.trim() || fallbackMimeType) ? { mimeType: media.mime_type?.trim().toLowerCase() || fallbackMimeType } : {}),
492
+ ...(Number.isSafeInteger(media.file_size) ? { byteCount: media.file_size } : {}),
493
+ });
494
+ };
495
+ const photo = message.photo?.filter((candidate) => typeof candidate.file_id === "string" && candidate.file_id.trim())
496
+ .sort((left, right) => (left.file_size ?? left.width ?? 0) - (right.file_size ?? right.width ?? 0)).at(-1);
497
+ if (photo)
498
+ result.push({ fileId: photo.file_id, kind: "image", displayName: "telegram-photo.jpg", mimeType: "image/jpeg", ...(Number.isSafeInteger(photo.file_size) ? { byteCount: photo.file_size } : {}) });
499
+ push(message.document, "document", "telegram-document");
500
+ push(message.audio, "audio", "telegram-audio", "audio/mpeg");
501
+ push(message.voice, "audio", "telegram-voice.ogg", "audio/ogg");
502
+ push(message.video, "binary", "telegram-video.mp4", "video/mp4");
503
+ push(message.animation, "binary", "telegram-animation.mp4", "video/mp4");
504
+ push(message.sticker, message.sticker?.is_animated || message.sticker?.is_video ? "binary" : "image", message.sticker?.is_animated ? "telegram-sticker.tgs" : message.sticker?.is_video ? "telegram-sticker.webm" : "telegram-sticker.webp", message.sticker?.is_animated ? "application/x-tgsticker" : message.sticker?.is_video ? "video/webm" : "image/webp");
505
+ return result;
506
+ };
507
+ const rawAttachmentCount = (message) => [
508
+ message.document, message.audio, message.video, message.voice, message.animation, message.sticker,
509
+ ].filter(Boolean).length + (message.photo?.length ? 1 : 0);
482
510
  const authorizedMessage = (update) => {
483
511
  const message = update.message;
484
512
  const userId = message?.from ? String(message.from.id) : "";
485
513
  const chatId = message ? String(message.chat.id) : "";
486
- if (!message || message.chat.type !== "private" || userId !== options.expectedUserId || chatId !== options.expectedChatId || typeof message.text !== "string")
514
+ if (!message || message.chat.type !== "private" || userId !== options.expectedUserId || chatId !== options.expectedChatId)
515
+ return null;
516
+ const attachments = inboundAttachments(message);
517
+ const attachmentCount = rawAttachmentCount(message);
518
+ const attachmentNotices = attachmentCount > attachments.length
519
+ ? ["attachment unavailable: Telegram media metadata was incomplete"]
520
+ : [];
521
+ const text = message.text ?? message.caption ?? "";
522
+ if (typeof text !== "string" || (!text && attachmentCount === 0))
487
523
  return null;
488
524
  return {
489
525
  updateId: update.update_id,
490
526
  messageId: String(message.message_id),
491
527
  userId,
492
528
  chatId,
493
- text: message.text,
529
+ text,
530
+ attachments,
531
+ ...(attachmentNotices.length ? { attachmentNotices } : {}),
494
532
  ...(Number.isSafeInteger(message.reply_to_message?.message_id) && message.reply_to_message.message_id > 0
495
533
  ? { replyToMessageId: String(message.reply_to_message.message_id) }
496
534
  : {}),
@@ -506,6 +544,8 @@ function createTelegramLongPoll(options) {
506
544
  return null;
507
545
  const displayLabel = [message.from.first_name, message.from.last_name].filter(Boolean).join(" ")
508
546
  || (message.from.username ? `@${message.from.username}` : `Telegram user ${userId}`);
547
+ const attachments = inboundAttachments(message);
548
+ const attachmentCount = rawAttachmentCount(message);
509
549
  return {
510
550
  updateId: update.update_id,
511
551
  messageId: message.message_id,
@@ -514,8 +554,9 @@ function createTelegramLongPoll(options) {
514
554
  chatId,
515
555
  text: message.text ?? message.caption ?? "",
516
556
  displayLabel,
517
- hasAttachments: Boolean(message.document || message.photo?.length || message.audio || message.video
518
- || message.voice || message.animation || message.sticker),
557
+ hasAttachments: attachmentCount > 0,
558
+ attachments,
559
+ ...(attachmentCount > attachments.length ? { attachmentNotices: ["attachment unavailable: Telegram media metadata was incomplete"] } : {}),
519
560
  };
520
561
  };
521
562
  const authorizedCallback = (update) => {
@@ -676,7 +676,7 @@ function appendTelegramInboundEvent(envelope, input) {
676
676
  name: "telegram-user",
677
677
  toolCallId: null,
678
678
  toolCalls: [],
679
- attachments: [],
679
+ attachments: [...new Set(input.attachmentIds ?? [])],
680
680
  time: { authoredAt: null, authoredAtSource: "unknown", observedAt: input.recordedAt, observedAtSource: "ingest", recordedAt: input.recordedAt, recordedAtSource: "save" },
681
681
  relations: {
682
682
  replyToEventId: input.relations?.replyToEventId ?? null,
@@ -710,14 +710,16 @@ async function recordTelegramEffectsInSession(input) {
710
710
  let changed = false;
711
711
  if (input.inbound) {
712
712
  const existing = envelope.events.filter((event) => event.relations.references.includes(input.inbound.reference));
713
- if (existing.length > 1 || (existing.length === 1 && (existing[0].role !== "user" || existing[0].content !== input.inbound.text))) {
713
+ const expectedAttachmentIds = [...new Set(input.inbound.attachmentIds ?? [])];
714
+ if (existing.length > 1 || (existing.length === 1 && (existing[0].role !== "user" || existing[0].content !== input.inbound.text
715
+ || JSON.stringify(existing[0].attachments) !== JSON.stringify(expectedAttachmentIds)))) {
714
716
  throw new Error("Telegram session has conflicting inbound ingress");
715
717
  }
716
718
  if (existing.length === 1) {
717
719
  inboundReceipt = { eventId: existing[0].id, reference: input.inbound.reference };
718
720
  }
719
721
  else {
720
- envelope = appendTelegramInboundEvent(envelope, { ...input.inbound, recordedAt: new Date().toISOString() });
722
+ envelope = appendTelegramInboundEvent(envelope, { ...input.inbound, attachmentIds: expectedAttachmentIds, recordedAt: new Date().toISOString() });
721
723
  inboundReceipt = { eventId: envelope.events.at(-1).id, reference: input.inbound.reference };
722
724
  changed = true;
723
725
  }
@@ -78,6 +78,8 @@ const approval_store_1 = require("../heart/approval-store");
78
78
  const tools_awaiting_1 = require("../repertoire/tools-awaiting");
79
79
  const obligations_1 = require("../arc/obligations");
80
80
  const orientation_frame_1 = require("../heart/orientation-frame");
81
+ const render_1 = require("../heart/attachments/render");
82
+ const telegram_attachments_1 = require("./telegram-attachments");
81
83
  const pending_1 = require("../mind/pending");
82
84
  const relationship_authorization_1 = require("../repertoire/relationship-authorization");
83
85
  const telegram_admission_1 = require("./telegram-admission");
@@ -1094,9 +1096,11 @@ function createTelegramSenseApp(options) {
1094
1096
  source: {
1095
1097
  kind: "telegram_newly_admitted",
1096
1098
  authority: "presentation_only",
1097
- routingHint: input.orientation.attachmentsNeedResend
1098
- ? "This is this person’s first admitted turn. Welcome them warmly and briefly explain what the household Butler can help with before answering their request. Their original message included attachments that were not downloaded; ask them to resend those attachments now."
1099
- : "This is this person’s first admitted turn. Welcome them warmly and briefly explain what the household Butler can help with before answering their request.",
1099
+ routingHint: [
1100
+ "This is this person’s first admitted turn. Welcome them warmly and briefly explain what the household Butler can help with before answering their request.",
1101
+ input.orientation.relationship ? `The owner identified this person as their ${input.orientation.relationship}.` : null,
1102
+ input.orientation.attachmentsNeedResend ? "Their original message included attachments that were not downloaded; ask them to resend those attachments now." : null,
1103
+ ].filter(Boolean).join(" "),
1100
1104
  },
1101
1105
  }
1102
1106
  : undefined;
@@ -1110,7 +1114,7 @@ function createTelegramSenseApp(options) {
1110
1114
  ingressRelations: { replyToEventId: null, threadRootEventId: null, references: [input.reference] },
1111
1115
  precommittedIngress: { eventId: input.eventId, reference: input.reference },
1112
1116
  ...(orientationFrame ? { orientationFrame } : {}),
1113
- toolContext: { ...(toolContext ?? {}) },
1117
+ toolContext: { ...(toolContext ?? {}), attachmentIds: input.attachmentIds ?? [] },
1114
1118
  prepareRunAgentOptions: prepareRelationshipRunAgentOptions({ friendId: input.friendId, requestId: input.requestId,
1115
1119
  sessionEventId: input.eventId, userId: input.userId, chatId: input.chatId, sessionKey: input.sessionKey }),
1116
1120
  deliverySink: { onDelivery: (delivery) => deliver(delivery.text, delivery.kind === "settle") },
@@ -1202,7 +1206,26 @@ function createTelegramSenseApp(options) {
1202
1206
  await recordAcceptedEffects((0, shared_turn_1.getSenseSessionPath)(options.agentName, target.friendId, "telegram", target.sessionKey, agentRoot), [artifact]);
1203
1207
  }
1204
1208
  };
1205
- const onMessageBody = async (message) => {
1209
+ const hydrateAuthorizedMessage = async (message) => {
1210
+ if (!message.attachments?.length)
1211
+ return { text: [message.text, ...(message.attachmentNotices ?? [])].filter(Boolean).join("\n"), attachmentIds: [] };
1212
+ const ingested = await (0, telegram_attachments_1.ingestTelegramAttachments)({
1213
+ agentName: options.agentName,
1214
+ agentRoot,
1215
+ botToken,
1216
+ api,
1217
+ ...(options.attachmentFetch ? { fetch: options.attachmentFetch } : {}),
1218
+ attachments: message.attachments ?? [],
1219
+ });
1220
+ const attachmentIds = ingested.attachments.map((attachment) => attachment.id);
1221
+ return {
1222
+ text: [message.text, (0, render_1.renderAttachmentBlock)(ingested.attachments), ...(message.attachmentNotices ?? []), ...ingested.notices].filter(Boolean).join("\n"),
1223
+ attachmentIds,
1224
+ };
1225
+ };
1226
+ const onMessageBody = async (rawMessage) => {
1227
+ const hydrated = await hydrateAuthorizedMessage(rawMessage);
1228
+ const message = { ...rawMessage, text: hydrated.text };
1206
1229
  const currentFriendId = configuredOwnerFriendId;
1207
1230
  const currentSessionKey = configuredOwnerSessionKey;
1208
1231
  const currentSessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, currentFriendId, "telegram", currentSessionKey, agentRoot);
@@ -1268,7 +1291,7 @@ function createTelegramSenseApp(options) {
1268
1291
  store: getEffectJournal(),
1269
1292
  sessionPath: currentSessionPath,
1270
1293
  artifacts: [],
1271
- inbound: { text: message.text, reference: inboundReference, relations: ingressRelations },
1294
+ inbound: { text: message.text, reference: inboundReference, attachmentIds: hydrated.attachmentIds, relations: ingressRelations },
1272
1295
  })
1273
1296
  : null;
1274
1297
  const collected = await collectToolReceipts(() => runTurn({
@@ -1297,7 +1320,9 @@ function createTelegramSenseApp(options) {
1297
1320
  }
1298
1321
  },
1299
1322
  },
1300
- ...(toolContext ? { toolContext } : {}),
1323
+ ...(toolContext || options.resolveRelationshipAuthorization ? {
1324
+ toolContext: { ...(toolContext ?? {}), ...(options.resolveRelationshipAuthorization ? { attachmentIds: hydrated.attachmentIds } : {}) },
1325
+ } : {}),
1301
1326
  ...(options.resolveRelationshipAuthorization ? {
1302
1327
  prepareRunAgentOptions: prepareRelationshipRunAgentOptions({
1303
1328
  friendId: currentFriendId,
@@ -1369,7 +1394,7 @@ function createTelegramSenseApp(options) {
1369
1394
  const fallback = "I couldn't complete that turn. The failure was recorded; please try again.";
1370
1395
  if (deliveredMessageIds.length === 0)
1371
1396
  turnEffects.push(await deliverButlerEffect(fallback, `turn:${subject}:${message.updateId}:fallback`, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); }));
1372
- await recordAcceptedEffects(currentSessionPath, turnEffects, { text: message.text, reference: inboundReference });
1397
+ await recordAcceptedEffects(currentSessionPath, turnEffects, { text: message.text, reference: inboundReference, attachmentIds: hydrated.attachmentIds });
1373
1398
  }
1374
1399
  finally {
1375
1400
  if (acceptanceMarker) {
@@ -1412,10 +1437,10 @@ function createTelegramSenseApp(options) {
1412
1437
  }
1413
1438
  };
1414
1439
  const onMessage = (message) => runWithAcceptanceAuditOwner(async () => {
1415
- const ownerDecision = admissionController?.parseOwnerDecision({
1440
+ const ownerDecision = !message.attachments?.length && !message.attachmentNotices?.length ? admissionController?.parseOwnerDecision({
1416
1441
  text: message.text,
1417
1442
  ...(message.replyToMessageId ? { replyToMessageId: Number(message.replyToMessageId) } : {}),
1418
- });
1443
+ }) : null;
1419
1444
  if (ownerDecision) {
1420
1445
  const actor = await options.admission?.resolveOwner({ botId: botId, userId: message.userId, chatId: message.chatId, sessionKey: configuredOwnerSessionKey });
1421
1446
  if (!actor)
@@ -1423,6 +1448,11 @@ function createTelegramSenseApp(options) {
1423
1448
  await admissionController.decide({ ...ownerDecision, actorFriendId: actor.friendId });
1424
1449
  return;
1425
1450
  }
1451
+ if ((message.attachments?.length || message.attachmentNotices?.length) && options.admission) {
1452
+ const actor = await options.admission.resolveOwner({ botId: botId, userId: message.userId, chatId: message.chatId, sessionKey: configuredOwnerSessionKey });
1453
+ if (!actor || actor.friendId !== configuredOwnerFriendId)
1454
+ throw new Error("Telegram attachment owner relationship is not active");
1455
+ }
1426
1456
  await onMessageBody(message);
1427
1457
  });
1428
1458
  const onUnknownMessage = admissionController && options.admission ? async (message) => {
@@ -1431,12 +1461,21 @@ function createTelegramSenseApp(options) {
1431
1461
  await admissionController.handleUnknown(message);
1432
1462
  return;
1433
1463
  }
1464
+ const hydrated = await hydrateAuthorizedMessage({
1465
+ updateId: message.updateId,
1466
+ messageId: String(message.messageId),
1467
+ userId: message.userId,
1468
+ chatId: message.chatId,
1469
+ text: message.text,
1470
+ attachments: message.attachments,
1471
+ attachmentNotices: message.attachmentNotices,
1472
+ });
1434
1473
  const sessionKey = `telegram:${message.botId}:${message.userId}`;
1435
1474
  const reference = `telegram-inbound:${(0, node_crypto_1.createHmac)("sha256", identityKey).update(`${message.botId}\0${message.userId}\0${message.updateId}\0${message.messageId}`).digest("hex")}`;
1436
1475
  const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, approved.friendId, "telegram", sessionKey, agentRoot);
1437
- const receipt = await (0, telegram_effect_adapter_1.recordTelegramEffectsInSession)({ store: getEffectJournal(), sessionPath, artifacts: [], inbound: { text: message.text, reference } });
1476
+ const receipt = await (0, telegram_effect_adapter_1.recordTelegramEffectsInSession)({ store: getEffectJournal(), sessionPath, artifacts: [], inbound: { text: hydrated.text, reference, attachmentIds: hydrated.attachmentIds } });
1438
1477
  await dispatchResolvedRelationshipTurn({ friendId: approved.friendId, sessionKey, requestId: reference, eventId: receipt.eventId,
1439
- reference, text: message.text, userId: message.userId, chatId: message.chatId });
1478
+ reference, text: hydrated.text, attachmentIds: hydrated.attachmentIds, userId: message.userId, chatId: message.chatId });
1440
1479
  } : undefined;
1441
1480
  const onUpdate = async (update) => {
1442
1481
  const callback = update.callback_query;
@@ -1812,8 +1851,14 @@ async function createProductionTelegramRelationshipComposition(agentName, creden
1812
1851
  target: { kind: "create", record: { id: (0, node_crypto_1.randomUUID)(), name: "Household member" } },
1813
1852
  });
1814
1853
  const current = claimed.record;
1815
- const updated = { ...current, trustLevel: input.defaults.trustLevel, admissionState: input.defaults.admissionState,
1816
- initiativePolicy: input.defaults.initiativePolicy, capabilityProfileId: input.defaults.capabilityProfileId, updatedAt: new Date().toISOString() };
1854
+ const now = new Date().toISOString();
1855
+ const connections = input.relationship
1856
+ ? [{ name: owner.name, relationship: input.relationship }, ...(current.connections ?? []).filter((connection) => !(connection.name === owner.name && connection.relationship === input.relationship))]
1857
+ : current.connections;
1858
+ const updated = { ...current,
1859
+ ...(connections ? { connections } : {}),
1860
+ trustLevel: input.defaults.trustLevel, admissionState: input.defaults.admissionState,
1861
+ initiativePolicy: input.defaults.initiativePolicy, capabilityProfileId: input.defaults.capabilityProfileId, updatedAt: now };
1817
1862
  await store.put(current.id, updated);
1818
1863
  return { kind: claimed.status === "created" ? "created" : "existing", friendId: current.id };
1819
1864
  },
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.750",
3
+ "version": "0.1.0-alpha.752",
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.750",
9
+ "version": "0.1.0-alpha.752",
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.750",
3
+ "version": "0.1.0-alpha.752",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },