@ouro.bot/cli 0.1.0-alpha.751 → 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.
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.752",
6
+ "changes": [
7
+ "Make Telegram attachments usable and relationship-private for the Sanctuary Butler.",
8
+ "Preserve owner-confirmed household kinship without expanding authority.",
9
+ "Accept verified routine-restart policy grants through the typed boolean contract."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.751",
6
14
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.751",
2
+ "runtimeVersion": "0.1.0-alpha.752",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-08-30T00:00:00.000Z"
5
5
  }
@@ -2,15 +2,15 @@
2
2
  "version": 2,
3
3
  "profiles": {
4
4
  "sanctuary-owner": {
5
- "version": 5,
5
+ "version": 6,
6
6
  "contextScopes": ["household.status", "household.policy", "household.private"],
7
- "toolNames": ["external_event_disposition", "query_active_work", "save_friend_note", "telegram_contact_manage", "query_cares", "care_manage", "await_condition", "resolve_await", "cancel_await", "unraid_list_containers", "unraid_get_container_logs", "unraid_get_storage", "sanctuary_get_media_optimization", "unraid_get_disks", "unraid_get_notifications", "unraid_get_system", "unraid_check_services", "sanctuary_get_download_queue", "sanctuary_resume_download_queue", "unraid_restart_container", "steward_policy_manage", "send_message", "ponder", "rest", "settle", "speak"],
7
+ "toolNames": ["external_event_disposition", "query_active_work", "save_friend_note", "telegram_contact_manage", "query_cares", "care_manage", "await_condition", "resolve_await", "cancel_await", "list_recent_attachments", "materialize_attachment", "describe_image", "unraid_list_containers", "unraid_get_container_logs", "unraid_get_storage", "sanctuary_get_media_optimization", "unraid_get_disks", "unraid_get_notifications", "unraid_get_system", "unraid_check_services", "sanctuary_get_download_queue", "sanctuary_resume_download_queue", "unraid_restart_container", "steward_policy_manage", "send_message", "ponder", "rest", "settle", "speak"],
8
8
  "effectScopes": ["telegram.proactive", "telegram.request_return", "telegram.owner_event"]
9
9
  },
10
10
  "sanctuary-household": {
11
- "version": 4,
11
+ "version": 5,
12
12
  "contextScopes": ["household.status", "own_requests"],
13
- "toolNames": ["save_friend_note", "await_condition", "resolve_await", "cancel_await", "unraid_list_containers", "unraid_get_storage", "unraid_get_disks", "unraid_get_system", "unraid_check_services", "unraid_restart_container", "settle", "speak"],
13
+ "toolNames": ["save_friend_note", "await_condition", "resolve_await", "cancel_await", "list_recent_attachments", "materialize_attachment", "describe_image", "unraid_list_containers", "unraid_get_storage", "unraid_get_disks", "unraid_get_system", "unraid_check_services", "unraid_restart_container", "settle", "speak"],
14
14
  "effectScopes": ["telegram.request_return"]
15
15
  },
16
16
  "sanctuary-event": {
@@ -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.751</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.752</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -3,9 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getAttachmentSourceAdapter = getAttachmentSourceAdapter;
4
4
  const bluebubbles_1 = require("./bluebubbles");
5
5
  const cli_local_file_1 = require("./cli-local-file");
6
+ const telegram_1 = require("./telegram");
6
7
  const ATTACHMENT_SOURCE_ADAPTERS = {
7
8
  bluebubbles: bluebubbles_1.blueBubblesAttachmentSourceAdapter,
8
9
  "cli-local-file": cli_local_file_1.cliLocalFileAttachmentSourceAdapter,
10
+ telegram: telegram_1.telegramAttachmentSourceAdapter,
9
11
  };
10
12
  function getAttachmentSourceAdapter(source) {
11
13
  const adapter = ATTACHMENT_SOURCE_ADAPTERS[source];
@@ -0,0 +1,71 @@
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.telegramAttachmentSourceAdapter = void 0;
37
+ exports.buildTelegramAttachmentRecord = buildTelegramAttachmentRecord;
38
+ const path = __importStar(require("node:path"));
39
+ const originals_1 = require("../originals");
40
+ const types_1 = require("../types");
41
+ const runtime_1 = require("../../../nerves/runtime");
42
+ function buildTelegramAttachmentRecord(input, now = Date.now()) {
43
+ if (!input.fileId.trim())
44
+ throw new Error("Telegram attachment file id is required");
45
+ if (!path.isAbsolute(input.localPath))
46
+ throw new Error("Telegram attachment local path must be absolute");
47
+ (0, runtime_1.emitNervesEvent)({ component: "engine", event: "engine.telegram_attachment_record_built", message: "Telegram attachment record entered the shared attachment substrate", meta: {} });
48
+ return (0, types_1.createAttachmentRecord)({
49
+ source: "telegram",
50
+ sourceId: input.fileId,
51
+ displayName: input.displayName,
52
+ mimeType: input.mimeType,
53
+ byteCount: input.byteCount,
54
+ sourceData: { localPath: path.resolve(input.localPath) },
55
+ }, now);
56
+ }
57
+ exports.telegramAttachmentSourceAdapter = {
58
+ source: "telegram",
59
+ async materializeOriginal({ attachment, agentRoot }) {
60
+ if (attachment.source !== "telegram")
61
+ throw new Error(`telegram adapter cannot materialize ${attachment.source} attachments`);
62
+ const localPath = attachment.sourceData.localPath;
63
+ if (typeof localPath !== "string" || !path.isAbsolute(localPath))
64
+ throw new Error("Telegram attachment local path is invalid");
65
+ const ownedRoot = path.resolve(agentRoot, "state", "attachments", "materialized", "telegram");
66
+ if (!path.resolve(localPath).startsWith(`${ownedRoot}${path.sep}`))
67
+ throw new Error("Telegram attachment local path is outside the owned attachment root");
68
+ await (0, originals_1.ensureReadableFile)(localPath);
69
+ return (0, originals_1.buildOriginalMaterializedAttachment)(attachment, localPath);
70
+ },
71
+ };
@@ -41,6 +41,7 @@ exports.getRecentAttachment = getRecentAttachment;
41
41
  exports.listRecentAttachments = listRecentAttachments;
42
42
  const fs = __importStar(require("node:fs"));
43
43
  const path = __importStar(require("node:path"));
44
+ const node_crypto_1 = require("node:crypto");
44
45
  const runtime_1 = require("../../nerves/runtime");
45
46
  const identity_1 = require("../identity");
46
47
  exports.MAX_RECENT_ATTACHMENTS = 100;
@@ -75,8 +76,36 @@ function readRecentAttachments(agentName, agentRoot = (0, identity_1.getAgentRoo
75
76
  }
76
77
  function writeRecentAttachments(agentName, agentRoot, attachments) {
77
78
  const targetPath = getRecentAttachmentsPath(agentName, agentRoot);
78
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
79
- fs.writeFileSync(targetPath, JSON.stringify(attachments, null, 2), "utf-8");
79
+ const directory = path.dirname(targetPath);
80
+ fs.mkdirSync(directory, { recursive: true });
81
+ const temporaryPath = `${targetPath}.tmp-${process.pid}-${(0, node_crypto_1.randomUUID)()}`;
82
+ try {
83
+ fs.writeFileSync(temporaryPath, JSON.stringify(attachments, null, 2), { encoding: "utf-8", mode: 0o600 });
84
+ const temporary = fs.openSync(temporaryPath, "r");
85
+ try {
86
+ fs.fsyncSync(temporary);
87
+ }
88
+ finally {
89
+ fs.closeSync(temporary);
90
+ }
91
+ fs.renameSync(temporaryPath, targetPath);
92
+ const directoryHandle = fs.openSync(directory, "r");
93
+ try {
94
+ fs.fsyncSync(directoryHandle);
95
+ }
96
+ finally {
97
+ fs.closeSync(directoryHandle);
98
+ }
99
+ }
100
+ finally {
101
+ try {
102
+ fs.unlinkSync(temporaryPath);
103
+ }
104
+ catch (error) {
105
+ if (error.code !== "ENOENT")
106
+ throw error;
107
+ }
108
+ }
80
109
  }
81
110
  function cacheRecentAttachment(agentName, attachment, agentRoot = (0, identity_1.getAgentRoot)(agentName)) {
82
111
  const deduped = readRecentAttachments(agentName, agentRoot).filter((entry) => entry.id !== attachment.id);
@@ -1279,6 +1279,11 @@ function relationshipFriendContextSection(context) {
1279
1279
  `channel: ${context.channel.channel}`,
1280
1280
  "this conversation and this friend's own relationship notes are the only personal context available in this turn.",
1281
1281
  ];
1282
+ const connection = friend.connections?.[0];
1283
+ const connectionName = connection?.name.replace(/[\r\n\t]+/gu, " ").trim().slice(0, 80);
1284
+ const relationship = connection?.relationship.replace(/[\r\n\t]+/gu, " ").trim().slice(0, 40);
1285
+ if (connectionName && relationship)
1286
+ lines.push(`relationship: ${connectionName}’s ${relationship}`);
1282
1287
  if (Object.keys(friend.notes).length > 0) {
1283
1288
  lines.push("", "## what i know about this friend");
1284
1289
  for (const [key, entry] of Object.entries(friend.notes)) {
@@ -180,13 +180,14 @@ exports.attachmentToolDefinitions = [
180
180
  },
181
181
  },
182
182
  },
183
- handler: async (args) => {
183
+ handler: async (args, ctx) => {
184
184
  const agentName = (0, identity_1.getAgentName)();
185
185
  const kind = trimArg(args.kind);
186
+ const allowed = ctx?.attachmentIds ? new Set(ctx.attachmentIds) : null;
186
187
  const attachments = (0, store_1.listRecentAttachments)(agentName, {
187
188
  kind: kind ? kind : undefined,
188
- limit: parseLimit(args.limit),
189
- });
189
+ limit: allowed ? undefined : parseLimit(args.limit),
190
+ }).filter((attachment) => !allowed || allowed.has(attachment.id)).slice(0, parseLimit(args.limit));
190
191
  return (0, tool_results_1.okToolResult)("list_recent_attachments", attachments);
191
192
  },
192
193
  summaryKeys: ["kind", "limit"],
@@ -218,11 +219,13 @@ exports.attachmentToolDefinitions = [
218
219
  },
219
220
  },
220
221
  },
221
- handler: async (args) => {
222
+ handler: async (args, ctx) => {
222
223
  const attachmentId = resolveAttachmentId(args);
223
224
  if (!attachmentId) {
224
225
  return missingAttachmentId("materialize_attachment");
225
226
  }
227
+ if (ctx?.attachmentIds && !ctx.attachmentIds.includes(attachmentId))
228
+ return attachmentNotFound("materialize_attachment", attachmentId);
226
229
  const agentName = (0, identity_1.getAgentName)();
227
230
  const variant = trimArg(args.variant) === "vision_safe" ? "vision_safe" : "original";
228
231
  try {
@@ -262,11 +265,13 @@ exports.attachmentToolDefinitions = [
262
265
  },
263
266
  },
264
267
  },
265
- handler: async (args) => {
268
+ handler: async (args, ctx) => {
266
269
  const attachmentId = resolveAttachmentId(args);
267
270
  if (!attachmentId) {
268
271
  return missingAttachmentId("describe_image");
269
272
  }
273
+ if (ctx?.attachmentIds && !ctx.attachmentIds.includes(attachmentId))
274
+ return attachmentNotFound("describe_image", attachmentId);
270
275
  const prompt = trimArg(args.prompt);
271
276
  if (!prompt) {
272
277
  return missingPrompt();
@@ -63,15 +63,20 @@ exports.stewardPolicyToolDefinition = {
63
63
  else if (args.action === "grant_routine_action") {
64
64
  if (args.provenance !== "stated" && args.provenance !== "installed_explicit_policy")
65
65
  throw new Error("routine action provenance is invalid");
66
+ const targets = arrayArgument(args.targetsJson, "targetsJson");
67
+ const exclusions = arrayArgument(args.exclusionsJson, "exclusionsJson");
68
+ const verificationRequired = args.verificationRequired;
69
+ if (verificationRequired !== true)
70
+ throw new Error("verificationRequired must be true");
66
71
  mutation = {
67
72
  kind: "grant_routine_action",
68
73
  key: args.key ?? "",
69
74
  action: args.routineAction ?? "",
70
- targets: arrayArgument(args.targetsJson, "targetsJson"),
75
+ targets,
71
76
  maxCount: Number(args.maxCount),
72
77
  windowMs: Number(args.windowMs),
73
- verificationRequired: args.verificationRequired === "true",
74
- exclusions: arrayArgument(args.exclusionsJson, "exclusionsJson"),
78
+ verificationRequired,
79
+ exclusions,
75
80
  provenance: args.provenance,
76
81
  ...(args.expiresAt ? { expiresAt: args.expiresAt } : {}),
77
82
  };
@@ -58,6 +58,9 @@ const SANCTUARY_RELATIONSHIP_BASE_TOOLS = new Set([
58
58
  "await_condition",
59
59
  "resolve_await",
60
60
  "cancel_await",
61
+ "list_recent_attachments",
62
+ "materialize_attachment",
63
+ "describe_image",
61
64
  "send_message",
62
65
  ]);
63
66
  const SANCTUARY_TELEGRAM_SURFACE = new Set([
@@ -69,6 +72,9 @@ const SANCTUARY_TELEGRAM_SURFACE = new Set([
69
72
  "await_condition",
70
73
  "resolve_await",
71
74
  "cancel_await",
75
+ "list_recent_attachments",
76
+ "materialize_attachment",
77
+ "describe_image",
72
78
  ...tools_unraid_1.unraidToolDefinitions.map((definition) => definition.tool.function.name),
73
79
  "steward_policy_manage",
74
80
  "ponder",
@@ -56,6 +56,17 @@ const TERMINAL_STATUSES = new Set([
56
56
  ]);
57
57
  const ADMISSION_ID = /^[a-f0-9]{20}$/u;
58
58
  const CANONICAL_ID = /^[1-9][0-9]*$/u;
59
+ const KINSHIP = new Set(["brother", "sister", "sibling", "mother", "father", "parent", "son", "daughter", "aunt", "uncle", "cousin", "grandmother", "grandfather", "wife", "husband", "partner"]);
60
+ function normalizeKinship(value) {
61
+ const normalized = value === "mom" || value === "mum" ? "mother"
62
+ : value === "dad" ? "father"
63
+ : value === "grandma" ? "grandmother"
64
+ : value === "grandpa" ? "grandfather"
65
+ : value;
66
+ if (!KINSHIP.has(normalized))
67
+ throw new Error("Telegram admission kinship is invalid");
68
+ return normalized;
69
+ }
59
70
  const DEFAULT_LIMITS = {
60
71
  maxPendingContacts: 32,
61
72
  maxTextBytes: 16 * 1024,
@@ -107,6 +118,7 @@ function validateRecord(value, expectedId) {
107
118
  || typeof record.displayLabel !== "string" || [...record.displayLabel].length > 120
108
119
  || typeof record.displayCode !== "string" || record.displayCode.length < 4 || record.displayCode.length > 32
109
120
  || typeof record.hasAttachments !== "boolean"
121
+ || (record.ownerRelationship !== undefined && !KINSHIP.has(record.ownerRelationship))
110
122
  || ![record.createdAt, record.updatedAt, record.expiresAt].every(Number.isSafeInteger)
111
123
  || (record.friendId !== null && (typeof record.friendId !== "string" || !record.friendId))
112
124
  || (record.acknowledgementArtifactId !== null && typeof record.acknowledgementArtifactId !== "string")
@@ -422,6 +434,7 @@ class FileTelegramAdmissionStore {
422
434
  ...(input.friendId ? { friendId: input.friendId } : {}),
423
435
  ...(input.acknowledgementArtifactId ? { acknowledgementArtifactId: input.acknowledgementArtifactId } : {}),
424
436
  ...(input.ownerCardArtifactId ? { ownerCardArtifactId: input.ownerCardArtifactId } : {}),
437
+ ...(input.ownerRelationship ? { ownerRelationship: input.ownerRelationship } : {}),
425
438
  ...(input.ingress ? { ingressSessionKey: input.ingress.sessionKey, ingressEventId: input.ingress.eventId, ingressReference: input.ingress.reference } : {}),
426
439
  ...(terminal || input.ingress ? { quarantinedText: null } : {}),
427
440
  };
@@ -532,6 +545,7 @@ function createTelegramAdmissionController(options) {
532
545
  chatId: record.chatId,
533
546
  admissionId: record.id,
534
547
  displayLabel: record.displayLabel,
548
+ ...(record.ownerRelationship ? { relationship: record.ownerRelationship } : {}),
535
549
  defaults: {
536
550
  trustLevel: "friend",
537
551
  admissionState: "active",
@@ -565,7 +579,7 @@ function createTelegramAdmissionController(options) {
565
579
  });
566
580
  const committed = {
567
581
  ...committedBase,
568
- orientation: { ...exports.NEWLY_ADMITTED_ORIENTATION, attachmentsNeedResend: record.hasAttachments },
582
+ orientation: { ...exports.NEWLY_ADMITTED_ORIENTATION, attachmentsNeedResend: record.hasAttachments, ...(record.ownerRelationship ? { relationship: record.ownerRelationship } : {}) },
569
583
  };
570
584
  if (committed.admissionId !== record.id || committed.friendId !== record.friendId || committed.reference !== `telegram-admission:${record.id}`) {
571
585
  throw new Error("Telegram admission committed ingress changed identity");
@@ -584,7 +598,7 @@ function createTelegramAdmissionController(options) {
584
598
  sessionKey: record.ingressSessionKey,
585
599
  eventId: record.ingressEventId,
586
600
  reference: record.ingressReference,
587
- orientation: { ...exports.NEWLY_ADMITTED_ORIENTATION, attachmentsNeedResend: record.hasAttachments },
601
+ orientation: { ...exports.NEWLY_ADMITTED_ORIENTATION, attachmentsNeedResend: record.hasAttachments, ...(record.ownerRelationship ? { relationship: record.ownerRelationship } : {}) },
588
602
  });
589
603
  if (settlement === "indeterminate") {
590
604
  await compensateFriend(record);
@@ -638,7 +652,9 @@ function createTelegramAdmissionController(options) {
638
652
  if (!input.replyToMessageId || Buffer.byteLength(input.text, "utf8") > 80)
639
653
  return null;
640
654
  const normalized = input.text.normalize("NFKC").trim().toLocaleLowerCase("en-US");
641
- const decision = /^(?:yes|allow|approve|let (?:them|him|her) in|yes,? (?:that(?:'|’)s|that is) my (?:brother|sister|sibling|mom|mother|mum|dad|father|parent|son|daughter|aunt|uncle|cousin|grandmother|grandma|grandfather|grandpa|wife|husband|partner))$/u.test(normalized) ? "allow"
655
+ const kinshipMatch = /^yes,? (?:that(?:'|’)s|that is) my (brother|sister|sibling|mom|mother|mum|dad|father|parent|son|daughter|aunt|uncle|cousin|grandmother|grandma|grandfather|grandpa|wife|husband|partner)$/u.exec(normalized);
656
+ const relationship = kinshipMatch ? normalizeKinship(kinshipMatch[1]) : undefined;
657
+ const decision = /^(?:yes|allow|approve|let (?:them|him|her) in)$/u.test(normalized) || relationship ? "allow"
642
658
  : /^(?:no|deny|decline)$/u.test(normalized) ? "deny"
643
659
  : /^(?:block|block (?:them|him|her))$/u.test(normalized) ? "block"
644
660
  : null;
@@ -650,7 +666,7 @@ function createTelegramAdmissionController(options) {
650
666
  const record = options.store.read(binding.admissionId);
651
667
  if (record.ownerCardArtifactId !== binding.artifactId || record.status !== "pending")
652
668
  return null;
653
- return { admissionId: record.id, decision };
669
+ return { admissionId: record.id, decision, ...(relationship ? { relationship } : {}) };
654
670
  },
655
671
  async decide(input) {
656
672
  return withDecisionLease(input.admissionId, async () => {
@@ -671,7 +687,7 @@ function createTelegramAdmissionController(options) {
671
687
  await settleOwnerCard(record);
672
688
  return record;
673
689
  }
674
- record = options.store.compareAndSwap({ admissionId: record.id, expectedStatus: "pending", nextStatus: "approved" });
690
+ record = options.store.compareAndSwap({ admissionId: record.id, expectedStatus: "pending", nextStatus: "approved", ...(input.relationship ? { ownerRelationship: input.relationship } : {}) });
675
691
  record = await resume(record.id);
676
692
  await settleOwnerCard(record);
677
693
  return record;
@@ -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.751",
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.751",
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.751",
3
+ "version": "0.1.0-alpha.752",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },