@ouro.bot/cli 0.1.0-alpha.742 → 0.1.0-alpha.744

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.
Files changed (71) hide show
  1. package/changelog.json +32 -0
  2. package/deploy/unraid/Dockerfile +7 -2
  3. package/deploy/unraid/README.txt +704 -64
  4. package/deploy/unraid/migrate-sanctuary-bundle.mjs +21 -0
  5. package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
  6. package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
  7. package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
  8. package/deploy/unraid/ouro-events/install-usenet-guard.sh +434 -0
  9. package/deploy/unraid/ouro-events/usenet-health.sh +178 -0
  10. package/deploy/unraid/sanctuary-acceptance-contract.json +4 -3
  11. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +5 -1
  12. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +2 -2
  13. package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +1 -1
  14. package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -1
  15. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -1
  16. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +4 -0
  17. package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +6 -2
  18. package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +4 -2
  19. package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
  20. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +19 -17
  21. package/deploy/unraid/sanctuary.xml +13 -10
  22. package/dist/arc/cares.js +171 -37
  23. package/dist/arc/obligations.js +38 -0
  24. package/dist/heart/awaiting/await-alert.js +4 -2
  25. package/dist/heart/awaiting/await-expiry.js +39 -4
  26. package/dist/heart/awaiting/await-parser.js +8 -0
  27. package/dist/heart/core.js +37 -7
  28. package/dist/heart/cross-chat-delivery.js +1 -0
  29. package/dist/heart/daemon/container-spec-auditor-main.js +11 -7
  30. package/dist/heart/daemon/container-spec-auditor.js +38 -11
  31. package/dist/heart/daemon/daemon-entry.js +19 -5
  32. package/dist/heart/daemon/daemon.js +154 -22
  33. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +126 -42
  34. package/dist/heart/daemon/sanctuary-acceptance-harness.js +26 -29
  35. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +42 -51
  36. package/dist/heart/daemon/sanctuary-bundle-migration.js +398 -0
  37. package/dist/heart/external-events/router.js +1025 -43
  38. package/dist/heart/session-events.js +23 -5
  39. package/dist/heart/steward-policy.js +302 -0
  40. package/dist/heart/tool-approval.js +3 -1
  41. package/dist/heart/turn-context.js +1 -0
  42. package/dist/mind/context.js +8 -3
  43. package/dist/mind/pending.js +25 -0
  44. package/dist/mind/prompt.js +49 -1
  45. package/dist/repertoire/guardrails.js +2 -1
  46. package/dist/repertoire/relationship-authorization.js +194 -0
  47. package/dist/repertoire/tools-awaiting.js +229 -26
  48. package/dist/repertoire/tools-base.js +15 -0
  49. package/dist/repertoire/tools-continuity.js +259 -4
  50. package/dist/repertoire/tools-notes.js +29 -3
  51. package/dist/repertoire/tools-session.js +219 -4
  52. package/dist/repertoire/tools-steward-policy.js +88 -0
  53. package/dist/repertoire/tools-telegram-contacts.js +37 -0
  54. package/dist/repertoire/tools-unraid.js +113 -6
  55. package/dist/repertoire/tools.js +104 -9
  56. package/dist/repertoire/unraid-restart.js +86 -4
  57. package/dist/senses/private-runtime-worker.js +36 -3
  58. package/dist/senses/private-runtime.js +310 -63
  59. package/dist/senses/sanctuary-health-acceptance-probe.js +8 -6
  60. package/dist/senses/sanctuary-health-runner.js +47 -103
  61. package/dist/senses/sanctuary-health.js +53 -105
  62. package/dist/senses/sanctuary-runtime.js +36 -2
  63. package/dist/senses/sanctuary-sab.js +80 -0
  64. package/dist/senses/shared-turn.js +64 -9
  65. package/dist/senses/telegram-admission.js +684 -0
  66. package/dist/senses/telegram-approval-runtime.js +46 -6
  67. package/dist/senses/telegram-client.js +212 -66
  68. package/dist/senses/telegram-effect-adapter.js +787 -0
  69. package/dist/senses/telegram.js +728 -48
  70. package/npm-shrinkwrap.json +6 -6
  71. package/package.json +2 -2
@@ -33,12 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createSabQueueProtectiveStateVerifier = createSabQueueProtectiveStateVerifier;
36
37
  exports.telegramAcceptanceAuditOwnerDigest = telegramAcceptanceAuditOwnerDigest;
37
38
  exports.sanctuaryTelegramTurnReceiptDigest = sanctuaryTelegramTurnReceiptDigest;
38
39
  exports.sanctuaryTelegramTurnReceiptMac = sanctuaryTelegramTurnReceiptMac;
39
40
  exports.sanctuaryTelegramAuditLifecycleMac = sanctuaryTelegramAuditLifecycleMac;
40
41
  exports.sanctuaryTelegramUnauthorizedDropMac = sanctuaryTelegramUnauthorizedDropMac;
41
42
  exports.sanctuaryTelegramApprovalEvidenceMac = sanctuaryTelegramApprovalEvidenceMac;
43
+ exports.telegramBotIdFromToken = telegramBotIdFromToken;
42
44
  exports.readOrCreateTelegramIdentityKey = readOrCreateTelegramIdentityKey;
43
45
  exports.opaqueTelegramSubject = opaqueTelegramSubject;
44
46
  exports.migrateTelegramFriendIdentity = migrateTelegramFriendIdentity;
@@ -46,7 +48,10 @@ exports.migrateTelegramSessionIdentity = migrateTelegramSessionIdentity;
46
48
  exports.parseTelegramSenseCredentials = parseTelegramSenseCredentials;
47
49
  exports.createTelegramSenseApp = createTelegramSenseApp;
48
50
  exports.loadTelegramSenseCredentials = loadTelegramSenseCredentials;
51
+ exports.createProductionTelegramRelationshipComposition = createProductionTelegramRelationshipComposition;
49
52
  exports.startTelegramSenseApp = startTelegramSenseApp;
53
+ exports.sendTelegramExternalEventDecision = sendTelegramExternalEventDecision;
54
+ exports.sendTelegramAwaitFollowUp = sendTelegramAwaitFollowUp;
50
55
  const node_crypto_1 = require("node:crypto");
51
56
  const node_async_hooks_1 = require("node:async_hooks");
52
57
  const node_fs_1 = require("node:fs");
@@ -58,12 +63,37 @@ const runtime_credentials_1 = require("../heart/runtime-credentials");
58
63
  const runtime_1 = require("../nerves/runtime");
59
64
  const nerves_1 = require("../nerves");
60
65
  const sanctuary_interactive_control_1 = require("./sanctuary-interactive-control");
66
+ const sanctuary_sab_1 = require("./sanctuary-sab");
61
67
  const shared_turn_1 = require("./shared-turn");
62
68
  const telegram_client_1 = require("./telegram-client");
63
69
  const sanctuary_runtime_1 = require("./sanctuary-runtime");
64
70
  const sanctuary_grounding_1 = require("./sanctuary-grounding");
65
71
  const telegram_approval_runtime_1 = require("./telegram-approval-runtime");
66
72
  const telegram_audit_ledger_1 = require("./telegram-audit-ledger");
73
+ const session_events_1 = require("../heart/session-events");
74
+ const router_1 = require("../heart/external-events/router");
75
+ const session_transaction_1 = require("../mind/session-transaction");
76
+ const approval_files_1 = require("../heart/approval-files");
77
+ const approval_store_1 = require("../heart/approval-store");
78
+ const tools_awaiting_1 = require("../repertoire/tools-awaiting");
79
+ const obligations_1 = require("../arc/obligations");
80
+ const orientation_frame_1 = require("../heart/orientation-frame");
81
+ const pending_1 = require("../mind/pending");
82
+ const relationship_authorization_1 = require("../repertoire/relationship-authorization");
83
+ const telegram_admission_1 = require("./telegram-admission");
84
+ const telegram_effect_adapter_1 = require("./telegram-effect-adapter");
85
+ const SANCTUARY_SAB_CONFIG_PATH = "/run/sanctuary/sabnzbd.ini";
86
+ function createSabQueueProtectiveStateVerifier(options = {}) {
87
+ let client = null;
88
+ return async (action) => {
89
+ client ??= (0, sanctuary_sab_1.createSanctuarySabClient)({ iniPath: options.iniPath ?? SANCTUARY_SAB_CONFIG_PATH, fetch: options.fetch, now: options.now });
90
+ const snapshot = await client.readQueue();
91
+ const paused = snapshot.paused;
92
+ const digest = (0, node_crypto_1.createHash)("sha256").update(`sabnzbd.queue.paused=${String(paused)}`).digest("hex");
93
+ const verified = action.action === "sabnzbd.pause" && paused && action.verification.verified && action.verification.digest === digest;
94
+ return { verified, reference: `sabnzbd.queue.paused:${digest}:${snapshot.observedAt}` };
95
+ };
96
+ }
67
97
  const APPROVAL_EXPIRY_RECONCILE_INTERVAL_MS = 1_000;
