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

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 +26 -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
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.telegramApprovalCommitBarrierHooks = telegramApprovalCommitBarrierHooks;
37
37
  exports.telegramApprovalDecisionBarrierHooks = telegramApprovalDecisionBarrierHooks;
38
38
  exports.approvalContinuationRunAgentOptions = approvalContinuationRunAgentOptions;
39
+ exports.formatTelegramApprovalPrompt = formatTelegramApprovalPrompt;
39
40
  exports.executeApprovedTelegramTool = executeApprovedTelegramTool;
40
41
  exports.createTelegramApprovalRuntime = createTelegramApprovalRuntime;
41
42
  const path = __importStar(require("node:path"));
@@ -45,6 +46,7 @@ const approval_store_1 = require("../heart/approval-store");
45
46
  const tool_approval_1 = require("../heart/tool-approval");
46
47
  const core_1 = require("../heart/core");
47
48
  const identity_1 = require("../heart/identity");
49
+ const session_events_1 = require("../heart/session-events");
48
50
  const sanctuary_acceptance_marker_1 = require("../heart/daemon/sanctuary-acceptance-marker");
49
51
  const telegram_1 = require("./telegram");
50
52
  const context_1 = require("../mind/context");
@@ -69,6 +71,14 @@ function telegramApprovalDecisionBarrierHooks(effectBarrier) {
69
71
  function approvalContinuationRunAgentOptions(toolContext, approvalCoordinator) {
70
72
  return { toolContext: toolContext, approvalCoordinator };
71
73
  }
74
+ function formatTelegramApprovalPrompt(toolName, args) {
75
+ if (toolName === "sanctuary_resume_download_queue")
76
+ return "Resume household downloads? This can spend prepaid download credit. I’ll verify the queue actually resumed.";
77
+ const container = args.container;
78
+ if (toolName === "unraid_restart_container" && typeof container === "string" && container.length > 0 && container.length <= 128 && !container.includes("\n"))
79
+ return `Restart ${container}?`;
80
+ return `Approve ${toolName} with exact arguments ${JSON.stringify(args)}?`;
81
+ }
72
82
  async function executeApprovedTelegramTool(name, args, execute, scenarioHandleDigest, approvalId, effectBarrier = () => undefined) {
73
83
  if (name === "unraid_restart_container")
74
84
  (0, runtime_1.emitNervesEvent)({
@@ -80,6 +90,21 @@ async function executeApprovedTelegramTool(name, args, execute, scenarioHandleDi
80
90
  try {
81
91
  effectBarrier();
82
92
  const result = await execute(name, args);
93
+ if (name === "sanctuary_resume_download_queue") {
94
+ let parsed;
95
+ try {
96
+ parsed = JSON.parse(result);
97
+ }
98
+ catch {
99
+ throw new tool_approval_1.ApprovalExecutionFailedError("approved download resume returned an invalid result");
100
+ }
101
+ const data = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
102
+ const outcome = data?.data && typeof data.data === "object" && !Array.isArray(data.data) ? data.data : null;
103
+ const after = outcome?.after && typeof outcome.after === "object" && !Array.isArray(outcome.after) ? outcome.after : null;
104
+ if (data?.ok !== true || outcome?.verified !== true || after?.paused !== false)
105
+ throw new tool_approval_1.ApprovalExecutionFailedError("approved download resume was not independently verified");
106
+ return result;
107
+ }
83
108
  if (name !== "unraid_restart_container")
84
109
  return result;
85
110
  let parsed;
@@ -152,7 +177,9 @@ function createTelegramApprovalRuntime(options) {
152
177
  const provider = options.dependencies?.runProvider ?? core_1.runAgent;
153
178
  const resolveTool = options.dependencies?.resolveTool ?? tools_1.resolveToolDefinition;
154
179
  const executeTool = options.dependencies?.executeTool ?? tools_1.execTool;
155
- const stateRoot = path.join(options.dependencies?.agentRoot ?? (0, identity_1.getAgentRoot)(options.agentName), "state", "approvals");
180
+ const agentRoot = options.dependencies?.agentRoot ?? (0, identity_1.getAgentRoot)(options.agentName);
181
+ const liveToolContext = { ...options.toolContext, agentRoot: options.toolContext.agentRoot ?? agentRoot };
182
+ const stateRoot = path.join(agentRoot, "state", "approvals");
156
183
  const store = (0, approval_store_1.openApprovalStore)({ databasePath: path.join(stateRoot, "approvals.sqlite"), now: () => new Date(now()) });
157
184
  const checkpoints = new approval_files_1.FileApprovalCheckpointStore(path.join(stateRoot, "checkpoints.json"));
158
185
  const tokens = new approval_files_1.FileApprovalTokenStore(path.join(stateRoot, "tokens.json"));
@@ -212,7 +239,7 @@ function createTelegramApprovalRuntime(options) {
212
239
  preCallMessages: request.preCallMessages,
213
240
  hooks: telegramApprovalCommitBarrierHooks(effectBarrier),
214
241
  });
215
- const prompt = `Approve ${request.toolCall.function.name} with exact arguments ${JSON.stringify(request.arguments)}?`;
242
+ const prompt = formatTelegramApprovalPrompt(request.toolCall.function.name, request.arguments);
216
243
  effectBarrier();
217
244
  const actionDigest = (0, node_crypto_1.createHash)("sha256").update(JSON.stringify({ toolName: committed.record.toolName, argumentDigest: committed.record.argumentDigest })).digest("hex");
218
245
  const targetDigest = (0, node_crypto_1.createHash)("sha256").update(JSON.stringify({ container: committed.record.arguments.container })).digest("hex");
@@ -269,6 +296,7 @@ function createTelegramApprovalRuntime(options) {
269
296
  return { accepted: false, terminalText: "⚠️ Approval checkpoint is unavailable" };
270
297
  const continuationOwnerId = `telegram-continuation-${(0, node_crypto_1.randomUUID)()}`;
271
298
  let continuationEpoch = 0;
299
+ let continuationCausalEventId;
272
300
  const continuationCoordinator = {
273
301
  propose: (request) => coordinator({
274
302
  sessionPath: record.sessionPath,
@@ -293,11 +321,18 @@ function createTelegramApprovalRuntime(options) {
293
321
  markContinuationAttempted: () => { effectBarrier(); store.markContinuationAttempted({ approvalId: record.approvalId, ownerId: continuationOwnerId, epoch: continuationEpoch }); },
294
322
  completeContinuation: () => { effectBarrier(); store.completeContinuation({ approvalId: record.approvalId, ownerId: continuationOwnerId, epoch: continuationEpoch }); },
295
323
  runAgent: provider,
296
- runAgentOptions: approvalContinuationRunAgentOptions(options.toolContext, continuationCoordinator),
297
- persist: (messages, result) => { effectBarrier(); (0, context_1.saveSession)(record.sessionPath, messages, result?.usage, undefined, lease); },
324
+ runAgentOptions: approvalContinuationRunAgentOptions(liveToolContext, continuationCoordinator),
325
+ persist: (messages, result) => {
326
+ effectBarrier();
327
+ const existingEventIds = new Set((0, session_events_1.loadSessionEnvelopeFile)(record.sessionPath)?.events.map((event) => event.id) ?? []);
328
+ const events = (0, context_1.saveSession)(record.sessionPath, messages, result?.usage, undefined, lease);
329
+ continuationCausalEventId = result
330
+ ? [...events].reverse().find((event) => event.role === "assistant" && !existingEventIds.has(event.id))?.id
331
+ : undefined;
332
+ },
298
333
  deliver: async (text) => {
299
334
  effectBarrier();
300
- const messageIds = await (0, telegram_client_1.sendTelegramText)(options.api, options.authorizedChatId, text);
335
+ const messageIds = await options.effects.sendText({ idempotencyKey: `approval:${record.approvalId}:continuation:${(0, node_crypto_1.createHash)("sha256").update(text).digest("hex")}`, chatId: options.authorizedChatId, text, authorClass: "butler", ...(continuationCausalEventId ? { causalEventId: continuationCausalEventId } : {}) });
301
336
  effectBarrier();
302
337
  if (acceptanceBinding) {
303
338
  const unsigned = {
@@ -322,6 +357,7 @@ function createTelegramApprovalRuntime(options) {
322
357
  };
323
358
  transport = (0, telegram_client_1.createTelegramApprovalTransport)({
324
359
  api: options.api,
360
+ effects: options.effects,
325
361
  expectedUserId: options.authorizedUserId,
326
362
  expectedChatId: options.authorizedChatId,
327
363
  pendingStore,
@@ -375,11 +411,15 @@ function createTelegramApprovalRuntime(options) {
375
411
  ownerId,
376
412
  currentSessionRevision: (0, session_transaction_1.readSessionTransaction)(existing.sessionPath, lease).revision,
377
413
  resolveTool,
414
+ resolveApprovalPolicy: (name, args) => (0, tools_1.approvalPolicyForInvocation)(name, args, liveToolContext),
378
415
  liveGuard: async () => ({ ok: true }),
379
416
  liveRisk: async () => ({ ok: true }),
380
417
  hooks: telegramApprovalDecisionBarrierHooks(effectBarrier),
381
418
  execute: (name, args) => {
382
- const execute = () => executeApprovedTelegramTool(name, args, (toolName, toolArgs) => executeTool(toolName, toolArgs, options.toolContext), decisionScenarioDigest, existing.approvalId, effectBarrier);
419
+ const approvedToolContext = name === "unraid_restart_container"
420
+ ? { ...options.toolContext, relationshipAuthorization: undefined }
421
+ : options.toolContext;
422
+ const execute = () => executeApprovedTelegramTool(name, args, (toolName, toolArgs) => executeTool(toolName, toolArgs, approvedToolContext), decisionScenarioDigest, existing.approvalId, effectBarrier);
383
423
  return decisionScenarioDigest
384
424
  ? (0, sanctuary_acceptance_marker_1.runWithSanctuaryAcceptanceApproval)({ approvalId: existing.approvalId, argumentDigest: existing.argumentDigest }, execute)
385
425
  : execute();
@@ -84,6 +84,11 @@ const TELEGRAM_UPDATE_DIGEST_DOMAIN = "ouroboros.telegram.update.v1";
84
84
  const TELEGRAM_UPDATE_SEQUENCE_DOMAIN = "ouroboros.telegram.update-sequence.v1";
85
85
  const TELEGRAM_UPDATE_DIGEST = /^tgu_[A-Za-z0-9_-]{43}$/u;
86
86
  const TELEGRAM_UPDATE_SEQUENCE_DIGEST = /^tgs_[A-Za-z0-9_-]{43}$/u;
87
+ const TELEGRAM_ADMISSION_ID = /^[a-f0-9]{20}$/u;
88
+ const TELEGRAM_SESSION_EVENT_ID = /^evt-[0-9]{6,}$/u;
89
+ function boundedTelegramWorkText(value, maxLength) {
90
+ return typeof value === "string" && value.length > 0 && value.length <= maxLength;
91
+ }
87
92
  function updateReceipt(update) {
88
93
  return {
89
94
  digest: `tgu_${(0, node_crypto_1.createHash)("sha256").update(`${TELEGRAM_UPDATE_DIGEST_DOMAIN}\0${JSON.stringify(update)}`, "utf8").digest("base64url")}`,
@@ -147,6 +152,47 @@ class FileTelegramUpdateInboxStore {
147
152
  && Number.isSafeInteger(record.quarantinedAt)
148
153
  && record.quarantinedAt >= 0
149
154
  && typeof record.warningAcknowledged === "boolean";
155
+ const validAdmitted = (record) => Boolean(record)
156
+ && typeof record === "object"
157
+ && !Array.isArray(record)
158
+ && Object.keys(record).sort().join(",") === "admissionId,eventId,friendId,reference,sessionKey"
159
+ && TELEGRAM_ADMISSION_ID.test(record.admissionId)
160
+ && boundedTelegramWorkText(record.friendId, 512)
161
+ && boundedTelegramWorkText(record.sessionKey, 1_024)
162
+ && TELEGRAM_SESSION_EVENT_ID.test(record.eventId)
163
+ && record.reference === `telegram-admission:${record.admissionId}`;
164
+ const validIndeterminateAdmitted = (record) => {
165
+ if (!record || typeof record !== "object" || Array.isArray(record)
166
+ || Object.keys(record).sort().join(",") !== "admissionId,eventId,friendId,quarantinedAt,reference,sessionKey")
167
+ return false;
168
+ const { quarantinedAt, ...work } = record;
169
+ return Number.isSafeInteger(quarantinedAt) && quarantinedAt >= 0 && validAdmitted(work);
170
+ };
171
+ if (value.version === 5) {
172
+ if (Object.keys(value).sort().join(",") !== "admittedDispatching,admittedIndeterminate,admittedPending,admittedSettled,dispatching,indeterminate,pending,settled,version"
173
+ || !Array.isArray(value.pending) || !Array.isArray(value.dispatching) || !Array.isArray(value.indeterminate) || !Array.isArray(value.settled)
174
+ || !Array.isArray(value.admittedPending) || !Array.isArray(value.admittedDispatching)
175
+ || !Array.isArray(value.admittedSettled) || !Array.isArray(value.admittedIndeterminate)
176
+ || !value.pending.every(validReceipt) || !value.dispatching.every(validReceipt) || !value.settled.every(validReceipt)
177
+ || !value.indeterminate.every(validIndeterminate)
178
+ || !value.admittedPending.every(validAdmitted) || !value.admittedDispatching.every(validAdmitted)
179
+ || !value.admittedSettled.every(validAdmitted) || !value.admittedIndeterminate.every(validIndeterminateAdmitted)) {
180
+ throw new Error("invalid bounded inbox shape");
181
+ }
182
+ const state = structuredClone(value);
183
+ const timestamp = this.timestamp();
184
+ const prunedUpdates = this.prune(state.indeterminate, timestamp);
185
+ const prunedAdmitted = state.admittedIndeterminate
186
+ .filter((work) => work.quarantinedAt >= timestamp - this.indeterminateRetentionMs)
187
+ .sort((left, right) => left.quarantinedAt - right.quarantinedAt || left.admissionId.localeCompare(right.admissionId))
188
+ .slice(-this.maxIndeterminateReceipts);
189
+ if (prunedUpdates.length !== state.indeterminate.length || prunedAdmitted.length !== state.admittedIndeterminate.length) {
190
+ state.indeterminate = prunedUpdates;
191
+ state.admittedIndeterminate = prunedAdmitted;
192
+ this.write(state);
193
+ }
194
+ return state;
195
+ }
150
196
  if (value.version === 4) {
151
197
  if (Object.keys(value).sort().join(",") !== "dispatching,indeterminate,pending,settled,version"
152
198
  || !Array.isArray(value.pending) || !Array.isArray(value.dispatching) || !Array.isArray(value.indeterminate)
@@ -155,12 +201,17 @@ class FileTelegramUpdateInboxStore {
155
201
  || !value.settled.every(validReceipt)
156
202
  || !value.indeterminate.every(validIndeterminate))
157
203
  throw new Error("invalid bounded inbox shape");
158
- const state = structuredClone(value);
204
+ const state = {
205
+ ...structuredClone(value),
206
+ version: 5,
207
+ admittedPending: [],
208
+ admittedDispatching: [],
209
+ admittedSettled: [],
210
+ admittedIndeterminate: [],
211
+ };
159
212
  const pruned = this.prune(state.indeterminate, this.timestamp());
160
- if (pruned.length !== state.indeterminate.length) {
161
- state.indeterminate = pruned;
162
- this.write(state);
163
- }
213
+ state.indeterminate = pruned;
214
+ this.write(state);
164
215
  return state;
165
216
  }
166
217
  if (value.version === 3) {
@@ -171,11 +222,12 @@ class FileTelegramUpdateInboxStore {
171
222
  throw new Error("invalid bounded inbox shape");
172
223
  const timestamp = this.timestamp();
173
224
  const migrated = {
174
- version: 4,
225
+ version: 5,
175
226
  pending: structuredClone(value.pending),
176
227
  dispatching: structuredClone(value.dispatching),
177
228
  settled: [],
178
229
  indeterminate: this.prune(structuredClone(value.indeterminate), timestamp),
230
+ admittedPending: [], admittedDispatching: [], admittedSettled: [], admittedIndeterminate: [],
179
231
  };
180
232
  this.write(migrated);
181
233
  return migrated;
@@ -190,7 +242,7 @@ class FileTelegramUpdateInboxStore {
190
242
  throw new Error("invalid opaque inbox receipt");
191
243
  const timestamp = this.timestamp();
192
244
  const migrated = {
193
- version: 4,
245
+ version: 5,
194
246
  pending: structuredClone(value.pending),
195
247
  dispatching: structuredClone(value.dispatching),
196
248
  settled: [],
@@ -199,6 +251,7 @@ class FileTelegramUpdateInboxStore {
199
251
  quarantinedAt: timestamp,
200
252
  warningAcknowledged: false,
201
253
  })), timestamp),
254
+ admittedPending: [], admittedDispatching: [], admittedSettled: [], admittedIndeterminate: [],
202
255
  };
203
256
  this.write(migrated);
204
257
  return migrated;
@@ -217,7 +270,7 @@ class FileTelegramUpdateInboxStore {
217
270
  throw new Error("invalid legacy inbox record");
218
271
  const timestamp = this.timestamp();
219
272
  const migrated = {
220
- version: 4,
273
+ version: 5,
221
274
  pending: [],
222
275
  dispatching: [],
223
276
  settled: [],
@@ -226,13 +279,17 @@ class FileTelegramUpdateInboxStore {
226
279
  quarantinedAt: timestamp,
227
280
  warningAcknowledged: false,
228
281
  })), timestamp),
282
+ admittedPending: [], admittedDispatching: [], admittedSettled: [], admittedIndeterminate: [],
229
283
  };
230
284
  this.write(migrated);
231
285
  return migrated;
232
286
  }
233
287
  catch (error) {
234
288
  if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
235
- return { version: 4, pending: [], dispatching: [], settled: [], indeterminate: [] };
289
+ return {
290
+ version: 5, pending: [], dispatching: [], settled: [], indeterminate: [],
291
+ admittedPending: [], admittedDispatching: [], admittedSettled: [], admittedIndeterminate: [],
292
+ };
236
293
  throw new Error("Telegram update inbox state is corrupt", { cause: error });
237
294
  }
238
295
  }
@@ -322,6 +379,94 @@ class FileTelegramUpdateInboxStore {
322
379
  state.settled = settled;
323
380
  this.write(state);
324
381
  }
382
+ captureAdmittedWork(work) {
383
+ if (!this.validAdmittedWork(work))
384
+ throw new Error("Telegram admitted work is invalid");
385
+ const state = this.read();
386
+ const existing = [
387
+ ...state.admittedPending,
388
+ ...state.admittedDispatching,
389
+ ...state.admittedSettled,
390
+ ...state.admittedIndeterminate,
391
+ ].find((candidate) => candidate.admissionId === work.admissionId);
392
+ if (existing) {
393
+ const { quarantinedAt: _quarantinedAt, ...canonical } = existing;
394
+ if (JSON.stringify(canonical) !== JSON.stringify(work))
395
+ throw new Error("Telegram admitted work has a conflicting receipt");
396
+ return false;
397
+ }
398
+ state.admittedPending.push(structuredClone(work));
399
+ this.write(state);
400
+ return true;
401
+ }
402
+ loadPendingAdmittedWork() {
403
+ return structuredClone(this.read().admittedPending);
404
+ }
405
+ claimAdmittedWork(admissionId) {
406
+ if (!TELEGRAM_ADMISSION_ID.test(admissionId))
407
+ throw new Error("Telegram admission id is invalid");
408
+ const state = this.read();
409
+ if (state.admittedDispatching.some((candidate) => candidate.admissionId === admissionId)
410
+ || state.admittedSettled.some((candidate) => candidate.admissionId === admissionId)
411
+ || state.admittedIndeterminate.some((candidate) => candidate.admissionId === admissionId))
412
+ return null;
413
+ const index = state.admittedPending.findIndex((candidate) => candidate.admissionId === admissionId);
414
+ if (index < 0)
415
+ return null;
416
+ const [work] = state.admittedPending.splice(index, 1);
417
+ state.admittedDispatching.push(work);
418
+ this.write(state);
419
+ return structuredClone(work);
420
+ }
421
+ completeAdmittedWork(admissionId) {
422
+ if (!TELEGRAM_ADMISSION_ID.test(admissionId))
423
+ throw new Error("Telegram admission id is invalid");
424
+ const state = this.read();
425
+ const index = state.admittedDispatching.findIndex((candidate) => candidate.admissionId === admissionId);
426
+ if (index < 0)
427
+ return;
428
+ const [work] = state.admittedDispatching.splice(index, 1);
429
+ state.admittedSettled.push(work);
430
+ state.admittedSettled = state.admittedSettled.slice(-this.maxIndeterminateReceipts);
431
+ this.write(state);
432
+ }
433
+ quarantineStrandedAdmittedWork() {
434
+ const state = this.read();
435
+ if (state.admittedDispatching.length === 0)
436
+ return [];
437
+ const timestamp = this.timestamp();
438
+ const stranded = state.admittedDispatching.map((work) => ({ ...work, quarantinedAt: timestamp }));
439
+ state.admittedDispatching = [];
440
+ state.admittedIndeterminate = [...state.admittedIndeterminate, ...stranded]
441
+ .filter((work) => work.quarantinedAt >= timestamp - this.indeterminateRetentionMs)
442
+ .sort((left, right) => left.quarantinedAt - right.quarantinedAt || left.admissionId.localeCompare(right.admissionId))
443
+ .slice(-this.maxIndeterminateReceipts);
444
+ this.write(state);
445
+ return stranded.map(({ quarantinedAt: _quarantinedAt, ...work }) => work);
446
+ }
447
+ admittedWorkState(admissionId) {
448
+ if (!TELEGRAM_ADMISSION_ID.test(admissionId))
449
+ throw new Error("Telegram admission id is invalid");
450
+ const state = this.read();
451
+ if (state.admittedPending.some((work) => work.admissionId === admissionId))
452
+ return "pending";
453
+ if (state.admittedDispatching.some((work) => work.admissionId === admissionId))
454
+ return "dispatching";
455
+ if (state.admittedSettled.some((work) => work.admissionId === admissionId))
456
+ return "settled";
457
+ if (state.admittedIndeterminate.some((work) => work.admissionId === admissionId))
458
+ return "indeterminate";
459
+ return null;
460
+ }
461
+ validAdmittedWork(work) {
462
+ return Boolean(work) && typeof work === "object" && !Array.isArray(work)
463
+ && Object.keys(work).sort().join(",") === "admissionId,eventId,friendId,reference,sessionKey"
464
+ && TELEGRAM_ADMISSION_ID.test(work.admissionId)
465
+ && boundedTelegramWorkText(work.friendId, 512)
466
+ && boundedTelegramWorkText(work.sessionKey, 1_024)
467
+ && TELEGRAM_SESSION_EVENT_ID.test(work.eventId)
468
+ && work.reference === `telegram-admission:${work.admissionId}`;
469
+ }
325
470
  }
326
471
  exports.FileTelegramUpdateInboxStore = FileTelegramUpdateInboxStore;
327
472
  function createTelegramLongPoll(options) {
@@ -340,7 +485,38 @@ function createTelegramLongPoll(options) {
340
485
  const chatId = message ? String(message.chat.id) : "";
341
486
  if (!message || message.chat.type !== "private" || userId !== options.expectedUserId || chatId !== options.expectedChatId || typeof message.text !== "string")
342
487
  return null;
343
- return { updateId: update.update_id, messageId: String(message.message_id), userId, chatId, text: message.text };
488
+ return {
489
+ updateId: update.update_id,
490
+ messageId: String(message.message_id),
491
+ userId,
492
+ chatId,
493
+ text: message.text,
494
+ ...(Number.isSafeInteger(message.reply_to_message?.message_id) && message.reply_to_message.message_id > 0
495
+ ? { replyToMessageId: String(message.reply_to_message.message_id) }
496
+ : {}),
497
+ };
498
+ };
499
+ const unknownMessage = (update) => {
500
+ const message = update.message;
501
+ if (!options.onUnknownMessage || !options.botId || !message?.from || message.chat.type !== "private")
502
+ return null;
503
+ const userId = String(message.from.id);
504
+ const chatId = String(message.chat.id);
505
+ if (userId === options.expectedUserId && chatId === options.expectedChatId)
506
+ return null;
507
+ const displayLabel = [message.from.first_name, message.from.last_name].filter(Boolean).join(" ")
508
+ || (message.from.username ? `@${message.from.username}` : `Telegram user ${userId}`);
509
+ return {
510
+ updateId: update.update_id,
511
+ messageId: message.message_id,
512
+ botId: options.botId,
513
+ userId,
514
+ chatId,
515
+ text: message.text ?? message.caption ?? "",
516
+ displayLabel,
517
+ hasAttachments: Boolean(message.document || message.photo?.length || message.audio || message.video
518
+ || message.voice || message.animation || message.sticker),
519
+ };
344
520
  };
345
521
  const authorizedCallback = (update) => {
346
522
  const callback = update.callback_query;
@@ -349,7 +525,9 @@ function createTelegramLongPoll(options) {
349
525
  && String(callback.message.chat.id) === options.expectedChatId);
350
526
  };
351
527
  const dispatch = async (update) => {
352
- const handled = await options.onUpdate?.(update) ?? false;
528
+ const handled = !update.callback_query || authorizedCallback(update)
529
+ ? await options.onUpdate?.(update) ?? false
530
+ : false;
353
531
  if (handled)
354
532
  return;
355
533
  const message = authorizedMessage(update);
@@ -357,6 +535,11 @@ function createTelegramLongPoll(options) {
357
535
  await options.onMessage(message);
358
536
  return;
359
537
  }
538
+ const stranger = unknownMessage(update);
539
+ if (stranger) {
540
+ await options.onUnknownMessage(stranger);
541
+ return;
542
+ }
360
543
  const distinctAccount = Boolean(update.message?.from && String(update.message.from.id) !== options.expectedUserId);
361
544
  (0, runtime_1.emitNervesEvent)({
362
545
  component: "senses",
@@ -399,7 +582,7 @@ function createTelegramLongPoll(options) {
399
582
  continue;
400
583
  options.onBeforeDispatch?.();
401
584
  const next = update.update_id + 1;
402
- const requiresDurableDispatch = Boolean(authorizedCallback(update) || authorizedMessage(update));
585
+ const requiresDurableDispatch = Boolean(authorizedCallback(update) || authorizedMessage(update) || unknownMessage(update));
403
586
  const newlyCaptured = requiresDurableDispatch ? (options.inboxStore?.capture(update) ?? true) : true;
404
587
  if (newlyCaptured) {
405
588
  if (requiresDurableDispatch && options.inboxStore && !options.inboxStore.claim(update)) {
@@ -833,31 +1016,14 @@ function createTelegramApprovalTransport(options) {
833
1016
  if (pending.messageId === null)
834
1017
  return now();
835
1018
  const terminalEditStartedAt = now();
836
- const base = {
837
- chat_id: options.expectedChatId,
838
- message_id: Number(pending.messageId),
839
- reply_markup: { inline_keyboard: [] },
840
- };
841
- const editSignal = AbortSignal.timeout(exports.TELEGRAM_APPROVAL_TERMINAL_EDIT_TIMEOUT_MS);
842
1019
  try {
843
1020
  effectBarrier();
844
- await options.api.request("editMessageText", { ...base, text: escapeTelegramHtml(terminalText), parse_mode: "HTML" }, editSignal);
1021
+ await options.effects.edit({ idempotencyKey: `approval:${pending.approvalId}:edit:${(0, node_crypto_1.createHash)("sha256").update(terminalText).digest("hex")}`, chatId: options.expectedChatId, messageId: Number(pending.messageId), text: terminalText });
845
1022
  }
846
1023
  catch (error) {
847
1024
  const alreadyTerminal = error instanceof TelegramApiError && error.status === 400 && /message is not modified/i.test(error.message);
848
- if (!alreadyTerminal) {
849
- if (!(error instanceof TelegramApiError) || error.status !== 400)
850
- throw error;
851
- try {
852
- effectBarrier();
853
- await options.api.request("editMessageText", { ...base, text: terminalText }, editSignal);
854
- }
855
- catch (fallbackError) {
856
- if (!(fallbackError instanceof TelegramApiError) || fallbackError.status !== 400 || !/message is not modified/i.test(fallbackError.message)) {
857
- throw fallbackError;
858
- }
859
- }
860
- }
1025
+ if (!alreadyTerminal)
1026
+ throw error;
861
1027
  }
862
1028
  const terminalizedAt = now();
863
1029
  if (pending.acceptanceBinding)
@@ -876,11 +1042,11 @@ function createTelegramApprovalTransport(options) {
876
1042
  const acknowledge = async (callbackQueryId, invalid) => {
877
1043
  try {
878
1044
  effectBarrier();
879
- await options.api.request("answerCallbackQuery", invalid ? {
880
- callback_query_id: callbackQueryId,
881
- text: "This approval is no longer valid.",
882
- show_alert: true,
883
- } : { callback_query_id: callbackQueryId });
1045
+ await options.effects.acknowledge({
1046
+ idempotencyKey: `approval-callback:${(0, node_crypto_1.createHash)("sha256").update(callbackQueryId).digest("hex")}`,
1047
+ callbackQueryId,
1048
+ ...(invalid ? { text: "This approval is no longer valid.", showAlert: true } : {}),
1049
+ });
884
1050
  return "acknowledged";
885
1051
  }
886
1052
  catch (error) {
@@ -1027,16 +1193,6 @@ function createTelegramApprovalTransport(options) {
1027
1193
  if (pendingByCallback.has(approveCallbackData) || pendingByCallback.has(denyCallbackData)) {
1028
1194
  throw new Error("Telegram approval callback handle collision");
1029
1195
  }
1030
- const replyMarkup = { inline_keyboard: [[
1031
- { text: "Approve", callback_data: approveCallbackData },
1032
- { text: "Deny", callback_data: denyCallbackData },
1033
- ]] };
1034
- const htmlBody = {
1035
- chat_id: options.expectedChatId,
1036
- text: escapeTelegramHtml(input.prompt),
1037
- parse_mode: "HTML",
1038
- reply_markup: replyMarkup,
1039
- };
1040
1196
  const pending = {
1041
1197
  approvalId: input.approvalId,
1042
1198
  decisionToken: input.decisionToken,
@@ -1065,26 +1221,16 @@ function createTelegramApprovalTransport(options) {
1065
1221
  throw error;
1066
1222
  }
1067
1223
  try {
1068
- let result;
1069
- try {
1070
- effectBarrier();
1071
- result = await options.api.request("sendMessage", htmlBody);
1072
- }
1073
- catch (error) {
1074
- if (!(error instanceof TelegramApiError) || error.status !== 400)
1075
- throw error;
1076
- effectBarrier();
1077
- result = await options.api.request("sendMessage", {
1078
- chat_id: options.expectedChatId,
1079
- text: input.prompt,
1080
- reply_markup: replyMarkup,
1081
- });
1082
- }
1083
- if (!result || typeof result !== "object" || Array.isArray(result) || !("message_id" in result)
1084
- || typeof result.message_id !== "number" || !Number.isSafeInteger(result.message_id) || result.message_id <= 0) {
1224
+ effectBarrier();
1225
+ const messageId = await options.effects.sendCard({
1226
+ idempotencyKey: `approval:${input.approvalId}:prompt`,
1227
+ chatId: options.expectedChatId,
1228
+ text: input.prompt,
1229
+ buttons: [[{ text: "Approve", callbackData: approveCallbackData }, { text: "Deny", callbackData: denyCallbackData }]],
1230
+ });
1231
+ if (!Number.isSafeInteger(messageId) || messageId <= 0)
1085
1232
  throw new Error("Telegram sendMessage response did not include a canonical message_id");
1086
- }
1087
- pending.messageId = String(result.message_id);
1233
+ pending.messageId = String(messageId);
1088
1234
  pending.deliveryState = "bound";
1089
1235
  if (input.acceptanceBinding) {
1090
1236
  const boundAt = now();