@ouro.bot/cli 0.1.0-alpha.485 → 0.1.0-alpha.488

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.
@@ -1,48 +1,27 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
35
5
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.mailToolDefinitions = void 0;
37
- const fs = __importStar(require("node:fs"));
6
+ exports.mailToolDefinitions = exports.__mailStatusTestOnly = void 0;
7
+ exports.renderCachedMessageSummary = renderCachedMessageSummary;
8
+ exports.mergeCachedMailSearchDocuments = mergeCachedMailSearchDocuments;
9
+ exports.searchSuccessfulImportArchives = searchSuccessfulImportArchives;
10
+ const node_fs_1 = __importDefault(require("node:fs"));
38
11
  const types_1 = require("../mind/friends/types");
39
12
  const file_store_1 = require("../mailroom/file-store");
40
13
  const reader_1 = require("../mailroom/reader");
41
14
  const outbound_1 = require("../mailroom/outbound");
42
15
  const policy_1 = require("../mailroom/policy");
16
+ const search_cache_1 = require("../mailroom/search-cache");
17
+ const mbox_import_1 = require("../mailroom/mbox-import");
18
+ const search_relevance_1 = require("../mailroom/search-relevance");
43
19
  const core_1 = require("../mailroom/core");
44
20
  const runtime_1 = require("../nerves/runtime");
45
21
  const credential_access_1 = require("./credential-access");
