@ouro.bot/cli 0.1.0-alpha.733 → 0.1.0-alpha.735
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 +34 -0
- package/deploy/unraid/Dockerfile +27 -0
- package/deploy/unraid/README.txt +1605 -0
- package/deploy/unraid/audit-container-spec.sh +4 -0
- package/deploy/unraid/container-runtime.json +1 -0
- package/deploy/unraid/sanctuary-acceptance-adapter.sh +37 -0
- package/deploy/unraid/sanctuary-acceptance-contract.json +200 -0
- package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
- package/deploy/unraid/sanctuary-deployment-target.mjs +705 -0
- package/deploy/unraid/sanctuary-safe-rename.py +24 -0
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +1777 -0
- package/deploy/unraid/sanctuary-unit16-run.sh +502 -0
- package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
- package/deploy/unraid/sanctuary.ouro/agent.json +24 -0
- package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
- package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
- package/deploy/unraid/sanctuary.ouro/provider-readiness.json +18 -0
- package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -0
- package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -0
- package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +3 -0
- package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +5 -0
- package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +7 -0
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +21 -0
- package/deploy/unraid/sanctuary.xml +25 -0
- package/dist/heart/approval-files.js +113 -0
- package/dist/heart/approval-store.js +168 -9
- package/dist/heart/auth/auth-flow.js +17 -1
- package/dist/heart/autonomy-budget.js +44 -1
- package/dist/heart/awaiting/await-scheduler.js +6 -2
- package/dist/heart/config.js +8 -0
- package/dist/heart/core.js +117 -132
- package/dist/heart/daemon/cli-exec.js +73 -1
- package/dist/heart/daemon/cli-help.js +5 -0
- package/dist/heart/daemon/cli-parse.js +6 -4
- package/dist/heart/daemon/container-credential-bootstrap.js +208 -0
- package/dist/heart/daemon/container-healthcheck.js +94 -0
- package/dist/heart/daemon/container-runtime.js +84 -0
- package/dist/heart/daemon/container-spec-auditor-main.js +156 -0
- package/dist/heart/daemon/container-spec-auditor.js +156 -0
- package/dist/heart/daemon/daemon-bootstrap-startup.js +41 -0
- package/dist/heart/daemon/daemon-entry.js +116 -28
- package/dist/heart/daemon/daemon-health.js +10 -0
- package/dist/heart/daemon/daemon.js +92 -5
- package/dist/heart/daemon/runtime-logging.js +6 -0
- package/dist/heart/daemon/runtime-mode.js +13 -2
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2365 -0
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +1624 -0
- package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +985 -0
- package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
- package/dist/heart/daemon/sanctuary-scheduler-origin.js +126 -0
- package/dist/heart/daemon/sense-manager.js +25 -6
- package/dist/heart/daemon/supercronic-supervisor.js +267 -0
- package/dist/heart/habits/habit-scheduler.js +7 -3
- package/dist/heart/identity.js +13 -3
- package/dist/heart/provider-credentials.js +9 -1
- package/dist/heart/provider-models.js +8 -0
- package/dist/heart/provider-ping.js +5 -2
- package/dist/heart/providers/openai-compatible.js +177 -0
- package/dist/heart/runtime-credentials.js +217 -8
- package/dist/heart/sense-truth.js +2 -0
- package/dist/heart/tool-approval.js +1 -0
- package/dist/heart/turn-context.js +8 -0
- package/dist/mind/prompt.js +9 -0
- package/dist/nerves/coverage/source-scanner.js +4 -4
- package/dist/nerves/index.js +75 -9
- package/dist/nerves/runtime.js +12 -0
- package/dist/repertoire/tool-arguments.js +39 -0
- package/dist/repertoire/tools-unraid.js +353 -0
- package/dist/repertoire/tools.js +19 -1
- package/dist/repertoire/unraid-client.js +155 -0
- package/dist/repertoire/unraid-restart.js +130 -0
- package/dist/senses/private-runtime-worker.js +3 -2
- package/dist/senses/sanctuary-grounding.js +118 -0
- package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
- package/dist/senses/sanctuary-health-acceptance-probe.js +772 -0
- package/dist/senses/sanctuary-health-runner.js +146 -0
- package/dist/senses/sanctuary-health.js +447 -0
- package/dist/senses/sanctuary-interactive-control.js +306 -0
- package/dist/senses/sanctuary-runtime.js +206 -0
- package/dist/senses/shared-turn.js +11 -3
- package/dist/senses/telegram-approval-runtime.js +526 -0
- package/dist/senses/telegram-audit-ledger.js +220 -0
- package/dist/senses/telegram-client.js +1471 -0
- package/dist/senses/telegram-entry.js +80 -0
- package/dist/senses/telegram.js +1232 -0
- package/npm-shrinkwrap.json +6558 -0
- package/package.json +4 -2
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runSanctuaryHealthPrivateTurn = runSanctuaryHealthPrivateTurn;
|
|
37
|
+
exports.runSanctuaryHealthHabit = runSanctuaryHealthHabit;
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const identity_1 = require("../heart/identity");
|
|
40
|
+
const core_1 = require("../heart/core");
|
|
41
|
+
const autonomy_budget_1 = require("../heart/autonomy-budget");
|
|
42
|
+
const runtime_1 = require("../nerves/runtime");
|
|
43
|
+
const tools_1 = require("../repertoire/tools");
|
|
44
|
+
const telegram_client_1 = require("./telegram-client");
|
|
45
|
+
const sanctuary_health_1 = require("./sanctuary-health");
|
|
46
|
+
const sanctuary_runtime_1 = require("./sanctuary-runtime");
|
|
47
|
+
const telegram_1 = require("./telegram");
|
|
48
|
+
function privateTurnCallbacks(onProviderInvocation) {
|
|
49
|
+
return {
|
|
50
|
+
onModelStart() { onProviderInvocation?.(); }, onModelStreamStart() { }, onTextChunk() { }, onReasoningChunk() { },
|
|
51
|
+
onToolStart() { }, onToolEnd() { }, onError() { }, onClearText() { },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function runSanctuaryHealthPrivateTurn(input) {
|
|
55
|
+
const definition = (0, tools_1.resolveToolDefinition)("send_message");
|
|
56
|
+
if (!definition)
|
|
57
|
+
throw new Error("canonical send_message definition is unavailable");
|
|
58
|
+
const agentRoot = (0, identity_1.getAgentRoot)(input.agentName);
|
|
59
|
+
const budgetTarget = { eventId: input.eventId, payloadStored: false };
|
|
60
|
+
const budget = (0, autonomy_budget_1.reserveAutonomyBudget)(agentRoot, {
|
|
61
|
+
agent: input.agentName,
|
|
62
|
+
triggerType: "habit",
|
|
63
|
+
sourceKind: "private-runtime",
|
|
64
|
+
senseOrHabit: "sanctuary-health",
|
|
65
|
+
target: budgetTarget,
|
|
66
|
+
idempotencyKey: `sanctuary-health:${input.eventId}`,
|
|
67
|
+
}, (0, autonomy_budget_1.resolveAutonomyBudgetPolicy)(agentRoot, input.agentName));
|
|
68
|
+
if (!budget.allowed && budget.status !== "duplicate")
|
|
69
|
+
throw new Error(`Sanctuary health private turn blocked: ${budget.reason}`);
|
|
70
|
+
let delivered = false;
|
|
71
|
+
const result = await (0, core_1.runAgent)([
|
|
72
|
+
{
|
|
73
|
+
role: "user",
|
|
74
|
+
content: [
|
|
75
|
+
"A durable Sanctuary health event is pending.",
|
|
76
|
+
`Event id: ${input.eventId}`,
|
|
77
|
+
`Deterministic payload: ${input.payload}`,
|
|
78
|
+
"Summarize it briefly for Ari. Call send_message exactly once with friendId=operator, channel=telegram, and the summary as content, then rest.",
|
|
79
|
+
].join("\n"),
|
|
80
|
+
},
|
|
81
|
+
], privateTurnCallbacks(input.onProviderInvocation), "inner", undefined, {
|
|
82
|
+
toolProfile: "sanctuary-health-private",
|
|
83
|
+
tools: [definition.tool],
|
|
84
|
+
toolContext: { signin: async () => undefined },
|
|
85
|
+
execTool: async (name, args) => {
|
|
86
|
+
if (name !== "send_message")
|
|
87
|
+
throw new Error(`Sanctuary health profile cannot execute ${name}`);
|
|
88
|
+
if (delivered)
|
|
89
|
+
throw new Error("Sanctuary health Telegram delivery was already attempted");
|
|
90
|
+
if (args.friendId !== "operator" || args.channel !== "telegram" || typeof args.content !== "string" || args.content.trim().length === 0) {
|
|
91
|
+
throw new Error("Sanctuary health send_message target or content is invalid");
|
|
92
|
+
}
|
|
93
|
+
await input.deliver(args.content);
|
|
94
|
+
delivered = true;
|
|
95
|
+
return "delivered";
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
if (result.outcome === "errored") {
|
|
99
|
+
throw result.error ?? new Error("Sanctuary health private turn failed");
|
|
100
|
+
}
|
|
101
|
+
return { delivered };
|
|
102
|
+
}
|
|
103
|
+
async function runSanctuaryHealthHabit(agentName, options = {}) {
|
|
104
|
+
const sweep = options.createSweep?.(agentName) ?? (0, sanctuary_health_1.createSanctuaryHealthSweep)({
|
|
105
|
+
toolContext: (0, sanctuary_runtime_1.createSanctuaryToolContext)(agentName),
|
|
106
|
+
statePath: path.join((0, identity_1.getAgentRoot)(agentName), "state", "health", "sanctuary-health.json"),
|
|
107
|
+
});
|
|
108
|
+
const result = await sweep();
|
|
109
|
+
if (!result.message || !result.deliveryId) {
|
|
110
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.sanctuary_health_habit", message: "Sanctuary native health habit completed without paid work", meta: { agentName, incidentCount: result.incidents.length, delivered: false } });
|
|
111
|
+
return { ok: true, message: "health sweep completed with no alert", data: { incidentCount: result.incidents.length, delivered: false } };
|
|
112
|
+
}
|
|
113
|
+
const credentials = (options.credentials ?? telegram_1.loadTelegramSenseCredentials)(agentName);
|
|
114
|
+
const api = (options.createApi ?? ((token) => (0, telegram_client_1.createTelegramBotApi)({ token })))(credentials.botToken);
|
|
115
|
+
const eventId = result.deliveryId;
|
|
116
|
+
const payload = result.message;
|
|
117
|
+
let attempted = false;
|
|
118
|
+
try {
|
|
119
|
+
const deliver = async (content) => {
|
|
120
|
+
if (attempted)
|
|
121
|
+
throw new Error("Sanctuary health Telegram delivery was already attempted");
|
|
122
|
+
attempted = true;
|
|
123
|
+
await sweep.cacheDeliveryPayload?.(result.deliveryId, content);
|
|
124
|
+
await sweep.markDeliveryAttempting?.(result.deliveryId);
|
|
125
|
+
const messageIds = await (0, telegram_client_1.sendTelegramText)(api, credentials.authorizedChatId, content);
|
|
126
|
+
await sweep.markDelivered?.(result.deliveryId, messageIds);
|
|
127
|
+
};
|
|
128
|
+
const privateResult = result.cachedMessage
|
|
129
|
+
? (await deliver(result.cachedMessage), { delivered: true })
|
|
130
|
+
: await (async () => {
|
|
131
|
+
options.acceptanceMetrics?.onPrivateTurnStart();
|
|
132
|
+
return (options.runPrivateTurn ?? runSanctuaryHealthPrivateTurn)({
|
|
133
|
+
agentName,
|
|
134
|
+
eventId,
|
|
135
|
+
payload,
|
|
136
|
+
deliver,
|
|
137
|
+
onProviderInvocation: options.acceptanceMetrics?.onProviderInvocation,
|
|
138
|
+
});
|
|
139
|
+
})();
|
|
140
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.sanctuary_health_habit", message: "Sanctuary native health habit completed", meta: { agentName, incidentCount: result.incidents.length, delivered: privateResult.delivered } });
|
|
141
|
+
return { ok: true, message: privateResult.delivered ? "health sweep completed and delivered" : "health event remains pending", data: { incidentCount: result.incidents.length, delivered: privateResult.delivered } };
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
api.stop();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.withSanctuaryHealthStateLease = withSanctuaryHealthStateLease;
|
|
37
|
+
exports.probeSanctuaryEndpoint = probeSanctuaryEndpoint;
|
|
38
|
+
exports.createSanctuaryHealthSweep = createSanctuaryHealthSweep;
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const node_crypto_1 = require("node:crypto");
|
|
42
|
+
const runtime_1 = require("../nerves/runtime");
|
|
43
|
+
const sanctuary_acceptance_marker_1 = require("../heart/daemon/sanctuary-acceptance-marker");
|
|
44
|
+
const ENDPOINTS = [
|
|
45
|
+
"https://media.mendelow.cloud/",
|
|
46
|
+
"https://books.mendelow.cloud/",
|
|
47
|
+
"https://requests.mendelow.cloud/",
|
|
48
|
+
"https://readarr.mendelow.cloud/",
|
|
49
|
+
];
|
|
50
|
+
const REQUIRED_CONTAINERS = new Set(["calibre", "calibre-web", "Cloudflare-DDNS"]);
|
|
51
|
+
const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
52
|
+
const MAX_HEALTH_STATE_BYTES = 4 * 1024 * 1024;
|
|
53
|
+
const MAX_HEALTH_TEXT_BYTES = 50_000;
|
|
54
|
+
function canonicalIsoTimestamp(value) {
|
|
55
|
+
if (typeof value !== "string" || value.length > 30)
|
|
56
|
+
return false;
|
|
57
|
+
const parsed = Date.parse(value);
|
|
58
|
+
return Number.isFinite(parsed) && new Date(parsed).toISOString() === value;
|
|
59
|
+
}
|
|
60
|
+
function boundedHealthText(value) {
|
|
61
|
+
if (Buffer.byteLength(value) <= MAX_HEALTH_TEXT_BYTES)
|
|
62
|
+
return value;
|
|
63
|
+
const ellipsis = "…";
|
|
64
|
+
const contentByteLimit = MAX_HEALTH_TEXT_BYTES - Buffer.byteLength(ellipsis);
|
|
65
|
+
let output = "";
|
|
66
|
+
for (const character of value) {
|
|
67
|
+
if (Buffer.byteLength(output) + Buffer.byteLength(character) > contentByteLimit)
|
|
68
|
+
break;
|
|
69
|
+
output += character;
|
|
70
|
+
}
|
|
71
|
+
return `${output}${ellipsis}`;
|
|
72
|
+
}
|
|
73
|
+
const healthLockTails = new Map();
|
|
74
|
+
const healthLeaseBrand = Symbol("sanctuary-health-state-lease");
|
|
75
|
+
const activeHealthLeases = new Set();
|
|
76
|
+
function healthLeaseOwner(value) {
|
|
77
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
78
|
+
throw new Error("Sanctuary health lease ownership is invalid");
|
|
79
|
+
const owner = value;
|
|
80
|
+
if (JSON.stringify(Object.keys(owner).sort()) !== JSON.stringify(["nonce", "pid", "schemaVersion"])
|
|
81
|
+
|| owner.schemaVersion !== 1 || !Number.isSafeInteger(owner.pid) || Number(owner.pid) < 1
|
|
82
|
+
|| typeof owner.nonce !== "string" || !/^[0-9a-f]{64}$/u.test(owner.nonce)) {
|
|
83
|
+
throw new Error("Sanctuary health lease ownership is invalid");
|
|
84
|
+
}
|
|
85
|
+
return owner;
|
|
86
|
+
}
|
|
87
|
+
function processIsAlive(pid) {
|
|
88
|
+
try {
|
|
89
|
+
process.kill(pid, 0);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
return error.code === "EPERM";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function removeHealthLeaseDirectory(leasePath) {
|
|
97
|
+
const entries = fs.readdirSync(leasePath, { withFileTypes: true });
|
|
98
|
+
if (entries.some((entry) => !entry.isFile() || entry.name !== "owner.json"))
|
|
99
|
+
throw new Error("Sanctuary health lease contains unexpected entries");
|
|
100
|
+
if (entries.length === 1)
|
|
101
|
+
fs.unlinkSync(path.join(leasePath, "owner.json"));
|
|
102
|
+
fs.rmdirSync(leasePath);
|
|
103
|
+
}
|
|
104
|
+
async function acquireHealthFileLease(statePath, timeoutMs) {
|
|
105
|
+
const leasePath = `${statePath}.lease`;
|
|
106
|
+
const ownerPath = path.join(leasePath, "owner.json");
|
|
107
|
+
const deadline = Date.now() + timeoutMs;
|
|
108
|
+
const nonce = (0, node_crypto_1.createHash)("sha256").update((0, node_crypto_1.randomUUID)()).digest("hex");
|
|
109
|
+
for (;;) {
|
|
110
|
+
try {
|
|
111
|
+
fs.mkdirSync(leasePath, { recursive: false, mode: 0o700 });
|
|
112
|
+
fs.writeFileSync(ownerPath, `${JSON.stringify({ schemaVersion: 1, pid: process.pid, nonce })}\n`, { flag: "wx", mode: 0o600 });
|
|
113
|
+
return { statePath, nonce, [healthLeaseBrand]: true };
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error.code !== "EEXIST") {
|
|
117
|
+
try {
|
|
118
|
+
if (fs.existsSync(leasePath) && !fs.existsSync(ownerPath))
|
|
119
|
+
fs.rmdirSync(leasePath);
|
|
120
|
+
}
|
|
121
|
+
catch { /* preserve the original failure */ }
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
let owner;
|
|
125
|
+
try {
|
|
126
|
+
owner = healthLeaseOwner(JSON.parse(fs.readFileSync(ownerPath, "utf8")));
|
|
127
|
+
}
|
|
128
|
+
catch (ownerError) {
|
|
129
|
+
if (ownerError.code === "ENOENT" && Date.now() < deadline) {
|
|
130
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (ownerError instanceof Error && ownerError.message.includes("ownership is invalid"))
|
|
134
|
+
throw ownerError;
|
|
135
|
+
throw new Error("Sanctuary health lease ownership is invalid", { cause: ownerError });
|
|
136
|
+
}
|
|
137
|
+
if (!processIsAlive(owner.pid)) {
|
|
138
|
+
removeHealthLeaseDirectory(leasePath);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (Date.now() >= deadline)
|
|
142
|
+
throw new Error("Sanctuary health state lease timed out");
|
|
143
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function releaseHealthFileLease(lease) {
|
|
148
|
+
const leasePath = `${lease.statePath}.lease`;
|
|
149
|
+
const owner = healthLeaseOwner(JSON.parse(fs.readFileSync(path.join(leasePath, "owner.json"), "utf8")));
|
|
150
|
+
if (owner.nonce !== lease.nonce || owner.pid !== process.pid)
|
|
151
|
+
throw new Error("Sanctuary health lease ownership changed");
|
|
152
|
+
removeHealthLeaseDirectory(leasePath);
|
|
153
|
+
}
|
|
154
|
+
async function withSanctuaryHealthStateLease(statePath, operation, options = {}) {
|
|
155
|
+
const previous = healthLockTails.get(statePath) ?? Promise.resolve();
|
|
156
|
+
let releaseProcess;
|
|
157
|
+
const current = new Promise((resolve) => { releaseProcess = resolve; });
|
|
158
|
+
healthLockTails.set(statePath, current);
|
|
159
|
+
await previous;
|
|
160
|
+
let lease;
|
|
161
|
+
try {
|
|
162
|
+
lease = await acquireHealthFileLease(statePath, options.timeoutMs ?? 30_000);
|
|
163
|
+
activeHealthLeases.add(lease);
|
|
164
|
+
return await operation(lease);
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
if (lease) {
|
|
168
|
+
activeHealthLeases.delete(lease);
|
|
169
|
+
releaseHealthFileLease(lease);
|
|
170
|
+
}
|
|
171
|
+
releaseProcess();
|
|
172
|
+
if (healthLockTails.get(statePath) === current)
|
|
173
|
+
healthLockTails.delete(statePath);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function withHealthLock(statePath, operation, lease) {
|
|
177
|
+
if (lease) {
|
|
178
|
+
if (lease.statePath !== statePath || !activeHealthLeases.has(lease) || lease[healthLeaseBrand] !== true) {
|
|
179
|
+
throw new Error("Sanctuary health state lease is invalid");
|
|
180
|
+
}
|
|
181
|
+
return await operation();
|
|
182
|
+
}
|
|
183
|
+
return withSanctuaryHealthStateLease(statePath, async () => operation());
|
|
184
|
+
}
|
|
185
|
+
function load(filePath) {
|
|
186
|
+
try {
|
|
187
|
+
if (fs.statSync(filePath).size > MAX_HEALTH_STATE_BYTES)
|
|
188
|
+
throw new Error("health state exceeds its bound");
|
|
189
|
+
const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
190
|
+
const validDelivery = (delivery) => Boolean(delivery && typeof delivery === "object"
|
|
191
|
+
&& typeof delivery.id === "string" && delivery.id.length > 0 && delivery.id.length <= 128
|
|
192
|
+
&& typeof delivery.message === "string" && Buffer.byteLength(delivery.message) <= MAX_HEALTH_TEXT_BYTES
|
|
193
|
+
&& ["pending", "attempting"].includes(delivery.status)
|
|
194
|
+
&& canonicalIsoTimestamp(delivery.createdAt)
|
|
195
|
+
&& (delivery.kind === undefined || ["transition", "digest", "transition_and_digest", "legacy_unknown"].includes(delivery.kind))
|
|
196
|
+
&& (delivery.summarizedMessage === undefined || (typeof delivery.summarizedMessage === "string" && Buffer.byteLength(delivery.summarizedMessage) <= MAX_HEALTH_TEXT_BYTES)));
|
|
197
|
+
if (!value.incidents || typeof value.incidents !== "object" || Array.isArray(value.incidents))
|
|
198
|
+
throw new Error("invalid incidents");
|
|
199
|
+
if (value.lastDigestDay !== null && typeof value.lastDigestDay !== "string")
|
|
200
|
+
throw new Error("invalid digest day");
|
|
201
|
+
if (typeof value.updatedAt !== "string")
|
|
202
|
+
throw new Error("invalid update time");
|
|
203
|
+
if (value.outbox !== undefined && value.outbox !== null && !validDelivery(value.outbox))
|
|
204
|
+
throw new Error("invalid health outbox");
|
|
205
|
+
if (value.indeterminateDeliveries !== undefined && (!Array.isArray(value.indeterminateDeliveries) || !value.indeterminateDeliveries.every(validDelivery)))
|
|
206
|
+
throw new Error("invalid indeterminate deliveries");
|
|
207
|
+
if (value.deliveredReceipts !== undefined && (!Array.isArray(value.deliveredReceipts) || value.deliveredReceipts.length > 100 || !value.deliveredReceipts.every((receipt) => (receipt && typeof receipt.deliveryId === "string" && receipt.deliveryId.length > 0 && receipt.deliveryId.length <= 128 && canonicalIsoTimestamp(receipt.deliveredAt)
|
|
208
|
+
&& (receipt.kind === undefined || ["transition", "digest", "transition_and_digest", "legacy_unknown"].includes(receipt.kind))
|
|
209
|
+
&& Array.isArray(receipt.messageIds) && receipt.messageIds.length > 0 && receipt.messageIds.length <= 100
|
|
210
|
+
&& receipt.messageIds.every((id) => Number.isSafeInteger(id) && id > 0)))))
|
|
211
|
+
throw new Error("invalid delivered receipts");
|
|
212
|
+
if (value.sweepReceipts !== undefined && (!Array.isArray(value.sweepReceipts) || value.sweepReceipts.length > 500 || !value.sweepReceipts.every((receipt) => (receipt && typeof receipt.sweepId === "string" && UUID_V4.test(receipt.sweepId)
|
|
213
|
+
&& canonicalIsoTimestamp(receipt.startedAt)
|
|
214
|
+
&& canonicalIsoTimestamp(receipt.completedAt)
|
|
215
|
+
&& /^[0-9a-f]{64}$/u.test(receipt.incidentDigest) && Number.isSafeInteger(receipt.opened) && receipt.opened >= 0
|
|
216
|
+
&& Number.isSafeInteger(receipt.recovered) && receipt.recovered >= 0 && typeof receipt.digestDue === "boolean"
|
|
217
|
+
&& (receipt.deliveryId === undefined || (typeof receipt.deliveryId === "string" && receipt.deliveryId.length > 0 && receipt.deliveryId.length <= 128))
|
|
218
|
+
&& (receipt.scenarioHandleDigest === undefined || /^[0-9a-f]{64}$/u.test(receipt.scenarioHandleDigest))))))
|
|
219
|
+
throw new Error("invalid sweep receipts");
|
|
220
|
+
return {
|
|
221
|
+
incidents: value.incidents,
|
|
222
|
+
lastDigestDay: value.lastDigestDay,
|
|
223
|
+
updatedAt: value.updatedAt,
|
|
224
|
+
outbox: value.outbox ? { ...value.outbox, kind: value.outbox.kind ?? "legacy_unknown" } : null,
|
|
225
|
+
indeterminateDeliveries: (value.indeterminateDeliveries ?? []).map((delivery) => ({ ...delivery, kind: delivery.kind ?? "legacy_unknown" })),
|
|
226
|
+
deliveredReceipts: (value.deliveredReceipts ?? []).map((receipt) => ({ ...receipt, kind: receipt.kind ?? "legacy_unknown" })),
|
|
227
|
+
sweepReceipts: value.sweepReceipts ?? [],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
|
|
232
|
+
return { incidents: {}, lastDigestDay: null, updatedAt: new Date(0).toISOString(), outbox: null, indeterminateDeliveries: [], deliveredReceipts: [], sweepReceipts: [] };
|
|
233
|
+
throw new Error("Sanctuary health state is corrupt", { cause: error });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function save(filePath, state) {
|
|
237
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
238
|
+
const temporary = `${filePath}.tmp-${process.pid}-${(0, node_crypto_1.randomUUID)()}`;
|
|
239
|
+
fs.writeFileSync(temporary, `${JSON.stringify(state)}\n`, { mode: 0o600 });
|
|
240
|
+
const temporaryHandle = fs.openSync(temporary, "r");
|
|
241
|
+
try {
|
|
242
|
+
fs.fsyncSync(temporaryHandle);
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
fs.closeSync(temporaryHandle);
|
|
246
|
+
}
|
|
247
|
+
fs.renameSync(temporary, filePath);
|
|
248
|
+
const directoryHandle = fs.openSync(path.dirname(filePath), "r");
|
|
249
|
+
try {
|
|
250
|
+
fs.fsyncSync(directoryHandle);
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
fs.closeSync(directoryHandle);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function record(value) {
|
|
257
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
258
|
+
}
|
|
259
|
+
async function probeSanctuaryEndpoint(url, fetchImpl = fetch) {
|
|
260
|
+
const configured = new URL(url);
|
|
261
|
+
if (configured.protocol !== "https:" || configured.username || configured.password)
|
|
262
|
+
return { url, ok: false, status: 0 };
|
|
263
|
+
let current = configured;
|
|
264
|
+
try {
|
|
265
|
+
for (let redirects = 0;; redirects += 1) {
|
|
266
|
+
const response = await fetchImpl(current.href, { signal: AbortSignal.timeout(10_000), redirect: "manual" });
|
|
267
|
+
const redirect = response.status >= 300 && response.status < 400;
|
|
268
|
+
if (!redirect) {
|
|
269
|
+
await response.body?.cancel().catch(() => undefined);
|
|
270
|
+
return { url, ok: response.status >= 200 && response.status < 400, status: response.status };
|
|
271
|
+
}
|
|
272
|
+
const location = response.headers.get("location");
|
|
273
|
+
await response.body?.cancel().catch(() => undefined);
|
|
274
|
+
if (!location || redirects === 5)
|
|
275
|
+
return { url, ok: false, status: 0 };
|
|
276
|
+
const target = new URL(location, current);
|
|
277
|
+
if (target.protocol !== "https:" || target.origin !== configured.origin || target.username || target.password) {
|
|
278
|
+
return { url, ok: false, status: 0 };
|
|
279
|
+
}
|
|
280
|
+
current = target;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return { url, ok: false, status: 0 };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function createSanctuaryHealthSweep(options) {
|
|
288
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
289
|
+
const now = options.now ?? (() => new Date());
|
|
290
|
+
const acceptanceEventMeta = options.acceptanceEventMeta ?? (() => (0, sanctuary_acceptance_marker_1.sanctuaryAcceptanceEventMeta)("sanctuary"));
|
|
291
|
+
const runSweep = async () => {
|
|
292
|
+
const sweepId = (0, node_crypto_1.randomUUID)();
|
|
293
|
+
const startedAt = now().toISOString();
|
|
294
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.sanctuary_health_start", message: "Sanctuary deterministic health sweep started", meta: { statePath: options.statePath, ...acceptanceEventMeta() } });
|
|
295
|
+
const pendingState = load(options.statePath);
|
|
296
|
+
if (pendingState.outbox?.status === "pending") {
|
|
297
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.sanctuary_health_end", message: "Sanctuary health delivery recovered from durable outbox", meta: { incidentCount: Object.keys(pendingState.incidents).length, deliveryId: pendingState.outbox.id, deliveryStatus: pendingState.outbox.status, ...acceptanceEventMeta() } });
|
|
298
|
+
return {
|
|
299
|
+
message: pendingState.outbox.message,
|
|
300
|
+
incidents: Object.values(pendingState.incidents),
|
|
301
|
+
deliveryId: pendingState.outbox.id,
|
|
302
|
+
...(pendingState.outbox.summarizedMessage ? { cachedMessage: pendingState.outbox.summarizedMessage } : {}),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (pendingState.outbox?.status === "attempting") {
|
|
306
|
+
pendingState.indeterminateDeliveries.push(pendingState.outbox);
|
|
307
|
+
pendingState.outbox = null;
|
|
308
|
+
pendingState.updatedAt = now().toISOString();
|
|
309
|
+
save(options.statePath, pendingState);
|
|
310
|
+
}
|
|
311
|
+
const runtime = options.toolContext.sanctuary;
|
|
312
|
+
if (!runtime)
|
|
313
|
+
throw new Error("Sanctuary health runtime is unavailable");
|
|
314
|
+
const [containersResult, storageResult, disksResult, notificationsResult, endpoints] = await Promise.all([
|
|
315
|
+
runtime.listContainers(), runtime.getStorage(), runtime.getDisks(), runtime.getNotifications(),
|
|
316
|
+
Promise.all(ENDPOINTS.map((url) => probeSanctuaryEndpoint(url, fetchImpl))),
|
|
317
|
+
]);
|
|
318
|
+
const incidents = new Map();
|
|
319
|
+
const add = (id, summary) => incidents.set(id, { id, summary });
|
|
320
|
+
const containers = record(containersResult)?.ok ? record(record(containersResult)?.data)?.containers : null;
|
|
321
|
+
if (!Array.isArray(containers))
|
|
322
|
+
add("containers:unavailable", "container status is unavailable");
|
|
323
|
+
else
|
|
324
|
+
for (const container of containers) {
|
|
325
|
+
if ((container.autostart === true || REQUIRED_CONTAINERS.has(container.name)) && container.state !== "running")
|
|
326
|
+
add(`container:${container.id}:${container.state}:${container.exitCode ?? "none"}`, `${container.name} is ${container.state}`);
|
|
327
|
+
if (container.state === "unknown" || container.degraded === true)
|
|
328
|
+
add(`container:${container.id}:degraded`, `${container.name} status is degraded`);
|
|
329
|
+
}
|
|
330
|
+
const storage = record(storageResult)?.ok ? record(record(storageResult)?.data) : null;
|
|
331
|
+
const array = record(storage?.array);
|
|
332
|
+
if (!array || array.degraded || typeof array.usedPercent !== "number")
|
|
333
|
+
add("storage:array:degraded", "array capacity is unavailable");
|
|
334
|
+
else if (array.usedPercent >= 90)
|
|
335
|
+
add("storage:array:90", `array usage is ${array.usedPercent}%`);
|
|
336
|
+
for (const share of Array.isArray(storage?.shares) ? storage.shares : [])
|
|
337
|
+
if (share.degraded || typeof share.usedPercent !== "number")
|
|
338
|
+
add(`storage:share:${share.name}:degraded`, `${share.name} capacity is unavailable`);
|
|
339
|
+
else if (share.usedPercent >= 90)
|
|
340
|
+
add(`storage:share:${share.name}:90`, `${share.name} usage is ${share.usedPercent}%`);
|
|
341
|
+
const disks = record(disksResult)?.ok ? record(record(disksResult)?.data) : null;
|
|
342
|
+
if (!disks)
|
|
343
|
+
add("disks:unavailable", "disk health is unavailable");
|
|
344
|
+
else {
|
|
345
|
+
for (const disk of Array.isArray(disks.disks) ? disks.disks : []) {
|
|
346
|
+
if (disk.smart !== "passed")
|
|
347
|
+
add(`disk:${disk.id}:smart:${disk.smart}`, `${disk.name} SMART is ${disk.smart}`);
|
|
348
|
+
if (typeof disk.temperatureC !== "number")
|
|
349
|
+
add(`disk:${disk.id}:temperature:unknown`, `${disk.name} temperature is unavailable`);
|
|
350
|
+
else if (disk.temperatureC >= 50)
|
|
351
|
+
add(`disk:${disk.id}:temperature:50`, `${disk.name} is ${disk.temperatureC}°C`);
|
|
352
|
+
}
|
|
353
|
+
const parity = record(disks.parity);
|
|
354
|
+
if (!parity || parity.result !== "success" || typeof parity.ageHours !== "number" || parity.ageHours >= 45 * 24)
|
|
355
|
+
add("parity:stale-or-failed", "parity check is unsuccessful, unknown, or older than 45 days");
|
|
356
|
+
}
|
|
357
|
+
const notifications = record(notificationsResult)?.ok ? record(record(notificationsResult)?.data)?.unacknowledged : null;
|
|
358
|
+
if (!Array.isArray(notifications))
|
|
359
|
+
add("notifications:unavailable", "notification status is unavailable");
|
|
360
|
+
else
|
|
361
|
+
for (const item of notifications)
|
|
362
|
+
add(`notification:${item.id}`, `unacknowledged notification: ${item.title || item.id}`);
|
|
363
|
+
for (const endpoint of endpoints)
|
|
364
|
+
if (!endpoint.ok)
|
|
365
|
+
add(`endpoint:${endpoint.url}`, `${endpoint.url} returned ${endpoint.status || "no response"}`);
|
|
366
|
+
const previous = load(options.statePath);
|
|
367
|
+
const current = Object.fromEntries([...incidents.entries()].sort(([a], [b]) => a.localeCompare(b)));
|
|
368
|
+
const opened = Object.values(current).filter((incident) => !previous.incidents[incident.id]);
|
|
369
|
+
const recovered = Object.values(previous.incidents).filter((incident) => !current[incident.id]);
|
|
370
|
+
const localParts = new Intl.DateTimeFormat("en-CA", { timeZone: "America/Los_Angeles", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", hourCycle: "h23" }).formatToParts(now());
|
|
371
|
+
const part = (type) => localParts.find((item) => item.type === type)?.value ?? "";
|
|
372
|
+
const day = `${part("year")}-${part("month")}-${part("day")}`;
|
|
373
|
+
const digestDue = Number(part("hour")) >= 9
|
|
374
|
+
&& (Object.keys(current).length > 0 || previous.indeterminateDeliveries.length > 0)
|
|
375
|
+
&& previous.lastDigestDay !== day;
|
|
376
|
+
const lines = [
|
|
377
|
+
...opened.map((incident) => `🚨 ${incident.summary}`),
|
|
378
|
+
...recovered.map((incident) => `✅ recovered: ${incident.summary}`),
|
|
379
|
+
...(digestDue && Object.keys(current).length > 0 ? [`📋 still broken: ${Object.values(current).map((incident) => incident.summary).join("; ")}`] : []),
|
|
380
|
+
...(digestDue && previous.indeterminateDeliveries.length > 0
|
|
381
|
+
? previous.indeterminateDeliveries.map((delivery) => `⚠️ prior Telegram delivery was indeterminate: ${delivery.message}`)
|
|
382
|
+
: []),
|
|
383
|
+
];
|
|
384
|
+
const message = lines.length ? boundedHealthText(lines.join("\n")) : null;
|
|
385
|
+
const deliveryKind = digestDue && (opened.length > 0 || recovered.length > 0)
|
|
386
|
+
? "transition_and_digest"
|
|
387
|
+
: digestDue ? "digest" : "transition";
|
|
388
|
+
const delivery = message ? { id: (0, node_crypto_1.randomUUID)(), message, status: "pending", createdAt: now().toISOString(), kind: deliveryKind } : null;
|
|
389
|
+
const acceptanceMeta = acceptanceEventMeta();
|
|
390
|
+
const completedAt = now().toISOString();
|
|
391
|
+
const sweepReceipt = {
|
|
392
|
+
sweepId,
|
|
393
|
+
startedAt,
|
|
394
|
+
completedAt,
|
|
395
|
+
incidentDigest: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(current)).digest("hex"),
|
|
396
|
+
opened: opened.length,
|
|
397
|
+
recovered: recovered.length,
|
|
398
|
+
digestDue,
|
|
399
|
+
...(delivery ? { deliveryId: delivery.id } : {}),
|
|
400
|
+
...(acceptanceMeta.scenarioHandleDigest ? { scenarioHandleDigest: acceptanceMeta.scenarioHandleDigest } : {}),
|
|
401
|
+
};
|
|
402
|
+
const next = {
|
|
403
|
+
incidents: current,
|
|
404
|
+
lastDigestDay: digestDue ? day : previous.lastDigestDay,
|
|
405
|
+
updatedAt: now().toISOString(),
|
|
406
|
+
outbox: delivery,
|
|
407
|
+
indeterminateDeliveries: digestDue ? [] : previous.indeterminateDeliveries,
|
|
408
|
+
deliveredReceipts: previous.deliveredReceipts,
|
|
409
|
+
sweepReceipts: [...previous.sweepReceipts, sweepReceipt].slice(-500),
|
|
410
|
+
};
|
|
411
|
+
save(options.statePath, next);
|
|
412
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.sanctuary_health_end", message: "Sanctuary deterministic health sweep completed", meta: { incidentCount: incidents.size, opened: opened.length, recovered: recovered.length, digestDue, ...acceptanceEventMeta() } });
|
|
413
|
+
return { message, incidents: Object.values(current), ...(delivery ? { deliveryId: delivery.id } : {}) };
|
|
414
|
+
};
|
|
415
|
+
const sweep = (() => withHealthLock(options.statePath, runSweep, options.lease));
|
|
416
|
+
sweep.markDeliveryAttempting = (deliveryId) => withHealthLock(options.statePath, () => {
|
|
417
|
+
const state = load(options.statePath);
|
|
418
|
+
if (!state.outbox || state.outbox.id !== deliveryId || state.outbox.status !== "pending")
|
|
419
|
+
throw new Error(`Sanctuary health delivery ${deliveryId} is not pending`);
|
|
420
|
+
state.outbox.status = "attempting";
|
|
421
|
+
state.updatedAt = now().toISOString();
|
|
422
|
+
save(options.statePath, state);
|
|
423
|
+
}, options.lease);
|
|
424
|
+
sweep.cacheDeliveryPayload = (deliveryId, message) => withHealthLock(options.statePath, () => {
|
|
425
|
+
if (typeof message !== "string" || message.trim().length === 0 || Buffer.byteLength(message) > MAX_HEALTH_TEXT_BYTES)
|
|
426
|
+
throw new Error("Sanctuary health cached delivery payload must be nonempty and bounded");
|
|
427
|
+
const state = load(options.statePath);
|
|
428
|
+
if (!state.outbox || state.outbox.id !== deliveryId || state.outbox.status !== "pending")
|
|
429
|
+
throw new Error(`Sanctuary health delivery ${deliveryId} is not pending`);
|
|
430
|
+
state.outbox.summarizedMessage = message;
|
|
431
|
+
state.updatedAt = now().toISOString();
|
|
432
|
+
save(options.statePath, state);
|
|
433
|
+
}, options.lease);
|
|
434
|
+
sweep.markDelivered = (deliveryId, messageIds) => withHealthLock(options.statePath, () => {
|
|
435
|
+
if (!Array.isArray(messageIds) || messageIds.length < 1 || messageIds.length > 100 || !messageIds.every((id) => Number.isSafeInteger(id) && id > 0)) {
|
|
436
|
+
throw new Error("Sanctuary health delivery receipt requires canonical Telegram message ids");
|
|
437
|
+
}
|
|
438
|
+
const state = load(options.statePath);
|
|
439
|
+
if (!state.outbox || state.outbox.id !== deliveryId || state.outbox.status !== "attempting")
|
|
440
|
+
throw new Error(`Sanctuary health delivery ${deliveryId} is not attempting`);
|
|
441
|
+
state.deliveredReceipts = [...state.deliveredReceipts, { deliveryId, kind: state.outbox.kind, messageIds: [...messageIds], deliveredAt: now().toISOString() }].slice(-100);
|
|
442
|
+
state.outbox = null;
|
|
443
|
+
state.updatedAt = now().toISOString();
|
|
444
|
+
save(options.statePath, state);
|
|
445
|
+
}, options.lease);
|
|
446
|
+
return sweep;
|
|
447
|
+
}
|