@ouro.bot/cli 0.1.0-alpha.741 → 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.
- package/changelog.json +42 -0
- package/deploy/unraid/Dockerfile +7 -2
- package/deploy/unraid/README.txt +717 -106
- package/deploy/unraid/migrate-sanctuary-bundle.mjs +21 -0
- package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
- package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
- package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
- package/deploy/unraid/ouro-events/install-usenet-guard.sh +434 -0
- package/deploy/unraid/ouro-events/usenet-health.sh +178 -0
- package/deploy/unraid/sanctuary-acceptance-contract.json +4 -3
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +5 -1
- package/deploy/unraid/sanctuary.ouro/agent.json +2 -2
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +2 -2
- package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +1 -1
- package/deploy/unraid/sanctuary.ouro/provider-readiness.json +3 -10
- package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -1
- package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -1
- package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +4 -0
- package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +6 -2
- package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +4 -2
- package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +19 -17
- package/deploy/unraid/sanctuary.xml +12 -9
- package/dist/arc/cares.js +171 -37
- package/dist/arc/obligations.js +38 -0
- package/dist/heart/awaiting/await-alert.js +4 -2
- package/dist/heart/awaiting/await-expiry.js +39 -4
- package/dist/heart/awaiting/await-parser.js +8 -0
- package/dist/heart/context-loss-gauntlet.js +31 -2
- package/dist/heart/context-loss-sentinel.js +2 -0
- package/dist/heart/core.js +37 -7
- package/dist/heart/cross-chat-delivery.js +1 -0
- package/dist/heart/daemon/container-healthcheck.js +3 -1
- package/dist/heart/daemon/container-runtime.js +13 -0
- package/dist/heart/daemon/container-spec-auditor-main.js +11 -7
- package/dist/heart/daemon/container-spec-auditor.js +38 -11
- package/dist/heart/daemon/daemon-entry.js +19 -5
- package/dist/heart/daemon/daemon.js +154 -22
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +151 -87
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +26 -29
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +42 -51
- package/dist/heart/daemon/sanctuary-bundle-migration.js +398 -0
- package/dist/heart/daemon/sanctuary-scheduler-origin.js +11 -5
- package/dist/heart/daemon/supercronic-supervisor.js +16 -1
- package/dist/heart/external-events/router.js +1025 -43
- package/dist/heart/provider-models.js +10 -3
- package/dist/heart/session-events.js +23 -5
- package/dist/heart/steward-policy.js +302 -0
- package/dist/heart/tool-approval.js +3 -1
- package/dist/heart/turn-context.js +1 -0
- package/dist/mind/context.js +8 -3
- package/dist/mind/pending.js +25 -0
- package/dist/mind/prompt.js +49 -1
- package/dist/repertoire/guardrails.js +2 -1
- package/dist/repertoire/relationship-authorization.js +194 -0
- package/dist/repertoire/tools-awaiting.js +229 -26
- package/dist/repertoire/tools-base.js +15 -0
- package/dist/repertoire/tools-continuity.js +259 -4
- package/dist/repertoire/tools-notes.js +29 -3
- package/dist/repertoire/tools-session.js +219 -4
- package/dist/repertoire/tools-steward-policy.js +88 -0
- package/dist/repertoire/tools-telegram-contacts.js +37 -0
- package/dist/repertoire/tools-unraid.js +113 -6
- package/dist/repertoire/tools.js +104 -9
- package/dist/repertoire/unraid-restart.js +86 -4
- package/dist/senses/private-runtime-worker.js +36 -3
- package/dist/senses/private-runtime.js +310 -63
- package/dist/senses/sanctuary-health-acceptance-probe.js +8 -6
- package/dist/senses/sanctuary-health-runner.js +51 -96
- package/dist/senses/sanctuary-health.js +54 -93
- package/dist/senses/sanctuary-runtime.js +36 -2
- package/dist/senses/sanctuary-sab.js +80 -0
- package/dist/senses/shared-turn.js +64 -9
- package/dist/senses/telegram-admission.js +684 -0
- package/dist/senses/telegram-approval-runtime.js +46 -6
- package/dist/senses/telegram-client.js +236 -72
- package/dist/senses/telegram-effect-adapter.js +787 -0
- package/dist/senses/telegram-entry.js +4 -1
- package/dist/senses/telegram.js +728 -48
- package/npm-shrinkwrap.json +6 -6
- 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
|
|
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 =
|
|
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(
|
|
297
|
-
persist: (messages, result) => {
|
|
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,
|
|
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
|
|
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 =
|
|
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
|
-
|
|
161
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 {
|
|
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,18 +379,144 @@ 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) {
|
|
328
473
|
let nextUpdateId = options.offsetStore.load();
|
|
329
474
|
const shutdown = new AbortController();
|
|
475
|
+
const retryAfterPollError = (signal) => new Promise((resolve, reject) => {
|
|
476
|
+
const timer = setTimeout(resolve, 1_000);
|
|
477
|
+
signal.addEventListener("abort", () => {
|
|
478
|
+
clearTimeout(timer);
|
|
479
|
+
reject(signal.reason);
|
|
480
|
+
}, { once: true });
|
|
481
|
+
});
|
|
330
482
|
const authorizedMessage = (update) => {
|
|
331
483
|
const message = update.message;
|
|
332
484
|
const userId = message?.from ? String(message.from.id) : "";
|
|
333
485
|
const chatId = message ? String(message.chat.id) : "";
|
|
334
486
|
if (!message || message.chat.type !== "private" || userId !== options.expectedUserId || chatId !== options.expectedChatId || typeof message.text !== "string")
|
|
335
487
|
return null;
|
|
336
|
-
return {
|
|
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
|
+
};
|
|
337
520
|
};
|
|
338
521
|
const authorizedCallback = (update) => {
|
|
339
522
|
const callback = update.callback_query;
|
|
@@ -342,7 +525,9 @@ function createTelegramLongPoll(options) {
|
|
|
342
525
|
&& String(callback.message.chat.id) === options.expectedChatId);
|
|
343
526
|
};
|
|
344
527
|
const dispatch = async (update) => {
|
|
345
|
-
const handled =
|
|
528
|
+
const handled = !update.callback_query || authorizedCallback(update)
|
|
529
|
+
? await options.onUpdate?.(update) ?? false
|
|
530
|
+
: false;
|
|
346
531
|
if (handled)
|
|
347
532
|
return;
|
|
348
533
|
const message = authorizedMessage(update);
|
|
@@ -350,6 +535,11 @@ function createTelegramLongPoll(options) {
|
|
|
350
535
|
await options.onMessage(message);
|
|
351
536
|
return;
|
|
352
537
|
}
|
|
538
|
+
const stranger = unknownMessage(update);
|
|
539
|
+
if (stranger) {
|
|
540
|
+
await options.onUnknownMessage(stranger);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
353
543
|
const distinctAccount = Boolean(update.message?.from && String(update.message.from.id) !== options.expectedUserId);
|
|
354
544
|
(0, runtime_1.emitNervesEvent)({
|
|
355
545
|
component: "senses",
|
|
@@ -392,7 +582,7 @@ function createTelegramLongPoll(options) {
|
|
|
392
582
|
continue;
|
|
393
583
|
options.onBeforeDispatch?.();
|
|
394
584
|
const next = update.update_id + 1;
|
|
395
|
-
const requiresDurableDispatch = Boolean(authorizedCallback(update) || authorizedMessage(update));
|
|
585
|
+
const requiresDurableDispatch = Boolean(authorizedCallback(update) || authorizedMessage(update) || unknownMessage(update));
|
|
396
586
|
const newlyCaptured = requiresDurableDispatch ? (options.inboxStore?.capture(update) ?? true) : true;
|
|
397
587
|
if (newlyCaptured) {
|
|
398
588
|
if (requiresDurableDispatch && options.inboxStore && !options.inboxStore.claim(update)) {
|
|
@@ -436,13 +626,24 @@ function createTelegramLongPoll(options) {
|
|
|
436
626
|
pollOnce,
|
|
437
627
|
async run(signal) {
|
|
438
628
|
const runSignal = signal ? AbortSignal.any([shutdown.signal, signal]) : shutdown.signal;
|
|
439
|
-
|
|
440
|
-
|
|
629
|
+
while (!runSignal.aborted) {
|
|
630
|
+
try {
|
|
441
631
|
await pollOnce(runSignal);
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
if (shutdown.signal.aborted || signal?.aborted)
|
|
635
|
+
return;
|
|
636
|
+
if (!(error instanceof TelegramApiError))
|
|
637
|
+
throw error;
|
|
638
|
+
(0, runtime_1.emitNervesEvent)({
|
|
639
|
+
level: "warn",
|
|
640
|
+
component: "senses",
|
|
641
|
+
event: "telegram.poll_retry",
|
|
642
|
+
message: "Telegram long poll request failed; retrying",
|
|
643
|
+
meta: { status: error.status, errorCode: error.errorCode },
|
|
644
|
+
});
|
|
645
|
+
await retryAfterPollError(runSignal);
|
|
646
|
+
}
|
|
446
647
|
}
|
|
447
648
|
},
|
|
448
649
|
stop() {
|
|
@@ -815,31 +1016,14 @@ function createTelegramApprovalTransport(options) {
|
|
|
815
1016
|
if (pending.messageId === null)
|
|
816
1017
|
return now();
|
|
817
1018
|
const terminalEditStartedAt = now();
|
|
818
|
-
const base = {
|
|
819
|
-
chat_id: options.expectedChatId,
|
|
820
|
-
message_id: Number(pending.messageId),
|
|
821
|
-
reply_markup: { inline_keyboard: [] },
|
|
822
|
-
};
|
|
823
|
-
const editSignal = AbortSignal.timeout(exports.TELEGRAM_APPROVAL_TERMINAL_EDIT_TIMEOUT_MS);
|
|
824
1019
|
try {
|
|
825
1020
|
effectBarrier();
|
|
826
|
-
await options.
|
|
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 });
|
|
827
1022
|
}
|
|
828
1023
|
catch (error) {
|
|
829
1024
|
const alreadyTerminal = error instanceof TelegramApiError && error.status === 400 && /message is not modified/i.test(error.message);
|
|
830
|
-
if (!alreadyTerminal)
|
|
831
|
-
|
|
832
|
-
throw error;
|
|
833
|
-
try {
|
|
834
|
-
effectBarrier();
|
|
835
|
-
await options.api.request("editMessageText", { ...base, text: terminalText }, editSignal);
|
|
836
|
-
}
|
|
837
|
-
catch (fallbackError) {
|
|
838
|
-
if (!(fallbackError instanceof TelegramApiError) || fallbackError.status !== 400 || !/message is not modified/i.test(fallbackError.message)) {
|
|
839
|
-
throw fallbackError;
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
}
|
|
1025
|
+
if (!alreadyTerminal)
|
|
1026
|
+
throw error;
|
|
843
1027
|
}
|
|
844
1028
|
const terminalizedAt = now();
|
|
845
1029
|
if (pending.acceptanceBinding)
|
|
@@ -858,11 +1042,11 @@ function createTelegramApprovalTransport(options) {
|
|
|
858
1042
|
const acknowledge = async (callbackQueryId, invalid) => {
|
|
859
1043
|
try {
|
|
860
1044
|
effectBarrier();
|
|
861
|
-
await options.
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
}
|
|
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
|
+
});
|
|
866
1050
|
return "acknowledged";
|
|
867
1051
|
}
|
|
868
1052
|
catch (error) {
|
|
@@ -1009,16 +1193,6 @@ function createTelegramApprovalTransport(options) {
|
|
|
1009
1193
|
if (pendingByCallback.has(approveCallbackData) || pendingByCallback.has(denyCallbackData)) {
|
|
1010
1194
|
throw new Error("Telegram approval callback handle collision");
|
|
1011
1195
|
}
|
|
1012
|
-
const replyMarkup = { inline_keyboard: [[
|
|
1013
|
-
{ text: "Approve", callback_data: approveCallbackData },
|
|
1014
|
-
{ text: "Deny", callback_data: denyCallbackData },
|
|
1015
|
-
]] };
|
|
1016
|
-
const htmlBody = {
|
|
1017
|
-
chat_id: options.expectedChatId,
|
|
1018
|
-
text: escapeTelegramHtml(input.prompt),
|
|
1019
|
-
parse_mode: "HTML",
|
|
1020
|
-
reply_markup: replyMarkup,
|
|
1021
|
-
};
|
|
1022
1196
|
const pending = {
|
|
1023
1197
|
approvalId: input.approvalId,
|
|
1024
1198
|
decisionToken: input.decisionToken,
|
|
@@ -1047,26 +1221,16 @@ function createTelegramApprovalTransport(options) {
|
|
|
1047
1221
|
throw error;
|
|
1048
1222
|
}
|
|
1049
1223
|
try {
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
effectBarrier();
|
|
1059
|
-
result = await options.api.request("sendMessage", {
|
|
1060
|
-
chat_id: options.expectedChatId,
|
|
1061
|
-
text: input.prompt,
|
|
1062
|
-
reply_markup: replyMarkup,
|
|
1063
|
-
});
|
|
1064
|
-
}
|
|
1065
|
-
if (!result || typeof result !== "object" || Array.isArray(result) || !("message_id" in result)
|
|
1066
|
-
|| 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)
|
|
1067
1232
|
throw new Error("Telegram sendMessage response did not include a canonical message_id");
|
|
1068
|
-
|
|
1069
|
-
pending.messageId = String(result.message_id);
|
|
1233
|
+
pending.messageId = String(messageId);
|
|
1070
1234
|
pending.deliveryState = "bound";
|
|
1071
1235
|
if (input.acceptanceBinding) {
|
|
1072
1236
|
const boundAt = now();
|