@ouro.bot/cli 0.1.0-alpha.732 → 0.1.0-alpha.734
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/README.md +1 -1
- package/changelog.json +34 -0
- package/deploy/unraid/Dockerfile +27 -0
- package/deploy/unraid/README.txt +1368 -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 +74 -2
- package/dist/heart/daemon/cli-help.js +6 -1
- package/dist/heart/daemon/cli-parse.js +6 -4
- package/dist/heart/daemon/container-credential-bootstrap.js +185 -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/doctor.js +56 -7
- 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/mailroom/blob-store.js +97 -19
- package/dist/mailroom/cache-sync-cli.js +5 -1
- package/dist/mailroom/core.js +23 -8
- package/dist/mailroom/hosted-cache-sync.js +179 -44
- package/dist/mailroom/search-cache.js +109 -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
|
@@ -30,6 +30,9 @@ function sameOptionalText(left, right) {
|
|
|
30
30
|
return (left ?? "").toLowerCase() === (right ?? "").toLowerCase();
|
|
31
31
|
}
|
|
32
32
|
function mailSearchCacheDocumentMatchesRecord(document, record, requireKeyProvenance) {
|
|
33
|
+
const bodyProvenanceMatches = !requireKeyProvenance ||
|
|
34
|
+
(document.bodyForm === "plaintext" && document.decryptionKeyId === undefined) ||
|
|
35
|
+
(document.bodyForm === "encrypted" && typeof document.decryptionKeyId === "string");
|
|
33
36
|
return document.schemaVersion === 1
|
|
34
37
|
&& document.agentId === record.agentId
|
|
35
38
|
&& document.messageId === record.id
|
|
@@ -38,7 +41,7 @@ function mailSearchCacheDocumentMatchesRecord(document, record, requireKeyProven
|
|
|
38
41
|
&& document.compartmentKind === record.compartmentKind
|
|
39
42
|
&& sameOptionalText(document.source, record.source)
|
|
40
43
|
&& document.textProjectionVersion === search_cache_1.MAIL_SEARCH_TEXT_PROJECTION_VERSION
|
|
41
|
-
&&
|
|
44
|
+
&& bodyProvenanceMatches;
|
|
42
45
|
}
|
|
43
46
|
function messageMatchesRecord(message, record) {
|
|
44
47
|
return message.id === record.id
|
|
@@ -63,37 +66,31 @@ async function fetchIndexedMessage(store, id) {
|
|
|
63
66
|
function removeCanonicalIfPresent(agentId, messageId, options) {
|
|
64
67
|
return (0, search_cache_1.removeMailSearchCacheDocument)(agentId, messageId, options) ? 1 : 0;
|
|
65
68
|
}
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
return null;
|
|
69
|
-
const keyId = error.keyId;
|
|
70
|
-
return typeof keyId === "string" && keyId.length > 0 ? keyId : null;
|
|
69
|
+
function recordFingerprint(record) {
|
|
70
|
+
return (0, blob_store_1.canonicalMailIndexRecordSnapshot)([record]).messageIndexFingerprint;
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
let records;
|
|
80
|
-
if (input.mode === "full-convergence") {
|
|
81
|
-
const observer = input.authority ?? input.store;
|
|
82
|
-
if (!observer.observeMessageIndexAuthority) {
|
|
83
|
-
throw new Error("hosted mail authority observer is unavailable");
|
|
84
|
-
}
|
|
85
|
-
authority = await observer.observeMessageIndexAuthority(input.agentId);
|
|
86
|
-
if (authority.parseFailureCount > 0 || authority.duplicateIds.length > 0) {
|
|
87
|
-
throw new Error(`hosted mail authority is ambiguous: ${authority.parseFailureCount} malformed index name(s), ${authority.duplicateIds.length} duplicate message id(s)`);
|
|
88
|
-
}
|
|
89
|
-
if (authority.records.length === 0) {
|
|
90
|
-
throw new Error("hosted mail authority is empty and cannot safely drive full convergence");
|
|
91
|
-
}
|
|
92
|
-
records = authority.records;
|
|
72
|
+
const FULL_CONVERGENCE_MAX_PASSES = 3;
|
|
73
|
+
const FULL_CONVERGENCE_CONCURRENCY = 20;
|
|
74
|
+
const PROGRESS_SETTLEMENT_INTERVAL = 250;
|
|
75
|
+
const PROGRESS_HEARTBEAT_MS = 30_000;
|
|
76
|
+
function safeProgress(input, progress) {
|
|
77
|
+
try {
|
|
78
|
+
input.onProgress?.(progress);
|
|
93
79
|
}
|
|
94
|
-
|
|
95
|
-
|
|
80
|
+
catch {
|
|
81
|
+
// Progress is advisory terminal UX. A broken sink must not alter convergence.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function validateAuthorityObservation(authority) {
|
|
85
|
+
if (authority.parseFailureCount > 0 || authority.duplicateIds.length > 0) {
|
|
86
|
+
throw new Error(`hosted mail authority is ambiguous: ${authority.parseFailureCount} malformed index name(s), ${authority.duplicateIds.length} duplicate message id(s)`);
|
|
87
|
+
}
|
|
88
|
+
if (authority.records.length === 0) {
|
|
89
|
+
throw new Error("hosted mail authority is empty and cannot safely drive full convergence");
|
|
96
90
|
}
|
|
91
|
+
return authority;
|
|
92
|
+
}
|
|
93
|
+
async function reconcileRecords(input, records, pass, progressCursor) {
|
|
97
94
|
const recordsById = new Map(records.map((record) => [record.id, record]));
|
|
98
95
|
const canonicalDocuments = new Map();
|
|
99
96
|
let removed = 0;
|
|
@@ -108,6 +105,12 @@ async function performHostedMailSearchCacheSync(input) {
|
|
|
108
105
|
}
|
|
109
106
|
canonicalDocuments.set(document.messageId, document);
|
|
110
107
|
}
|
|
108
|
+
for (const inspected of (0, search_cache_1.inspectMailSearchSkipReceipts)(input.agentId, input.cacheOptions)) {
|
|
109
|
+
const receipt = inspected.receipt;
|
|
110
|
+
if (!receipt || !inspected.canonical || !recordsById.has(receipt.messageId)) {
|
|
111
|
+
(0, search_cache_1.removeMailSearchSkipReceiptFile)(input.agentId, inspected.fileName, input.cacheOptions);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
111
114
|
}
|
|
112
115
|
else {
|
|
113
116
|
for (const document of (0, search_cache_1.searchMailSearchCache)({
|
|
@@ -122,17 +125,39 @@ async function performHostedMailSearchCacheSync(input) {
|
|
|
122
125
|
let fetched = 0;
|
|
123
126
|
let alreadyCached = 0;
|
|
124
127
|
let skipped = 0;
|
|
128
|
+
let settled = 0;
|
|
129
|
+
let nextRecord = 0;
|
|
125
130
|
const failures = [];
|
|
126
|
-
|
|
131
|
+
progressCursor.pass = pass;
|
|
132
|
+
progressCursor.settled = settled;
|
|
133
|
+
progressCursor.total = records.length;
|
|
134
|
+
safeProgress(input, { phase: "pass-start", pass, settled, total: records.length });
|
|
135
|
+
async function processRecord(record) {
|
|
127
136
|
const cached = canonicalDocuments.get(record.id);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
const cachedEncryptedKeyMissing = input.mode === "full-convergence" &&
|
|
138
|
+
cached?.bodyForm === "encrypted" &&
|
|
139
|
+
typeof cached.decryptionKeyId === "string" &&
|
|
140
|
+
!input.privateKeys[cached.decryptionKeyId];
|
|
141
|
+
if (cached &&
|
|
142
|
+
!cachedEncryptedKeyMissing &&
|
|
143
|
+
mailSearchCacheDocumentMatchesRecord(cached, record, input.mode === "full-convergence")) {
|
|
144
|
+
if (input.mode === "full-convergence") {
|
|
145
|
+
(0, search_cache_1.removeMailSearchSkipReceipt)(input.agentId, record.id, input.cacheOptions);
|
|
146
|
+
}
|
|
134
147
|
alreadyCached += 1;
|
|
135
|
-
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (input.mode === "full-convergence") {
|
|
151
|
+
const receipt = (0, search_cache_1.readMailSearchSkipReceipt)(input.agentId, record.id, input.cacheOptions);
|
|
152
|
+
if (receipt &&
|
|
153
|
+
receipt.recordFingerprint === recordFingerprint(record) &&
|
|
154
|
+
!input.privateKeys[receipt.missingKeyId]) {
|
|
155
|
+
removed += removeCanonicalIfPresent(input.agentId, record.id, input.cacheOptions);
|
|
156
|
+
skipped += 1;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (receipt)
|
|
160
|
+
(0, search_cache_1.removeMailSearchSkipReceipt)(input.agentId, record.id, input.cacheOptions);
|
|
136
161
|
}
|
|
137
162
|
try {
|
|
138
163
|
const message = await fetchIndexedMessage(input.store, record.id);
|
|
@@ -146,29 +171,79 @@ async function performHostedMailSearchCacheSync(input) {
|
|
|
146
171
|
(0, search_cache_1.upsertMailSearchCacheDocument)(message, decrypted.private, input.cacheOptions, {
|
|
147
172
|
...(message.bodyForm === "encrypted" ? { decryptionKeyId: message.privateEnvelope.keyId } : {}),
|
|
148
173
|
});
|
|
174
|
+
if (input.mode === "full-convergence") {
|
|
175
|
+
(0, search_cache_1.removeMailSearchSkipReceipt)(input.agentId, record.id, input.cacheOptions);
|
|
176
|
+
}
|
|
149
177
|
}
|
|
150
178
|
catch (error) {
|
|
151
|
-
if (
|
|
179
|
+
if (error instanceof core_1.MissingPrivateMailKeyError) {
|
|
152
180
|
removed += removeCanonicalIfPresent(input.agentId, record.id, input.cacheOptions);
|
|
153
181
|
skipped += 1;
|
|
182
|
+
if (input.mode === "full-convergence") {
|
|
183
|
+
(0, search_cache_1.writeMailSearchSkipReceipt)({
|
|
184
|
+
schemaVersion: 1,
|
|
185
|
+
agentId: input.agentId,
|
|
186
|
+
messageId: record.id,
|
|
187
|
+
recordFingerprint: recordFingerprint(record),
|
|
188
|
+
missingKeyId: error.keyId,
|
|
189
|
+
reason: "missing-private-key",
|
|
190
|
+
observedAt: new Date().toISOString(),
|
|
191
|
+
}, input.cacheOptions);
|
|
192
|
+
}
|
|
154
193
|
}
|
|
155
194
|
else {
|
|
156
|
-
|
|
195
|
+
throw error;
|
|
157
196
|
}
|
|
158
197
|
}
|
|
159
198
|
}
|
|
199
|
+
async function worker() {
|
|
200
|
+
while (true) {
|
|
201
|
+
const recordIndex = nextRecord;
|
|
202
|
+
nextRecord += 1;
|
|
203
|
+
if (recordIndex >= records.length)
|
|
204
|
+
return;
|
|
205
|
+
try {
|
|
206
|
+
await processRecord(records[recordIndex]);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
failures.push({
|
|
210
|
+
index: recordIndex,
|
|
211
|
+
id: records[recordIndex].id,
|
|
212
|
+
error: error instanceof Error ? error.message : String(error),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
settled += 1;
|
|
217
|
+
progressCursor.settled = settled;
|
|
218
|
+
if (settled % PROGRESS_SETTLEMENT_INTERVAL === 0) {
|
|
219
|
+
safeProgress(input, { phase: "settled", pass, settled, total: records.length });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const workerCount = Math.min(FULL_CONVERGENCE_CONCURRENCY, records.length);
|
|
226
|
+
await Promise.allSettled(Array.from({ length: workerCount }, () => worker()));
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
safeProgress(input, { phase: "pass-complete", pass, settled, total: records.length });
|
|
230
|
+
}
|
|
160
231
|
if (failures.length > 0) {
|
|
232
|
+
failures.sort((left, right) => left.index - right.index);
|
|
161
233
|
const sample = failures.slice(0, 3).map((entry) => `${entry.id}: ${entry.error}`).join("; ");
|
|
162
|
-
throw new Error(`mail search index refresh incomplete; ${failures.length}
|
|
234
|
+
throw new Error(`mail search index refresh incomplete; ${failures.length} record operation(s) failed. first failure(s): ${sample}`);
|
|
163
235
|
}
|
|
236
|
+
return { fetched, alreadyCached, removed, skipped };
|
|
237
|
+
}
|
|
238
|
+
function writeCoverage(input, records, snapshot) {
|
|
239
|
+
const recordsById = new Map(records.map((record) => [record.id, record]));
|
|
164
240
|
const cachedIds = new Set((0, search_cache_1.searchMailSearchCache)({
|
|
165
241
|
agentId: input.agentId,
|
|
166
242
|
placement: input.placement,
|
|
167
243
|
compartmentKind: input.scope,
|
|
168
244
|
source: input.source,
|
|
169
245
|
}, input.cacheOptions).filter((document) => recordsById.has(document.messageId)).map((document) => document.messageId));
|
|
170
|
-
|
|
171
|
-
const coverage = (0, search_cache_1.writeMailSearchCoverageRecord)({
|
|
246
|
+
return (0, search_cache_1.writeMailSearchCoverageRecord)({
|
|
172
247
|
schemaVersion: 1,
|
|
173
248
|
agentId: input.agentId,
|
|
174
249
|
storeKind: input.storeKind,
|
|
@@ -179,12 +254,60 @@ async function performHostedMailSearchCacheSync(input) {
|
|
|
179
254
|
visibleMessageCount: records.length,
|
|
180
255
|
cachedMessageCount: cachedIds.size,
|
|
181
256
|
decryptableMessageCount: cachedIds.size,
|
|
182
|
-
skippedMessageCount:
|
|
257
|
+
skippedMessageCount: records.length - cachedIds.size,
|
|
183
258
|
messageIndexFingerprint: snapshot.messageIndexFingerprint,
|
|
184
259
|
textProjectionVersion: search_cache_1.MAIL_SEARCH_TEXT_PROJECTION_VERSION,
|
|
185
260
|
...(snapshot.oldestReceivedAt ? { oldestReceivedAt: snapshot.oldestReceivedAt } : {}),
|
|
186
261
|
...(snapshot.newestReceivedAt ? { newestReceivedAt: snapshot.newestReceivedAt } : {}),
|
|
187
262
|
}, input.cacheOptions);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Converge a local decrypted cache against hosted index authority, or perform
|
|
266
|
+
* the legacy bounded upsert used by the in-agent refresh tool. This core never
|
|
267
|
+
* writes remote mail state and never records access; adapters own audit policy.
|
|
268
|
+
*/
|
|
269
|
+
async function performHostedMailSearchCacheSyncWithProgress(input, progressCursor) {
|
|
270
|
+
let finalRecords;
|
|
271
|
+
let finalSnapshot;
|
|
272
|
+
let fetched = 0;
|
|
273
|
+
let removed = 0;
|
|
274
|
+
let alreadyCached = 0;
|
|
275
|
+
if (input.mode === "full-convergence") {
|
|
276
|
+
const observer = input.authority ?? input.store;
|
|
277
|
+
if (!observer.observeMessageIndexAuthority) {
|
|
278
|
+
throw new Error("hosted mail authority observer is unavailable");
|
|
279
|
+
}
|
|
280
|
+
let authority = validateAuthorityObservation(await observer.observeMessageIndexAuthority(input.agentId));
|
|
281
|
+
let stable = false;
|
|
282
|
+
for (let pass = 1; pass <= FULL_CONVERGENCE_MAX_PASSES; pass += 1) {
|
|
283
|
+
const reconciliation = await reconcileRecords(input, authority.records, pass, progressCursor);
|
|
284
|
+
fetched += reconciliation.fetched;
|
|
285
|
+
removed += reconciliation.removed;
|
|
286
|
+
alreadyCached = reconciliation.alreadyCached;
|
|
287
|
+
const followup = validateAuthorityObservation(await observer.observeMessageIndexAuthority(input.agentId));
|
|
288
|
+
if (followup.snapshot.messageIndexFingerprint === authority.snapshot.messageIndexFingerprint) {
|
|
289
|
+
authority = followup;
|
|
290
|
+
stable = true;
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
authority = followup;
|
|
294
|
+
}
|
|
295
|
+
if (!stable) {
|
|
296
|
+
throw new Error(`hosted mail authority did not stabilize after ${FULL_CONVERGENCE_MAX_PASSES} passes`);
|
|
297
|
+
}
|
|
298
|
+
finalRecords = authority.records;
|
|
299
|
+
finalSnapshot = authority.snapshot;
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
finalRecords = await scopedRecords(input);
|
|
303
|
+
const reconciliation = await reconcileRecords(input, finalRecords, 1, progressCursor);
|
|
304
|
+
fetched = reconciliation.fetched;
|
|
305
|
+
removed = reconciliation.removed;
|
|
306
|
+
alreadyCached = reconciliation.alreadyCached;
|
|
307
|
+
finalSnapshot = (0, blob_store_1.canonicalMailIndexRecordSnapshot)(finalRecords);
|
|
308
|
+
}
|
|
309
|
+
const coverage = writeCoverage(input, finalRecords, finalSnapshot);
|
|
310
|
+
const skipped = coverage.skippedMessageCount;
|
|
188
311
|
(0, runtime_1.emitNervesEvent)({
|
|
189
312
|
component: "senses",
|
|
190
313
|
event: "senses.mail_search_cache_synced",
|
|
@@ -192,7 +315,7 @@ async function performHostedMailSearchCacheSync(input) {
|
|
|
192
315
|
meta: {
|
|
193
316
|
agentId: input.agentId,
|
|
194
317
|
mode: input.mode,
|
|
195
|
-
visible:
|
|
318
|
+
visible: finalRecords.length,
|
|
196
319
|
fetched,
|
|
197
320
|
alreadyCached,
|
|
198
321
|
removed,
|
|
@@ -201,6 +324,18 @@ async function performHostedMailSearchCacheSync(input) {
|
|
|
201
324
|
});
|
|
202
325
|
return { coverage, fetched, alreadyCached, removed, skipped };
|
|
203
326
|
}
|
|
327
|
+
async function performHostedMailSearchCacheSync(input) {
|
|
328
|
+
const progressCursor = { pass: 1, settled: 0, total: 0 };
|
|
329
|
+
const heartbeat = setInterval(() => {
|
|
330
|
+
safeProgress(input, { phase: "heartbeat", ...progressCursor });
|
|
331
|
+
}, PROGRESS_HEARTBEAT_MS);
|
|
332
|
+
try {
|
|
333
|
+
return await performHostedMailSearchCacheSyncWithProgress(input, progressCursor);
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
clearInterval(heartbeat);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
204
339
|
async function syncHostedMailSearchCache(input) {
|
|
205
340
|
const traceId = (0, node_crypto_1.randomUUID)();
|
|
206
341
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -35,6 +35,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.MAIL_SEARCH_TEXT_PROJECTION_VERSION = void 0;
|
|
37
37
|
exports.buildMailSearchCacheDocument = buildMailSearchCacheDocument;
|
|
38
|
+
exports.writeMailSearchSkipReceipt = writeMailSearchSkipReceipt;
|
|
39
|
+
exports.readMailSearchSkipReceipt = readMailSearchSkipReceipt;
|
|
40
|
+
exports.inspectMailSearchSkipReceipts = inspectMailSearchSkipReceipts;
|
|
41
|
+
exports.removeMailSearchSkipReceipt = removeMailSearchSkipReceipt;
|
|
42
|
+
exports.removeMailSearchSkipReceiptFile = removeMailSearchSkipReceiptFile;
|
|
38
43
|
exports.upsertMailSearchCacheDocument = upsertMailSearchCacheDocument;
|
|
39
44
|
exports.inspectMailSearchCacheFiles = inspectMailSearchCacheFiles;
|
|
40
45
|
exports.reloadMailSearchCache = reloadMailSearchCache;
|
|
@@ -69,6 +74,13 @@ function cachePath(agentId, messageId, options) {
|
|
|
69
74
|
function coverageDir(agentId, options) {
|
|
70
75
|
return path.join(cacheDir(agentId, options), "coverage");
|
|
71
76
|
}
|
|
77
|
+
function skippedDir(agentId, options) {
|
|
78
|
+
return path.join(cacheDir(agentId, options), "skipped");
|
|
79
|
+
}
|
|
80
|
+
function skipReceiptPath(agentId, messageId, options) {
|
|
81
|
+
const encoded = Buffer.from(messageId).toString("base64url");
|
|
82
|
+
return path.join(skippedDir(agentId, options), `${encoded}.json`);
|
|
83
|
+
}
|
|
72
84
|
function normalizedCoverageKey(key) {
|
|
73
85
|
return {
|
|
74
86
|
agentId: key.agentId,
|
|
@@ -100,6 +112,39 @@ function readJsonDocument(filePath) {
|
|
|
100
112
|
return null;
|
|
101
113
|
}
|
|
102
114
|
}
|
|
115
|
+
function readSkipReceipt(filePath) {
|
|
116
|
+
let value;
|
|
117
|
+
try {
|
|
118
|
+
value = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
if (typeof value !== "object" || value === null)
|
|
124
|
+
return null;
|
|
125
|
+
const receipt = value;
|
|
126
|
+
if (receipt.schemaVersion !== 1 ||
|
|
127
|
+
typeof receipt.agentId !== "string" || receipt.agentId.length === 0 ||
|
|
128
|
+
typeof receipt.messageId !== "string" || receipt.messageId.length === 0 ||
|
|
129
|
+
typeof receipt.recordFingerprint !== "string" || !/^[a-f0-9]{64}$/.test(receipt.recordFingerprint) ||
|
|
130
|
+
typeof receipt.missingKeyId !== "string" || receipt.missingKeyId.length === 0 ||
|
|
131
|
+
receipt.reason !== "missing-private-key" ||
|
|
132
|
+
typeof receipt.observedAt !== "string" || !Number.isFinite(Date.parse(receipt.observedAt))) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return receipt;
|
|
136
|
+
}
|
|
137
|
+
function isRegularFileNoFollow(filePath) {
|
|
138
|
+
try {
|
|
139
|
+
return fs.lstatSync(filePath).isFile();
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (typeof error === "object" && error !== null && "code" in error &&
|
|
143
|
+
(error.code === "ENOENT" || error.code === "ENOTDIR"))
|
|
144
|
+
return false;
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
103
148
|
function writeJsonAtomically(filePath, value) {
|
|
104
149
|
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
105
150
|
try {
|
|
@@ -158,10 +203,74 @@ function buildMailSearchCacheDocument(message, privateEnvelope, provenance = {})
|
|
|
158
203
|
untrustedContentWarning: privateEnvelope.untrustedContentWarning,
|
|
159
204
|
searchText: normalizeSearchText(privateEnvelope),
|
|
160
205
|
textProjectionVersion: exports.MAIL_SEARCH_TEXT_PROJECTION_VERSION,
|
|
206
|
+
bodyForm: message.bodyForm ?? (decryptionKeyId ? "encrypted" : "plaintext"),
|
|
161
207
|
attachmentCount: privateEnvelope.attachments.length,
|
|
162
208
|
...(decryptionKeyId ? { decryptionKeyId } : {}),
|
|
163
209
|
};
|
|
164
210
|
}
|
|
211
|
+
function writeMailSearchSkipReceipt(receipt, options) {
|
|
212
|
+
const dir = skippedDir(receipt.agentId, options);
|
|
213
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
214
|
+
writeJsonAtomically(skipReceiptPath(receipt.agentId, receipt.messageId, options), receipt);
|
|
215
|
+
return receipt;
|
|
216
|
+
}
|
|
217
|
+
function readMailSearchSkipReceipt(agentId, messageId, options) {
|
|
218
|
+
const filePath = skipReceiptPath(agentId, messageId, options);
|
|
219
|
+
if (!isRegularFileNoFollow(filePath))
|
|
220
|
+
return null;
|
|
221
|
+
const receipt = readSkipReceipt(filePath);
|
|
222
|
+
return receipt?.agentId === agentId && receipt.messageId === messageId ? receipt : null;
|
|
223
|
+
}
|
|
224
|
+
function inspectMailSearchSkipReceipts(agentId, options) {
|
|
225
|
+
const dir = skippedDir(agentId, options);
|
|
226
|
+
if (!fs.existsSync(dir))
|
|
227
|
+
return [];
|
|
228
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
229
|
+
.filter((entry) => (entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(".json"))
|
|
230
|
+
.map((entry) => {
|
|
231
|
+
const filePath = path.join(dir, entry.name);
|
|
232
|
+
const receipt = entry.isFile() ? readSkipReceipt(filePath) : null;
|
|
233
|
+
return {
|
|
234
|
+
fileName: entry.name,
|
|
235
|
+
filePath,
|
|
236
|
+
receipt: receipt?.agentId === agentId ? receipt : null,
|
|
237
|
+
canonical: receipt?.agentId === agentId && entry.name === path.basename(skipReceiptPath(agentId, receipt.messageId, options)),
|
|
238
|
+
};
|
|
239
|
+
})
|
|
240
|
+
.sort((left, right) => left.fileName.localeCompare(right.fileName));
|
|
241
|
+
}
|
|
242
|
+
function removeMailSearchSkipReceipt(agentId, messageId, options) {
|
|
243
|
+
const filePath = skipReceiptPath(agentId, messageId, options);
|
|
244
|
+
try {
|
|
245
|
+
const entry = fs.lstatSync(filePath);
|
|
246
|
+
if (!entry.isFile() && !entry.isSymbolicLink())
|
|
247
|
+
return false;
|
|
248
|
+
fs.unlinkSync(filePath);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
if (!(typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT"))
|
|
253
|
+
throw error;
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function removeMailSearchSkipReceiptFile(agentId, fileName, options) {
|
|
258
|
+
if (path.basename(fileName) !== fileName || !fileName.endsWith(".json"))
|
|
259
|
+
return false;
|
|
260
|
+
const filePath = path.join(skippedDir(agentId, options), fileName);
|
|
261
|
+
try {
|
|
262
|
+
const entry = fs.lstatSync(filePath);
|
|
263
|
+
if (!entry.isFile() && !entry.isSymbolicLink())
|
|
264
|
+
return false;
|
|
265
|
+
fs.unlinkSync(filePath);
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
if (!(typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT"))
|
|
270
|
+
throw error;
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
165
274
|
function upsertMailSearchCacheDocument(message, privateEnvelope, options, provenance) {
|
|
166
275
|
const document = buildMailSearchCacheDocument(message, privateEnvelope, provenance);
|
|
167
276
|
const dir = cacheDir(message.agentId, options);
|
package/dist/mind/prompt.js
CHANGED
|
@@ -348,6 +348,7 @@ const PROCESS_TYPE_LABELS = {
|
|
|
348
348
|
voice: "voice handler",
|
|
349
349
|
a2a: "a2a handler",
|
|
350
350
|
mcp: "mcp bridge",
|
|
351
|
+
telegram: "telegram handler",
|
|
351
352
|
};
|
|
352
353
|
function processTypeLabel(channel) {
|
|
353
354
|
return PROCESS_TYPE_LABELS[channel];
|
|
@@ -468,6 +469,7 @@ function localSenseStatusLines() {
|
|
|
468
469
|
mail: configuredSenses.mail ?? { enabled: false },
|
|
469
470
|
voice: configuredSenses.voice ?? { enabled: false },
|
|
470
471
|
a2a: configuredSenses.a2a ?? { enabled: false },
|
|
472
|
+
telegram: configuredSenses.telegram ?? { enabled: false },
|
|
471
473
|
workbench: configuredSenses.workbench ?? { enabled: false },
|
|
472
474
|
};
|
|
473
475
|
const payload = (0, config_1.loadConfig)();
|
|
@@ -493,6 +495,9 @@ function localSenseStatusLines() {
|
|
|
493
495
|
&& hasTextField(voice, "whisperCliPath")
|
|
494
496
|
&& hasTextField(voice, "whisperModelPath"),
|
|
495
497
|
a2a: true,
|
|
498
|
+
telegram: hasTextField(runtimePayload, "telegramBotToken")
|
|
499
|
+
&& hasTextField(runtimePayload, "telegramAuthorizedUserId")
|
|
500
|
+
&& hasTextField(runtimePayload, "telegramAuthorizedChatId"),
|
|
496
501
|
workbench: false,
|
|
497
502
|
};
|
|
498
503
|
const rows = [
|
|
@@ -517,6 +522,10 @@ function localSenseStatusLines() {
|
|
|
517
522
|
label: "A2A",
|
|
518
523
|
status: senses.a2a.enabled ? "ready" : "disabled",
|
|
519
524
|
},
|
|
525
|
+
{
|
|
526
|
+
label: "Telegram",
|
|
527
|
+
status: !senses.telegram.enabled ? "disabled" : configured.telegram ? "ready" : "needs_config",
|
|
528
|
+
},
|
|
520
529
|
{
|
|
521
530
|
label: "Workbench",
|
|
522
531
|
// Workbench can be injected at runtime (the Workbench app spawns
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Static source scanner for emitNervesEvent calls.
|
|
3
|
+
* Static source scanner for emitNervesEvent and emitNervesEventDurable calls.
|
|
4
4
|
*
|
|
5
5
|
* Extracts component:event keys from production source files by
|
|
6
|
-
* regex-matching
|
|
6
|
+
* regex-matching either static emitter with literal component/event fields.
|
|
7
7
|
* calls. Only accepts static string literals (single or double quotes).
|
|
8
8
|
* Template literals and variable references are rejected.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.scanSourceForNervesKeys = scanSourceForNervesKeys;
|
|
12
|
-
const EMIT_CALL_RE = /emitNervesEvent
|
|
12
|
+
const EMIT_CALL_RE = /emitNervesEvent(?:Durable)?\s*\(\s*\{([\s\S]*?)\}\s*\)/g;
|
|
13
13
|
function extractStringLiteral(block, field) {
|
|
14
14
|
const re = new RegExp(`${field}\\s*:\\s*(['"])((?:(?!\\1).)+)\\1`);
|
|
15
15
|
const match = re.exec(block);
|
|
16
16
|
return match ? match[2] : null;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* Scan a source file's content for
|
|
19
|
+
* Scan a source file's content for synchronous or durable Nerves calls and extract
|
|
20
20
|
* component:event keys. Only static string literals are accepted.
|
|
21
21
|
*/
|
|
22
22
|
function scanSourceForNervesKeys(source) {
|
package/dist/nerves/index.js
CHANGED
|
@@ -61,6 +61,24 @@ const LEVEL_PRIORITY = {
|
|
|
61
61
|
warn: 30,
|
|
62
62
|
error: 40,
|
|
63
63
|
};
|
|
64
|
+
function fsyncFileAndDirectory(filePath) {
|
|
65
|
+
if ((0, fs_1.existsSync)(filePath)) {
|
|
66
|
+
const fileDescriptor = (0, fs_1.openSync)(filePath, "r");
|
|
67
|
+
try {
|
|
68
|
+
(0, fs_1.fsyncSync)(fileDescriptor);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
(0, fs_1.closeSync)(fileDescriptor);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const directoryDescriptor = (0, fs_1.openSync)((0, path_1.dirname)(filePath), "r");
|
|
75
|
+
try {
|
|
76
|
+
(0, fs_1.fsyncSync)(directoryDescriptor);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
(0, fs_1.closeSync)(directoryDescriptor);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
64
82
|
const GLOBAL_SINKS_KEY = Symbol.for("ouroboros.nerves.global-sinks");
|
|
65
83
|
function resolveGlobalSinks() {
|
|
66
84
|
const scope = globalThis;
|
|
@@ -248,6 +266,7 @@ function rotateIfNeeded(filePath, optionsOrMaxSize) {
|
|
|
248
266
|
if ((0, fs_1.existsSync)(plain1)) {
|
|
249
267
|
(0, fs_1.unlinkSync)(plain1);
|
|
250
268
|
}
|
|
269
|
+
fsyncFileAndDirectory(filePath);
|
|
251
270
|
(0, fs_1.renameSync)(filePath, plain1);
|
|
252
271
|
// Step 3: gzip (or keep plain) the renamed file into the .1 generation.
|
|
253
272
|
if (compress) {
|
|
@@ -259,8 +278,10 @@ function rotateIfNeeded(filePath, optionsOrMaxSize) {
|
|
|
259
278
|
const buf = (0, fs_1.readFileSync)(plain1);
|
|
260
279
|
const compressed = zlib.gzipSync(buf);
|
|
261
280
|
(0, fs_1.writeFileSync)(gz1, compressed);
|
|
281
|
+
fsyncFileAndDirectory(gz1);
|
|
262
282
|
(0, fs_1.unlinkSync)(plain1);
|
|
263
283
|
}
|
|
284
|
+
fsyncFileAndDirectory(compress ? generationGzPath(base, ext, 1) : plain1);
|
|
264
285
|
completed = true;
|
|
265
286
|
(0, runtime_1.emitNervesEvent)({
|
|
266
287
|
component: "nerves",
|
|
@@ -298,6 +319,33 @@ function createNdjsonFileSink(filePath, optionsOrMaxSize) {
|
|
|
298
319
|
const queue = [];
|
|
299
320
|
let flushing = false;
|
|
300
321
|
let bytesSinceCheck = 0;
|
|
322
|
+
let enqueued = 0;
|
|
323
|
+
let completed = 0;
|
|
324
|
+
let appendFailure = null;
|
|
325
|
+
const barriers = [];
|
|
326
|
+
function settleBarriers() {
|
|
327
|
+
const ready = barriers.filter((barrier) => barrier.target <= completed);
|
|
328
|
+
if (ready.length === 0)
|
|
329
|
+
return;
|
|
330
|
+
for (const barrier of ready)
|
|
331
|
+
barriers.splice(barriers.indexOf(barrier), 1);
|
|
332
|
+
if (appendFailure) {
|
|
333
|
+
for (const barrier of ready)
|
|
334
|
+
barrier.reject(appendFailure);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
fsyncFileAndDirectory(filePath);
|
|
339
|
+
for (const barrier of ready)
|
|
340
|
+
barrier.resolve();
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
344
|
+
appendFailure = failure;
|
|
345
|
+
for (const barrier of ready)
|
|
346
|
+
barrier.reject(failure);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
301
349
|
function flush() {
|
|
302
350
|
if (flushing || queue.length === 0)
|
|
303
351
|
return;
|
|
@@ -310,31 +358,42 @@ function createNdjsonFileSink(filePath, optionsOrMaxSize) {
|
|
|
310
358
|
try {
|
|
311
359
|
rotateIfNeeded(filePath, options);
|
|
312
360
|
}
|
|
313
|
-
catch {
|
|
314
|
-
|
|
361
|
+
catch (rotationError) {
|
|
362
|
+
appendFailure ??= rotationError instanceof Error ? rotationError : new Error(String(rotationError));
|
|
315
363
|
}
|
|
316
364
|
}
|
|
317
|
-
(0, fs_1.appendFile)(filePath, line, "utf8", () => {
|
|
365
|
+
(0, fs_1.appendFile)(filePath, line, "utf8", (error) => {
|
|
366
|
+
completed += 1;
|
|
367
|
+
if (error)
|
|
368
|
+
appendFailure ??= error;
|
|
318
369
|
if (checkAfterAppend) {
|
|
319
370
|
try {
|
|
371
|
+
fsyncFileAndDirectory(filePath);
|
|
320
372
|
rotateIfNeeded(filePath, options);
|
|
321
373
|
}
|
|
322
|
-
catch {
|
|
323
|
-
|
|
374
|
+
catch (rotationError) {
|
|
375
|
+
appendFailure ??= rotationError instanceof Error ? rotationError : new Error(String(rotationError));
|
|
324
376
|
}
|
|
325
377
|
}
|
|
378
|
+
settleBarriers();
|
|
326
379
|
flushing = false;
|
|
327
380
|
flush();
|
|
328
381
|
});
|
|
329
382
|
}
|
|
330
|
-
|
|
383
|
+
const sink = ((entry) => {
|
|
331
384
|
const line = verbose
|
|
332
385
|
? `${JSON.stringify(entry)}\n`
|
|
333
386
|
: `${(0, redact_1.redactString)(JSON.stringify((0, redact_1.redactLogEntry)(entry)))}\n`;
|
|
334
387
|
bytesSinceCheck += line.length;
|
|
388
|
+
enqueued += 1;
|
|
335
389
|
queue.push(line);
|
|
336
390
|
flush();
|
|
337
|
-
};
|
|
391
|
+
});
|
|
392
|
+
sink.barrier = () => new Promise((resolve, reject) => {
|
|
393
|
+
barriers.push({ target: enqueued, resolve, reject });
|
|
394
|
+
settleBarriers();
|
|
395
|
+
});
|
|
396
|
+
return sink;
|
|
338
397
|
}
|
|
339
398
|
function registerGlobalLogSink(sink) {
|
|
340
399
|
globalSinks.add(sink);
|
|
@@ -356,9 +415,10 @@ function createLogger(options = {}) {
|
|
|
356
415
|
const configuredLevel = options.level ?? "info";
|
|
357
416
|
const sinks = options.sinks ?? [createStderrSink()];
|
|
358
417
|
const sink = createFanoutSink(sinks);
|
|
418
|
+
const durableSinks = sinks.filter((candidate) => "barrier" in candidate && typeof candidate.barrier === "function");
|
|
359
419
|
const now = options.now ?? (() => new Date());
|
|
360
|
-
function emit(level, entry) {
|
|
361
|
-
if (!shouldEmit(configuredLevel, level)) {
|
|
420
|
+
function emit(level, entry, force = false) {
|
|
421
|
+
if (!force && !shouldEmit(configuredLevel, level)) {
|
|
362
422
|
return;
|
|
363
423
|
}
|
|
364
424
|
const payload = {
|
|
@@ -378,5 +438,11 @@ function createLogger(options = {}) {
|
|
|
378
438
|
info: (entry) => emit("info", entry),
|
|
379
439
|
warn: (entry) => emit("warn", entry),
|
|
380
440
|
error: (entry) => emit("error", entry),
|
|
441
|
+
durabilityBarrier: async () => {
|
|
442
|
+
if (durableSinks.length === 0)
|
|
443
|
+
throw new Error("No durable Nerves sink is configured");
|
|
444
|
+
await Promise.all(durableSinks.map((durableSink) => durableSink.barrier()));
|
|
445
|
+
},
|
|
446
|
+
emitDurable: (level, entry) => emit(level, entry, true),
|
|
381
447
|
};
|
|
382
448
|
}
|