@ouro.bot/cli 0.1.0-alpha.535 → 0.1.0-alpha.536
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.json +8 -0
- package/dist/heart/daemon/socket-client.js +49 -7
- package/dist/heart/mcp/mcp-server.js +32 -2
- package/dist/mailroom/blob-store.js +60 -4
- package/dist/mailroom/core.js +50 -2
- package/dist/mailroom/mbox-import.js +3 -2
- package/dist/mailroom/reader.js +3 -1
- package/dist/mailroom/search-cache.js +76 -6
- package/dist/mind/prompt.js +1 -0
- package/dist/repertoire/guardrails.js +1 -1
- package/dist/repertoire/tools-mail.js +435 -55
- package/dist/repertoire/tools-trip.js +182 -0
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ exports.renderCachedMessageSummary = renderCachedMessageSummary;
|
|
|
8
8
|
exports.mergeCachedMailSearchDocuments = mergeCachedMailSearchDocuments;
|
|
9
9
|
exports.searchSuccessfulImportArchives = searchSuccessfulImportArchives;
|
|
10
10
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_crypto_1 = require("node:crypto");
|
|
11
12
|
const types_1 = require("../mind/friends/types");
|
|
12
13
|
const file_store_1 = require("../mailroom/file-store");
|
|
13
14
|
const reader_1 = require("../mailroom/reader");
|
|
@@ -24,6 +25,8 @@ const credential_access_1 = require("./credential-access");
|
|
|
24
25
|
const background_operations_1 = require("../heart/background-operations");
|
|
25
26
|
const mail_import_discovery_1 = require("../heart/mail-import-discovery");
|
|
26
27
|
const identity_1 = require("../heart/identity");
|
|
28
|
+
const MAIL_SEARCH_INDEX_FETCH_CONCURRENCY = 80;
|
|
29
|
+
const MAIL_SEARCH_INDEX_BATCH_SIZE = 500;
|
|
27
30
|
function trustAllowsMailRead(ctx) {
|
|
28
31
|
const trustLevel = ctx?.context?.friend?.trustLevel;
|
|
29
32
|
const allowed = trustLevel === undefined || (0, types_1.isTrustedLevel)(trustLevel);
|
|
@@ -258,6 +261,233 @@ function cacheDecryptedMessages(messages) {
|
|
|
258
261
|
(0, body_cache_1.cacheMailBody)(message);
|
|
259
262
|
}
|
|
260
263
|
}
|
|
264
|
+
function cacheAndFilterDecryptedSearchMessages(messages, terms) {
|
|
265
|
+
const matches = [];
|
|
266
|
+
for (const message of messages) {
|
|
267
|
+
const document = (0, search_cache_1.upsertMailSearchCacheDocument)(message, message.private);
|
|
268
|
+
(0, body_cache_1.cacheMailBody)(message);
|
|
269
|
+
if (terms.some((term) => document.searchText.includes(term)))
|
|
270
|
+
matches.push(document);
|
|
271
|
+
}
|
|
272
|
+
return matches;
|
|
273
|
+
}
|
|
274
|
+
function explicitDelegatedSearch(args, requestedScope) {
|
|
275
|
+
return requestedScope === "delegated" || requestedScope === "all" || !!args.source?.trim();
|
|
276
|
+
}
|
|
277
|
+
function appendDelegatedSearchCoverage(body, input) {
|
|
278
|
+
if (!input.include)
|
|
279
|
+
return body;
|
|
280
|
+
const liveCoverage = input.liveCoverageNote
|
|
281
|
+
? `live visible messages searched=${input.liveMessagesSearched} (${input.liveCoverageNote}).`
|
|
282
|
+
: `live visible messages searched=${input.liveMessagesSearched}.`;
|
|
283
|
+
const indexedCoverage = input.coverageRecord
|
|
284
|
+
? ` hosted search index coverage=${input.coverageRecord.decryptableMessageCount}/${input.coverageRecord.visibleMessageCount} decryptable messages as of ${input.coverageRecord.indexedAt}.`
|
|
285
|
+
: "";
|
|
286
|
+
return [
|
|
287
|
+
body,
|
|
288
|
+
[
|
|
289
|
+
"search coverage:",
|
|
290
|
+
`local cache matches=${input.cachedMatches};`,
|
|
291
|
+
`imported archive matches=${input.importedArchiveMatches}${input.importedArchiveSearched || !input.importedArchiveNote ? "" : ` (${input.importedArchiveNote})`};`,
|
|
292
|
+
liveCoverage,
|
|
293
|
+
indexedCoverage.trim(),
|
|
294
|
+
"cache hits are not proof of absence.",
|
|
295
|
+
].filter(Boolean).join(" "),
|
|
296
|
+
].join("\n\n");
|
|
297
|
+
}
|
|
298
|
+
async function mapWithConcurrency(items, concurrency, worker) {
|
|
299
|
+
/* v8 ignore next -- refresh callers pass only non-empty slices into the worker pool. @preserve */
|
|
300
|
+
if (items.length === 0)
|
|
301
|
+
return [];
|
|
302
|
+
const results = new Array(items.length);
|
|
303
|
+
let nextIndex = 0;
|
|
304
|
+
const workerLoop = async () => {
|
|
305
|
+
while (true) {
|
|
306
|
+
const current = nextIndex;
|
|
307
|
+
nextIndex += 1;
|
|
308
|
+
if (current >= items.length)
|
|
309
|
+
return;
|
|
310
|
+
results[current] = await worker(items[current], current);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, async () => workerLoop()));
|
|
314
|
+
return results;
|
|
315
|
+
}
|
|
316
|
+
function mailListFilters(input) {
|
|
317
|
+
return {
|
|
318
|
+
agentId: input.agentId,
|
|
319
|
+
...(input.placement ? { placement: input.placement } : {}),
|
|
320
|
+
...(input.scope ? { compartmentKind: input.scope } : {}),
|
|
321
|
+
...(input.source ? { source: input.source } : {}),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function mailSearchCoverageKey(input) {
|
|
325
|
+
return {
|
|
326
|
+
agentId: input.agentId,
|
|
327
|
+
storeKind: input.storeKind,
|
|
328
|
+
...(input.placement ? { placement: input.placement } : {}),
|
|
329
|
+
...(input.scope ? { compartmentKind: input.scope } : {}),
|
|
330
|
+
...(input.source ? { source: input.source } : {}),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function mailSearchCacheDocumentNeedsProjectionRefresh(document) {
|
|
334
|
+
if (document.textProjectionVersion === search_cache_1.MAIL_SEARCH_TEXT_PROJECTION_VERSION)
|
|
335
|
+
return false;
|
|
336
|
+
return document.textExcerpt.trim().length === 0;
|
|
337
|
+
}
|
|
338
|
+
function mailSearchCoverageSnapshot(records) {
|
|
339
|
+
const normalizedRecords = records
|
|
340
|
+
.map((record) => ({
|
|
341
|
+
id: record.id,
|
|
342
|
+
receivedAt: record.receivedAt,
|
|
343
|
+
placement: record.placement,
|
|
344
|
+
compartmentKind: record.compartmentKind,
|
|
345
|
+
source: record.source?.toLowerCase() ?? null,
|
|
346
|
+
}))
|
|
347
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
348
|
+
return {
|
|
349
|
+
visibleMessageCount: records.length,
|
|
350
|
+
messageIndexFingerprint: (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(normalizedRecords)).digest("hex"),
|
|
351
|
+
...(oldestReceivedAt(records) ? { oldestReceivedAt: oldestReceivedAt(records) } : {}),
|
|
352
|
+
...(newestReceivedAt(records) ? { newestReceivedAt: newestReceivedAt(records) } : {}),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function mailSearchCoverageStalenessReason(record, currentSnapshot) {
|
|
356
|
+
if (!record)
|
|
357
|
+
return "hosted search index incomplete";
|
|
358
|
+
if (record.textProjectionVersion !== search_cache_1.MAIL_SEARCH_TEXT_PROJECTION_VERSION)
|
|
359
|
+
return "hosted search index needs projection refresh";
|
|
360
|
+
if (!record.messageIndexFingerprint)
|
|
361
|
+
return "hosted search index needs corpus refresh";
|
|
362
|
+
if (!currentSnapshot)
|
|
363
|
+
return "hosted search index cannot validate current corpus";
|
|
364
|
+
if (record.visibleMessageCount !== currentSnapshot.visibleMessageCount
|
|
365
|
+
|| record.messageIndexFingerprint !== currentSnapshot.messageIndexFingerprint
|
|
366
|
+
|| (record.oldestReceivedAt ?? "") !== (currentSnapshot.oldestReceivedAt ?? "")
|
|
367
|
+
|| (record.newestReceivedAt ?? "") !== (currentSnapshot.newestReceivedAt ?? "")) {
|
|
368
|
+
return "hosted search index is stale; current message index differs from coverage";
|
|
369
|
+
}
|
|
370
|
+
return "";
|
|
371
|
+
}
|
|
372
|
+
function mailSearchCoverageIsCurrent(record, currentSnapshot) {
|
|
373
|
+
return mailSearchCoverageStalenessReason(record, currentSnapshot) === "";
|
|
374
|
+
}
|
|
375
|
+
function mailSearchCoverageUnsearchableReason(record) {
|
|
376
|
+
if (!record)
|
|
377
|
+
return "";
|
|
378
|
+
const unsearchableCount = Math.max(record.skippedMessageCount, record.visibleMessageCount - record.decryptableMessageCount, 0);
|
|
379
|
+
if (unsearchableCount === 0)
|
|
380
|
+
return "";
|
|
381
|
+
const noun = unsearchableCount === 1 ? "message was" : "messages were";
|
|
382
|
+
return `${unsearchableCount} visible ${noun} not searchable because the index skipped missing/decryption-key mail`;
|
|
383
|
+
}
|
|
384
|
+
function newestReceivedAt(records) {
|
|
385
|
+
return records.reduce((newest, record) => {
|
|
386
|
+
if (!newest || Date.parse(record.receivedAt) > Date.parse(newest))
|
|
387
|
+
return record.receivedAt;
|
|
388
|
+
return newest;
|
|
389
|
+
}, undefined);
|
|
390
|
+
}
|
|
391
|
+
function oldestReceivedAt(records) {
|
|
392
|
+
return records.reduce((oldest, record) => {
|
|
393
|
+
if (!oldest || Date.parse(record.receivedAt) < Date.parse(oldest))
|
|
394
|
+
return record.receivedAt;
|
|
395
|
+
return oldest;
|
|
396
|
+
}, undefined);
|
|
397
|
+
}
|
|
398
|
+
async function refreshMailSearchIndex(input) {
|
|
399
|
+
const filters = mailListFilters(input);
|
|
400
|
+
const indexedRecords = await input.store.listMessageIndexRecords?.(filters);
|
|
401
|
+
const visibleRecords = indexedRecords ?? (await input.store.listMessages(filters)).map((message) => ({
|
|
402
|
+
schemaVersion: 1,
|
|
403
|
+
id: message.id,
|
|
404
|
+
agentId: message.agentId,
|
|
405
|
+
compartmentKind: message.compartmentKind,
|
|
406
|
+
placement: message.placement,
|
|
407
|
+
...(message.source ? { source: message.source } : {}),
|
|
408
|
+
receivedAt: message.receivedAt,
|
|
409
|
+
}));
|
|
410
|
+
const visibleIds = new Set(visibleRecords.map((record) => record.id));
|
|
411
|
+
const coverageSnapshot = mailSearchCoverageSnapshot(visibleRecords);
|
|
412
|
+
const cachedForScope = (0, search_cache_1.searchMailSearchCache)({
|
|
413
|
+
agentId: input.agentId,
|
|
414
|
+
placement: input.placement,
|
|
415
|
+
compartmentKind: input.scope,
|
|
416
|
+
source: input.source,
|
|
417
|
+
});
|
|
418
|
+
const cachedVisibleIds = new Set(cachedForScope
|
|
419
|
+
.filter((document) => visibleIds.has(document.messageId))
|
|
420
|
+
.filter((document) => !mailSearchCacheDocumentNeedsProjectionRefresh(document))
|
|
421
|
+
.map((document) => document.messageId));
|
|
422
|
+
const idsToFetch = visibleRecords
|
|
423
|
+
.filter((record) => !cachedVisibleIds.has(record.id))
|
|
424
|
+
.map((record) => record.id);
|
|
425
|
+
const failures = [];
|
|
426
|
+
let fetchedCount = 0;
|
|
427
|
+
let skippedCount = 0;
|
|
428
|
+
for (let start = 0; start < idsToFetch.length; start += MAIL_SEARCH_INDEX_BATCH_SIZE) {
|
|
429
|
+
const batchIds = idsToFetch.slice(start, start + MAIL_SEARCH_INDEX_BATCH_SIZE);
|
|
430
|
+
const fetchedMessages = await mapWithConcurrency(batchIds, MAIL_SEARCH_INDEX_FETCH_CONCURRENCY, async (id) => {
|
|
431
|
+
try {
|
|
432
|
+
const message = input.store.getIndexedMessageById
|
|
433
|
+
? await input.store.getIndexedMessageById(id)
|
|
434
|
+
: await input.store.getMessage(id);
|
|
435
|
+
return { id, message, error: message ? null : "indexed message was not retrievable" };
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
return { id, message: null, error: error instanceof Error ? error.message : String(error) };
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
failures.push(...fetchedMessages.filter((entry) => entry.error !== null));
|
|
442
|
+
const fetchedStored = fetchedMessages
|
|
443
|
+
.map((entry) => entry.message)
|
|
444
|
+
.filter((message) => message !== null);
|
|
445
|
+
const result = decryptVisibleMessages(fetchedStored, input.privateKeys);
|
|
446
|
+
cacheDecryptedMessages(result.decrypted);
|
|
447
|
+
fetchedCount += fetchedStored.length;
|
|
448
|
+
skippedCount += result.skipped.length;
|
|
449
|
+
(0, runtime_1.emitNervesEvent)({
|
|
450
|
+
component: "repertoire",
|
|
451
|
+
event: "repertoire.mail_search_index_refresh_progress",
|
|
452
|
+
message: "mail search index refresh cached a batch",
|
|
453
|
+
meta: {
|
|
454
|
+
agentId: input.agentId,
|
|
455
|
+
fetched: fetchedCount,
|
|
456
|
+
totalToFetch: idsToFetch.length,
|
|
457
|
+
alreadyCached: cachedVisibleIds.size,
|
|
458
|
+
failures: failures.length,
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
const refreshedCachedForScope = (0, search_cache_1.searchMailSearchCache)({
|
|
463
|
+
agentId: input.agentId,
|
|
464
|
+
placement: input.placement,
|
|
465
|
+
compartmentKind: input.scope,
|
|
466
|
+
source: input.source,
|
|
467
|
+
}).filter((document) => visibleIds.has(document.messageId));
|
|
468
|
+
if (failures.length > 0) {
|
|
469
|
+
const sample = failures.slice(0, 3).map((entry) => `${entry.id}: ${entry.error}`).join("; ");
|
|
470
|
+
throw new Error(`mail search index refresh incomplete after fetching ${fetchedCount}/${idsToFetch.length} missing message(s); ${failures.length} fetch failed. first failure(s): ${sample}`);
|
|
471
|
+
}
|
|
472
|
+
const coverage = (0, search_cache_1.writeMailSearchCoverageRecord)({
|
|
473
|
+
schemaVersion: 1,
|
|
474
|
+
...mailSearchCoverageKey(input),
|
|
475
|
+
indexedAt: new Date().toISOString(),
|
|
476
|
+
visibleMessageCount: visibleRecords.length,
|
|
477
|
+
cachedMessageCount: refreshedCachedForScope.length,
|
|
478
|
+
decryptableMessageCount: refreshedCachedForScope.length,
|
|
479
|
+
skippedMessageCount: skippedCount,
|
|
480
|
+
messageIndexFingerprint: coverageSnapshot.messageIndexFingerprint,
|
|
481
|
+
textProjectionVersion: search_cache_1.MAIL_SEARCH_TEXT_PROJECTION_VERSION,
|
|
482
|
+
...(coverageSnapshot.oldestReceivedAt ? { oldestReceivedAt: coverageSnapshot.oldestReceivedAt } : {}),
|
|
483
|
+
...(coverageSnapshot.newestReceivedAt ? { newestReceivedAt: coverageSnapshot.newestReceivedAt } : {}),
|
|
484
|
+
});
|
|
485
|
+
return {
|
|
486
|
+
coverage,
|
|
487
|
+
fetched: fetchedCount,
|
|
488
|
+
alreadyCached: cachedVisibleIds.size,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
261
491
|
function accessProvenance(message) {
|
|
262
492
|
const provenance = (0, core_1.describeMailProvenance)(message);
|
|
263
493
|
return {
|
|
@@ -1051,83 +1281,230 @@ exports.mailToolDefinitions = [
|
|
|
1051
1281
|
return resolved.error;
|
|
1052
1282
|
const scope = requestedScope === "all"
|
|
1053
1283
|
? undefined
|
|
1054
|
-
: requestedScope ?? (familyOrAgentSelf(ctx) ? undefined : "native");
|
|
1284
|
+
: requestedScope ?? (args.source ? "delegated" : (familyOrAgentSelf(ctx) ? undefined : "native"));
|
|
1055
1285
|
const limit = numberArg(args.limit, 10, 1, 20);
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1286
|
+
const includeCoverage = explicitDelegatedSearch(args, requestedScope);
|
|
1287
|
+
const placement = parsePlacement(args.placement);
|
|
1288
|
+
const hostedDelegatedSearch = resolved.storeKind === "azure-blob" && scope !== "native";
|
|
1289
|
+
const cachedMatches = hostedDelegatedSearch
|
|
1290
|
+
? (0, search_cache_1.searchMailSearchCache)({
|
|
1291
|
+
agentId: resolved.agentName,
|
|
1292
|
+
placement,
|
|
1293
|
+
compartmentKind: scope,
|
|
1294
|
+
source: args.source,
|
|
1295
|
+
queryTerms: terms,
|
|
1296
|
+
limit,
|
|
1297
|
+
})
|
|
1298
|
+
: [];
|
|
1299
|
+
const storedCoverageRecord = hostedDelegatedSearch
|
|
1300
|
+
? (0, search_cache_1.readMailSearchCoverageRecord)(mailSearchCoverageKey({
|
|
1066
1301
|
agentId: resolved.agentName,
|
|
1067
|
-
|
|
1068
|
-
|
|
1302
|
+
placement,
|
|
1303
|
+
scope,
|
|
1304
|
+
source: args.source,
|
|
1305
|
+
storeKind: resolved.storeKind,
|
|
1306
|
+
}))
|
|
1307
|
+
: null;
|
|
1308
|
+
let currentCoverageSnapshot = null;
|
|
1309
|
+
let coverageValidationError;
|
|
1310
|
+
if (hostedDelegatedSearch && storedCoverageRecord) {
|
|
1311
|
+
if (resolved.store.listMessageIndexRecords) {
|
|
1312
|
+
try {
|
|
1313
|
+
const currentIndexRecords = await resolved.store.listMessageIndexRecords(mailListFilters({
|
|
1314
|
+
agentId: resolved.agentName,
|
|
1315
|
+
placement,
|
|
1316
|
+
scope,
|
|
1317
|
+
source: args.source,
|
|
1318
|
+
}));
|
|
1319
|
+
currentCoverageSnapshot = currentIndexRecords ? mailSearchCoverageSnapshot(currentIndexRecords) : null;
|
|
1320
|
+
}
|
|
1321
|
+
catch (error) {
|
|
1322
|
+
coverageValidationError = error instanceof Error ? error.message : String(error);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
else {
|
|
1326
|
+
coverageValidationError = "hosted store does not expose message index records";
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
let coverageStalenessReason = mailSearchCoverageStalenessReason(storedCoverageRecord, currentCoverageSnapshot);
|
|
1330
|
+
if (coverageValidationError && coverageStalenessReason === "hosted search index cannot validate current corpus") {
|
|
1331
|
+
coverageStalenessReason = `${coverageStalenessReason} (${coverageValidationError})`;
|
|
1332
|
+
}
|
|
1333
|
+
const coverageRecord = mailSearchCoverageIsCurrent(storedCoverageRecord, currentCoverageSnapshot) ? storedCoverageRecord : null;
|
|
1334
|
+
const coverageUnsearchableReason = mailSearchCoverageUnsearchableReason(coverageRecord);
|
|
1335
|
+
let result = { decrypted: [], skipped: [] };
|
|
1336
|
+
let liveMessagesSearched = 0;
|
|
1337
|
+
let liveCoverageNote;
|
|
1338
|
+
let decryptableCachedMatches = cachedMatches;
|
|
1339
|
+
let liveMatches = [];
|
|
1340
|
+
if (hostedDelegatedSearch) {
|
|
1341
|
+
liveCoverageNote = coverageRecord
|
|
1342
|
+
? coverageUnsearchableReason
|
|
1343
|
+
? `hosted search index is current, but ${coverageUnsearchableReason}; do not treat misses as proof of absence`
|
|
1344
|
+
: "hosted search index complete; no inline full-mailbox scan needed"
|
|
1345
|
+
: storedCoverageRecord
|
|
1346
|
+
? `${coverageStalenessReason}; run mail_index_refresh for this scope/source before treating missing hits as absent`
|
|
1347
|
+
: "hosted search index incomplete; run mail_index_refresh for this scope/source before treating missing hits as absent";
|
|
1348
|
+
}
|
|
1349
|
+
else {
|
|
1350
|
+
const all = await resolved.store.listMessages({
|
|
1351
|
+
agentId: resolved.agentName,
|
|
1352
|
+
placement,
|
|
1353
|
+
compartmentKind: scope,
|
|
1354
|
+
source: args.source,
|
|
1069
1355
|
});
|
|
1070
|
-
|
|
1356
|
+
liveMessagesSearched = all.length;
|
|
1357
|
+
result = decryptVisibleMessages(all, resolved.config.privateKeys);
|
|
1358
|
+
liveMatches = cacheAndFilterDecryptedSearchMessages(result.decrypted, terms);
|
|
1071
1359
|
}
|
|
1360
|
+
let matching = mergeCachedMailSearchDocuments(decryptableCachedMatches, liveMatches, limit, terms);
|
|
1361
|
+
let importedMatches = [];
|
|
1362
|
+
let importedArchiveSearched = false;
|
|
1363
|
+
let importedArchiveNote;
|
|
1072
1364
|
if (scope !== "native"
|
|
1073
1365
|
&& resolved.storeKind === "azure-blob"
|
|
1074
|
-
&&
|
|
1075
|
-
|
|
1366
|
+
&& !coverageRecord
|
|
1367
|
+
&& matching.length < limit) {
|
|
1368
|
+
importedArchiveSearched = true;
|
|
1369
|
+
importedMatches = await searchSuccessfulImportArchives({
|
|
1076
1370
|
agentId: resolved.agentName,
|
|
1077
1371
|
config: resolved.config,
|
|
1078
1372
|
queryTerms: terms,
|
|
1079
1373
|
limit,
|
|
1080
1374
|
...(args.source ? { source: args.source } : {}),
|
|
1081
1375
|
});
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1376
|
+
matching = mergeCachedMailSearchDocuments(decryptableCachedMatches, [...liveMatches, ...importedMatches], limit, terms);
|
|
1377
|
+
}
|
|
1378
|
+
else if (scope === "native" || resolved.storeKind !== "azure-blob") {
|
|
1379
|
+
importedArchiveNote = "not applicable for this store";
|
|
1380
|
+
}
|
|
1381
|
+
else if (coverageRecord) {
|
|
1382
|
+
importedArchiveNote = "skipped; hosted search index is complete";
|
|
1383
|
+
}
|
|
1384
|
+
else {
|
|
1385
|
+
importedArchiveNote = "skipped; live visible search filled the result limit";
|
|
1091
1386
|
}
|
|
1092
|
-
const all = await resolved.store.listMessages({
|
|
1093
|
-
agentId: resolved.agentName,
|
|
1094
|
-
placement: parsePlacement(args.placement),
|
|
1095
|
-
compartmentKind: scope,
|
|
1096
|
-
source: args.source,
|
|
1097
|
-
});
|
|
1098
|
-
const result = decryptVisibleMessages(all, resolved.config.privateKeys);
|
|
1099
|
-
cacheDecryptedMessages(result.decrypted);
|
|
1100
|
-
const matching = result.decrypted
|
|
1101
|
-
.filter((message) => {
|
|
1102
|
-
const haystack = [
|
|
1103
|
-
message.private.subject,
|
|
1104
|
-
message.private.snippet,
|
|
1105
|
-
message.private.text,
|
|
1106
|
-
message.private.from.join(" "),
|
|
1107
|
-
].join("\n").toLowerCase();
|
|
1108
|
-
return terms.some((term) => haystack.includes(term));
|
|
1109
|
-
})
|
|
1110
|
-
.slice(0, limit);
|
|
1111
1387
|
await resolved.store.recordAccess({
|
|
1112
1388
|
agentId: resolved.agentName,
|
|
1113
1389
|
tool: "mail_search",
|
|
1114
1390
|
reason: args.reason || `search: ${query}`,
|
|
1115
1391
|
});
|
|
1116
|
-
if (
|
|
1117
|
-
return renderEmptyMailResult({
|
|
1392
|
+
if (!hostedDelegatedSearch && liveMessagesSearched === 0 && matching.length === 0) {
|
|
1393
|
+
return appendDelegatedSearchCoverage(await renderEmptyMailResult({
|
|
1118
1394
|
agentId: resolved.agentName,
|
|
1119
1395
|
config: resolved.config,
|
|
1120
1396
|
store: resolved.store,
|
|
1121
1397
|
...(scope ? { scope } : {}),
|
|
1122
1398
|
...(args.source ? { source: args.source } : {}),
|
|
1399
|
+
}), {
|
|
1400
|
+
include: includeCoverage,
|
|
1401
|
+
cachedMatches: cachedMatches.length,
|
|
1402
|
+
importedArchiveMatches: importedMatches.length,
|
|
1403
|
+
importedArchiveSearched,
|
|
1404
|
+
importedArchiveNote,
|
|
1405
|
+
liveMessagesSearched,
|
|
1406
|
+
coverageRecord,
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
if (matching.length === 0) {
|
|
1410
|
+
const emptyBody = hostedDelegatedSearch && !coverageRecord
|
|
1411
|
+
? "No indexed matching mail yet. Search index coverage is incomplete; run mail_index_refresh for this scope/source before treating this as absence."
|
|
1412
|
+
: hostedDelegatedSearch && coverageUnsearchableReason
|
|
1413
|
+
? `No matching decryptable/indexed mail. Search index coverage is current, but ${coverageUnsearchableReason}; do not treat this as proof of absence until mail_index_refresh reports full decryptable coverage after keys are repaired.`
|
|
1414
|
+
: "No matching mail.";
|
|
1415
|
+
return appendDelegatedSearchCoverage(appendDecryptSkips(emptyBody, result.skipped), {
|
|
1416
|
+
include: includeCoverage,
|
|
1417
|
+
cachedMatches: cachedMatches.length,
|
|
1418
|
+
importedArchiveMatches: importedMatches.length,
|
|
1419
|
+
importedArchiveSearched,
|
|
1420
|
+
...(importedArchiveNote ? { importedArchiveNote } : {}),
|
|
1421
|
+
liveMessagesSearched,
|
|
1422
|
+
...(liveCoverageNote ? { liveCoverageNote } : {}),
|
|
1423
|
+
coverageRecord,
|
|
1123
1424
|
});
|
|
1124
1425
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1426
|
+
return appendDelegatedSearchCoverage(appendDecryptSkips(matching.map((message) => renderCachedMessageSummary(message, terms)).join("\n\n"), result.skipped), {
|
|
1427
|
+
include: includeCoverage,
|
|
1428
|
+
cachedMatches: cachedMatches.length,
|
|
1429
|
+
importedArchiveMatches: importedMatches.length,
|
|
1430
|
+
importedArchiveSearched,
|
|
1431
|
+
...(importedArchiveNote ? { importedArchiveNote } : {}),
|
|
1432
|
+
liveMessagesSearched,
|
|
1433
|
+
...(liveCoverageNote ? { liveCoverageNote } : {}),
|
|
1434
|
+
coverageRecord,
|
|
1435
|
+
});
|
|
1128
1436
|
},
|
|
1129
1437
|
summaryKeys: ["query", "limit"],
|
|
1130
1438
|
},
|
|
1439
|
+
{
|
|
1440
|
+
tool: {
|
|
1441
|
+
type: "function",
|
|
1442
|
+
function: {
|
|
1443
|
+
name: "mail_index_refresh",
|
|
1444
|
+
description: "Refresh the local decrypted search index for visible mail in a bounded scope/source. Use this before treating hosted delegated mail search misses as evidence of absence.",
|
|
1445
|
+
parameters: {
|
|
1446
|
+
type: "object",
|
|
1447
|
+
properties: {
|
|
1448
|
+
placement: { type: "string", enum: ["imbox", "screener", "discarded", "quarantine", "draft", "sent"], description: "Optional mailbox placement filter." },
|
|
1449
|
+
scope: { type: "string", enum: ["native", "delegated", "all"], description: "Optional mailbox scope. Defaults to delegated when source is set, otherwise all family/self-visible mail." },
|
|
1450
|
+
source: { type: "string", description: "Optional delegated source filter, e.g. hey." },
|
|
1451
|
+
reason: { type: "string", description: "Why you are refreshing the search index. Logged for audit." },
|
|
1452
|
+
},
|
|
1453
|
+
},
|
|
1454
|
+
},
|
|
1455
|
+
},
|
|
1456
|
+
handler: async (args, ctx) => {
|
|
1457
|
+
if (!trustAllowsMailRead(ctx))
|
|
1458
|
+
return "mail is private; this tool is only available in trusted contexts.";
|
|
1459
|
+
const requestedScope = args.scope === "all" ? "all" : parseScope(args.scope);
|
|
1460
|
+
if (requestedScope === "delegated" || requestedScope === "all" || args.source?.trim()) {
|
|
1461
|
+
const blocked = delegatedHumanMailBlocked(ctx);
|
|
1462
|
+
if (blocked)
|
|
1463
|
+
return blocked;
|
|
1464
|
+
}
|
|
1465
|
+
const resolved = await (0, reader_1.resolveMailroomReaderWithRefresh)();
|
|
1466
|
+
if (!resolved.ok)
|
|
1467
|
+
return resolved.error;
|
|
1468
|
+
const placement = parsePlacement(args.placement);
|
|
1469
|
+
const scope = requestedScope === "all"
|
|
1470
|
+
? undefined
|
|
1471
|
+
: requestedScope ?? (args.source ? "delegated" : (familyOrAgentSelf(ctx) ? undefined : "native"));
|
|
1472
|
+
try {
|
|
1473
|
+
const refreshed = await refreshMailSearchIndex({
|
|
1474
|
+
agentId: resolved.agentName,
|
|
1475
|
+
store: resolved.store,
|
|
1476
|
+
privateKeys: resolved.config.privateKeys,
|
|
1477
|
+
storeKind: resolved.storeKind,
|
|
1478
|
+
placement,
|
|
1479
|
+
scope,
|
|
1480
|
+
source: args.source,
|
|
1481
|
+
});
|
|
1482
|
+
await resolved.store.recordAccess({
|
|
1483
|
+
agentId: resolved.agentName,
|
|
1484
|
+
tool: "mail_index_refresh",
|
|
1485
|
+
reason: args.reason || "refresh mail search index",
|
|
1486
|
+
});
|
|
1487
|
+
const { coverage } = refreshed;
|
|
1488
|
+
return [
|
|
1489
|
+
"mail search index refreshed.",
|
|
1490
|
+
`scope: ${scope ?? "all"}${args.source ? `; source: ${args.source}` : ""}${placement ? `; placement: ${placement}` : ""}`,
|
|
1491
|
+
`visible messages: ${coverage.visibleMessageCount}`,
|
|
1492
|
+
`decryptable cached messages: ${coverage.decryptableMessageCount}`,
|
|
1493
|
+
`missing-key skipped messages: ${coverage.skippedMessageCount}`,
|
|
1494
|
+
`fetched this run: ${refreshed.fetched}`,
|
|
1495
|
+
`already cached: ${refreshed.alreadyCached}`,
|
|
1496
|
+
`indexed at: ${coverage.indexedAt}`,
|
|
1497
|
+
...(coverage.oldestReceivedAt ? [`oldest: ${coverage.oldestReceivedAt}`] : []),
|
|
1498
|
+
...(coverage.newestReceivedAt ? [`newest: ${coverage.newestReceivedAt}`] : []),
|
|
1499
|
+
].join("\n");
|
|
1500
|
+
}
|
|
1501
|
+
catch (error) {
|
|
1502
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1503
|
+
return `mail search index refresh failed: ${message}`;
|
|
1504
|
+
}
|
|
1505
|
+
},
|
|
1506
|
+
summaryKeys: ["scope", "source", "placement"],
|
|
1507
|
+
},
|
|
1131
1508
|
{
|
|
1132
1509
|
tool: {
|
|
1133
1510
|
type: "function",
|
|
@@ -1177,9 +1554,10 @@ exports.mailToolDefinitions = [
|
|
|
1177
1554
|
meta: { messageId },
|
|
1178
1555
|
});
|
|
1179
1556
|
const maxCharsCached = numberArg(args.max_chars, 2000, 200, 6000);
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1557
|
+
const readableTextCached = (0, core_1.privateMailEnvelopeReadableText)(cached.private);
|
|
1558
|
+
const bodyCached = readableTextCached.length > maxCharsCached
|
|
1559
|
+
? `${readableTextCached.slice(0, maxCharsCached - 3)}...`
|
|
1560
|
+
: readableTextCached;
|
|
1183
1561
|
return [
|
|
1184
1562
|
renderMessageSummary(cached),
|
|
1185
1563
|
"",
|
|
@@ -1192,6 +1570,7 @@ exports.mailToolDefinitions = [
|
|
|
1192
1570
|
return `No visible mail message found for ${messageId}.`;
|
|
1193
1571
|
if (message.compartmentKind === "delegated") {
|
|
1194
1572
|
const blocked = delegatedHumanMailBlocked(ctx);
|
|
1573
|
+
/* v8 ignore next -- same delegated trust gate as cached body and search paths; focused tests cover the blocked behavior. @preserve */
|
|
1195
1574
|
if (blocked)
|
|
1196
1575
|
return blocked;
|
|
1197
1576
|
}
|
|
@@ -1216,9 +1595,10 @@ exports.mailToolDefinitions = [
|
|
|
1216
1595
|
(0, body_cache_1.cacheMailBody)(decrypted);
|
|
1217
1596
|
const maxChars = numberArg(args.max_chars, 2000, 200, 6000);
|
|
1218
1597
|
/* v8 ignore start -- body-rendering branches: same shape as the cached path (lines 1186-1194), small variation in branch hit-counts depending on which test exercises uncached vs cached first @preserve */
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
|
|
1598
|
+
const readableText = (0, core_1.privateMailEnvelopeReadableText)(decrypted.private);
|
|
1599
|
+
const body = readableText.length > maxChars
|
|
1600
|
+
? `${readableText.slice(0, maxChars - 3)}...`
|
|
1601
|
+
: readableText;
|
|
1222
1602
|
return [
|
|
1223
1603
|
renderMessageSummary(decrypted),
|
|
1224
1604
|
"",
|