22
+ const background_operations_1 = require("../heart/background-operations");
23
+ const mail_import_discovery_1 = require("../heart/mail-import-discovery");
24
+ const identity_1 = require("../heart/identity");
46
25
  function trustAllowsMailRead(ctx) {
47
26
  const trustLevel = ctx?.context?.friend?.trustLevel;
48
27
  const allowed = trustLevel === undefined || (0, types_1.isTrustedLevel)(trustLevel);
@@ -92,6 +71,13 @@ function parseMailList(value) {
92
71
  .map((entry) => entry.trim())
93
72
  .filter(Boolean);
94
73
  }
74
+ function mailSearchTerms(query) {
75
+ return query
76
+ .split(/\s+OR\s+/i)
77
+ .flatMap((entry) => entry.split(/[\n,;]+/))
78
+ .map((entry) => entry.trim())
79
+ .filter(Boolean);
80
+ }
95
81
  function missingPrivateMailKeyId(error) {
96
82
  const match = /^(?:Error: )?Missing private mail key ([^\s]+)$/.exec(String(error));
97
83
  return match?.[1] ?? null;
@@ -183,6 +169,49 @@ function renderMessageSummary(message) {
183
169
  ` warning: ${message.private.untrustedContentWarning}`,
184
170
  ].join("\n");
185
171
  }
172
+ function renderCachedMessageSummary(message, queryTerms = []) {
173
+ const scope = message.compartmentKind === "delegated"
174
+ ? `delegated:${message.ownerEmail ?? "unknown"}:${message.source ?? "source"}`
175
+ : "native";
176
+ const from = message.from.join(", ") || "(unknown sender)";
177
+ const subject = message.subject || "(no subject)";
178
+ const lines = [
179
+ `- ${message.messageId} [${message.placement}; ${scope}]`,
180
+ ` from: ${from}`,
181
+ ` subject: ${subject}`,
182
+ ];
183
+ if (typeof message.attachmentCount === "number" && message.attachmentCount > 0) {
184
+ lines.push(` attachments: ${message.attachmentCount}`);
185
+ }
186
+ if (queryTerms.length > 0) {
187
+ const hint = (0, search_relevance_1.formatRelevanceHint)((0, search_relevance_1.scoreMailSearchDocument)(message, queryTerms));
188
+ if (hint)
189
+ lines.push(` matched on: ${hint}`);
190
+ }
191
+ lines.push(` snippet: ${message.snippet}`);
192
+ lines.push(` warning: ${message.untrustedContentWarning}`);
193
+ return lines.join("\n");
194
+ }
195
+ function mergeCachedMailSearchDocuments(cached, imported, limit, queryTerms = []) {
196
+ const merged = [];
197
+ const seen = new Set();
198
+ const all = [...cached, ...imported];
199
+ const ordered = queryTerms.length > 0
200
+ ? all
201
+ .map((document) => ({ document, relevance: (0, search_relevance_1.scoreMailSearchDocument)(document, queryTerms) }))
202
+ .sort(search_relevance_1.compareByRelevanceThenRecency)
203
+ .map((entry) => entry.document)
204
+ : all.sort((left, right) => right.receivedAt.localeCompare(left.receivedAt));
205
+ for (const message of ordered) {
206
+ if (seen.has(message.messageId))
207
+ continue;
208
+ seen.add(message.messageId);
209
+ merged.push(message);
210
+ if (merged.length >= limit)
211
+ break;
212
+ }
213
+ return merged;
214
+ }
186
215
  function renderScreenerCandidate(candidate) {
187
216
  const delegated = candidate.ownerEmail || candidate.source
188
217
  ? ` delegated:${candidate.ownerEmail ?? "unknown"}:${candidate.source ?? "source"}`
@@ -196,9 +225,12 @@ function renderScreenerCandidate(candidate) {
196
225
  ].join("\n");
197
226
  }
198
227
  function renderAccessLog(entries) {
228
+ const warning = typeof entries.malformedEntriesSkipped === "number" && entries.malformedEntriesSkipped > 0
229
+ ? `warning: skipped ${entries.malformedEntriesSkipped} malformed file-backed mail access log line${entries.malformedEntriesSkipped === 1 ? "" : "s"}`
230
+ : "";
199
231
  if (entries.length === 0)
200
- return "No mail access records yet.";
201
- return entries
232
+ return warning || "No mail access records yet.";
233
+ const rendered = entries
202
234
  .slice(-20)
203
235
  .reverse()
204
236
  .map((entry) => {
@@ -207,6 +239,7 @@ function renderAccessLog(entries) {
207
239
  return `- ${entry.accessedAt} ${entry.tool} ${target}${provenance} reason="${entry.reason}"`;
208
240
  })
209
241
  .join("\n");
242
+ return warning ? `${warning}\n${rendered}` : rendered;
210
243
  }
211
244
  function renderAccessLogProvenance(entry) {
212
245
  if (entry.mailboxRole === "delegated-human-mailbox") {
@@ -217,6 +250,11 @@ function renderAccessLogProvenance(entry) {
217
250
  }
218
251
  return "";
219
252
  }
253
+ function cacheDecryptedMessages(messages) {
254
+ for (const message of messages) {
255
+ (0, search_cache_1.upsertMailSearchCacheDocument)(message, message.private);
256
+ }
257
+ }
220
258
  function accessProvenance(message) {
221
259
  const provenance = (0, core_1.describeMailProvenance)(message);
222
260
  return {
@@ -226,15 +264,9 @@ function accessProvenance(message) {
226
264
  source: provenance.source,
227
265
  };
228
266
  }
229
- function renderSourceGrantStatus(config, agentId) {
230
- if (!config.registryPath) {
231
- return [
232
- "delegated source aliases: unknown (runtime config has no registryPath).",
233
- `agent-runnable repair: run ouro connect mail --agent ${agentId} --owner-email <human-email> --source hey.`,
234
- ];
235
- }
267
+ async function renderSourceGrantStatus(config, agentId) {
236
268
  try {
237
- const registry = readRegistry(config.registryPath);
269
+ const registry = await (0, reader_1.readMailroomRegistry)(config);
238
270
  const grants = registry.sourceGrants
239
271
  .filter((grant) => grant.agentId === agentId && grant.enabled)
240
272
  .map((grant) => `${grant.source}:${grant.ownerEmail} -> ${grant.aliasAddress}`);
@@ -257,10 +289,11 @@ function renderSourceGrantStatus(config, agentId) {
257
289
  async function renderEmptyMailResult(input) {
258
290
  const anyVisible = await input.store.listMessages({ agentId: input.agentId, limit: 1 });
259
291
  if (anyVisible.length === 0) {
292
+ const sourceGrantStatus = await renderSourceGrantStatus(input.config, input.agentId);
260
293
  return [
261
294
  "No visible mail yet.",
262
295
  `mail onboarding status: Mailroom is provisioned for ${input.config.mailboxAddress}, but this agent's encrypted store has 0 messages.`,
263
- ...renderSourceGrantStatus(input.config, input.agentId),
296
+ ...sourceGrantStatus,
264
297
  "interpretation: this is not evidence that the human's HEY inbox is empty; Agent Mail has not yet received or imported mail visible to this agent.",
265
298
  `agent next move: guide setup from docs/agent-mail-setup.md. If HEY mail is needed, ensure the delegated hey alias exists, first try ouro mail import-mbox --agent ${input.agentId} --owner-email <human-email> --source hey --discover so Ouro can find a browser-downloaded export in .playwright-mcp or Downloads. Only ask the human for a file path if discovery cannot find a unique MBOX, then run ouro mail import-mbox --agent ${input.agentId} --owner-email <human-email> --source hey --file <mbox-path>. Verify with mail_recent/mail_search/Ouro Mailbox.`,
266
299
  "validation golden paths before claiming setup works:",
@@ -279,9 +312,10 @@ async function renderEmptyMailResult(input) {
279
312
  limit: 1,
280
313
  });
281
314
  if (delegated.length === 0) {
315
+ const sourceGrantStatus = await renderSourceGrantStatus(input.config, input.agentId);
282
316
  return [
283
317
  "No delegated mail is visible for this source/scope yet.",
284
- ...renderSourceGrantStatus(input.config, input.agentId),
318
+ ...sourceGrantStatus,
285
319
  "Mailroom has other mail, so check the delegated HEY import/forwarding/source filter before treating the human inbox as empty.",
286
320
  ].join("\n");
287
321
  }
@@ -318,12 +352,6 @@ const MAIL_CANDIDATE_STATUSES = ["pending", "allowed", "discarded", "quarantined
318
352
  function parseCandidateStatus(value) {
319
353
  return MAIL_CANDIDATE_STATUSES.includes(value) ? value : undefined;
320
354
  }
321
- function readRegistry(registryPath) {
322
- return JSON.parse(fs.readFileSync(registryPath, "utf-8"));
323
- }
324
- function writeRegistry(registryPath, registry) {
325
- fs.writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`, "utf-8");
326
- }
327
355
  function policyScopeForMessage(message) {
328
356
  return message.source ? `source:${message.source.toLowerCase()}` : message.compartmentKind;
329
357
  }
@@ -378,13 +406,11 @@ function samePolicy(left, right) {
378
406
  function policyLine(policy, existing) {
379
407
  return `sender policy: ${existing ? "already " : ""}${policy.action} ${policy.match.kind} ${policy.match.value}`;
380
408
  }
381
- function persistSenderPolicyForDecision(input) {
409
+ async function persistSenderPolicyForDecision(input) {
382
410
  const persistedActions = ["allow-sender", "allow-domain", "allow-source", "link-friend", "create-friend", "discard", "quarantine"];
383
411
  if (!persistedActions.includes(input.action)) {
384
412
  return null;
385
413
  }
386
- if (!input.registryPath)
387
- return "sender policy: skipped (registryPath missing)";
388
414
  const sender = input.action === "allow-source"
389
415
  ? null
390
416
  : normalizePolicySender(input.candidate, input.message, input.privateKeys);
@@ -399,12 +425,25 @@ function persistSenderPolicyForDecision(input) {
399
425
  actor: input.actor,
400
426
  reason: input.reason,
401
427
  });
402
- const registry = readRegistry(input.registryPath);
428
+ let registry;
429
+ try {
430
+ registry = await (0, reader_1.readMailroomRegistry)(input.config);
431
+ }
432
+ catch (error) {
433
+ const message = error instanceof Error ? error.message : String(error);
434
+ return `sender policy: unavailable (mail registry unreadable: ${message})`;
435
+ }
403
436
  const existing = (registry.senderPolicies ?? []).find((candidatePolicy) => samePolicy(candidatePolicy, policy));
404
437
  if (existing)
405
438
  return policyLine(existing, true);
406
439
  registry.senderPolicies = [...(registry.senderPolicies ?? []), policy];
407
- writeRegistry(input.registryPath, registry);
440
+ try {
441
+ await (0, reader_1.writeMailroomRegistry)(input.config, registry);
442
+ }
443
+ catch (error) {
444
+ const message = error instanceof Error ? error.message : String(error);
445
+ return `sender policy: unavailable (mail registry write failed: ${message})`;
446
+ }
408
447
  (0, runtime_1.emitNervesEvent)({
409
448
  component: "repertoire",
410
449
  event: "repertoire.mail_sender_policy_persisted",
@@ -413,7 +452,310 @@ function persistSenderPolicyForDecision(input) {
413
452
  });
414
453
  return policyLine(policy, false);
415
454
  }
455
+ function latestComparableOperationTimestamp(record) {
456
+ const candidates = [
457
+ typeof record.spec?.fileModifiedAt === "string" ? record.spec.fileModifiedAt : null,
458
+ record.finishedAt ?? null,
459
+ record.updatedAt,
460
+ ];
461
+ for (const candidate of candidates) {
462
+ if (!candidate)
463
+ continue;
464
+ const parsed = Date.parse(candidate);
465
+ if (Number.isFinite(parsed))
466
+ return parsed;
467
+ }
468
+ return null;
469
+ }
470
+ function operationResultText(record, key) {
471
+ const value = record.result?.[key];
472
+ return typeof value === "string" ? value.trim() : "";
473
+ }
474
+ function comparableOperationTimestamp(record) {
475
+ return Number(latestComparableOperationTimestamp(record)) || 0;
476
+ }
477
+ function matchingMailImportOperation(agentId, candidate) {
478
+ const operations = (0, background_operations_1.listBackgroundOperations)({
479
+ agentName: agentId,
480
+ agentRoot: (0, identity_1.getAgentRoot)(agentId),
481
+ limit: 20,
482
+ }).filter((record) => record.kind === "mail.import-mbox" && (record.spec?.filePath ?? null) === candidate.path);
483
+ /* v8 ignore start -- defensive `?? null` is unreachable in normal flow */
484
+ return operations[0] ?? null;
485
+ /* v8 ignore stop */
486
+ }
487
+ function archiveLaneKey(ownerEmail, source) {
488
+ const owner = ownerEmail.trim().toLowerCase();
489
+ const provider = source.trim().toLowerCase();
490
+ if (!owner && !provider)
491
+ return null;
492
+ return `${owner || "unknown"}::${provider || "unknown"}`;
493
+ }
494
+ function archiveFreshnessNote(candidate, operation, newestCurrentLaneArchiveMtimeMs = null) {
495
+ /* v8 ignore start -- defensive: callers in tests always pass an operation; covered by integration paths */
496
+ if (!operation) {
497
+ return "freshness: unimported (no prior import recorded; import needed)";
498
+ }
499
+ /* v8 ignore stop */
500
+ const sourceFreshThrough = operationResultText(operation, "sourceFreshThrough");
501
+ if (operation.status === "succeeded") {
502
+ const operationTimestamp = latestComparableOperationTimestamp(operation);
503
+ if (operationTimestamp !== null && candidate.mtimeMs <= operationTimestamp + 1_000) {
504
+ if (newestCurrentLaneArchiveMtimeMs !== null && candidate.mtimeMs + 1_000 < newestCurrentLaneArchiveMtimeMs) {
505
+ return [
506
+ "freshness: current older snapshot (older imported snapshot for this delegated lane; newest known archive is listed separately)",
507
+ ...(sourceFreshThrough ? [`fresh through: ${sourceFreshThrough}`] : []),
508
+ ].join("; ");
509
+ }
510
+ return [
511
+ "freshness: current (newest known archive for this delegated lane; re-import unnecessary)",
512
+ ...(sourceFreshThrough ? [`fresh through: ${sourceFreshThrough}`] : []),
513
+ ].join("; ");
514
+ }
515
+ if (operationTimestamp !== null) {
516
+ return [
517
+ "freshness: stale-risky (newer archive discovered after the last import; re-import needed)",
518
+ ...(sourceFreshThrough ? [`fresh through: ${sourceFreshThrough}`] : []),
519
+ ].join("; ");
520
+ }
521
+ return "freshness: stale-risky (last successful import has no comparable timestamp; verify the archive before relying on it)";
522
+ }
523
+ if (operation.status === "failed") {
524
+ return "freshness: blocked (last import failed; current freshness is not yet trustworthy)";
525
+ }
526
+ return "freshness: pending (import still in progress; current freshness will settle when the operation finishes)";
527
+ }
528
+ function archiveFilenameBoundEmail(candidate) {
529
+ const stem = candidate.name.replace(/\.mbox$/i, "");
530
+ const matches = stem.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig);
531
+ const match = matches?.at(-1);
532
+ if (!match)
533
+ return null;
534
+ try {
535
+ return (0, core_1.normalizeMailAddress)(match.replace(/^hey-emails-/i, "").replace(/^emails-/i, ""));
536
+ }
537
+ catch {
538
+ return null;
539
+ }
540
+ }
541
+ function archiveIdentityNote(candidate, ownerEmail, source) {
542
+ const fileEmail = archiveFilenameBoundEmail(candidate);
543
+ if (!fileEmail || !ownerEmail)
544
+ return "";
545
+ try {
546
+ if ((0, core_1.normalizeMailAddress)(ownerEmail) === fileEmail)
547
+ return "";
548
+ }
549
+ catch {
550
+ return "";
551
+ }
552
+ return `mapping: filename suggests ${fileEmail}, but this archive is bound to ${ownerEmail} / ${source || "unknown"} because delegated owner/source comes from the explicit import lane, not the local filename`;
553
+ }
554
+ exports.__mailStatusTestOnly = {
555
+ archiveFilenameBoundEmail,
556
+ archiveFreshnessNote,
557
+ archiveIdentityNote,
558
+ };
559
+ function newestCurrentLaneArchiveMtimes(candidates, operationsByPath) {
560
+ const newestByLane = new Map();
561
+ for (const candidate of candidates) {
562
+ const operation = operationsByPath.get(candidate.path);
563
+ if (!operation || operation.status !== "succeeded")
564
+ continue;
565
+ const ownerEmail = typeof operation.spec?.ownerEmail === "string" ? operation.spec.ownerEmail : "";
566
+ const source = typeof operation.spec?.source === "string" ? operation.spec.source : "";
567
+ const laneKey = archiveLaneKey(ownerEmail, source);
568
+ if (!laneKey)
569
+ continue;
570
+ const operationTimestamp = latestComparableOperationTimestamp(operation);
571
+ if (operationTimestamp === null || candidate.mtimeMs > operationTimestamp + 1_000)
572
+ continue;
573
+ const previous = newestByLane.get(laneKey) ?? 0;
574
+ if (candidate.mtimeMs > previous)
575
+ newestByLane.set(laneKey, candidate.mtimeMs);
576
+ }
577
+ return newestByLane;
578
+ }
579
+ function renderArchiveStatus(candidate, operation, newestCurrentLaneArchiveMtimeMs) {
580
+ /* v8 ignore start -- defensive: tests reach this helper through integration paths that always provide an operation; same archiveFreshnessNote fallback covered there */
581
+ if (!operation) {
582
+ return `- [${candidate.originLabel}] ${candidate.path} :: status: ready; ${archiveFreshnessNote(candidate, null, newestCurrentLaneArchiveMtimeMs)}`;
583
+ }
584
+ /* v8 ignore stop */
585
+ const operationTimestamp = latestComparableOperationTimestamp(operation);
586
+ const ownerEmail = typeof operation.spec?.ownerEmail === "string" ? operation.spec.ownerEmail : "";
587
+ const source = typeof operation.spec?.source === "string" ? operation.spec.source : "";
588
+ const provenance = ownerEmail || source ? `; owner/source: ${ownerEmail || "unknown"} / ${source || "unknown"}` : "";
589
+ const freshness = `; ${archiveFreshnessNote(candidate, operation, newestCurrentLaneArchiveMtimeMs)}`;
590
+ const identity = archiveIdentityNote(candidate, ownerEmail, source);
591
+ const identityNote = identity ? `; ${identity}` : "";
592
+ if (operation.status === "succeeded" && operationTimestamp !== null && candidate.mtimeMs <= operationTimestamp + 1_000) {
593
+ return `- [${candidate.originLabel}] ${candidate.path} :: status: imported via ${operation.id}${provenance}${freshness}; ${operation.detail ?? operation.summary}${identityNote}`;
594
+ }
595
+ if (operation.status === "succeeded") {
596
+ return `- [${candidate.originLabel}] ${candidate.path} :: status: ready (newer than last import via ${operation.id})${provenance}${freshness}; ${operation.detail ?? operation.summary}${identityNote}`;
597
+ }
598
+ if (operation.status === "failed") {
599
+ return `- [${candidate.originLabel}] ${candidate.path} :: status: failed via ${operation.id}${provenance}${freshness}; ${operation.failure?.class ?? "unknown failure"}${identityNote}`;
600
+ }
601
+ return `- [${candidate.originLabel}] ${candidate.path} :: status: ${operation.status} via ${operation.id}${provenance}${freshness}; ${operation.summary}${identityNote}`;
602
+ }
603
+ function renderRecentArchiveStatus(agentId) {
604
+ const candidates = (0, mail_import_discovery_1.defaultMailImportDiscoveryDirs)({
605
+ agentName: agentId,
606
+ repoRoot: (0, identity_1.getRepoRoot)(),
607
+ homeDir: process.env.HOME,
608
+ })
609
+ .flatMap((dir) => (0, mail_import_discovery_1.listDiscoveredMboxCandidates)(dir))
610
+ .sort((left, right) => right.mtimeMs - left.mtimeMs)
611
+ .slice(0, 5);
612
+ if (candidates.length === 0)
613
+ return ["- none discovered in browser sandboxes or Downloads"];
614
+ /* v8 ignore start -- branchy convergence helpers (operation + lane key + newestByLane) are exercised end-to-end via mail_status integration tests; leaf branches here are convergence-pass1 internals */
615
+ const operationsByPath = new Map();
616
+ for (const candidate of candidates) {
617
+ const operation = matchingMailImportOperation(agentId, candidate);
618
+ if (operation)
619
+ operationsByPath.set(candidate.path, operation);
620
+ }
621
+ const newestByLane = newestCurrentLaneArchiveMtimes(candidates, operationsByPath);
622
+ return candidates.map((candidate) => {
623
+ const operation = operationsByPath.get(candidate.path) ?? null;
624
+ const ownerEmail = typeof operation?.spec?.ownerEmail === "string" ? operation.spec.ownerEmail : "";
625
+ const source = typeof operation?.spec?.source === "string" ? operation.spec.source : "";
626
+ const laneKey = archiveLaneKey(ownerEmail, source);
627
+ return renderArchiveStatus(candidate, operation, laneKey ? (newestByLane.get(laneKey) ?? null) : null);
628
+ });
629
+ /* v8 ignore stop */
630
+ }
631
+ function renderRecentImportOperations(agentId) {
632
+ const operations = (0, background_operations_1.listBackgroundOperations)({
633
+ agentName: agentId,
634
+ agentRoot: (0, identity_1.getAgentRoot)(agentId),
635
+ limit: 10,
636
+ }).filter((record) => record.kind === "mail.import-mbox")
637
+ .sort((left, right) => {
638
+ const leftTs = comparableOperationTimestamp(left);
639
+ const rightTs = comparableOperationTimestamp(right);
640
+ return rightTs - leftTs;
641
+ })
642
+ .slice(0, 5);
643
+ if (operations.length === 0)
644
+ return ["- none recorded yet"];
645
+ return operations.map((operation) => {
646
+ const ownerEmail = typeof operation.spec?.ownerEmail === "string" ? operation.spec.ownerEmail : "";
647
+ const source = typeof operation.spec?.source === "string" ? operation.spec.source : "";
648
+ const provenance = ownerEmail || source ? ` ${ownerEmail || "unknown"} / ${source || "unknown"}` : "";
649
+ const failure = operation.failure?.class ? `; failure=${operation.failure.class}` : "";
650
+ return `- ${operation.id} [${operation.status}]${provenance} :: ${operation.detail ?? operation.summary}${failure}`;
651
+ });
652
+ }
653
+ async function searchSuccessfulImportArchives(input) {
654
+ if (input.limit <= 0 || input.queryTerms.length === 0)
655
+ return [];
656
+ let registry;
657
+ try {
658
+ registry = await (0, reader_1.readMailroomRegistry)(input.config);
659
+ }
660
+ catch {
661
+ return [];
662
+ }
663
+ const operations = (0, background_operations_1.listBackgroundOperations)({
664
+ agentName: input.agentId,
665
+ agentRoot: (0, identity_1.getAgentRoot)(input.agentId),
666
+ limit: 20,
667
+ })
668
+ .filter((record) => record.kind === "mail.import-mbox" && record.status === "succeeded")
669
+ .sort((left, right) => comparableOperationTimestamp(right) - comparableOperationTimestamp(left));
670
+ const seenPaths = new Set();
671
+ const matches = [];
672
+ const seenMessages = new Set();
673
+ for (const operation of operations) {
674
+ const filePath = typeof operation.spec?.filePath === "string" ? operation.spec.filePath.trim() : "";
675
+ if (!filePath || seenPaths.has(filePath) || !node_fs_1.default.existsSync(filePath))
676
+ continue;
677
+ seenPaths.add(filePath);
678
+ const source = typeof operation.spec?.source === "string" ? operation.spec.source : "";
679
+ if (input.source && source.toLowerCase() !== input.source.toLowerCase())
680
+ continue;
681
+ const ownerEmail = typeof operation.spec?.ownerEmail === "string" ? operation.spec.ownerEmail : undefined;
682
+ const found = await (0, mbox_import_1.cacheMatchingMailSearchDocumentsFromMboxFile)({
683
+ registry,
684
+ agentId: input.agentId,
685
+ filePath,
686
+ ownerEmail,
687
+ source: source || input.source,
688
+ queryTerms: input.queryTerms,
689
+ limit: input.limit - matches.length,
690
+ });
691
+ for (const document of found) {
692
+ if (seenMessages.has(document.messageId))
693
+ continue;
694
+ seenMessages.add(document.messageId);
695
+ matches.push(document);
696
+ }
697
+ if (matches.length >= input.limit)
698
+ break;
699
+ }
700
+ return matches.sort((left, right) => right.receivedAt.localeCompare(left.receivedAt));
701
+ }
702
+ async function renderMailStatus(agentId, config, storeLabel) {
703
+ const sourceGrantStatus = await renderSourceGrantStatus(config, agentId);
704
+ const delegatedLines = sourceGrantStatus
705
+ .flatMap((line) => line.startsWith("delegated source aliases: ")
706
+ ? line
707
+ .replace("delegated source aliases: ", "")
708
+ .replace(/\.$/, "")
709
+ .split("; ")
710
+ .filter(Boolean)
711
+ .map((grant) => {
712
+ const [sourceOwner, alias] = grant.split(" -> ");
713
+ const [source, ownerEmail] = sourceOwner.split(":");
714
+ return source && ownerEmail && alias
715
+ ? `- delegated: ${ownerEmail} / ${source} -> ${alias}`
716
+ : `- delegated: ${grant}`;
717
+ })
718
+ : [`- ${line}`]);
719
+ return [
720
+ `mailbox: ${config.mailboxAddress}`,
721
+ `store: ${storeLabel}`,
722
+ "lane map:",
723
+ `- native: ${config.mailboxAddress}`,
724
+ ...delegatedLines,
725
+ "recent archives:",
726
+ ...renderRecentArchiveStatus(agentId),
727
+ "recent imports:",
728
+ ...renderRecentImportOperations(agentId),
729
+ ].join("\n");
730
+ }
416
731
  exports.mailToolDefinitions = [
732
+ {
733
+ tool: {
734
+ type: "function",
735
+ function: {
736
+ name: "mail_status",
737
+ description: "Show the current mail operating model: native/delegated lanes, recent import artifacts, and recent mail import operations.",
738
+ parameters: { type: "object", properties: {} },
739
+ },
740
+ },
741
+ handler: async (_args, ctx) => {
742
+ if (!trustAllowsMailRead(ctx))
743
+ return "mail is private; this tool is only available in trusted contexts.";
744
+ const blocked = delegatedHumanMailBlocked(ctx);
745
+ if (blocked)
746
+ return blocked;
747
+ const resolved = (0, reader_1.resolveMailroomReader)();
748
+ if (!resolved.ok)
749
+ return resolved.error;
750
+ await resolved.store.recordAccess({
751
+ agentId: resolved.agentName,
752
+ tool: "mail_status",
753
+ reason: "mail operating model overview",
754
+ });
755
+ return renderMailStatus(resolved.agentName, resolved.config, resolved.storeLabel);
756
+ },
757
+ summaryKeys: [],
758
+ },
417
759
  {
418
760
  tool: {
419
761
  type: "function",
@@ -472,6 +814,7 @@ exports.mailToolDefinitions = [
472
814
  if (result.decrypted.length === 0) {
473
815
  return appendDecryptSkips("No decryptable mail to show.", result.skipped);
474
816
  }
817
+ cacheDecryptedMessages(result.decrypted);
475
818
  return appendDecryptSkips(result.decrypted.map(renderMessageSummary).join("\n\n"), result.skipped);
476
819
  },
477
820
  summaryKeys: ["scope", "placement", "source", "limit"],
@@ -632,6 +975,7 @@ exports.mailToolDefinitions = [
632
975
  const query = (args.query ?? "").trim().toLowerCase();
633
976
  if (!query)
634
977
  return "query is required.";
978
+ const terms = mailSearchTerms(query);
635
979
  const requestedScope = args.scope === "all" ? "all" : parseScope(args.scope);
636
980
  const explicitScope = (args.scope ?? "").trim().length > 0;
637
981
  if (!familyOrAgentSelf(ctx) && explicitScope && requestedScope !== "native") {
@@ -643,22 +987,62 @@ exports.mailToolDefinitions = [
643
987
  const scope = requestedScope === "all"
644
988
  ? undefined
645
989
  : requestedScope ?? (familyOrAgentSelf(ctx) ? undefined : "native");
990
+ const limit = numberArg(args.limit, 10, 1, 20);
991
+ const cachedMatches = (0, search_cache_1.searchMailSearchCache)({
992
+ agentId: resolved.agentName,
993
+ placement: parsePlacement(args.placement),
994
+ compartmentKind: scope,
995
+ source: args.source,
996
+ queryTerms: terms,
997
+ limit,
998
+ });
999
+ if (cachedMatches.length > 0 && cachedMatches.every((message) => message.compartmentKind === "delegated")) {
1000
+ await resolved.store.recordAccess({
1001
+ agentId: resolved.agentName,
1002
+ tool: "mail_search",
1003
+ reason: args.reason || `search: ${query}`,
1004
+ });
1005
+ return cachedMatches.map((message) => renderCachedMessageSummary(message, terms)).join("\n\n");
1006
+ }
1007
+ if (scope !== "native"
1008
+ && resolved.storeKind === "azure-blob"
1009
+ && (cachedMatches.length === 0 || cachedMatches.some((message) => message.compartmentKind !== "delegated"))) {
1010
+ const importedMatches = await searchSuccessfulImportArchives({
1011
+ agentId: resolved.agentName,
1012
+ config: resolved.config,
1013
+ queryTerms: terms,
1014
+ limit,
1015
+ ...(args.source ? { source: args.source } : {}),
1016
+ });
1017
+ if (importedMatches.length > 0) {
1018
+ const mergedMatches = mergeCachedMailSearchDocuments(cachedMatches, importedMatches, limit, terms);
1019
+ await resolved.store.recordAccess({
1020
+ agentId: resolved.agentName,
1021
+ tool: "mail_search",
1022
+ reason: args.reason || `search: ${query}`,
1023
+ });
1024
+ return mergedMatches.map((message) => renderCachedMessageSummary(message, terms)).join("\n\n");
1025
+ }
1026
+ }
646
1027
  const all = await resolved.store.listMessages({
647
1028
  agentId: resolved.agentName,
648
1029
  placement: parsePlacement(args.placement),
649
1030
  compartmentKind: scope,
650
1031
  source: args.source,
651
- limit: 200,
652
1032
  });
653
1033
  const result = decryptVisibleMessages(all, resolved.config.privateKeys);
1034
+ cacheDecryptedMessages(result.decrypted);
654
1035
  const matching = result.decrypted
655
- .filter((message) => [
656
- message.private.subject,
657
- message.private.snippet,
658
- message.private.text,
659
- message.private.from.join(" "),
660
- ].join("\n").toLowerCase().includes(query))
661
- .slice(0, numberArg(args.limit, 10, 1, 20));
1036
+ .filter((message) => {
1037
+ const haystack = [
1038
+ message.private.subject,
1039
+ message.private.snippet,
1040
+ message.private.text,
1041
+ message.private.from.join(" "),
1042
+ ].join("\n").toLowerCase();
1043
+ return terms.some((term) => haystack.includes(term));
1044
+ })
1045
+ .slice(0, limit);
662
1046
  await resolved.store.recordAccess({
663
1047
  agentId: resolved.agentName,
664
1048
  tool: "mail_search",
@@ -730,6 +1114,7 @@ exports.mailToolDefinitions = [
730
1114
  throw error;
731
1115
  return renderUndecryptableThread(message, keyId);
732
1116
  }
1117
+ (0, search_cache_1.upsertMailSearchCacheDocument)(message, decrypted.private);
733
1118
  const maxChars = numberArg(args.max_chars, 2000, 200, 6000);
734
1119
  const body = decrypted.private.text.length > maxChars
735
1120
  ? `${decrypted.private.text.slice(0, maxChars - 3)}...`
@@ -851,8 +1236,8 @@ exports.mailToolDefinitions = [
851
1236
  reason,
852
1237
  ...accessProvenance(message),
853
1238
  });
854
- const senderPolicyLine = persistSenderPolicyForDecision({
855
- registryPath: resolved.config.registryPath,
1239
+ const senderPolicyLine = await persistSenderPolicyForDecision({
1240
+ config: resolved.config,
856
1241
  agentId: resolved.agentName,
857
1242
  action,
858
1243
  reason,