@ouro.bot/cli 0.1.0-alpha.534 → 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 +15 -0
- package/dist/heart/daemon/daemon.js +1 -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
package/changelog.json
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.536",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Delegated hosted mail search now tracks durable coverage records and refuses to treat stale cache misses as absence proof, with `mail_index_refresh` repairing projection-incomplete cache entries before coverage is considered current.",
|
|
8
|
+
"Mail ingestion, search caching, and body rendering now preserve searchable text from HTML-only messages, preventing travel facts from disappearing when an email lacks a plain-text body.",
|
|
9
|
+
"MCP send-message and daemon socket commands now fail with bounded timeout errors instead of hanging forever, and trip ledgers can be rendered as chronological calendars with evidence-backed `trip_calendar` output."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.1.0-alpha.535",
|
|
14
|
+
"changes": [
|
|
15
|
+
"Daemon orphan cleanup now recognizes managed mail sense workers, so stale `senses/mail-entry.js --agent <name>` processes from old runtimes are terminated on production daemon startup instead of lingering across restarts.",
|
|
16
|
+
"Adds regression coverage for the full daemon-managed entrypoint set used by orphan cleanup, including inner-dialog, daemon, BlueBubbles, Teams, and mail workers."
|
|
17
|
+
]
|
|
18
|
+
},
|
|
4
19
|
{
|
|
5
20
|
"version": "0.1.0-alpha.534",
|
|
6
21
|
"changes": [
|
|
@@ -105,6 +105,7 @@ function parseOrphanPidsFromPs(psOutput, selfPid) {
|
|
|
105
105
|
if (!line.includes("agent-entry.js")
|
|
106
106
|
&& !line.includes("daemon-entry.js")
|
|
107
107
|
&& !line.includes("bluebubbles/entry.js")
|
|
108
|
+
&& !line.includes("mail-entry.js")
|
|
108
109
|
&& !line.includes("teams-entry.js"))
|
|
109
110
|
continue;
|
|
110
111
|
// Parse `<pid> <ppid> <command...>`. ps pads these with leading spaces.
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.DEFAULT_DAEMON_SOCKET_PATH = void 0;
|
|
36
|
+
exports.DEFAULT_DAEMON_COMMAND_TIMEOUT_MS = exports.DEFAULT_DAEMON_SOCKET_PATH = void 0;
|
|
37
37
|
exports.__bypassVitestGuardForTests = __bypassVitestGuardForTests;
|
|
38
38
|
exports.sendDaemonCommand = sendDaemonCommand;
|
|
39
39
|
exports.checkDaemonSocketAlive = checkDaemonSocketAlive;
|
|
@@ -42,6 +42,7 @@ const fs = __importStar(require("fs"));
|
|
|
42
42
|
const net = __importStar(require("net"));
|
|
43
43
|
const runtime_1 = require("../../nerves/runtime");
|
|
44
44
|
exports.DEFAULT_DAEMON_SOCKET_PATH = "/tmp/ouroboros-daemon.sock";
|
|
45
|
+
exports.DEFAULT_DAEMON_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
|
|
45
46
|
/**
|
|
46
47
|
* Defense-in-depth: detect if we're running under vitest. Tests that forget
|
|
47
48
|
* to `vi.mock("../../heart/daemon/socket-client")` would otherwise leak real
|
|
@@ -106,7 +107,7 @@ function shouldSuppressSocketCall(socketPath) {
|
|
|
106
107
|
return true;
|
|
107
108
|
return globalThis[BYPASS_KEY] !== true;
|
|
108
109
|
}
|
|
109
|
-
function sendDaemonCommand(socketPath, command) {
|
|
110
|
+
function sendDaemonCommand(socketPath, command, options = {}) {
|
|
110
111
|
if (shouldSuppressSocketCall(socketPath)) {
|
|
111
112
|
(0, runtime_1.emitNervesEvent)({
|
|
112
113
|
level: "warn",
|
|
@@ -126,6 +127,41 @@ function sendDaemonCommand(socketPath, command) {
|
|
|
126
127
|
return new Promise((resolve, reject) => {
|
|
127
128
|
const client = net.createConnection(socketPath);
|
|
128
129
|
let raw = "";
|
|
130
|
+
let settled = false;
|
|
131
|
+
const timeoutMs = options.timeoutMs ?? exports.DEFAULT_DAEMON_COMMAND_TIMEOUT_MS;
|
|
132
|
+
const resolveOnce = (response) => {
|
|
133
|
+
/* v8 ignore next -- duplicate settlement guard; callers also guard event handlers before reaching this helper @preserve */
|
|
134
|
+
if (settled)
|
|
135
|
+
return;
|
|
136
|
+
settled = true;
|
|
137
|
+
resolve(response);
|
|
138
|
+
};
|
|
139
|
+
const rejectOnce = (error) => {
|
|
140
|
+
/* v8 ignore next -- duplicate timeout/error races should not re-reject the command promise @preserve */
|
|
141
|
+
if (settled)
|
|
142
|
+
return;
|
|
143
|
+
settled = true;
|
|
144
|
+
reject(error);
|
|
145
|
+
};
|
|
146
|
+
if ("setTimeout" in client && typeof client.setTimeout === "function") {
|
|
147
|
+
client.setTimeout(timeoutMs, () => {
|
|
148
|
+
const error = new Error(`Daemon command ${command.kind} timed out after ${timeoutMs}ms waiting for a response.`);
|
|
149
|
+
(0, runtime_1.emitNervesEvent)({
|
|
150
|
+
level: "error",
|
|
151
|
+
component: "daemon",
|
|
152
|
+
event: "daemon.socket_command_timeout",
|
|
153
|
+
message: "daemon socket command timed out",
|
|
154
|
+
meta: {
|
|
155
|
+
socketPath,
|
|
156
|
+
kind: command.kind,
|
|
157
|
+
timeoutMs,
|
|
158
|
+
error: error.message,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
client.destroy();
|
|
162
|
+
rejectOnce(error);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
129
165
|
client.on("connect", () => {
|
|
130
166
|
// Write the command + newline delimiter. DO NOT call client.end()
|
|
131
167
|
// afterwards — the server closes the connection once it has written
|
|
@@ -149,6 +185,9 @@ function sendDaemonCommand(socketPath, command) {
|
|
|
149
185
|
raw += chunk.toString("utf-8");
|
|
150
186
|
});
|
|
151
187
|
client.on("error", (error) => {
|
|
188
|
+
/* v8 ignore next -- duplicate post-settlement socket errors are ignored defensively @preserve */
|
|
189
|
+
if (settled)
|
|
190
|
+
return;
|
|
152
191
|
(0, runtime_1.emitNervesEvent)({
|
|
153
192
|
level: "error",
|
|
154
193
|
component: "daemon",
|
|
@@ -160,9 +199,12 @@ function sendDaemonCommand(socketPath, command) {
|
|
|
160
199
|
error: error.message,
|
|
161
200
|
},
|
|
162
201
|
});
|
|
163
|
-
|
|
202
|
+
rejectOnce(error);
|
|
164
203
|
});
|
|
165
204
|
client.on("end", () => {
|
|
205
|
+
/* v8 ignore next -- duplicate post-settlement socket end events are ignored defensively @preserve */
|
|
206
|
+
if (settled)
|
|
207
|
+
return;
|
|
166
208
|
const trimmed = raw.trim();
|
|
167
209
|
if (trimmed.length === 0 && command.kind === "daemon.stop") {
|
|
168
210
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -171,7 +213,7 @@ function sendDaemonCommand(socketPath, command) {
|
|
|
171
213
|
message: "daemon socket command completed",
|
|
172
214
|
meta: { socketPath, kind: command.kind, ok: true },
|
|
173
215
|
});
|
|
174
|
-
|
|
216
|
+
resolveOnce({ ok: true, message: "daemon stopped" });
|
|
175
217
|
return;
|
|
176
218
|
}
|
|
177
219
|
if (trimmed.length === 0) {
|
|
@@ -187,7 +229,7 @@ function sendDaemonCommand(socketPath, command) {
|
|
|
187
229
|
error: error.message,
|
|
188
230
|
},
|
|
189
231
|
});
|
|
190
|
-
|
|
232
|
+
rejectOnce(error);
|
|
191
233
|
return;
|
|
192
234
|
}
|
|
193
235
|
try {
|
|
@@ -202,7 +244,7 @@ function sendDaemonCommand(socketPath, command) {
|
|
|
202
244
|
ok: parsed.ok,
|
|
203
245
|
},
|
|
204
246
|
});
|
|
205
|
-
|
|
247
|
+
resolveOnce(parsed);
|
|
206
248
|
}
|
|
207
249
|
catch (error) {
|
|
208
250
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -216,7 +258,7 @@ function sendDaemonCommand(socketPath, command) {
|
|
|
216
258
|
error: error instanceof Error ? error.message : String(error),
|
|
217
259
|
},
|
|
218
260
|
});
|
|
219
|
-
|
|
261
|
+
rejectOnce(error);
|
|
220
262
|
}
|
|
221
263
|
});
|
|
222
264
|
});
|
|
@@ -33,8 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports._senseTurnRetryDelays = exports.SENSE_TURN_RETRY_DELAYS_MS = exports.SENSE_TURN_MAX_RETRIES = void 0;
|
|
36
|
+
exports._senseTurnCommandTimeoutMs = exports._senseTurnRetryDelays = exports.SENSE_TURN_COMMAND_TIMEOUT_MS = exports.SENSE_TURN_RETRY_DELAYS_MS = exports.SENSE_TURN_MAX_RETRIES = void 0;
|
|
37
37
|
exports._setSenseTurnRetryDelays = _setSenseTurnRetryDelays;
|
|
38
|
+
exports._setSenseTurnCommandTimeoutMs = _setSenseTurnCommandTimeoutMs;
|
|
38
39
|
exports.createMcpServer = createMcpServer;
|
|
39
40
|
exports.getToolSchemas = getToolSchemas;
|
|
40
41
|
const socket_client_1 = require("../daemon/socket-client");
|
|
@@ -44,9 +45,38 @@ const session_id_resolver_1 = require("../daemon/session-id-resolver");
|
|
|
44
45
|
const pending_1 = require("../../mind/pending");
|
|
45
46
|
exports.SENSE_TURN_MAX_RETRIES = 3;
|
|
46
47
|
exports.SENSE_TURN_RETRY_DELAYS_MS = [1000, 2000, 4000];
|
|
48
|
+
exports.SENSE_TURN_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
|
|
47
49
|
// Allow test override
|
|
48
50
|
exports._senseTurnRetryDelays = exports.SENSE_TURN_RETRY_DELAYS_MS;
|
|
49
51
|
function _setSenseTurnRetryDelays(delays) { exports._senseTurnRetryDelays = delays; }
|
|
52
|
+
exports._senseTurnCommandTimeoutMs = exports.SENSE_TURN_COMMAND_TIMEOUT_MS;
|
|
53
|
+
function _setSenseTurnCommandTimeoutMs(timeoutMs) { exports._senseTurnCommandTimeoutMs = timeoutMs; }
|
|
54
|
+
async function withSenseTurnTimeout(promise, timeoutMs, command) {
|
|
55
|
+
let timer = null;
|
|
56
|
+
try {
|
|
57
|
+
return await Promise.race([
|
|
58
|
+
promise,
|
|
59
|
+
new Promise((_, reject) => {
|
|
60
|
+
timer = setTimeout(() => {
|
|
61
|
+
const error = new Error(`MCP send_message to ${command.agent} timed out after ${timeoutMs}ms waiting for daemon response; command status is unknown.`);
|
|
62
|
+
(0, runtime_1.emitNervesEvent)({
|
|
63
|
+
level: "error",
|
|
64
|
+
component: "daemon",
|
|
65
|
+
event: "daemon.mcp_sense_turn_timeout",
|
|
66
|
+
message: "MCP senseTurn timed out waiting for daemon response",
|
|
67
|
+
meta: { agent: command.agent, friendId: command.friendId, sessionKey: command.sessionKey, timeoutMs },
|
|
68
|
+
});
|
|
69
|
+
reject(error);
|
|
70
|
+
}, timeoutMs);
|
|
71
|
+
}),
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
/* v8 ignore next -- Promise.race installs the timer synchronously; null is only a defensive cleanup guard @preserve */
|
|
76
|
+
if (timer)
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
50
80
|
/**
|
|
51
81
|
* Send a senseTurn command to the daemon with retry logic.
|
|
52
82
|
* Retries on transient failures: empty response (daemon mid-restart),
|
|
@@ -57,7 +87,7 @@ async function sendSenseTurnWithRetry(socketPath, command) {
|
|
|
57
87
|
/* v8 ignore start -- retry loop: functionally tested via mcp-send-message retry tests @preserve */
|
|
58
88
|
for (let attempt = 0; attempt <= exports.SENSE_TURN_MAX_RETRIES; attempt++) {
|
|
59
89
|
try {
|
|
60
|
-
const response = await (0, socket_client_1.sendDaemonCommand)(socketPath, command);
|
|
90
|
+
const response = await withSenseTurnTimeout((0, socket_client_1.sendDaemonCommand)(socketPath, command), exports._senseTurnCommandTimeoutMs, command);
|
|
61
91
|
return response;
|
|
62
92
|
}
|
|
63
93
|
catch (error) {
|
|
@@ -83,6 +83,17 @@ function isRetryableBlobDownloadError(error) {
|
|
|
83
83
|
message.includes("etimedout") ||
|
|
84
84
|
message.includes("socket closed");
|
|
85
85
|
}
|
|
86
|
+
function isBlobNotFoundError(error) {
|
|
87
|
+
const maybeStatus = typeof error === "object" && error !== null && "statusCode" in error
|
|
88
|
+
? error.statusCode
|
|
89
|
+
: undefined;
|
|
90
|
+
if (maybeStatus === 404)
|
|
91
|
+
return true;
|
|
92
|
+
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
93
|
+
return message.includes("blobnotfound") ||
|
|
94
|
+
message.includes("the specified blob does not exist") ||
|
|
95
|
+
message.includes("missing blob");
|
|
96
|
+
}
|
|
86
97
|
async function downloadJson(blob, timeoutMs) {
|
|
87
98
|
if (!await blob.exists())
|
|
88
99
|
return null;
|
|
@@ -102,6 +113,25 @@ async function downloadJson(blob, timeoutMs) {
|
|
|
102
113
|
}
|
|
103
114
|
throw normalizeBlobOperationError("download", blob, timeoutMs, lastError);
|
|
104
115
|
}
|
|
116
|
+
async function downloadIndexedJson(blob, timeoutMs) {
|
|
117
|
+
let lastError = null;
|
|
118
|
+
for (let attempt = 1; attempt <= DEFAULT_BLOB_DOWNLOAD_ATTEMPTS; attempt += 1) {
|
|
119
|
+
try {
|
|
120
|
+
const buffer = await withBlobOperationTimeout(timeoutMs, (abortSignal) => {
|
|
121
|
+
return blob.downloadToBuffer(undefined, undefined, { abortSignal });
|
|
122
|
+
});
|
|
123
|
+
return JSON.parse(buffer.toString("utf-8"));
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (isBlobNotFoundError(error))
|
|
127
|
+
return null;
|
|
128
|
+
lastError = error;
|
|
129
|
+
if (attempt >= DEFAULT_BLOB_DOWNLOAD_ATTEMPTS || !isRetryableBlobDownloadError(error))
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw normalizeBlobOperationError("download", blob, timeoutMs, lastError);
|
|
134
|
+
}
|
|
105
135
|
async function uploadJson(blob, value, timeoutMs) {
|
|
106
136
|
try {
|
|
107
137
|
await withBlobOperationTimeout(timeoutMs, (abortSignal) => {
|
|
@@ -275,20 +305,35 @@ class AzureBlobMailroomStore {
|
|
|
275
305
|
await Promise.all(Array.from({ length: Math.min(MESSAGE_LIST_SCAN_CONCURRENCY, Math.max(messageBlobNames.length, 1)) }, async () => worker()));
|
|
276
306
|
return applyOptionalLimit(matches.sort(compareNewestFirst), filters.limit);
|
|
277
307
|
}
|
|
278
|
-
async
|
|
279
|
-
|
|
308
|
+
async listMessageIndexRecords(filters) {
|
|
309
|
+
await this.ensureContainer();
|
|
310
|
+
const records = [];
|
|
280
311
|
let sawIndex = false;
|
|
281
312
|
for await (const item of this.container.listBlobsFlat({ prefix: messageIndexPrefix(filters.agentId) })) {
|
|
282
313
|
sawIndex = true;
|
|
283
314
|
const parsed = parseMessageIndexBlobName(item.name);
|
|
284
315
|
if (!parsed || !messageMatchesFilters(parsed, filters))
|
|
285
316
|
continue;
|
|
286
|
-
|
|
287
|
-
if (typeof filters.limit === "number" &&
|
|
317
|
+
records.push(parsed);
|
|
318
|
+
if (typeof filters.limit === "number" && records.length >= filters.limit)
|
|
288
319
|
break;
|
|
289
320
|
}
|
|
290
321
|
if (!sawIndex)
|
|
291
322
|
return null;
|
|
323
|
+
const sorted = records.sort((left, right) => Date.parse(right.receivedAt) - Date.parse(left.receivedAt));
|
|
324
|
+
(0, runtime_1.emitNervesEvent)({
|
|
325
|
+
component: "senses",
|
|
326
|
+
event: "senses.mail_blob_store_message_indexes_listed",
|
|
327
|
+
message: "azure blob mailroom store listed message indexes",
|
|
328
|
+
meta: { agentId: filters.agentId, count: sorted.length },
|
|
329
|
+
});
|
|
330
|
+
return applyOptionalLimit(sorted, filters.limit);
|
|
331
|
+
}
|
|
332
|
+
async listMessagesFromIndexes(filters) {
|
|
333
|
+
const records = await this.listMessageIndexRecords(filters);
|
|
334
|
+
if (records === null)
|
|
335
|
+
return null;
|
|
336
|
+
const messageIds = records.map((record) => record.id);
|
|
292
337
|
const messages = (await mapWithConcurrency(messageIds, this.messageFetchConcurrency, async (id) => {
|
|
293
338
|
return downloadJson(this.messageBlob(id), this.blobOperationTimeoutMs);
|
|
294
339
|
}))
|
|
@@ -404,6 +449,17 @@ class AzureBlobMailroomStore {
|
|
|
404
449
|
});
|
|
405
450
|
return message;
|
|
406
451
|
}
|
|
452
|
+
async getIndexedMessageById(id) {
|
|
453
|
+
await this.ensureContainer();
|
|
454
|
+
const message = await downloadIndexedJson(this.messageBlob(id), this.blobOperationTimeoutMs);
|
|
455
|
+
(0, runtime_1.emitNervesEvent)({
|
|
456
|
+
component: "senses",
|
|
457
|
+
event: "senses.mail_blob_store_indexed_message_read",
|
|
458
|
+
message: "azure blob mailroom store read message from known index",
|
|
459
|
+
meta: { id, found: message !== null },
|
|
460
|
+
});
|
|
461
|
+
return message;
|
|
462
|
+
}
|
|
407
463
|
async listMessages(filters) {
|
|
408
464
|
await this.ensureContainer();
|
|
409
465
|
let filtered = await this.listMessagesFromIndexes(filters);
|
package/dist/mailroom/core.js
CHANGED
|
@@ -46,6 +46,8 @@ exports.encryptJsonForMailKey = encryptJsonForMailKey;
|
|
|
46
46
|
exports.decryptMailJson = decryptMailJson;
|
|
47
47
|
exports.resolveMailAddress = resolveMailAddress;
|
|
48
48
|
exports.describeMailProvenance = describeMailProvenance;
|
|
49
|
+
exports.htmlMailBodyToText = htmlMailBodyToText;
|
|
50
|
+
exports.privateMailEnvelopeReadableText = privateMailEnvelopeReadableText;
|
|
49
51
|
exports.buildStoredMailMessage = buildStoredMailMessage;
|
|
50
52
|
exports.decryptStoredMailMessage = decryptStoredMailMessage;
|
|
51
53
|
exports.provisionMailboxRegistry = provisionMailboxRegistry;
|
|
@@ -396,10 +398,56 @@ function candidateSender(input) {
|
|
|
396
398
|
function normalizedIngestProvenance(input) {
|
|
397
399
|
return input ?? { schemaVersion: 1, kind: "smtp" };
|
|
398
400
|
}
|
|
401
|
+
function decodeHtmlEntity(entity) {
|
|
402
|
+
const named = {
|
|
403
|
+
amp: "&",
|
|
404
|
+
apos: "'",
|
|
405
|
+
gt: ">",
|
|
406
|
+
lt: "<",
|
|
407
|
+
nbsp: " ",
|
|
408
|
+
quot: "\"",
|
|
409
|
+
};
|
|
410
|
+
const lowered = entity.toLowerCase();
|
|
411
|
+
if (named[lowered] !== undefined)
|
|
412
|
+
return named[lowered];
|
|
413
|
+
if (lowered.startsWith("#x")) {
|
|
414
|
+
const parsed = Number.parseInt(lowered.slice(2), 16);
|
|
415
|
+
return Number.isFinite(parsed) && parsed >= 0 && parsed <= 0x10ffff ? String.fromCodePoint(parsed) : `&${entity};`;
|
|
416
|
+
}
|
|
417
|
+
if (lowered.startsWith("#")) {
|
|
418
|
+
const parsed = Number.parseInt(lowered.slice(1), 10);
|
|
419
|
+
return Number.isFinite(parsed) && parsed >= 0 && parsed <= 0x10ffff ? String.fromCodePoint(parsed) : `&${entity};`;
|
|
420
|
+
}
|
|
421
|
+
return `&${entity};`;
|
|
422
|
+
}
|
|
423
|
+
function htmlMailBodyToText(html) {
|
|
424
|
+
return html
|
|
425
|
+
.replace(/<\s*(script|style|head|title|noscript)\b[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi, " ")
|
|
426
|
+
.replace(/<\s*br\s*\/?\s*>/gi, "\n")
|
|
427
|
+
.replace(/<\s*\/?\s*(address|article|aside|blockquote|body|caption|div|figcaption|figure|footer|h[1-6]|header|hr|li|main|nav|ol|p|pre|section|table|tbody|td|tfoot|th|thead|tr|ul)\b[^>]*>/gi, "\n")
|
|
428
|
+
.replace(/<[^>]+>/g, " ")
|
|
429
|
+
.replace(/&([a-zA-Z][a-zA-Z0-9]+|#[0-9]+|#x[0-9a-fA-F]+);/g, (_match, entity) => decodeHtmlEntity(entity))
|
|
430
|
+
.replace(/\r\n?/g, "\n")
|
|
431
|
+
.replace(/[ \t\f\v]+/g, " ")
|
|
432
|
+
.replace(/\n[ \t]+/g, "\n")
|
|
433
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
434
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
435
|
+
.trim();
|
|
436
|
+
}
|
|
437
|
+
function privateMailEnvelopeReadableText(privateEnvelope) {
|
|
438
|
+
if (privateEnvelope.text.trim().length > 0)
|
|
439
|
+
return privateEnvelope.text;
|
|
440
|
+
if (!privateEnvelope.html || privateEnvelope.html.trim().length === 0)
|
|
441
|
+
return "";
|
|
442
|
+
return htmlMailBodyToText(privateEnvelope.html);
|
|
443
|
+
}
|
|
399
444
|
async function buildStoredMailMessage(input) {
|
|
400
445
|
const parsed = await (0, mailparser_1.simpleParser)(input.rawMime);
|
|
401
446
|
const id = messageStorageId(input.envelope, input.rawMime);
|
|
402
|
-
const
|
|
447
|
+
const html = typeof parsed.html === "string" ? parsed.html : undefined;
|
|
448
|
+
const parsedText = parsed.text ?? "";
|
|
449
|
+
/* v8 ignore next -- mailparser body-shape ternary permutations are covered by plain-text, HTML-only, and empty-MIME tests; this line is a projection adapter, not policy. @preserve */
|
|
450
|
+
const text = parsedText.trim().length > 0 ? parsedText : html ? htmlMailBodyToText(html) : "";
|
|
403
451
|
const inReplyTo = typeof parsed.inReplyTo === "string" && parsed.inReplyTo.trim().length > 0
|
|
404
452
|
? parsed.inReplyTo.trim()
|
|
405
453
|
: undefined;
|
|
@@ -422,7 +470,7 @@ async function buildStoredMailMessage(input) {
|
|
|
422
470
|
subject: parsed.subject ?? "",
|
|
423
471
|
date: parsed.date?.toISOString(),
|
|
424
472
|
text,
|
|
425
|
-
html
|
|
473
|
+
html,
|
|
426
474
|
snippet: snippet(text || parsed.subject || "(no text body)"),
|
|
427
475
|
attachments: parsed.attachments.map((attachment) => ({
|
|
428
476
|
filename: attachment.filename ?? "(unnamed attachment)",
|
|
@@ -372,8 +372,9 @@ async function cacheMatchingMailSearchDocumentsFromMboxFile(input) {
|
|
|
372
372
|
continue;
|
|
373
373
|
(0, search_cache_1.upsertMailSearchCacheDocument)(message, privateEnvelope);
|
|
374
374
|
matches.push(document);
|
|
375
|
-
|
|
376
|
-
|
|
375
|
+
matches.sort((left, right) => (0, search_relevance_1.compareByRelevanceThenRecency)({ document: left, relevance: (0, search_relevance_1.scoreMailSearchDocument)(left, queryTerms) }, { document: right, relevance: (0, search_relevance_1.scoreMailSearchDocument)(right, queryTerms) }));
|
|
376
|
+
if (matches.length > input.limit)
|
|
377
|
+
matches.length = input.limit;
|
|
377
378
|
}
|
|
378
379
|
return matches
|
|
379
380
|
.map((document) => ({ document, relevance: (0, search_relevance_1.scoreMailSearchDocument)(document, queryTerms) }))
|
package/dist/mailroom/reader.js
CHANGED
|
@@ -220,7 +220,9 @@ function resolveMailroomReader(agentName = (0, identity_2.getAgentName)()) {
|
|
|
220
220
|
}
|
|
221
221
|
async function resolveMailroomReaderWithRefresh(agentName = (0, identity_2.getAgentName)()) {
|
|
222
222
|
const resolved = resolveMailroomReader(agentName);
|
|
223
|
-
if (resolved.ok
|
|
223
|
+
if (resolved.ok
|
|
224
|
+
|| resolved.reason !== "auth-required"
|
|
225
|
+
|| (!resolved.error.includes(" is unavailable") && !resolved.error.includes(" is missing"))) {
|
|
224
226
|
return resolved;
|
|
225
227
|
}
|
|
226
228
|
await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agentName, { preserveCachedOnFailure: true }).catch(() => undefined);
|
|
@@ -33,17 +33,22 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.MAIL_SEARCH_TEXT_PROJECTION_VERSION = void 0;
|
|
36
37
|
exports.buildMailSearchCacheDocument = buildMailSearchCacheDocument;
|
|
37
38
|
exports.upsertMailSearchCacheDocument = upsertMailSearchCacheDocument;
|
|
38
39
|
exports.syncMailSearchCacheMetadata = syncMailSearchCacheMetadata;
|
|
39
40
|
exports.searchMailSearchCache = searchMailSearchCache;
|
|
41
|
+
exports.readMailSearchCoverageRecord = readMailSearchCoverageRecord;
|
|
42
|
+
exports.writeMailSearchCoverageRecord = writeMailSearchCoverageRecord;
|
|
40
43
|
exports.resetMailSearchCacheForTests = resetMailSearchCacheForTests;
|
|
41
44
|
const fs = __importStar(require("node:fs"));
|
|
42
45
|
const path = __importStar(require("node:path"));
|
|
43
46
|
const identity_1 = require("../heart/identity");
|
|
44
47
|
const runtime_1 = require("../nerves/runtime");
|
|
48
|
+
const core_1 = require("./core");
|
|
45
49
|
const search_relevance_1 = require("./search-relevance");
|
|
46
50
|
const SEARCH_TEXT_EXCERPT_LIMIT = 16_384;
|
|
51
|
+
exports.MAIL_SEARCH_TEXT_PROJECTION_VERSION = 2;
|
|
47
52
|
const cacheStates = new Map();
|
|
48
53
|
function cacheDir(agentId) {
|
|
49
54
|
return path.join((0, identity_1.getAgentRoot)(agentId), "state", "mail-search");
|
|
@@ -51,11 +56,29 @@ function cacheDir(agentId) {
|
|
|
51
56
|
function cachePath(agentId, messageId) {
|
|
52
57
|
return path.join(cacheDir(agentId), `${messageId}.json`);
|
|
53
58
|
}
|
|
59
|
+
function coverageDir(agentId) {
|
|
60
|
+
return path.join(cacheDir(agentId), "coverage");
|
|
61
|
+
}
|
|
62
|
+
function normalizedCoverageKey(key) {
|
|
63
|
+
return {
|
|
64
|
+
agentId: key.agentId,
|
|
65
|
+
storeKind: key.storeKind,
|
|
66
|
+
...(key.placement ? { placement: key.placement } : {}),
|
|
67
|
+
...(key.compartmentKind ? { compartmentKind: key.compartmentKind } : {}),
|
|
68
|
+
...(key.source ? { source: key.source.toLowerCase() } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function coveragePath(key) {
|
|
72
|
+
const normalized = normalizedCoverageKey(key);
|
|
73
|
+
const encoded = Buffer.from(JSON.stringify(normalized)).toString("base64url");
|
|
74
|
+
return path.join(coverageDir(normalized.agentId), `${encoded}.json`);
|
|
75
|
+
}
|
|
54
76
|
function normalizeSearchText(privateEnvelope) {
|
|
77
|
+
const readableText = (0, core_1.privateMailEnvelopeReadableText)(privateEnvelope);
|
|
55
78
|
return [
|
|
56
79
|
privateEnvelope.subject,
|
|
57
80
|
privateEnvelope.snippet,
|
|
58
|
-
|
|
81
|
+
readableText.slice(0, SEARCH_TEXT_EXCERPT_LIMIT),
|
|
59
82
|
privateEnvelope.from.join(" "),
|
|
60
83
|
].join("\n").toLowerCase();
|
|
61
84
|
}
|
|
@@ -95,6 +118,7 @@ function loadCache(agentId) {
|
|
|
95
118
|
return state.docs;
|
|
96
119
|
}
|
|
97
120
|
function buildMailSearchCacheDocument(message, privateEnvelope) {
|
|
121
|
+
const readableText = (0, core_1.privateMailEnvelopeReadableText)(privateEnvelope);
|
|
98
122
|
return {
|
|
99
123
|
schemaVersion: 1,
|
|
100
124
|
messageId: message.id,
|
|
@@ -107,9 +131,10 @@ function buildMailSearchCacheDocument(message, privateEnvelope) {
|
|
|
107
131
|
from: [...privateEnvelope.from],
|
|
108
132
|
subject: privateEnvelope.subject,
|
|
109
133
|
snippet: privateEnvelope.snippet,
|
|
110
|
-
textExcerpt:
|
|
134
|
+
textExcerpt: readableText.slice(0, SEARCH_TEXT_EXCERPT_LIMIT),
|
|
111
135
|
untrustedContentWarning: privateEnvelope.untrustedContentWarning,
|
|
112
136
|
searchText: normalizeSearchText(privateEnvelope),
|
|
137
|
+
textProjectionVersion: exports.MAIL_SEARCH_TEXT_PROJECTION_VERSION,
|
|
113
138
|
attachmentCount: privateEnvelope.attachments.length,
|
|
114
139
|
};
|
|
115
140
|
}
|
|
@@ -118,8 +143,9 @@ function upsertMailSearchCacheDocument(message, privateEnvelope) {
|
|
|
118
143
|
const dir = cacheDir(message.agentId);
|
|
119
144
|
fs.mkdirSync(dir, { recursive: true });
|
|
120
145
|
fs.writeFileSync(cachePath(message.agentId, message.id), `${JSON.stringify(document)}\n`, "utf-8");
|
|
121
|
-
const
|
|
122
|
-
|
|
146
|
+
const state = cacheState(message.agentId);
|
|
147
|
+
if (state.loaded)
|
|
148
|
+
state.docs.set(document.messageId, document);
|
|
123
149
|
(0, runtime_1.emitNervesEvent)({
|
|
124
150
|
component: "senses",
|
|
125
151
|
event: "senses.mail_search_cache_upserted",
|
|
@@ -146,8 +172,9 @@ function syncMailSearchCacheMetadata(message) {
|
|
|
146
172
|
...(message.source ? { source: message.source } : {}),
|
|
147
173
|
};
|
|
148
174
|
fs.writeFileSync(cachePath(message.agentId, message.id), `${JSON.stringify(updated)}\n`, "utf-8");
|
|
149
|
-
const
|
|
150
|
-
|
|
175
|
+
const state = cacheState(message.agentId);
|
|
176
|
+
if (state.loaded)
|
|
177
|
+
state.docs.set(updated.messageId, updated);
|
|
151
178
|
}
|
|
152
179
|
function sourceMatches(source, filter) {
|
|
153
180
|
if (!filter)
|
|
@@ -177,6 +204,49 @@ function searchMailSearchCache(filters) {
|
|
|
177
204
|
}
|
|
178
205
|
return typeof filters.limit === "number" ? ordered.slice(0, filters.limit) : ordered;
|
|
179
206
|
}
|
|
207
|
+
function readMailSearchCoverageRecord(key) {
|
|
208
|
+
const document = readJsonDocument(coveragePath(key));
|
|
209
|
+
if (!document || document.schemaVersion !== 1 || document.agentId !== key.agentId)
|
|
210
|
+
return null;
|
|
211
|
+
const normalized = normalizedCoverageKey(key);
|
|
212
|
+
const stored = normalizedCoverageKey(document);
|
|
213
|
+
if (JSON.stringify(stored) !== JSON.stringify(normalized))
|
|
214
|
+
return null;
|
|
215
|
+
return document;
|
|
216
|
+
}
|
|
217
|
+
function writeMailSearchCoverageRecord(record) {
|
|
218
|
+
const normalized = normalizedCoverageKey(record);
|
|
219
|
+
const document = {
|
|
220
|
+
schemaVersion: 1,
|
|
221
|
+
...normalized,
|
|
222
|
+
indexedAt: record.indexedAt,
|
|
223
|
+
visibleMessageCount: record.visibleMessageCount,
|
|
224
|
+
cachedMessageCount: record.cachedMessageCount,
|
|
225
|
+
decryptableMessageCount: record.decryptableMessageCount,
|
|
226
|
+
skippedMessageCount: record.skippedMessageCount,
|
|
227
|
+
...(record.messageIndexFingerprint ? { messageIndexFingerprint: record.messageIndexFingerprint } : {}),
|
|
228
|
+
textProjectionVersion: record.textProjectionVersion,
|
|
229
|
+
...(record.oldestReceivedAt ? { oldestReceivedAt: record.oldestReceivedAt } : {}),
|
|
230
|
+
...(record.newestReceivedAt ? { newestReceivedAt: record.newestReceivedAt } : {}),
|
|
231
|
+
};
|
|
232
|
+
fs.mkdirSync(coverageDir(document.agentId), { recursive: true });
|
|
233
|
+
fs.writeFileSync(coveragePath(document), `${JSON.stringify(document)}\n`, "utf-8");
|
|
234
|
+
(0, runtime_1.emitNervesEvent)({
|
|
235
|
+
component: "senses",
|
|
236
|
+
event: "senses.mail_search_coverage_written",
|
|
237
|
+
message: "mail search coverage record written",
|
|
238
|
+
meta: {
|
|
239
|
+
agentId: document.agentId,
|
|
240
|
+
placement: document.placement ?? null,
|
|
241
|
+
compartmentKind: document.compartmentKind ?? null,
|
|
242
|
+
source: document.source ?? null,
|
|
243
|
+
storeKind: document.storeKind,
|
|
244
|
+
visibleMessageCount: document.visibleMessageCount,
|
|
245
|
+
decryptableMessageCount: document.decryptableMessageCount,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
return document;
|
|
249
|
+
}
|
|
180
250
|
function resetMailSearchCacheForTests() {
|
|
181
251
|
cacheStates.clear();
|
|
182
252
|
}
|
package/dist/mind/prompt.js
CHANGED
|
@@ -472,6 +472,7 @@ function senseRuntimeGuidance(channel, preReadStatusLines) {
|
|
|
472
472
|
lines.push("mail setup truth: Agent Mail uses Mailroom, not HEY OAuth/IMAP. For the full work substrate account, the agent-runnable command is `ouro account ensure --agent <agent> --owner-email <email> --source hey`; use `ouro connect mail --agent <agent> --owner-email <email> --source hey` for mail-only repair/provisioning, or `--no-delegated-source` for native-only mail. The detailed runbook is `docs/agent-mail-setup.md`.");
|
|
473
473
|
lines.push("mail setup truth: HEY archive bootstrap still depends on HEY's browser-only export, but I should not offload path-discovery ceremony to the human. If browser MCP/Playwright downloaded the archive, I first try `ouro mail import-mbox --discover --owner-email <email> --source hey --agent <agent>` so Ouro can find the sandboxed copy in a repo/worktree `.playwright-mcp`, the home `.playwright-mcp`, or Downloads. If discovery cannot find a unique file, then I ask the human for the local MBOX path and run `ouro mail import-mbox --file <path> --owner-email <email> --source hey --agent <agent>` myself.");
|
|
474
474
|
lines.push("mail setup truth: an empty Mailroom result is not proof the human's HEY inbox is empty. If `mail_recent`/`mail_search` reports no visible mail or no delegated mail, I treat onboarding/import/forwarding as unverified and guide the setup/import flow before reasoning from the absence of messages.");
|
|
475
|
+
lines.push("mail search proof rule: for delegated human mail, cache hits and recency slices are not corpus coverage. Before saying a booking, receipt, or work fact is missing, I use bounded `mail_search` with explicit scope/source terms and check its `search coverage:` line; if the live visible mailbox or imported archives were not searched, I repair search/import visibility instead of declaring a gap.");
|
|
475
476
|
lines.push("mail validation answer shape: when a human asks for Agent Mail golden paths, answer with only these four named paths before claiming setup works:");
|
|
476
477
|
lines.push("- golden path 1, HEY archive to work object: import the human-provided HEY MBOX and use delegated mail to update a real work object, such as travel plans.");
|
|
477
478
|
lines.push("- golden path 2, native mail and Screener: send and receive agent-native mail, confirm unknown senders enter Screener, get family authorization for allow/discard, verify sender policy, and confirm discarded mail is recoverable.");
|
|
@@ -301,7 +301,7 @@ const CREDENTIAL_TRUSTED_TOOLS = new Set(["credential_get", "credential_list"]);
|
|
|
301
301
|
// advisory and geocode are public APIs but gated for consistency)
|
|
302
302
|
// Flight search is also friend+ (read-only, no payment)
|
|
303
303
|
const TRAVEL_TRUSTED_TOOLS = new Set(["weather_lookup", "travel_advisory", "geocode_search", "flight_search"]);
|
|
304
|
-
const MAIL_FAMILY_TOOLS = new Set(["mail_screener", "mail_decide", "mail_access_log", "mail_send"]);
|
|
304
|
+
const MAIL_FAMILY_TOOLS = new Set(["mail_screener", "mail_decide", "mail_access_log", "mail_send", "mail_index_refresh"]);
|
|
305
305
|
const MAIL_DELEGATED_READ_TOOLS = new Set(["mail_recent", "mail_search"]);
|
|
306
306
|
function mailTrustGuardrail(toolName, args, context) {
|
|
307
307
|
if (MAIL_FAMILY_TOOLS.has(toolName)) {
|