@ouro.bot/cli 0.1.0-alpha.955 → 0.1.0-alpha.957
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 +24 -0
- package/deploy/unraid/credit-gateway.md +95 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.ouro/mcp/media-mcp.mjs +22 -8
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/gateway/keys.js +117 -0
- package/dist/gateway/server.js +429 -0
- package/dist/heart/auth/auth-flow.js +17 -3
- package/dist/heart/config.js +5 -1
- package/dist/heart/core.js +33 -12
- package/dist/heart/daemon/cli-exec.js +25 -0
- package/dist/heart/daemon/cli-help.js +14 -2
- package/dist/heart/daemon/cli-parse.js +83 -0
- package/dist/heart/external-events/router.js +20 -12
- package/dist/heart/identity.js +1 -1
- package/dist/heart/provider-credentials.js +2 -2
- package/dist/heart/provider-ping.js +2 -1
- package/dist/heart/providers/anthropic.js +65 -7
- package/dist/heart/runtime-credentials.js +2 -1
- package/dist/repertoire/house-sweep.js +10 -8
- package/dist/repertoire/tools-continuity.js +16 -10
- package/dist/senses/private-runtime-worker.js +53 -1
- package/dist/senses/sanctuary-health-acceptance-probe.js +1 -1
- package/dist/senses/sanctuary-health.js +31 -5
- package/dist/senses/telegram-handoff.js +75 -1
- package/dist/senses/telegram.js +35 -4
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -40,6 +40,7 @@ const fs = __importStar(require("fs"));
|
|
|
40
40
|
const path = __importStar(require("path"));
|
|
41
41
|
const private_runtime_1 = require("./private-runtime");
|
|
42
42
|
const router_1 = require("../heart/external-events/router");
|
|
43
|
+
const telegram_handoff_1 = require("./telegram-handoff");
|
|
43
44
|
const runtime_1 = require("../nerves/runtime");
|
|
44
45
|
const identity_1 = require("../heart/identity");
|
|
45
46
|
const pending_1 = require("../mind/pending");
|
|
@@ -167,6 +168,50 @@ function reserveHabitAutonomyBudget(habitRun, nowIso) {
|
|
|
167
168
|
now: nowIso,
|
|
168
169
|
}, (0, autonomy_budget_1.resolveAutonomyBudgetPolicy)(habitRun.agentRoot, agentName));
|
|
169
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* External-event turns are paid model work, so they draw on their own daily allowance (the agent's
|
|
173
|
+
* habitPaidTurnsPerDay, counted under the "external-events" name so a busy event day cannot starve a
|
|
174
|
+
* habit) and share one storm breaker: three failed event turns within ten minutes pause event turns
|
|
175
|
+
* for thirty minutes.
|
|
176
|
+
*/
|
|
177
|
+
const EXTERNAL_EVENT_BUDGET_NAME = "external-events";
|
|
178
|
+
function externalEventStorm(agentName) {
|
|
179
|
+
return {
|
|
180
|
+
agent: agentName,
|
|
181
|
+
triggerType: "habit",
|
|
182
|
+
sourceKind: "private-runtime",
|
|
183
|
+
senseOrHabit: EXTERNAL_EVENT_BUDGET_NAME,
|
|
184
|
+
provider: "model",
|
|
185
|
+
target: { kind: EXTERNAL_EVENT_BUDGET_NAME },
|
|
186
|
+
normalizedErrorName: "external_event_turn_failed",
|
|
187
|
+
normalizedErrorCode: "turn_failed",
|
|
188
|
+
codeLocation: "private-runtime-worker",
|
|
189
|
+
idempotencyBucket: EXTERNAL_EVENT_BUDGET_NAME,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/** A blocked event turn waits this long (the breaker's block length) and keeps its retry attempts. */
|
|
193
|
+
const EXTERNAL_EVENT_BLOCKED_DEFER_MS = 30 * 60_000;
|
|
194
|
+
function reserveExternalEventAutonomyBudget(event, nowIso) {
|
|
195
|
+
// Disk, parity and array failures always get a turn: the owner must hear about them even on a busy day.
|
|
196
|
+
if ([event, ...(event.relatedEvents ?? [])].some((member) => (0, telegram_handoff_1.isCriticalNoticeKey)(member.eventId)))
|
|
197
|
+
return null;
|
|
198
|
+
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
199
|
+
const agentName = (0, identity_1.getAgentName)();
|
|
200
|
+
return (0, autonomy_budget_1.reserveAutonomyBudget)(agentRoot, {
|
|
201
|
+
agent: agentName,
|
|
202
|
+
triggerType: "habit",
|
|
203
|
+
sourceKind: "private-runtime",
|
|
204
|
+
senseOrHabit: EXTERNAL_EVENT_BUDGET_NAME,
|
|
205
|
+
target: { kind: EXTERNAL_EVENT_BUDGET_NAME },
|
|
206
|
+
idempotencyKey: `event:${event.recordPath}:${event.generation}:${event.claimOwner}`,
|
|
207
|
+
now: nowIso,
|
|
208
|
+
storm: externalEventStorm(agentName),
|
|
209
|
+
}, (0, autonomy_budget_1.resolveAutonomyBudgetPolicy)(agentRoot, agentName));
|
|
210
|
+
}
|
|
211
|
+
function recordExternalEventTurnFailure(nowIso) {
|
|
212
|
+
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
213
|
+
(0, autonomy_budget_1.recordAutonomyFailure)(agentRoot, { ...externalEventStorm((0, identity_1.getAgentName)()), occurredAt: nowIso }, (0, autonomy_budget_1.resolveAutonomyBudgetPolicy)(agentRoot, (0, identity_1.getAgentName)()));
|
|
214
|
+
}
|
|
170
215
|
/**
|
|
171
216
|
* Habit recursion detector thresholds. This protects against the *external*
|
|
172
217
|
* IPC self-loop where heartbeat-shaped messages get re-issued faster
|
|
@@ -537,7 +582,9 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
|
|
|
537
582
|
const turnStartedAt = new Date(nowSource()).toISOString();
|
|
538
583
|
const autonomyDecision = currentHabitRun && !currentHabitRun.blockedReason
|
|
539
584
|
? reserveHabitAutonomyBudget(currentHabitRun, turnStartedAt)
|
|
540
|
-
:
|
|
585
|
+
: currentExternalEvent
|
|
586
|
+
? reserveExternalEventAutonomyBudget(currentExternalEvent, turnStartedAt)
|
|
587
|
+
: null;
|
|
541
588
|
if (currentHabitRun)
|
|
542
589
|
recordHabitStartIfNeeded(currentHabitRun);
|
|
543
590
|
if (currentHabitRun?.blockedReason) {
|
|
@@ -620,15 +667,20 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
|
|
|
620
667
|
: blockedAutonomyTurn || turnOutcome === "blocked" || turnOutcome === "suspended"
|
|
621
668
|
? "external-event turn was blocked"
|
|
622
669
|
: "external-event turn completed without a terminal disposition";
|
|
670
|
+
const settledAt = nowSource();
|
|
671
|
+
const deferUntil = blockedAutonomyTurn ? new Date(settledAt + EXTERNAL_EVENT_BLOCKED_DEFER_MS).toISOString() : undefined;
|
|
623
672
|
let settledIncomplete = false;
|
|
624
673
|
for (const member of [currentExternalEvent, ...(currentExternalEvent.relatedEvents ?? [])]) {
|
|
625
674
|
const result = (0, router_1.settleExternalEventFailureIfOwned)(member.recordPath, {
|
|
626
675
|
owner: member.claimOwner,
|
|
627
676
|
expectedGeneration: member.generation,
|
|
628
677
|
error: failureReason,
|
|
678
|
+
...(deferUntil ? { deferUntil } : {}),
|
|
629
679
|
});
|
|
630
680
|
settledIncomplete = result.settled || settledIncomplete;
|
|
631
681
|
}
|
|
682
|
+
if (!blockedAutonomyTurn && (turnErrors.length > 0 || turnOutcome === "errored" || settledIncomplete))
|
|
683
|
+
recordExternalEventTurnFailure(new Date(settledAt).toISOString());
|
|
632
684
|
if (settledIncomplete && turnErrors.length === 0)
|
|
633
685
|
turnErrors.push(failureReason);
|
|
634
686
|
}
|
|
@@ -547,7 +547,7 @@ async function runSanctuaryHealthAcceptanceProbe(input, dependencies = createSan
|
|
|
547
547
|
}
|
|
548
548
|
const runPhase = async (name, trigger, fixtureStatus, fetchImpl, now) => {
|
|
549
549
|
const sweep = (0, sanctuary_health_1.createSanctuaryHealthSweep)({
|
|
550
|
-
toolContext: dependencies.toolContext, statePath, fetch: fetchImpl, now: () => now, lease,
|
|
550
|
+
toolContext: dependencies.toolContext, statePath, fetch: fetchImpl, now: () => now, lease, endpointDebounce: false,
|
|
551
551
|
acceptanceEventMeta: () => ({ scenarioHandleDigest: input.scenarioHandleDigest }),
|
|
552
552
|
});
|
|
553
553
|
await (0, sanctuary_health_runner_1.runSanctuaryHealthHabit)("sanctuary", {
|
|
@@ -226,6 +226,7 @@ function load(filePath) {
|
|
|
226
226
|
lastDigestDay: value.lastDigestDay,
|
|
227
227
|
updatedAt: value.updatedAt,
|
|
228
228
|
...(value.pendingRecovered?.length ? { pendingRecovered: value.pendingRecovered } : {}),
|
|
229
|
+
...(endpointFailuresOf(value.endpointFailures) ? { endpointFailures: endpointFailuresOf(value.endpointFailures) } : {}),
|
|
229
230
|
outbox: value.outbox ? { ...value.outbox, kind: value.outbox.kind ?? "legacy_unknown" } : null,
|
|
230
231
|
indeterminateDeliveries: (value.indeterminateDeliveries ?? []).map((delivery) => ({ ...delivery, kind: delivery.kind ?? "legacy_unknown" })),
|
|
231
232
|
deliveredReceipts: (value.deliveredReceipts ?? []).map((receipt) => ({ ...receipt, kind: receipt.kind ?? "legacy_unknown" })),
|
|
@@ -242,6 +243,13 @@ function load(filePath) {
|
|
|
242
243
|
function readSanctuaryHealthState(filePath) {
|
|
243
244
|
return load(filePath);
|
|
244
245
|
}
|
|
246
|
+
// A malformed failure counter only delays an endpoint incident by one sweep, so it is dropped rather than treated as corrupt state.
|
|
247
|
+
function endpointFailuresOf(value) {
|
|
248
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
249
|
+
return null;
|
|
250
|
+
const entries = Object.entries(value).filter(([, count]) => Number.isSafeInteger(count) && count > 0);
|
|
251
|
+
return entries.length > 0 ? Object.fromEntries(entries) : null;
|
|
252
|
+
}
|
|
245
253
|
function save(filePath, state) {
|
|
246
254
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
247
255
|
const temporary = `${filePath}.tmp-${process.pid}-${(0, node_crypto_1.randomUUID)()}`;
|
|
@@ -295,6 +303,16 @@ async function probeSanctuaryEndpoint(url, fetchImpl = fetch) {
|
|
|
295
303
|
}
|
|
296
304
|
function createSanctuaryHealthSweep(options) {
|
|
297
305
|
const fetchImpl = options.fetch ?? fetch;
|
|
306
|
+
const endpointRetryDelayMs = options.endpointRetryDelayMs ?? 3_000;
|
|
307
|
+
const endpointDebounce = options.endpointDebounce ?? true;
|
|
308
|
+
const endpointFailuresToOpen = endpointDebounce ? 2 : 1;
|
|
309
|
+
const probeWithRetry = async (url) => {
|
|
310
|
+
const first = await probeSanctuaryEndpoint(url, fetchImpl);
|
|
311
|
+
if (first.ok || !endpointDebounce)
|
|
312
|
+
return first;
|
|
313
|
+
await new Promise((resolve) => setTimeout(resolve, endpointRetryDelayMs));
|
|
314
|
+
return probeSanctuaryEndpoint(url, fetchImpl);
|
|
315
|
+
};
|
|
298
316
|
const now = options.now ?? (() => new Date());
|
|
299
317
|
const acceptanceEventMeta = options.acceptanceEventMeta ?? (() => (0, sanctuary_acceptance_marker_1.sanctuaryAcceptanceEventMeta)("sanctuary"));
|
|
300
318
|
const runSweep = async () => {
|
|
@@ -313,7 +331,7 @@ function createSanctuaryHealthSweep(options) {
|
|
|
313
331
|
throw new Error("Sanctuary health runtime is unavailable");
|
|
314
332
|
const [containersResult, storageResult, disksResult, notificationsResult, endpoints] = await Promise.all([
|
|
315
333
|
runtime.listContainers(), runtime.getStorage(), runtime.getDisks(), runtime.getNotifications(),
|
|
316
|
-
Promise.all(exports.SANCTUARY_PUBLIC_ENDPOINTS.map((url) =>
|
|
334
|
+
Promise.all(exports.SANCTUARY_PUBLIC_ENDPOINTS.map((url) => probeWithRetry(url))),
|
|
317
335
|
]);
|
|
318
336
|
const incidents = new Map();
|
|
319
337
|
const add = (id, rawSummary) => {
|
|
@@ -364,13 +382,20 @@ function createSanctuaryHealthSweep(options) {
|
|
|
364
382
|
const notifications = record(notificationsResult)?.ok ? record(record(notificationsResult)?.data)?.unacknowledged : null;
|
|
365
383
|
if (!Array.isArray(notifications))
|
|
366
384
|
add("notifications:unavailable", "notification status is unavailable");
|
|
385
|
+
// Informational (Unraid "normal") notifications are not incidents; warnings and alerts are.
|
|
367
386
|
else
|
|
368
387
|
for (const item of notifications)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (!endpoint.ok)
|
|
372
|
-
add(`endpoint:${endpoint.url}`, `${endpoint.url} returned ${endpoint.status || "no response"}`);
|
|
388
|
+
if (item.severity !== "info")
|
|
389
|
+
add(`notification:${item.id}`, `unacknowledged notification: ${item.title || item.id}`);
|
|
373
390
|
const previous = load(options.statePath);
|
|
391
|
+
const endpointFailures = {};
|
|
392
|
+
for (const endpoint of endpoints) {
|
|
393
|
+
if (endpoint.ok)
|
|
394
|
+
continue;
|
|
395
|
+
endpointFailures[endpoint.url] = (previous.endpointFailures?.[endpoint.url] ?? 0) + 1;
|
|
396
|
+
if (endpointFailures[endpoint.url] >= endpointFailuresToOpen)
|
|
397
|
+
add(`endpoint:${endpoint.url}`, `${endpoint.url} returned ${endpoint.status || "no response"}`);
|
|
398
|
+
}
|
|
374
399
|
const current = Object.fromEntries([...incidents.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([id, incident]) => {
|
|
375
400
|
const prior = previous.incidents[id];
|
|
376
401
|
const transition = !prior ? "opened" : prior.observationRevision === incident.observationRevision ? "unchanged" : "changed";
|
|
@@ -401,6 +426,7 @@ function createSanctuaryHealthSweep(options) {
|
|
|
401
426
|
lastDigestDay: null,
|
|
402
427
|
updatedAt: now().toISOString(),
|
|
403
428
|
...(unacknowledgedRecovered.length > 0 ? { pendingRecovered: unacknowledgedRecovered } : {}),
|
|
429
|
+
...(Object.keys(endpointFailures).length > 0 ? { endpointFailures } : {}),
|
|
404
430
|
outbox: null,
|
|
405
431
|
indeterminateDeliveries: [],
|
|
406
432
|
deliveredReceipts: previous.deliveredReceipts,
|
|
@@ -33,10 +33,17 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.OWNER_CHAT_HANDOFF_MS = void 0;
|
|
36
|
+
exports.QUIET_HOURS = exports.PROACTIVE_DAILY_CAP = exports.OWNER_CHAT_HANDOFF_MS = void 0;
|
|
37
37
|
exports.ownerChatHandedOff = ownerChatHandedOff;
|
|
38
38
|
exports.readHeldNotices = readHeldNotices;
|
|
39
39
|
exports.writeHeldNotices = writeHeldNotices;
|
|
40
|
+
exports.isQuietHour = isQuietHour;
|
|
41
|
+
exports.isCriticalNoticeKey = isCriticalNoticeKey;
|
|
42
|
+
exports.proactiveHoldReason = proactiveHoldReason;
|
|
43
|
+
exports.recordProactiveSend = recordProactiveSend;
|
|
44
|
+
exports.foldHeldNotices = foldHeldNotices;
|
|
45
|
+
exports.readReleasedHeldKeys = readReleasedHeldKeys;
|
|
46
|
+
exports.recordReleasedHeldKeys = recordReleasedHeldKeys;
|
|
40
47
|
/**
|
|
41
48
|
* Ari sometimes hands his Telegram chat to someone else ("I'm going to hand you to my wife, Rachel"). For a while after that,
|
|
42
49
|
* the chat is that person's conversation, so house notices (an outage, a recovery) wait instead of landing in it
|
|
@@ -45,6 +52,7 @@ exports.writeHeldNotices = writeHeldNotices;
|
|
|
45
52
|
*/
|
|
46
53
|
const fs = __importStar(require("node:fs"));
|
|
47
54
|
const path = __importStar(require("node:path"));
|
|
55
|
+
const node_crypto_1 = require("node:crypto");
|
|
48
56
|
const session_events_1 = require("../heart/session-events");
|
|
49
57
|
const runtime_1 = require("../nerves/runtime");
|
|
50
58
|
exports.OWNER_CHAT_HANDOFF_MS = 2 * 60 * 60 * 1000;
|
|
@@ -78,3 +86,69 @@ function writeHeldNotices(filePath, notices) {
|
|
|
78
86
|
fs.renameSync(temporary, filePath);
|
|
79
87
|
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.telegram_held_notices_saved", message: "saved the house notices held during a handoff", meta: { held: notices.length } });
|
|
80
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Proactive pacing: at most PROACTIVE_DAILY_CAP unprompted owner messages per local day, and none in quiet hours
|
|
91
|
+
* (23:00-08:00 local), except critical ones (array or disk failure, parity errors). A held message waits in the same
|
|
92
|
+
* held-notice file and goes out, folded into one message, at the next allowed moment.
|
|
93
|
+
*/
|
|
94
|
+
exports.PROACTIVE_DAILY_CAP = 3;
|
|
95
|
+
exports.QUIET_HOURS = { start: 23, end: 8 };
|
|
96
|
+
const CRITICAL_NOTICE = /\b(?:disk:[^"\s]*:smart|parity:|disk:array:state)/u;
|
|
97
|
+
function localParts(now, timeZone) {
|
|
98
|
+
const parts = Object.fromEntries(new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", hourCycle: "h23" })
|
|
99
|
+
.formatToParts(now).map((part) => [part.type, part.value]));
|
|
100
|
+
return { day: `${parts.year}-${parts.month}-${parts.day}`, hour: Number(parts.hour) };
|
|
101
|
+
}
|
|
102
|
+
function isQuietHour(now, pacing) {
|
|
103
|
+
const { hour } = localParts(now, pacing.timeZone);
|
|
104
|
+
return hour >= exports.QUIET_HOURS.start || hour < exports.QUIET_HOURS.end;
|
|
105
|
+
}
|
|
106
|
+
function isCriticalNoticeKey(key) {
|
|
107
|
+
return CRITICAL_NOTICE.test(key);
|
|
108
|
+
}
|
|
109
|
+
function readProactiveLedger(filePath) {
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
112
|
+
return typeof parsed.day === "string" && Number.isSafeInteger(parsed.count) ? { day: parsed.day, count: parsed.count } : null;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Why a non-critical proactive message must wait now, or null when it may go. */
|
|
119
|
+
function proactiveHoldReason(ledgerPath, now, pacing) {
|
|
120
|
+
if (isQuietHour(now, pacing))
|
|
121
|
+
return "quiet_hours";
|
|
122
|
+
const ledger = readProactiveLedger(ledgerPath);
|
|
123
|
+
return ledger?.day === localParts(now, pacing.timeZone).day && ledger.count >= exports.PROACTIVE_DAILY_CAP ? "daily_cap" : null;
|
|
124
|
+
}
|
|
125
|
+
function recordProactiveSend(ledgerPath, now, pacing) {
|
|
126
|
+
const day = localParts(now, pacing.timeZone).day;
|
|
127
|
+
const ledger = readProactiveLedger(ledgerPath);
|
|
128
|
+
fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });
|
|
129
|
+
fs.writeFileSync(ledgerPath, JSON.stringify({ day, count: ledger?.day === day ? ledger.count + 1 : 1 }), { mode: 0o600 });
|
|
130
|
+
}
|
|
131
|
+
/** Several held notices go out as one message so a quiet night never becomes a morning burst. */
|
|
132
|
+
function foldHeldNotices(held) {
|
|
133
|
+
if (held.length === 1)
|
|
134
|
+
return held[0];
|
|
135
|
+
return { key: `held-batch:${(0, node_crypto_1.createHash)("sha256").update(held.map((notice) => notice.key).join("\0")).digest("hex").slice(0, 32)}`, text: ["While you were away:", ...held.map((notice) => `- ${notice.text}`)].join("\n") };
|
|
136
|
+
}
|
|
137
|
+
const RELEASED_KEYS_LIMIT = 500;
|
|
138
|
+
/** Keys of held notices that went out inside a folded batch, so a retried decision sees its message already exists. */
|
|
139
|
+
function readReleasedHeldKeys(filePath) {
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
142
|
+
return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "string") : [];
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function recordReleasedHeldKeys(filePath, keys) {
|
|
149
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
150
|
+
const next = [...readReleasedHeldKeys(filePath).filter((key) => !keys.includes(key)), ...keys].slice(-RELEASED_KEYS_LIMIT);
|
|
151
|
+
const temporary = `${filePath}.${process.pid}.tmp`;
|
|
152
|
+
fs.writeFileSync(temporary, JSON.stringify(next), { mode: 0o600 });
|
|
153
|
+
fs.renameSync(temporary, filePath);
|
|
154
|
+
}
|
package/dist/senses/telegram.js
CHANGED
|
@@ -1137,13 +1137,22 @@ function createTelegramSenseApp(options) {
|
|
|
1137
1137
|
const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, configuredOwnerFriendId, "telegram", configuredOwnerSessionKey, agentRoot);
|
|
1138
1138
|
return (0, telegram_handoff_1.ownerChatHandedOff)((0, session_events_1.loadSessionEnvelopeFile)(sessionPath)?.events ?? [], Date.now());
|
|
1139
1139
|
};
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1140
|
+
const proactiveLedgerPath = path.join(agentRoot, "state", "telegram", "proactive-sends.json");
|
|
1141
|
+
const countProactiveSend = () => { if (options.proactivePacing)
|
|
1142
|
+
(0, telegram_handoff_1.recordProactiveSend)(proactiveLedgerPath, new Date(), options.proactivePacing); };
|
|
1143
|
+
const releasedHeldKeysPath = path.join(agentRoot, "state", "telegram", "released-held-keys.json");
|
|
1144
|
+
const holdNoticeDuringHandoff = (text, key, critical = (0, telegram_handoff_1.isCriticalNoticeKey)(key)) => {
|
|
1145
|
+
const handedOff = ownerChatIsHandedOff();
|
|
1146
|
+
const pacingReason = !handedOff && options.proactivePacing && !critical ? (0, telegram_handoff_1.proactiveHoldReason)(proactiveLedgerPath, new Date(), options.proactivePacing) : null;
|
|
1147
|
+
if (!handedOff && !pacingReason)
|
|
1142
1148
|
return false;
|
|
1143
1149
|
const held = (0, telegram_handoff_1.readHeldNotices)(heldNoticesPath);
|
|
1144
1150
|
if (!held.some((notice) => notice.key === key))
|
|
1145
1151
|
(0, telegram_handoff_1.writeHeldNotices)(heldNoticesPath, [...held, { key, text }]);
|
|
1146
|
-
|
|
1152
|
+
if (handedOff)
|
|
1153
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.telegram_notice_held_for_handoff", message: "held a house notice while the owner's chat is handed to someone else", meta: { agentName: options.agentName } });
|
|
1154
|
+
else
|
|
1155
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.telegram_notice_held_for_pacing", message: "held a house notice for quiet hours or the daily message cap", meta: { agentName: options.agentName, reason: pacingReason } });
|
|
1147
1156
|
return true;
|
|
1148
1157
|
};
|
|
1149
1158
|
let heldNoticeRelease = Promise.resolve();
|
|
@@ -1156,6 +1165,21 @@ function createTelegramSenseApp(options) {
|
|
|
1156
1165
|
if (ownerChatIsHandedOff())
|
|
1157
1166
|
return;
|
|
1158
1167
|
const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, configuredOwnerFriendId, "telegram", configuredOwnerSessionKey, agentRoot);
|
|
1168
|
+
if (options.proactivePacing) {
|
|
1169
|
+
// Held notices go out as one message, and only when an unprompted message is allowed now (quiet hours and the daily cap).
|
|
1170
|
+
if ((0, telegram_handoff_1.proactiveHoldReason)(proactiveLedgerPath, new Date(), options.proactivePacing))
|
|
1171
|
+
return;
|
|
1172
|
+
/* v8 ignore next -- the caller already saw held notices; only a concurrent release can empty the file before this re-read @preserve */
|
|
1173
|
+
if (held.length === 0)
|
|
1174
|
+
return;
|
|
1175
|
+
const folded = (0, telegram_handoff_1.foldHeldNotices)(held);
|
|
1176
|
+
if (held.length > 1)
|
|
1177
|
+
(0, telegram_handoff_1.recordReleasedHeldKeys)(releasedHeldKeysPath, held.map((notice) => notice.key));
|
|
1178
|
+
(0, telegram_handoff_1.writeHeldNotices)(heldNoticesPath, (0, telegram_handoff_1.readHeldNotices)(heldNoticesPath).filter((entry) => !held.some((notice) => notice.key === entry.key)));
|
|
1179
|
+
await recordAcceptedEffects(sessionPath, [await deliverButlerEffect(folded.text, folded.key, signal)]);
|
|
1180
|
+
countProactiveSend();
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1159
1183
|
for (const notice of held) {
|
|
1160
1184
|
(0, telegram_handoff_1.writeHeldNotices)(heldNoticesPath, (0, telegram_handoff_1.readHeldNotices)(heldNoticesPath).filter((entry) => entry.key !== notice.key));
|
|
1161
1185
|
await recordAcceptedEffects(sessionPath, [await deliverButlerEffect(notice.text, notice.key, signal)]);
|
|
@@ -1949,6 +1973,7 @@ function createTelegramSenseApp(options) {
|
|
|
1949
1973
|
return;
|
|
1950
1974
|
await releaseHeldNotices(signal);
|
|
1951
1975
|
const effect = await deliverButlerEffect(proactiveText, key, signal);
|
|
1976
|
+
countProactiveSend();
|
|
1952
1977
|
const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, configuredOwnerFriendId, "telegram", configuredOwnerSessionKey, agentRoot);
|
|
1953
1978
|
await recordAcceptedEffects(sessionPath, [effect]);
|
|
1954
1979
|
});
|
|
@@ -1967,7 +1992,9 @@ function createTelegramSenseApp(options) {
|
|
|
1967
1992
|
const text = requiredText(input.text, "external event decision");
|
|
1968
1993
|
const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, configuredOwnerFriendId, "telegram", configuredOwnerSessionKey, agentRoot);
|
|
1969
1994
|
const held = (0, telegram_handoff_1.readHeldNotices)(heldNoticesPath);
|
|
1995
|
+
const releasedKeys = (0, telegram_handoff_1.readReleasedHeldKeys)(releasedHeldKeysPath);
|
|
1970
1996
|
const ownerMessageExists = (candidate) => held.some((notice) => notice.key === candidate)
|
|
1997
|
+
|| releasedKeys.includes(candidate)
|
|
1971
1998
|
|| getEffectJournal().readByIdempotencyKey(candidate) !== null;
|
|
1972
1999
|
if (ownerMessageExists(key)) {
|
|
1973
2000
|
// A retry after the message went out (or was held): finish the original send, never send a reworded second one.
|
|
@@ -1991,10 +2018,11 @@ function createTelegramSenseApp(options) {
|
|
|
1991
2018
|
});
|
|
1992
2019
|
return { status: "covered", effectKey: coveredBy };
|
|
1993
2020
|
}
|
|
1994
|
-
if (holdNoticeDuringHandoff(text, key))
|
|
2021
|
+
if (holdNoticeDuringHandoff(text, key, [key, ...relatedKeys].some(telegram_handoff_1.isCriticalNoticeKey)))
|
|
1995
2022
|
return { status: "sent", effectKey: key };
|
|
1996
2023
|
await releaseHeldNotices(input.signal);
|
|
1997
2024
|
const effect = await deliverButlerEffect(text, key, input.signal);
|
|
2025
|
+
countProactiveSend();
|
|
1998
2026
|
await recordAcceptedEffects(sessionPath, [effect]);
|
|
1999
2027
|
return { status: "sent", effectKey: key };
|
|
2000
2028
|
});
|
|
@@ -2012,6 +2040,7 @@ function createTelegramSenseApp(options) {
|
|
|
2012
2040
|
return;
|
|
2013
2041
|
await releaseHeldNotices(input.signal);
|
|
2014
2042
|
const effect = await deliverButlerEffect(text, `owner-notice:${noticeId}`, input.signal);
|
|
2043
|
+
countProactiveSend();
|
|
2015
2044
|
const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, configuredOwnerFriendId, "telegram", configuredOwnerSessionKey, agentRoot);
|
|
2016
2045
|
await recordAcceptedEffects(sessionPath, [effect]);
|
|
2017
2046
|
});
|
|
@@ -2323,6 +2352,8 @@ async function startTelegramSenseApp(agentName, waitForAuthority = false) {
|
|
|
2323
2352
|
agentName,
|
|
2324
2353
|
credentials,
|
|
2325
2354
|
authorityTransport,
|
|
2355
|
+
// The owner is in Los Angeles; a container without TZ would otherwise run quiet hours on UTC.
|
|
2356
|
+
proactivePacing: { timeZone: process.env.TZ || "America/Los_Angeles" },
|
|
2326
2357
|
...(await createProductionTelegramRelationshipComposition(agentName, credentials, undefined, authorityTransport)),
|
|
2327
2358
|
...(agentName === "sanctuary" ? { privilegedFailsafe: {
|
|
2328
2359
|
eventRoot: (0, router_1.getExternalEventRoot)(),
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.957",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.957",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|