68
98
  const TELEGRAM_SUBJECT_DOMAIN = "ouroboros.telegram.subject.v1";
69
99
  const TELEGRAM_IDENTITY_KEY = /^[A-Za-z0-9_-]{43}$/u;
@@ -343,6 +373,12 @@ function canonicalTelegramId(value, label) {
343
373
  throw new Error(`Telegram ${label} must be a canonical positive decimal string`);
344
374
  return text;
345
375
  }
376
+ function telegramBotIdFromToken(token) {
377
+ const match = /^([1-9][0-9]*):/u.exec(token.trim());
378
+ if (!match)
379
+ throw new Error("Telegram bot token does not expose a canonical numeric bot id");
380
+ return match[1];
381
+ }
346
382
  function canonicalTelegramIdentityKey(value) {
347
383
  const key = value.trim();
348
384
  if (!TELEGRAM_IDENTITY_KEY.test(key) || Buffer.from(key, "base64url").length !== 32) {
@@ -626,17 +662,23 @@ function parseTelegramSenseCredentials(value) {
626
662
  }
627
663
  function createTelegramSenseApp(options) {
628
664
  const botToken = requiredText(options.credentials.botToken, "bot token");
665
+ const botId = options.credentials.botId !== undefined
666
+ ? canonicalTelegramId(options.credentials.botId, "bot id")
667
+ : options.admission ? telegramBotIdFromToken(botToken) : null;
629
668
  const authorizedUserId = canonicalTelegramId(options.credentials.authorizedUserId, "authorized user id");
630
669
  const authorizedChatId = canonicalTelegramId(options.credentials.authorizedChatId, "authorized chat id");
631
670
  const agentRoot = options._agentRoot ?? (0, identity_1.getAgentRoot)(options.agentName);
632
671
  const identityKey = options.identityKey === undefined
633
672
  ? readOrCreateTelegramIdentityKey(agentRoot)
634
673
  : canonicalTelegramIdentityKey(options.identityKey);
635
- const subject = opaqueTelegramSubject(identityKey, botToken, authorizedUserId, authorizedChatId);
674
+ const subject = opaqueTelegramSubject(identityKey, botId ?? botToken, authorizedUserId, authorizedChatId);
636
675
  const transportError = (error) => redactTelegramPrivateValues(error, [botToken, authorizedUserId, authorizedChatId]);
637
676
  const api = options.api ?? (0, telegram_client_1.createTelegramBotApi)({ token: botToken });
638
677
  const offsetStore = options.offsetStore ?? new telegram_client_1.FileTelegramOffsetStore(path.join(agentRoot, "state", "senses", "telegram", "offset.json"));
639
678
  const inboxStore = options.inboxStore ?? new telegram_client_1.FileTelegramUpdateInboxStore(path.join(agentRoot, "state", "senses", "telegram", "inbox.json"));
679
+ const admissionStore = options.admission
680
+ ? new telegram_admission_1.FileTelegramAdmissionStore(path.join(agentRoot, "state", "senses", "telegram", "admissions"))
681
+ : undefined;
640
682
  const runTurn = options.runTurn ?? options._runTurn ?? shared_turn_1.runSenseTurn;
641
683
  const collectToolReceipts = options._runWithToolReceiptCollection ?? sanctuary_runtime_1.runWithSanctuaryToolReceiptCollection;
642
684
  const useSanctuaryRuntime = options.agentName === "sanctuary" && !options.runTurn;
@@ -781,12 +823,59 @@ function createTelegramSenseApp(options) {
781
823
  }
782
824
  throw primaryError;
783
825
  };
826
+ let effectJournal;
827
+ const getEffectJournal = () => {
828
+ effectJournal ??= new telegram_effect_adapter_1.FileTelegramEffectJournal(path.join(agentRoot, "state", "telegram", "effects"));
829
+ return effectJournal;
830
+ };
831
+ const configuredOwnerFriendId = options.admission?.ownerFriendId ?? `telegram-user:${subject}`;
832
+ const configuredOwnerSessionKey = `telegram:${subject}`;
833
+ const defaultEffectAuthorization = async (input) => {
834
+ const target = input.target;
835
+ if (target.kind === "admission_gate") {
836
+ const pending = admissionStore.read(target.admissionId);
837
+ const authorization = (0, relationship_authorization_1.authorizeRelationshipAccess)({
838
+ profiles: [],
839
+ pendingAdmission: { admissionId: pending.id, botId: pending.botId, userId: pending.userId, chatId: pending.chatId, expiresAt: new Date(pending.expiresAt).toISOString() },
840
+ request: { kind: "admission_gate", admissionId: target.admissionId, botId: target.botId, userId: target.userId, chatId: target.chatId, effect: "fixed_ack", idempotencyKey: input.idempotencyKey, expiresAt: new Date(pending.expiresAt).toISOString() },
841
+ });
842
+ const admissionAuthorization = authorization;
843
+ return { allowed: true, receiptId: admissionAuthorization.receiptId, expiresAt: admissionAuthorization.expiresAt, transport: { chatId: target.chatId } };
844
+ }
845
+ if (input.authorClass === "system_failsafe" && (input.effect.kind !== "text" || input.effect.text !== telegram_effect_adapter_1.FIXED_USENET_SYSTEM_FAILSAFE || !input.idempotencyKey.startsWith("system-failsafe:"))) {
846
+ return { allowed: false, reason: "system failsafe shape is not fixed" };
847
+ }
848
+ if (options.authorizeRelationshipEffect) {
849
+ if (target.friendId === configuredOwnerFriendId && target.sessionKey !== configuredOwnerSessionKey)
850
+ return { allowed: false, reason: "owner relationship session binding changed" };
851
+ return options.authorizeRelationshipEffect(input);
852
+ }
853
+ if (target.friendId !== `telegram-user:${subject}` || target.sessionKey !== `telegram:${subject}`) {
854
+ return { allowed: false, reason: "effect target is not the configured owner relationship" };
855
+ }
856
+ return { allowed: true, receiptId: `configured-owner:${subject}`, expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(), transport: { chatId: authorizedChatId } };
857
+ };
858
+ const authorizeEffect = options.authorizeEffect ?? defaultEffectAuthorization;
859
+ let recordAcceptedEffects;
860
+ const configuredOwnerTarget = () => ({ kind: "approved_relationship", friendId: configuredOwnerFriendId, sessionKey: configuredOwnerSessionKey });
861
+ const executeAuthorizedEffect = (0, telegram_effect_adapter_1.createTelegramAuthorizedEffectExecutor)({
862
+ store: getEffectJournal,
863
+ api,
864
+ authorize: authorizeEffect,
865
+ barrier: acceptanceAuditBarrier,
866
+ });
867
+ const recordConfiguredOwnerEffect = async (artifact, causalEventId) => {
868
+ await recordAcceptedEffects((0, shared_turn_1.getSenseSessionPath)(options.agentName, configuredOwnerFriendId, "telegram", configuredOwnerSessionKey, agentRoot), [artifact], undefined, causalEventId ? { [artifact.id]: causalEventId } : undefined);
869
+ };
870
+ const approvalEffects = (0, telegram_effect_adapter_1.createTelegramApprovalEffectPort)({ target: configuredOwnerTarget(), chatId: authorizedChatId, execute: executeAuthorizedEffect, record: recordConfiguredOwnerEffect });
784
871
  let toolContext;
785
872
  let approvalRuntime;
786
873
  let approvalTransport;
787
874
  let interactiveControl;
788
875
  try {
789
876
  toolContext = useSanctuaryRuntime ? (options._toolContext ?? (0, sanctuary_runtime_1.createSanctuaryToolContext)(options.agentName)) : undefined;
877
+ if (toolContext && options.telegramContactManager)
878
+ toolContext.telegramContactManager = options.telegramContactManager;
790
879
  approvalRuntime = options.approvalRuntime ?? (useSanctuaryRuntime ? (options._createApprovalRuntime ?? telegram_approval_runtime_1.createTelegramApprovalRuntime)({
791
880
  agentName: options.agentName,
792
881
  api,
@@ -795,6 +884,7 @@ function createTelegramSenseApp(options) {
795
884
  subject,
796
885
  identityKey,
797
886
  toolContext: toolContext ?? {},
887
+ effects: approvalEffects,
798
888
  effectBarrier: acceptanceAuditBarrier,
799
889
  dependencies: {
800
890
  acceptanceMarker: () => {
@@ -818,7 +908,6 @@ function createTelegramSenseApp(options) {
818
908
  catch (error) {
819
909
  releaseAcceptanceAudit(error);
820
910
  }
821
- const healthSweep = options.healthSweep;
822
911
  const migrateIdentity = options.migrateIdentity ?? (async () => {
823
912
  const legacySubjects = new Set([
824
913
  ...readTelegramSubjectIndex(agentRoot, subject),
@@ -848,6 +937,7 @@ function createTelegramSenseApp(options) {
848
937
  let approvalReconciliationActive = false;
849
938
  const approvalReconciliationsInFlight = new Set();
850
939
  let nextApprovalReconcileDeadline;
940
+ let reconcileSystemFailsafes;
851
941
  let stopPromise;
852
942
  let runPromise;
853
943
  const clearApprovalReconcileTimer = () => {
@@ -877,46 +967,244 @@ function createTelegramSenseApp(options) {
877
967
  approvalReconciliationsInFlight.delete(reconciliation);
878
968
  });
879
969
  approvalReconciliationsInFlight.add(reconciliation);
970
+ const failsafe = runWithAcceptanceAuditOwner(reconcileSystemFailsafes)
971
+ .finally(() => { approvalReconciliationsInFlight.delete(failsafe); });
972
+ approvalReconciliationsInFlight.add(failsafe);
880
973
  }, delay);
881
974
  };
882
- const runHealthSweep = async () => {
883
- if (!healthSweep)
884
- return;
975
+ const deliverButlerEffect = async (text, idempotencyKey, signal, onMessageDelivered) => {
976
+ return executeAuthorizedEffect({
977
+ idempotencyKey,
978
+ target: configuredOwnerTarget(),
979
+ authorClass: "butler",
980
+ effect: { kind: "text", text },
981
+ ...(signal ? { signal } : {}),
982
+ ...(onMessageDelivered ? { onMessageDelivered } : {}),
983
+ });
984
+ };
985
+ recordAcceptedEffects = async (sessionPath, artifacts, bootstrapInbound, causalEventIds) => {
986
+ await (0, telegram_effect_adapter_1.recordTelegramEffectsInSession)({ store: getEffectJournal(), sessionPath, artifacts, ...(bootstrapInbound ? { inbound: bootstrapInbound } : {}), ...(causalEventIds ? { causalEventIds } : {}) });
987
+ for (const artifact of artifacts) {
988
+ if (artifact.target.kind !== "approved_relationship" || !artifact.target.requestId || artifact.effect.kind !== "text")
989
+ continue;
990
+ const recorded = getEffectJournal().read(artifact.id);
991
+ if (!recorded.parts.every((part) => part.state === "session_recorded"))
992
+ continue;
993
+ if (!artifact.obligationReturnId)
994
+ continue;
995
+ const obligation = (0, obligations_1.readObligation)(agentRoot, artifact.obligationReturnId);
996
+ if (obligation?.returnReadyAt && obligation.requestId === artifact.target.requestId
997
+ && obligation.owedTo?.friendId === artifact.target.friendId && obligation.owedTo.channel === "telegram"
998
+ && obligation.owedTo.key === artifact.target.sessionKey)
999
+ (0, obligations_1.fulfillObligation)(agentRoot, obligation.id);
1000
+ }
1001
+ };
1002
+ const deliverAwaitFollowUp = async (request) => {
1003
+ if (!request.requestId)
1004
+ return { status: "blocked", detail: "Telegram follow-up is missing its request binding" };
885
1005
  try {
886
- acceptanceAuditBarrier();
887
- const result = await healthSweep();
888
- if (result.message) {
889
- acceptanceAuditBarrier();
890
- if (result.deliveryId)
891
- await healthSweep.markDeliveryAttempting?.(result.deliveryId);
892
- const messageIds = await deliver(result.message);
893
- acceptanceAuditBarrier();
894
- if (result.deliveryId)
895
- await healthSweep.markDelivered?.(result.deliveryId, messageIds);
896
- (0, runtime_1.emitNervesEvent)({
897
- component: "senses",
898
- event: "senses.sanctuary_health_delivered",
899
- message: "Sanctuary health notification was delivered",
900
- meta: { agentName: options.agentName, deliveryCount: messageIds.length, ...(0, sanctuary_acceptance_marker_1.sanctuaryAcceptanceEventMeta)(options.agentName) },
901
- });
902
- }
1006
+ const obligation = (0, obligations_1.findPendingObligationForRequest)(agentRoot, { requestId: request.requestId, owedTo: { friendId: request.friendId, channel: "telegram", key: request.key } });
1007
+ if (obligation && !obligation.returnReadyAt)
1008
+ (0, obligations_1.markObligationReturnReady)(agentRoot, obligation.id, request.deliveryId ?? `telegram-return:${request.requestId}`);
1009
+ const expiry = request.deliveryId?.endsWith(":expired") === true;
1010
+ const artifact = await executeAuthorizedEffect({
1011
+ idempotencyKey: `await-${expiry ? "expiry-" : ""}follow-up:${request.requestId}:${(0, node_crypto_1.createHash)("sha256").update(request.deliveryId ?? request.content).digest("hex")}`,
1012
+ target: { kind: "approved_relationship", friendId: request.friendId, sessionKey: request.key, requestId: request.requestId },
1013
+ authorClass: "butler",
1014
+ effect: { kind: "text", text: request.content },
1015
+ ...(obligation ? { obligationReturnId: obligation.id } : {}),
1016
+ });
1017
+ await recordAcceptedEffects((0, shared_turn_1.getSenseSessionPath)(options.agentName, request.friendId, "telegram", request.key, agentRoot), [artifact]);
1018
+ return { status: "delivered_now", detail: "sent to the exact request-bound Telegram chat" };
903
1019
  }
904
1020
  catch (error) {
905
- acceptanceAuditBarrier();
906
- (0, runtime_1.emitNervesEvent)({
907
- level: "error",
908
- component: "senses",
909
- event: "senses.sanctuary_health_error",
910
- message: "Sanctuary deterministic health sweep failed",
911
- meta: { agentName: options.agentName, subject, error: transportError(error) },
1021
+ return { status: "blocked", detail: error instanceof Error ? error.message : String(error) };
1022
+ }
1023
+ };
1024
+ (0, tools_awaiting_1.setAwaitToolDeps)({
1025
+ buildDeliveryDeps: () => ({
1026
+ agentName: options.agentName,
1027
+ queuePending: (message) => (0, pending_1.queuePendingMessage)((0, pending_1.getPrivateRuntimePendingDir)(options.agentName), message),
1028
+ deliverers: {
1029
+ telegram: deliverAwaitFollowUp,
1030
+ },
1031
+ }),
1032
+ });
1033
+ reconcileSystemFailsafes = async () => {
1034
+ if (!options.privilegedFailsafe)
1035
+ return;
1036
+ const target = configuredOwnerTarget();
1037
+ try {
1038
+ await (0, telegram_effect_adapter_1.sweepTelegramSystemFailsafes)({
1039
+ eventRoot: options.privilegedFailsafe.eventRoot,
1040
+ target,
1041
+ verifyProtectiveState: options.privilegedFailsafe.verifyProtectiveState,
1042
+ execute: executeAuthorizedEffect,
1043
+ recordArtifact: async (artifact) => {
1044
+ await recordAcceptedEffects((0, shared_turn_1.getSenseSessionPath)(options.agentName, target.friendId, "telegram", target.sessionKey, agentRoot), [artifact]);
1045
+ },
912
1046
  });
913
1047
  }
1048
+ catch (error) {
1049
+ (0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.telegram_system_failsafe_error", message: "Telegram system failsafe reconciliation failed", meta: { agentName: options.agentName, subject, error: transportError(error) } });
1050
+ }
914
1051
  };
915
- const deliver = async (text, signal, onMessageDelivered) => {
916
- acceptanceAuditBarrier();
917
- return (0, telegram_client_1.sendTelegramText)(api, authorizedChatId, text, signal, onMessageDelivered);
1052
+ const prepareRelationshipRunAgentOptions = (input) => {
1053
+ if (!options.resolveRelationshipAuthorization)
1054
+ throw new Error("Telegram relationship authorization resolver is unavailable");
1055
+ const relationshipCoordinates = { ...input, botId: botId };
1056
+ const resolveLiveRelationshipAuthorization = async () => {
1057
+ const authorization = await options.resolveRelationshipAuthorization(relationshipCoordinates);
1058
+ if (authorization.subject.friendId !== input.friendId || authorization.subject.admissionState !== "active")
1059
+ throw new Error("Telegram relationship admission is not active");
1060
+ return {
1061
+ requestId: input.requestId,
1062
+ profileId: authorization.profileId,
1063
+ authorizedContextScopes: authorization.authorizedContextScopes,
1064
+ advertisedToolNames: authorization.advertisedToolNames,
1065
+ actor: authorization.actor,
1066
+ authorizeTool: async (name, args) => (await options.resolveRelationshipAuthorization(relationshipCoordinates)).authorizeTool(name, args),
1067
+ };
1068
+ };
1069
+ return async ({ runAgentOptions }) => ({
1070
+ ...runAgentOptions,
1071
+ toolContext: { ...runAgentOptions.toolContext, relationshipAuthorization: await resolveLiveRelationshipAuthorization() },
1072
+ });
1073
+ };
1074
+ const dispatchResolvedRelationshipTurn = async (input) => {
1075
+ const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, input.friendId, "telegram", input.sessionKey, agentRoot);
1076
+ const effects = [];
1077
+ let ordinal = 0;
1078
+ const deliver = async (text, terminalReturn = false) => {
1079
+ const obligation = terminalReturn ? (0, obligations_1.findPendingObligationForRequest)(agentRoot, {
1080
+ requestId: input.requestId,
1081
+ owedTo: { friendId: input.friendId, channel: "telegram", key: input.sessionKey },
1082
+ }) : undefined;
1083
+ effects.push(await executeAuthorizedEffect({
1084
+ idempotencyKey: `relationship-turn:${input.requestId}:delivery:${ordinal++}`,
1085
+ target: { kind: "approved_relationship", friendId: input.friendId, sessionKey: input.sessionKey, requestId: input.requestId },
1086
+ authorClass: "butler",
1087
+ effect: { kind: "text", text },
1088
+ ...(obligation?.returnReadyAt ? { obligationReturnId: obligation.id } : {}),
1089
+ }));
1090
+ };
1091
+ const orientationFrame = input.orientation?.kind === "newly_admitted"
1092
+ ? {
1093
+ ...(0, orientation_frame_1.buildOrientationFrame)({ channel: "telegram", messages: [{ role: "user", content: input.text }] }),
1094
+ source: {
1095
+ kind: "telegram_newly_admitted",
1096
+ authority: "presentation_only",
1097
+ routingHint: "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.",
1098
+ },
1099
+ }
1100
+ : undefined;
1101
+ const result = await runTurn({
1102
+ agentName: options.agentName,
1103
+ channel: "telegram",
1104
+ sessionKey: input.sessionKey,
1105
+ friendId: input.friendId,
1106
+ identity: { provider: "telegram-user", externalId: input.userId, displayName: "Household member", tenantId: botId },
1107
+ userMessage: input.text,
1108
+ ingressRelations: { replyToEventId: null, threadRootEventId: null, references: [input.reference] },
1109
+ precommittedIngress: { eventId: input.eventId, reference: input.reference },
1110
+ ...(orientationFrame ? { orientationFrame } : {}),
1111
+ toolContext: { ...(toolContext ?? {}) },
1112
+ prepareRunAgentOptions: prepareRelationshipRunAgentOptions({ friendId: input.friendId, requestId: input.requestId,
1113
+ sessionEventId: input.eventId, userId: input.userId, chatId: input.chatId, sessionKey: input.sessionKey }),
1114
+ deliverySink: { onDelivery: (delivery) => deliver(delivery.text, delivery.kind === "settle") },
1115
+ });
1116
+ if (effects.length === 0 && result.response.trim())
1117
+ await deliver(result.response, true);
1118
+ const causalEventIds = Object.fromEntries(effects.flatMap((artifact, index) => {
1119
+ const eventId = result.causalSessionEventIds?.[index] ?? (effects.length === 1 ? result.responseCausalSessionEventId : undefined);
1120
+ return eventId ? [[artifact.id, eventId]] : [];
1121
+ }));
1122
+ await recordAcceptedEffects(sessionPath, effects, undefined, causalEventIds);
1123
+ };
1124
+ const admissionController = options.admission && admissionStore ? (0, telegram_admission_1.createTelegramAdmissionController)({
1125
+ store: admissionStore,
1126
+ owner: { friendId: configuredOwnerFriendId, sessionKey: configuredOwnerSessionKey },
1127
+ sendEffect: async (request) => {
1128
+ const artifact = await executeAuthorizedEffect(request);
1129
+ if (artifact.target.kind === "approved_relationship") {
1130
+ await recordAcceptedEffects((0, shared_turn_1.getSenseSessionPath)(options.agentName, artifact.target.friendId, "telegram", artifact.target.sessionKey, agentRoot), [artifact]);
1131
+ }
1132
+ return artifact.id;
1133
+ },
1134
+ resolveOwnerCard: (messageId) => {
1135
+ const resolved = (0, telegram_effect_adapter_1.resolveTelegramControlArtifact)(getEffectJournal(), { messageId, friendId: configuredOwnerFriendId, sessionKey: configuredOwnerSessionKey });
1136
+ return resolved?.requestId
1137
+ ? { artifactId: resolved.artifactId, admissionId: resolved.requestId }
1138
+ : null;
1139
+ },
1140
+ resolveOwnerCardMessageId: (artifactId) => {
1141
+ const artifact = getEffectJournal().read(artifactId);
1142
+ if (artifact.authorClass !== "control" || artifact.target.kind !== "approved_relationship"
1143
+ || artifact.target.friendId !== configuredOwnerFriendId || artifact.target.sessionKey !== configuredOwnerSessionKey)
1144
+ return null;
1145
+ return artifact.parts.find((part) => part.state === "session_recorded" && part.messageId)?.messageId ?? null;
1146
+ },
1147
+ claimFriend: options.admission.claimFriend,
1148
+ revokeFriend: options.admission.revokeFriend,
1149
+ commitApprovedIngress: async (input) => {
1150
+ const sessionKey = `telegram:${input.botId}:${input.userId}`;
1151
+ const reference = `telegram-admission:${input.admissionId}`;
1152
+ const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, input.friendId, "telegram", sessionKey, agentRoot);
1153
+ const receipt = await (0, telegram_effect_adapter_1.recordTelegramEffectsInSession)({ store: getEffectJournal(), sessionPath, artifacts: [], inbound: { text: input.text, reference } });
1154
+ return { admissionId: input.admissionId, friendId: input.friendId, sessionKey, eventId: receipt.eventId, reference };
1155
+ },
1156
+ enqueueApprovedWork: async (input) => {
1157
+ if (!inboxStore.captureAdmittedWork)
1158
+ throw new Error("Telegram admitted work inbox is unavailable");
1159
+ inboxStore.captureAdmittedWork(input);
1160
+ },
1161
+ dispatchApprovedWork: async (input) => {
1162
+ if (!inboxStore.admittedWorkState || !inboxStore.claimAdmittedWork || !inboxStore.completeAdmittedWork)
1163
+ return "indeterminate";
1164
+ const state = inboxStore.admittedWorkState(input.admissionId);
1165
+ if (state === "settled")
1166
+ return "settled";
1167
+ if (state !== "pending" || !inboxStore.claimAdmittedWork(input.admissionId))
1168
+ return "indeterminate";
1169
+ try {
1170
+ const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, input.friendId, "telegram", input.sessionKey, agentRoot);
1171
+ const envelope = (0, session_events_1.loadSessionEnvelopeFile)(sessionPath);
1172
+ const event = envelope?.events.find((candidate) => candidate.id === input.eventId
1173
+ && candidate.role === "user" && candidate.relations.references.includes(input.reference));
1174
+ if (!event || typeof event.content !== "string")
1175
+ throw new Error("Telegram admitted work ingress is unavailable");
1176
+ const record = admissionStore.read(input.admissionId);
1177
+ await dispatchResolvedRelationshipTurn({ friendId: input.friendId, sessionKey: input.sessionKey, requestId: input.admissionId,
1178
+ eventId: input.eventId, reference: input.reference, text: event.content, userId: record.userId, chatId: record.chatId, orientation: input.orientation });
1179
+ inboxStore.completeAdmittedWork(input.admissionId);
1180
+ return "settled";
1181
+ }
1182
+ catch {
1183
+ return "indeterminate";
1184
+ }
1185
+ },
1186
+ withDecisionLease: (admissionId, work) => (0, session_transaction_1.withSessionTurnLease)(path.join(agentRoot, "state", "senses", "telegram", "admission-decisions", admissionId), async () => work()),
1187
+ ...(options.admission.createDisplayCode ? { createDisplayCode: options.admission.createDisplayCode } : {}),
1188
+ }) : undefined;
1189
+ const recoverEffectOutbox = async () => {
1190
+ if (!effectJournal && !(0, node_fs_1.existsSync)(path.join(agentRoot, "state", "telegram", "effects")))
1191
+ return;
1192
+ await (0, telegram_effect_adapter_1.recoverTelegramEffectOutbox)({
1193
+ store: getEffectJournal(),
1194
+ execute: executeAuthorizedEffect,
1195
+ matches: (artifact) => artifact.target.kind === "approved_relationship",
1196
+ });
1197
+ const accepted = getEffectJournal().list().filter((artifact) => artifact.target.kind === "approved_relationship" && artifact.parts.some((part) => part.state === "accepted"));
1198
+ for (const artifact of accepted) {
1199
+ const target = artifact.target;
1200
+ await recordAcceptedEffects((0, shared_turn_1.getSenseSessionPath)(options.agentName, target.friendId, "telegram", target.sessionKey, agentRoot), [artifact]);
1201
+ }
918
1202
  };
919
1203
  const onMessageBody = async (message) => {
1204
+ const currentFriendId = configuredOwnerFriendId;
1205
+ const currentSessionKey = configuredOwnerSessionKey;
1206
+ const currentSessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, currentFriendId, "telegram", currentSessionKey, agentRoot);
1207
+ const inboundReference = `telegram-inbound:${(0, node_crypto_1.createHmac)("sha256", identityKey).update(`${message.updateId}\0${message.messageId}`).digest("hex")}`;
920
1208
  const acceptanceMarker = options.acceptanceMarker ? options.acceptanceMarker() : (0, sanctuary_acceptance_marker_1.readSanctuaryAcceptanceMarker)(options.agentName);
921
1209
  const acceptanceMeta = (0, sanctuary_acceptance_marker_1.sanctuaryAcceptanceEventMeta)(options.agentName);
922
1210
  const groundedAcceptance = acceptanceMarker?.label === "unit-16d-whats-up" || acceptanceMarker?.label === "unit-16d-1-space";
@@ -950,23 +1238,51 @@ function createTelegramSenseApp(options) {
950
1238
  const turnMetricsObserver = { providerInvocationCount: 0, toolInvocationCount: 0 };
951
1239
  const toolReceiptObserver = { toolResultDigests: [], toolGroundings: [] };
952
1240
  const normalizedRequest = message.text.normalize("NFKC").trim().toLocaleLowerCase("en-US");
953
- const groundingIntentTool = /^(?:what['’]?s up|status)\??$/u.test(normalizedRequest) ? "unraid_get_system"
954
- : /^how much (?:space|storage) is left\??$/u.test(normalizedRequest) ? "unraid_get_storage"
955
- : null;
1241
+ const groundingIntentTool = groundedAcceptance
1242
+ ? /^(?:what['’]?s up|status)\??$/u.test(normalizedRequest) ? "unraid_get_system"
1243
+ : /^how much (?:space|storage) is left\??$/u.test(normalizedRequest) ? "unraid_get_storage"
1244
+ : null
1245
+ : null;
956
1246
  const bufferedGroundedDeliveries = [];
1247
+ const turnEffects = [];
1248
+ let deliveryOrdinal = 0;
1249
+ await recoverEffectOutbox();
1250
+ let ingressRelations = { replyToEventId: null, threadRootEventId: null, references: [inboundReference] };
1251
+ if (message.replyToMessageId && /^[1-9][0-9]*$/u.test(message.replyToMessageId)) {
1252
+ const replyMessageId = Number(message.replyToMessageId);
1253
+ const reply = (0, telegram_effect_adapter_1.resolveTelegramReply)(getEffectJournal(), { messageId: replyMessageId, friendId: currentFriendId, sessionKey: currentSessionKey });
1254
+ const session = reply ? (0, session_events_1.loadSessionEnvelopeFile)(currentSessionPath) : null;
1255
+ if (reply && session?.events.some((event) => event.id === reply.sessionEventId))
1256
+ ingressRelations = {
1257
+ replyToEventId: reply.sessionEventId,
1258
+ threadRootEventId: null,
1259
+ references: [inboundReference, `telegram-artifact:${reply.artifactId}`, ...(reply.requestId ? [`request:${reply.requestId}`] : [])],
1260
+ };
1261
+ }
957
1262
  try {
958
1263
  acceptanceAuditBarrier();
1264
+ const ingressReceipt = options.resolveRelationshipAuthorization
1265
+ ? await (0, telegram_effect_adapter_1.recordTelegramEffectsInSession)({
1266
+ store: getEffectJournal(),
1267
+ sessionPath: currentSessionPath,
1268
+ artifacts: [],
1269
+ inbound: { text: message.text, reference: inboundReference, relations: ingressRelations },
1270
+ })
1271
+ : null;
959
1272
  const collected = await collectToolReceipts(() => runTurn({
960
1273
  agentName: options.agentName,
961
1274
  channel: "telegram",
962
- sessionKey: `telegram:${subject}`,
963
- friendId: `telegram-user:${subject}`,
1275
+ sessionKey: currentSessionKey,
1276
+ friendId: currentFriendId,
964
1277
  identity: {
965
1278
  provider: "telegram-user",
966
- externalId: subject,
967
- displayName: `Telegram user ${subject}`,
1279
+ externalId: options.admission ? message.userId : subject,
1280
+ displayName: options.admission ? "Ari" : `Telegram user ${subject}`,
1281
+ ...(options.admission ? { tenantId: botId } : {}),
968
1282
  },
969
1283
  userMessage: message.text,
1284
+ ingressRelations,
1285
+ ...(ingressReceipt ? { precommittedIngress: ingressReceipt } : {}),
970
1286
  turnMetricsObserver,
971
1287
  deliverySink: {
972
1288
  onDelivery: async (delivery) => {
@@ -974,15 +1290,26 @@ function createTelegramSenseApp(options) {
974
1290
  bufferedGroundedDeliveries.push(delivery.kind);
975
1291
  }
976
1292
  else {
977
- await deliver(delivery.text, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); });
1293
+ turnEffects.push(await deliverButlerEffect(delivery.text, `turn:${subject}:${message.updateId}:delivery:${deliveryOrdinal++}`, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); }));
978
1294
  deliveryCount += 1;
979
1295
  }
980
1296
  },
981
1297
  },
982
1298
  ...(toolContext ? { toolContext } : {}),
1299
+ ...(options.resolveRelationshipAuthorization ? {
1300
+ prepareRunAgentOptions: prepareRelationshipRunAgentOptions({
1301
+ friendId: currentFriendId,
1302
+ requestId: inboundReference,
1303
+ sessionEventId: ingressReceipt.eventId,
1304
+ userId: message.userId,
1305
+ chatId: message.chatId,
1306
+ sessionKey: currentSessionKey,
1307
+ }),
1308
+ } : {}),
983
1309
  ...(approvalRuntime ? { approvalCoordinatorFactory: approvalRuntime.coordinator } : {}),
984
1310
  }), toolReceiptObserver);
985
1311
  const result = collected.result;
1312
+ let responseFallbackArtifactId;
986
1313
  turnMetricsObserver.providerInvocationCount = Math.max(turnMetricsObserver.providerInvocationCount, result.providerInvocationCount ?? 0);
987
1314
  turnMetricsObserver.toolInvocationCount = Math.max(turnMetricsObserver.toolInvocationCount, result.toolInvocationCount ?? 0);
988
1315
  if (groundingIntentTool) {
@@ -991,11 +1318,24 @@ function createTelegramSenseApp(options) {
991
1318
  || (bufferedGroundedDeliveries.length === 1 && bufferedGroundedDeliveries[0] !== "settle"))
992
1319
  throw new Error("Canonical Sanctuary query did not produce exactly one matching grounded settle");
993
1320
  const canonical = (0, sanctuary_grounding_1.renderSanctuaryGroundedResponse)(grounding.toolName, grounding.facts);
994
- await deliver(canonical, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); });
1321
+ turnEffects.push(await deliverButlerEffect(canonical, `turn:${subject}:${message.updateId}:delivery:${deliveryOrdinal++}`, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); }));
995
1322
  deliveryCount = 1;
996
1323
  }
997
1324
  else if (deliveryCount === 0 && result.response.trim()) {
998
- await deliver(result.response, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); });
1325
+ const artifact = await deliverButlerEffect(result.response, `turn:${subject}:${message.updateId}:delivery:${deliveryOrdinal++}`, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); });
1326
+ turnEffects.push(artifact);
1327
+ responseFallbackArtifactId = artifact.id;
1328
+ }
1329
+ if (result.sessionPath) {
1330
+ const causalEventIds = Object.fromEntries((groundingIntentTool ? [] : turnEffects).flatMap((artifact, index) => {
1331
+ const eventId = result.causalSessionEventIds?.[index];
1332
+ if (eventId)
1333
+ return [[artifact.id, eventId]];
1334
+ return artifact.id === responseFallbackArtifactId && result.responseCausalSessionEventId
1335
+ ? [[artifact.id, result.responseCausalSessionEventId]]
1336
+ : [];
1337
+ }));
1338
+ await recordAcceptedEffects(result.sessionPath, turnEffects, undefined, causalEventIds);
999
1339
  }
1000
1340
  (0, runtime_1.emitNervesEvent)({
1001
1341
  component: "senses",
@@ -1026,7 +1366,8 @@ function createTelegramSenseApp(options) {
1026
1366
  });
1027
1367
  const fallback = "I couldn't complete that turn. The failure was recorded; please try again.";
1028
1368
  if (deliveredMessageIds.length === 0)
1029
- await deliver(fallback, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); });
1369
+ turnEffects.push(await deliverButlerEffect(fallback, `turn:${subject}:${message.updateId}:fallback`, undefined, (messageId, chunk) => { deliveredMessageIds.push(messageId); deliveredChunks.push(chunk); }));
1370
+ await recordAcceptedEffects(currentSessionPath, turnEffects, { text: message.text, reference: inboundReference });
1030
1371
  }
1031
1372
  finally {
1032
1373
  if (acceptanceMarker) {
@@ -1068,9 +1409,54 @@ function createTelegramSenseApp(options) {
1068
1409
  }
1069
1410
  }
1070
1411
  };
1071
- const onMessage = (message) => runWithAcceptanceAuditOwner(() => onMessageBody(message));
1412
+ const onMessage = (message) => runWithAcceptanceAuditOwner(async () => {
1413
+ const ownerDecision = admissionController?.parseOwnerDecision({
1414
+ text: message.text,
1415
+ ...(message.replyToMessageId ? { replyToMessageId: Number(message.replyToMessageId) } : {}),
1416
+ });
1417
+ if (ownerDecision) {
1418
+ const actor = await options.admission?.resolveOwner({ botId: botId, userId: message.userId, chatId: message.chatId, sessionKey: configuredOwnerSessionKey });
1419
+ if (!actor)
1420
+ throw new Error("Telegram admission owner decision identity is invalid");
1421
+ await admissionController.decide({ ...ownerDecision, actorFriendId: actor.friendId });
1422
+ return;
1423
+ }
1424
+ await onMessageBody(message);
1425
+ });
1426
+ const onUnknownMessage = admissionController && options.admission ? async (message) => {
1427
+ const approved = await options.admission.resolveApprovedFriend(message);
1428
+ if (!approved) {
1429
+ await admissionController.handleUnknown(message);
1430
+ return;
1431
+ }
1432
+ const sessionKey = `telegram:${message.botId}:${message.userId}`;
1433
+ 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")}`;
1434
+ const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, approved.friendId, "telegram", sessionKey, agentRoot);
1435
+ const receipt = await (0, telegram_effect_adapter_1.recordTelegramEffectsInSession)({ store: getEffectJournal(), sessionPath, artifacts: [], inbound: { text: message.text, reference } });
1436
+ await dispatchResolvedRelationshipTurn({ friendId: approved.friendId, sessionKey, requestId: reference, eventId: receipt.eventId,
1437
+ reference, text: message.text, userId: message.userId, chatId: message.chatId });
1438
+ } : undefined;
1072
1439
  const onUpdate = async (update) => {
1073
- if (!update.callback_query || !approvalTransport)
1440
+ const callback = update.callback_query;
1441
+ if (callback?.data?.startsWith("admit:") && admissionController && callback.message) {
1442
+ return runWithAcceptanceAuditOwner(async () => {
1443
+ const actor = await options.admission?.resolveOwner({ botId: botId, userId: String(callback.from.id), chatId: String(callback.message.chat.id), sessionKey: configuredOwnerSessionKey });
1444
+ if (!actor)
1445
+ throw new Error("Telegram admission owner callback identity is invalid");
1446
+ const decision = admissionController.parseCallback(callback.data, callback.message.message_id);
1447
+ await admissionController.decide({ ...decision, actorFriendId: actor.friendId });
1448
+ const ownerTarget = configuredOwnerTarget();
1449
+ const artifact = await executeAuthorizedEffect({
1450
+ idempotencyKey: `admission-callback:${callback.id}`,
1451
+ target: { ...ownerTarget, requestId: decision.admissionId },
1452
+ authorClass: "control",
1453
+ effect: { kind: "callback_ack", callbackQueryId: callback.id },
1454
+ });
1455
+ await recordAcceptedEffects((0, shared_turn_1.getSenseSessionPath)(options.agentName, ownerTarget.friendId, "telegram", ownerTarget.sessionKey, agentRoot), [artifact]);
1456
+ return true;
1457
+ });
1458
+ }
1459
+ if (!callback || !approvalTransport)
1074
1460
  return false;
1075
1461
  return runWithAcceptanceAuditOwner(async () => (await approvalTransport.handleUpdate(update)).handled);
1076
1462
  };
@@ -1080,6 +1466,7 @@ function createTelegramSenseApp(options) {
1080
1466
  api,
1081
1467
  expectedUserId: authorizedUserId,
1082
1468
  expectedChatId: authorizedChatId,
1469
+ ...(onUnknownMessage ? { botId: botId, onUnknownMessage } : {}),
1083
1470
  offsetStore,
1084
1471
  inboxStore,
1085
1472
  onMessage,
@@ -1122,6 +1509,16 @@ function createTelegramSenseApp(options) {
1122
1509
  return runPromise;
1123
1510
  runPromise = (async () => {
1124
1511
  await runWithAcceptanceAuditOwner(migrateIdentity);
1512
+ inboxStore.quarantineStrandedAdmittedWork?.();
1513
+ await runWithAcceptanceAuditOwner(async () => { await admissionController?.recover(); });
1514
+ await runWithAcceptanceAuditOwner(recoverEffectOutbox);
1515
+ await runWithAcceptanceAuditOwner(reconcileSystemFailsafes);
1516
+ try {
1517
+ await runWithAcceptanceAuditOwner(async () => { await toolContext?.sanctuary?.recoverRoutineActions?.(); });
1518
+ }
1519
+ catch (error) {
1520
+ (0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.sanctuary_routine_recovery_error", message: "Sanctuary routine action recovery requires inspection", meta: { agentName: options.agentName, subject, category: error instanceof Error ? error.name : "unknown" } });
1521
+ }
1125
1522
  await runWithAcceptanceAuditOwner(async () => { await approvalRuntime?.recover(); });
1126
1523
  await runWithAcceptanceAuditOwner(async () => { await interactiveControl?.start(); });
1127
1524
  try {
@@ -1137,7 +1534,6 @@ function createTelegramSenseApp(options) {
1137
1534
  meta: { agentName: options.agentName, subject, error: transportError(error) },
1138
1535
  });
1139
1536
  }
1140
- await runWithAcceptanceAuditOwner(runHealthSweep);
1141
1537
  (0, runtime_1.emitNervesEvent)({
1142
1538
  component: "senses",
1143
1539
  event: "senses.telegram_poll_start",
@@ -1174,7 +1570,26 @@ function createTelegramSenseApp(options) {
1174
1570
  return runPromise;
1175
1571
  },
1176
1572
  async sendProactive(text, signal) {
1177
- await runWithAcceptanceAuditOwner(() => deliver(requiredText(text, "proactive message"), signal));
1573
+ await runWithAcceptanceAuditOwner(async () => {
1574
+ const effect = await deliverButlerEffect(requiredText(text, "proactive message"), `proactive:${(0, node_crypto_1.randomUUID)()}`, signal);
1575
+ const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, configuredOwnerFriendId, "telegram", configuredOwnerSessionKey, agentRoot);
1576
+ await recordAcceptedEffects(sessionPath, [effect]);
1577
+ });
1578
+ },
1579
+ async sendExternalEventDecision(input) {
1580
+ const source = requiredText(input.source, "external event source");
1581
+ const eventId = requiredText(input.eventId, "external event id");
1582
+ if (Buffer.byteLength(source) > 512 || Buffer.byteLength(eventId) > 512 || !Number.isSafeInteger(input.generation) || input.generation < 1)
1583
+ throw new Error("Telegram external event identity is invalid");
1584
+ await runWithAcceptanceAuditOwner(async () => {
1585
+ const receiptIdentity = JSON.stringify([source, eventId, input.generation]);
1586
+ const effect = await deliverButlerEffect(requiredText(input.text, "external event decision"), `external-event:${receiptIdentity}`, input.signal);
1587
+ const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, configuredOwnerFriendId, "telegram", configuredOwnerSessionKey, agentRoot);
1588
+ await recordAcceptedEffects(sessionPath, [effect]);
1589
+ });
1590
+ },
1591
+ async sendAwaitFollowUp(request) {
1592
+ return runWithAcceptanceAuditOwner(() => deliverAwaitFollowUp(request));
1178
1593
  },
1179
1594
  stop() {
1180
1595
  if (stopPromise)
@@ -1195,6 +1610,9 @@ function createTelegramSenseApp(options) {
1195
1610
  await attempt(async () => { await interactiveControl?.stop(); });
1196
1611
  await attempt(() => api.stop());
1197
1612
  await attempt(() => approvalRuntime?.close());
1613
+ await attempt(() => effectJournal?.close());
1614
+ await attempt(() => admissionStore?.close());
1615
+ await attempt(() => (0, tools_awaiting_1.resetAwaitToolDeps)());
1198
1616
  await attempt(() => runWithAcceptanceAuditOwner(() => {
1199
1617
  (0, runtime_1.emitNervesEvent)({
1200
1618
  component: "senses",
@@ -1220,8 +1638,252 @@ function loadTelegramSenseCredentials(agentName) {
1220
1638
  }
1221
1639
  return parseTelegramSenseCredentials(runtime.config);
1222
1640
  }
1641
+ async function createProductionTelegramRelationshipComposition(agentName, credentials, agentRootOverride) {
1642
+ const agentRoot = agentRootOverride ?? (0, identity_1.getAgentRoot)(agentName);
1643
+ const botId = canonicalTelegramId(credentials.botId ?? telegramBotIdFromToken(credentials.botToken), "bot id");
1644
+ const ownerUserId = canonicalTelegramId(credentials.authorizedUserId, "authorized user id");
1645
+ const ownerChatId = canonicalTelegramId(credentials.authorizedChatId, "authorized chat id");
1646
+ if (ownerUserId !== ownerChatId)
1647
+ throw new Error("Telegram owner relationship requires a private user-bound chat");
1648
+ const ownerSessionKey = `telegram:${opaqueTelegramSubject(readOrCreateTelegramIdentityKey(agentRoot), botId, ownerUserId, ownerChatId)}`;
1649
+ const store = new friends_1.FileFriendStore(path.join(agentRoot, "friends"));
1650
+ const sameBotTelegramBindings = (friend) => friend.externalIds.filter((external) => external.provider === "telegram-user" && external.tenantId === botId);
1651
+ const exactTelegramIdentity = (friend, userId) => {
1652
+ const bindings = sameBotTelegramBindings(friend);
1653
+ return bindings.length === 1 && /^[1-9][0-9]*$/u.test(bindings[0].externalId) && bindings[0].externalId === userId;
1654
+ };
1655
+ const canonicalOwner = (friend) => Boolean(friend
1656
+ && exactTelegramIdentity(friend, ownerUserId) && friend.admissionState === "active" && friend.trustLevel === "family"
1657
+ && friend.initiativePolicy === "proactive" && friend.capabilityProfileId === "sanctuary-owner");
1658
+ const canonicalHousehold = (friend, userId) => Boolean(friend
1659
+ && exactTelegramIdentity(friend, userId) && friend.admissionState === "active" && friend.trustLevel === "friend"
1660
+ && friend.initiativePolicy === "request_follow_up_only" && friend.capabilityProfileId === "sanctuary-household");
1661
+ const owner = await store.findByExternalId("telegram-user", ownerUserId, botId);
1662
+ if (!owner || !canonicalOwner(owner)) {
1663
+ throw new Error("Telegram owner Friend must be active with the sanctuary-owner profile and exact bot identity");
1664
+ }
1665
+ const registry = (0, relationship_authorization_1.loadRelationshipCapabilityRegistry)(agentRoot);
1666
+ if (!registry.profiles["sanctuary-owner"] || !registry.profiles["sanctuary-household"]) {
1667
+ throw new Error("Telegram relationship capability registry is missing canonical profiles");
1668
+ }
1669
+ const assertLiveOwner = async (actorFriendId) => {
1670
+ const current = await store.get(owner.id);
1671
+ if (actorFriendId !== owner.id || !canonicalOwner(current))
1672
+ throw new Error("Only the live Telegram owner may manage contacts");
1673
+ };
1674
+ const admissionsRoot = path.join(agentRoot, "state", "senses", "telegram", "admissions");
1675
+ const invalidatePendingApprovals = (sessionPath) => {
1676
+ const stateRoot = path.join(agentRoot, "state", "approvals");
1677
+ const checkpoints = new approval_files_1.FileApprovalCheckpointStore(path.join(stateRoot, "checkpoints.json"));
1678
+ const tokens = new approval_files_1.FileApprovalTokenStore(path.join(stateRoot, "tokens.json"));
1679
+ const approvals = (0, approval_store_1.openApprovalStore)({ databasePath: path.join(stateRoot, "approvals.sqlite") });
1680
+ try {
1681
+ for (const checkpoint of checkpoints.list()) {
1682
+ const approval = approvals.read(checkpoint.approvalId);
1683
+ if (!approval || path.resolve(approval.sessionPath) !== path.resolve(sessionPath))
1684
+ continue;
1685
+ const decisionToken = tokens.get(checkpoint.approvalId);
1686
+ if (approval.state === "preparing")
1687
+ approvals.recoverPreparing({ approvalId: approval.approvalId, state: "abandoned_before_attempt", reason: "requesting relationship was revoked" });
1688
+ else if (approval.state === "awaiting_prompt_binding")
1689
+ approvals.abandonPromptBinding({ approvalId: approval.approvalId, reason: "requesting relationship was revoked" });
1690
+ else if (approval.state === "proposed" && decisionToken)
1691
+ approvals.decide({
1692
+ approvalId: approval.approvalId, decisionToken, decision: "deny", requesterId: approval.requesterId,
1693
+ transport: approval.transport, transportUserId: approval.transportUserId, transportChatId: approval.transportChatId,
1694
+ transportMessageId: approval.transportMessageId, sessionKey: approval.sessionKey, ownerId: `relationship-revocation-${(0, node_crypto_1.randomUUID)()}`,
1695
+ });
1696
+ else if (approval.state === "proposed")
1697
+ throw new Error(`Cannot revoke relationship while approval ${approval.approvalId} is missing its decision token`);
1698
+ else if (approval.state === "claimed" && approval.ownerId)
1699
+ approvals.abandonBeforeAttempt({ approvalId: approval.approvalId, ownerId: approval.ownerId, epoch: approval.epoch, reason: "requesting relationship was revoked" });
1700
+ else if (approval.state === "claimed")
1701
+ throw new Error(`Cannot revoke relationship while approval ${approval.approvalId} is missing its claim owner`);
1702
+ const terminal = approvals.read(checkpoint.approvalId);
1703
+ if (terminal && terminal.state !== "attempted")
1704
+ checkpoints.remove(checkpoint.approvalId);
1705
+ tokens.remove(checkpoint.approvalId);
1706
+ }
1707
+ }
1708
+ finally {
1709
+ approvals.close();
1710
+ }
1711
+ };
1712
+ const telegramContactManager = {
1713
+ list: async ({ actorFriendId }) => {
1714
+ await assertLiveOwner(actorFriendId);
1715
+ const contacts = (await store.listAll()).flatMap((friend) => {
1716
+ if (friend.id === owner.id)
1717
+ return [];
1718
+ const bindings = sameBotTelegramBindings(friend);
1719
+ if (bindings.length !== 1 || !/^[1-9][0-9]*$/u.test(bindings[0].externalId))
1720
+ return [];
1721
+ return [{ friendId: friend.id, name: friend.name, userId: bindings[0].externalId, admissionState: friend.admissionState ?? "unverified", initiativePolicy: friend.initiativePolicy ?? "none" }];
1722
+ }).slice(0, 256);
1723
+ const admissions = new telegram_admission_1.FileTelegramAdmissionStore(admissionsRoot);
1724
+ try {
1725
+ const blocked = admissions.list().filter((record) => record.status === "blocked" && record.botId === botId)
1726
+ .slice(0, 256).map((record) => ({ admissionId: record.id, userId: record.userId, displayCode: record.displayCode, status: "blocked", createdAt: new Date(record.createdAt).toISOString() }));
1727
+ return { contacts, blocked };
1728
+ }
1729
+ finally {
1730
+ admissions.close();
1731
+ }
1732
+ },
1733
+ revoke: async ({ actorFriendId, friendId }) => {
1734
+ await assertLiveOwner(actorFriendId);
1735
+ if (friendId === owner.id)
1736
+ throw new Error("The Telegram owner relationship cannot revoke itself");
1737
+ const current = await store.get(friendId);
1738
+ if (!current)
1739
+ throw new Error("Telegram contact was not found");
1740
+ const bindings = sameBotTelegramBindings(current);
1741
+ if (bindings.length !== 1 || !/^[1-9][0-9]*$/u.test(bindings[0].externalId) || bindings[0].externalId === ownerUserId)
1742
+ throw new Error("Telegram contact identity is not exact");
1743
+ const sessionKey = `telegram:${botId}:${bindings[0].externalId}`;
1744
+ const sessionPath = (0, shared_turn_1.getSenseSessionPath)(agentName, current.id, "telegram", sessionKey, agentRoot);
1745
+ await (0, session_transaction_1.withSessionTurnLease)(sessionPath, async () => {
1746
+ invalidatePendingApprovals(sessionPath);
1747
+ const live = await store.get(current.id);
1748
+ if (!live || !exactTelegramIdentity(live, bindings[0].externalId))
1749
+ throw new Error("Telegram contact identity changed");
1750
+ const { capabilityProfileId: _removedProfile, ...withoutProfile } = live;
1751
+ await store.put(live.id, { ...withoutProfile, admissionState: "revoked", initiativePolicy: "none", updatedAt: new Date().toISOString() });
1752
+ (0, tools_awaiting_1.cancelRelationshipFollowUps)(agentRoot, agentName, { friendId: live.id, channel: "telegram", key: sessionKey });
1753
+ });
1754
+ return { revoked: true, friendId };
1755
+ },
1756
+ unblock: async ({ actorFriendId, admissionId }) => {
1757
+ await assertLiveOwner(actorFriendId);
1758
+ const admissions = new telegram_admission_1.FileTelegramAdmissionStore(admissionsRoot);
1759
+ try {
1760
+ const record = admissions.read(admissionId);
1761
+ if (record.botId !== botId || record.userId !== record.chatId)
1762
+ throw new Error("Blocked Telegram contact identity changed");
1763
+ admissions.releaseBlock(admissionId);
1764
+ }
1765
+ finally {
1766
+ admissions.close();
1767
+ }
1768
+ return { unblocked: true, admissionId };
1769
+ },
1770
+ };
1771
+ const resolveEvaluator = async (input) => {
1772
+ const friend = await store.get(input.friendId);
1773
+ const ownerBound = friend?.id === owner.id;
1774
+ const coordinatesValid = ownerBound
1775
+ ? input.botId === botId && input.userId === ownerUserId && input.chatId === ownerChatId && input.sessionKey === ownerSessionKey
1776
+ : input.botId === botId && input.userId === input.chatId && input.sessionKey === `telegram:${botId}:${input.userId}`;
1777
+ if (!friend || !coordinatesValid || (ownerBound ? !canonicalOwner(friend) : !canonicalHousehold(friend, input.userId))) {
1778
+ throw new Error("Telegram relationship identity binding is not active");
1779
+ }
1780
+ return (0, relationship_authorization_1.createRelationshipAuthorizationEvaluator)({
1781
+ friend,
1782
+ registry: (0, relationship_authorization_1.loadRelationshipCapabilityRegistry)(agentRoot),
1783
+ requestId: input.requestId,
1784
+ requestPhase: "inbound",
1785
+ sessionEventId: input.sessionEventId,
1786
+ });
1787
+ };
1788
+ const admission = {
1789
+ ownerFriendId: owner.id,
1790
+ resolveOwner: async ({ botId: candidateBotId, userId, chatId, sessionKey }) => {
1791
+ if (candidateBotId !== botId || userId !== ownerUserId || chatId !== ownerChatId || sessionKey !== ownerSessionKey)
1792
+ return null;
1793
+ const exact = await store.findByExternalId("telegram-user", userId, botId);
1794
+ const current = await store.get(owner.id);
1795
+ return exact?.id === owner.id && canonicalOwner(current)
1796
+ ? { friendId: owner.id }
1797
+ : null;
1798
+ },
1799
+ resolveApprovedFriend: async ({ botId: candidateBotId, userId, chatId }) => {
1800
+ if (candidateBotId !== botId || userId !== chatId)
1801
+ return null;
1802
+ const friend = await store.findByExternalId("telegram-user", userId, botId);
1803
+ return friend && canonicalHousehold(friend, userId) ? { friendId: friend.id } : null;
1804
+ },
1805
+ claimFriend: async (input) => {
1806
+ if (input.botId !== botId || input.userId !== input.chatId)
1807
+ return { kind: "collision", reason: "Telegram identity is not bound to this bot and private chat" };
1808
+ const claimed = await store.claimExternalId({
1809
+ externalId: { provider: "telegram-user", externalId: input.userId, tenantId: botId, linkedAt: new Date().toISOString() },
1810
+ target: { kind: "create", record: { id: (0, node_crypto_1.randomUUID)(), name: "Household member" } },
1811
+ });
1812
+ const current = claimed.record;
1813
+ const updated = { ...current, trustLevel: input.defaults.trustLevel, admissionState: input.defaults.admissionState,
1814
+ initiativePolicy: input.defaults.initiativePolicy, capabilityProfileId: input.defaults.capabilityProfileId, updatedAt: new Date().toISOString() };
1815
+ await store.put(current.id, updated);
1816
+ return { kind: claimed.status === "created" ? "created" : "existing", friendId: current.id };
1817
+ },
1818
+ revokeFriend: async (input) => {
1819
+ if (input.botId !== botId || input.userId !== input.chatId)
1820
+ return { kind: "collision", reason: "Telegram revocation identity is invalid" };
1821
+ const current = await store.findByExternalId("telegram-user", input.userId, botId);
1822
+ if (!current || current.id !== input.friendId || !exactTelegramIdentity(current, input.userId))
1823
+ return { kind: "collision", reason: "Telegram revocation identity changed" };
1824
+ const { capabilityProfileId: _removedProfile, ...withoutProfile } = current;
1825
+ await store.put(current.id, { ...withoutProfile, admissionState: "revoked", initiativePolicy: "none", updatedAt: new Date().toISOString() });
1826
+ (0, tools_awaiting_1.cancelRelationshipFollowUps)(agentRoot, agentName, { friendId: current.id, channel: "telegram", key: `telegram:${botId}:${input.userId}` });
1827
+ return { kind: "revoked" };
1828
+ },
1829
+ };
1830
+ return {
1831
+ telegramContactManager,
1832
+ admission,
1833
+ resolveRelationshipAuthorization: resolveEvaluator,
1834
+ authorizeRelationshipEffect: async (input) => {
1835
+ if (input.target.kind !== "approved_relationship")
1836
+ return { allowed: false, reason: "relationship effect target is required" };
1837
+ const friend = await store.get(input.target.friendId);
1838
+ if (!friend)
1839
+ return { allowed: false, reason: "relationship admission is not active" };
1840
+ const bindings = sameBotTelegramBindings(friend);
1841
+ if (bindings.length !== 1 || !/^[1-9][0-9]*$/u.test(bindings[0].externalId))
1842
+ return { allowed: false, reason: "relationship Telegram identity binding is unavailable" };
1843
+ const chatId = bindings[0].externalId;
1844
+ if (friend.id === owner.id ? !canonicalOwner(friend) : !canonicalHousehold(friend, chatId))
1845
+ return { allowed: false, reason: "relationship admission is not active or canonical" };
1846
+ if (friend.id === owner.id) {
1847
+ if (chatId !== ownerUserId || chatId !== ownerChatId || input.target.sessionKey !== ownerSessionKey)
1848
+ return { allowed: false, reason: "owner Telegram identity or session binding changed" };
1849
+ }
1850
+ else if (input.target.sessionKey !== `telegram:${botId}:${chatId}`)
1851
+ return { allowed: false, reason: "relationship Telegram session binding changed" };
1852
+ const inboundIdempotency = input.target.requestId
1853
+ ? new RegExp(`^relationship-turn:${input.target.requestId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}:delivery:[0-9]+$`, "u").test(input.idempotencyKey)
1854
+ : false;
1855
+ const durableFollowUp = friend.id !== owner.id && input.target.requestId
1856
+ ? (0, tools_awaiting_1.hasActiveRelationshipFollowUp)(agentRoot, { friendId: friend.id, channel: "telegram", key: input.target.sessionKey, requestId: input.target.requestId, allowElapsed: input.idempotencyKey.startsWith("await-expiry-follow-up:") })
1857
+ : false;
1858
+ if (friend.id !== owner.id && input.target.requestId && !inboundIdempotency && !durableFollowUp)
1859
+ return { allowed: false, reason: "relationship follow-up is not bound to an active request await" };
1860
+ const evaluator = (0, relationship_authorization_1.createRelationshipAuthorizationEvaluator)({
1861
+ friend,
1862
+ registry: (0, relationship_authorization_1.loadRelationshipCapabilityRegistry)(agentRoot),
1863
+ ...(input.target.requestId ? { requestId: input.target.requestId, requestPhase: inboundIdempotency ? "inbound" : "follow_up" } : {}),
1864
+ });
1865
+ const scope = input.target.friendId === owner.id
1866
+ ? (input.target.requestId ? "telegram.owner_event" : "telegram.proactive")
1867
+ : "telegram.request_return";
1868
+ const authorization = evaluator.authorizeEffect(scope);
1869
+ return authorization.allowed
1870
+ ? { allowed: true, receiptId: authorization.receiptId, expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(), transport: { chatId } }
1871
+ : authorization;
1872
+ },
1873
+ };
1874
+ }
1223
1875
  async function startTelegramSenseApp(agentName) {
1224
- const app = createTelegramSenseApp({ agentName, credentials: loadTelegramSenseCredentials(agentName) });
1876
+ const loaded = loadTelegramSenseCredentials(agentName);
1877
+ const credentials = { ...loaded, botId: telegramBotIdFromToken(loaded.botToken) };
1878
+ const app = createTelegramSenseApp({
1879
+ agentName,
1880
+ credentials,
1881
+ ...(await createProductionTelegramRelationshipComposition(agentName, credentials)),
1882
+ ...(agentName === "sanctuary" ? { privilegedFailsafe: {
1883
+ eventRoot: (0, router_1.getExternalEventRoot)(),
1884
+ verifyProtectiveState: createSabQueueProtectiveStateVerifier(),
1885
+ } } : {}),
1886
+ });
1225
1887
  (0, runtime_1.emitNervesEvent)({
1226
1888
  component: "senses",
1227
1889
  event: "senses.telegram_app_ready",
@@ -1230,3 +1892,21 @@ async function startTelegramSenseApp(agentName) {
1230
1892
  });
1231
1893
  return app;
1232
1894
  }
1895
+ async function sendTelegramExternalEventDecision(agentName, input) {
1896
+ const app = await startTelegramSenseApp(agentName);
1897
+ try {
1898
+ await app.sendExternalEventDecision(input);
1899
+ }
1900
+ finally {
1901
+ await app.stop();
1902
+ }
1903
+ }
1904
+ async function sendTelegramAwaitFollowUp(agentName, request) {
1905
+ const app = await startTelegramSenseApp(agentName);
1906
+ try {
1907
+ return await app.sendAwaitFollowUp(request);
1908
+ }
1909
+ finally {
1910
+ await app.stop();
1911
+ }
1912
+ }