@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.
Files changed (96) hide show
  1. package/README.md +1 -1
  2. package/changelog.json +34 -0
  3. package/deploy/unraid/Dockerfile +27 -0
  4. package/deploy/unraid/README.txt +1368 -0
  5. package/deploy/unraid/audit-container-spec.sh +4 -0
  6. package/deploy/unraid/container-runtime.json +1 -0
  7. package/deploy/unraid/sanctuary-acceptance-adapter.sh +37 -0
  8. package/deploy/unraid/sanctuary-acceptance-contract.json +200 -0
  9. package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
  10. package/deploy/unraid/sanctuary-deployment-target.mjs +705 -0
  11. package/deploy/unraid/sanctuary-safe-rename.py +24 -0
  12. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +1777 -0
  13. package/deploy/unraid/sanctuary-unit16-run.sh +502 -0
  14. package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
  15. package/deploy/unraid/sanctuary.ouro/agent.json +24 -0
  16. package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
  17. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
  18. package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
  19. package/deploy/unraid/sanctuary.ouro/provider-readiness.json +18 -0
  20. package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -0
  21. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -0
  22. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +3 -0
  23. package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +5 -0
  24. package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +7 -0
  25. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +21 -0
  26. package/deploy/unraid/sanctuary.xml +25 -0
  27. package/dist/heart/approval-files.js +113 -0
  28. package/dist/heart/approval-store.js +168 -9
  29. package/dist/heart/auth/auth-flow.js +17 -1
  30. package/dist/heart/autonomy-budget.js +44 -1
  31. package/dist/heart/awaiting/await-scheduler.js +6 -2
  32. package/dist/heart/config.js +8 -0
  33. package/dist/heart/core.js +117 -132
  34. package/dist/heart/daemon/cli-exec.js +74 -2
  35. package/dist/heart/daemon/cli-help.js +6 -1
  36. package/dist/heart/daemon/cli-parse.js +6 -4
  37. package/dist/heart/daemon/container-credential-bootstrap.js +185 -0
  38. package/dist/heart/daemon/container-healthcheck.js +94 -0
  39. package/dist/heart/daemon/container-runtime.js +84 -0
  40. package/dist/heart/daemon/container-spec-auditor-main.js +156 -0
  41. package/dist/heart/daemon/container-spec-auditor.js +156 -0
  42. package/dist/heart/daemon/daemon-bootstrap-startup.js +41 -0
  43. package/dist/heart/daemon/daemon-entry.js +116 -28
  44. package/dist/heart/daemon/daemon-health.js +10 -0
  45. package/dist/heart/daemon/daemon.js +92 -5
  46. package/dist/heart/daemon/doctor.js +56 -7
  47. package/dist/heart/daemon/runtime-logging.js +6 -0
  48. package/dist/heart/daemon/runtime-mode.js +13 -2
  49. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2365 -0
  50. package/dist/heart/daemon/sanctuary-acceptance-harness.js +1624 -0
  51. package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
  52. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +985 -0
  53. package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
  54. package/dist/heart/daemon/sanctuary-scheduler-origin.js +126 -0
  55. package/dist/heart/daemon/sense-manager.js +25 -6
  56. package/dist/heart/daemon/supercronic-supervisor.js +267 -0
  57. package/dist/heart/habits/habit-scheduler.js +7 -3
  58. package/dist/heart/identity.js +13 -3
  59. package/dist/heart/provider-credentials.js +9 -1
  60. package/dist/heart/provider-models.js +8 -0
  61. package/dist/heart/provider-ping.js +5 -2
  62. package/dist/heart/providers/openai-compatible.js +177 -0
  63. package/dist/heart/runtime-credentials.js +217 -8
  64. package/dist/heart/sense-truth.js +2 -0
  65. package/dist/heart/tool-approval.js +1 -0
  66. package/dist/heart/turn-context.js +8 -0
  67. package/dist/mailroom/blob-store.js +97 -19
  68. package/dist/mailroom/cache-sync-cli.js +5 -1
  69. package/dist/mailroom/core.js +23 -8
  70. package/dist/mailroom/hosted-cache-sync.js +179 -44
  71. package/dist/mailroom/search-cache.js +109 -0
  72. package/dist/mind/prompt.js +9 -0
  73. package/dist/nerves/coverage/source-scanner.js +4 -4
  74. package/dist/nerves/index.js +75 -9
  75. package/dist/nerves/runtime.js +12 -0
  76. package/dist/repertoire/tool-arguments.js +39 -0
  77. package/dist/repertoire/tools-unraid.js +353 -0
  78. package/dist/repertoire/tools.js +19 -1
  79. package/dist/repertoire/unraid-client.js +155 -0
  80. package/dist/repertoire/unraid-restart.js +130 -0
  81. package/dist/senses/private-runtime-worker.js +3 -2
  82. package/dist/senses/sanctuary-grounding.js +118 -0
  83. package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
  84. package/dist/senses/sanctuary-health-acceptance-probe.js +772 -0
  85. package/dist/senses/sanctuary-health-runner.js +146 -0
  86. package/dist/senses/sanctuary-health.js +447 -0
  87. package/dist/senses/sanctuary-interactive-control.js +306 -0
  88. package/dist/senses/sanctuary-runtime.js +206 -0
  89. package/dist/senses/shared-turn.js +11 -3
  90. package/dist/senses/telegram-approval-runtime.js +526 -0
  91. package/dist/senses/telegram-audit-ledger.js +220 -0
  92. package/dist/senses/telegram-client.js +1471 -0
  93. package/dist/senses/telegram-entry.js +80 -0
  94. package/dist/senses/telegram.js +1232 -0
  95. package/npm-shrinkwrap.json +6558 -0
  96. 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
- && (!requireKeyProvenance || typeof document.decryptionKeyId === "string");
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 missingPrivateKeyId(error) {
67
- if (!(error instanceof Error))
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
- * Converge a local decrypted cache against hosted index authority, or perform
74
- * the legacy bounded upsert used by the in-agent refresh tool. This core never
75
- * writes remote mail state and never records access; adapters own audit policy.
76
- */
77
- async function performHostedMailSearchCacheSync(input) {
78
- let authority = null;
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
- else {
95
- records = await scopedRecords(input);
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
- for (const record of records) {
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
- if (cached && input.mode === "full-convergence" && cached.decryptionKeyId && !input.privateKeys[cached.decryptionKeyId]) {
129
- removed += removeCanonicalIfPresent(input.agentId, record.id, input.cacheOptions);
130
- skipped += 1;
131
- continue;
132
- }
133
- if (cached && mailSearchCacheDocumentMatchesRecord(cached, record, input.mode === "full-convergence")) {
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
- continue;
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 (input.mode === "full-convergence" || missingPrivateKeyId(error)) {
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
- failures.push({ id: record.id, error: error instanceof Error ? error.message : String(error) });
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} fetch failed. first failure(s): ${sample}`);
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
- const snapshot = authority?.snapshot ?? (0, blob_store_1.canonicalMailIndexRecordSnapshot)(records);
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: skipped,
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: records.length,
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);
@@ -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 emitNervesEvent({ component: "...", event: "..." })
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\s*\(\s*\{([\s\S]*?)\}\s*\)/g;
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 emitNervesEvent calls and extract
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) {
@@ -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
- // Rotation errors are surfaced via nerves events; never block writes.
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
- // Rotation errors are surfaced via nerves events; never block writes.
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
- return (entry) => {
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
  }