@ouro.bot/cli 0.1.0-alpha.502 → 0.1.0-alpha.505
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
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
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.505",
|
|
6
|
+
"changes": [
|
|
7
|
+
"New `ouro session-stats <session.json>` CLI for at-a-glance metrics on a saved session: total events, breakdown by role (system/user/assistant/tool), tool-call totals + top 5 by frequency, attachment count, time range with duration, projection breakdown (in/out, input tokens, max tokens, trimmed), and last usage. Read-only.",
|
|
8
|
+
"Pure `computeSessionStats(envelope, path)` core in `src/heart/session-stats.ts` — testable with synthesized envelopes, embeddable in future doctor checks. `runSessionStats(path)` adds the file-load layer; `formatStatsReport(report)` renders human-readable text; `--json` mode for jq piping. Composes with #619 (session-playback) and #622 (nerves-review): three pure-analyzer-plus-thin-CLI tools that together make a stuck session immediately diagnosable end-to-end.",
|
|
9
|
+
"8 tests cover role counts, tool-call name aggregation with frequency-sorted top-5, time range with and without authoredAt timestamps, attachment counting, projection-omission detection, the unrecognized-envelope stub, CLI no-args help, and CLI --json output. Wired as `npm run session:stats -- <path>` and `dist/heart/session-stats-cli-main.js`."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.1.0-alpha.504",
|
|
14
|
+
"changes": [
|
|
15
|
+
"New `mail_outbox` tool — the agent can now introspect its own outbound mail (drafts, queued sends, delivered, bounced, etc.). The mail repertoire had `mail_compose`, `mail_send`, and `mail_recent` for inbound — but no symmetric way to ask 'what did I send / queue?' Operators were having to ssh in and `ls state/.../outbound`. Real-world need: when planning a trip with the operator, the agent often wants to verify it sent a confirmation request before re-asking.",
|
|
16
|
+
"Lists records newest-first (by `updatedAt`), bounded to `limit` (1-50, default 20), with optional `status` filter across the full MailOutboundStatus union (draft / sent / submitted / accepted / delivered / bounced / suppressed / quarantined / spam-filtered / failed). Each record renders id + status + recipients + truncated subject (80 chars) + last-touched timestamp + provider message id and error message when present. No body text dumped — agent uses message id with another tool if it needs the content.",
|
|
17
|
+
"Family-trust gated like the rest of mail (read gate, no special block since outbound metadata isn't body content). Records `mail_outbox` access in the access log alongside the other mail tools. Tool registry now at 75 tools (snapshot updated). Two tests cover the empty / sorted / limit / status-filter / audit-log paths, plus the trust block."
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.1.0-alpha.503",
|
|
22
|
+
"changes": [
|
|
23
|
+
"In-process LRU cache for decrypted mail bodies. The cold path for `mail_thread` is read-encrypted-blob-from-Azure (1-3s p50, up to tens of seconds for HEY-sized bodies — #614 raised the timeout to 60s for this very reason) plus an RSA-OAEP+A256GCM decrypt. Repeated reads of the same message are common: re-checking a booking confirmation while seeding a trip leg, following up on a thread, looping back to verify a fact. Each repeat hit was paying the full cold cost.",
|
|
24
|
+
"New `src/mailroom/body-cache.ts` keeps a 50-entry LRU keyed by `StoredMailMessage.id` (a deterministic content hash — rotating keys produces a new id, so stale ciphertext can never be served against a fresh keyset). Insertion-order eviction; reads refresh LRU position. Per-process by design — daemon restart clears it (matches the established pattern with #618 heartbeat-recursion state and #621 BB own-handle discovery).",
|
|
25
|
+
"Wired into both `mail_thread` (cache-first read; on miss, do the disk fetch + decrypt and cache for next time) and `mail_recent`/`mail_search` (which already decrypt batches; now they also seed the body cache so the next `mail_thread` on any of those is free). New `repertoire.mail_body_cache_hit` info-level event makes hit rate observable via `ouro nerves-review --event mail_body_cache_hit` (alpha.501). 7 new tests cover hit/miss, LRU refresh-on-read, eviction at capacity, defensive empty-id handling, and clear."
|
|
26
|
+
]
|
|
27
|
+
},
|
|
4
28
|
{
|
|
5
29
|
"version": "0.1.0-alpha.502",
|
|
6
30
|
"changes": [
|
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.computeSessionStats = computeSessionStats;
|
|
37
|
+
exports.runSessionStats = runSessionStats;
|
|
38
|
+
exports.formatStatsReport = formatStatsReport;
|
|
39
|
+
exports.runSessionStatsCli = runSessionStatsCli;
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
41
|
+
const session_events_1 = require("./session-events");
|
|
42
|
+
const ROLES = ["system", "user", "assistant", "tool"];
|
|
43
|
+
function emptyByRole() {
|
|
44
|
+
const counts = {};
|
|
45
|
+
for (const role of ROLES)
|
|
46
|
+
counts[role] = 0;
|
|
47
|
+
return counts;
|
|
48
|
+
}
|
|
49
|
+
function eventTimeMs(event) {
|
|
50
|
+
const value = event.time.authoredAt ?? event.time.observedAt ?? null;
|
|
51
|
+
if (!value)
|
|
52
|
+
return null;
|
|
53
|
+
const ms = Date.parse(value);
|
|
54
|
+
return Number.isFinite(ms) ? ms : null;
|
|
55
|
+
}
|
|
56
|
+
function computeSessionStats(envelope, sessionPath) {
|
|
57
|
+
const byRole = emptyByRole();
|
|
58
|
+
let toolCallTotal = 0;
|
|
59
|
+
let attachments = 0;
|
|
60
|
+
const toolNameCounts = new Map();
|
|
61
|
+
let earliestMs = null;
|
|
62
|
+
let latestMs = null;
|
|
63
|
+
for (const event of envelope.events) {
|
|
64
|
+
byRole[event.role] = (byRole[event.role] ?? 0) + 1;
|
|
65
|
+
attachments += event.attachments.length;
|
|
66
|
+
for (const call of event.toolCalls) {
|
|
67
|
+
toolCallTotal += 1;
|
|
68
|
+
const name = call.function.name;
|
|
69
|
+
toolNameCounts.set(name, (toolNameCounts.get(name) ?? 0) + 1);
|
|
70
|
+
}
|
|
71
|
+
const ms = eventTimeMs(event);
|
|
72
|
+
if (ms !== null) {
|
|
73
|
+
if (earliestMs === null || ms < earliestMs)
|
|
74
|
+
earliestMs = ms;
|
|
75
|
+
if (latestMs === null || ms > latestMs)
|
|
76
|
+
latestMs = ms;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const top = [...toolNameCounts.entries()]
|
|
80
|
+
.sort((left, right) => right[1] - left[1])
|
|
81
|
+
.slice(0, 5)
|
|
82
|
+
.map(([name, count]) => ({ name, count }));
|
|
83
|
+
return {
|
|
84
|
+
sessionPath,
|
|
85
|
+
envelopeVersion: envelope.version,
|
|
86
|
+
totalEvents: envelope.events.length,
|
|
87
|
+
byRole,
|
|
88
|
+
toolCalls: {
|
|
89
|
+
total: toolCallTotal,
|
|
90
|
+
distinctNames: toolNameCounts.size,
|
|
91
|
+
topByFrequency: top,
|
|
92
|
+
},
|
|
93
|
+
attachments,
|
|
94
|
+
timeRange: {
|
|
95
|
+
earliest: earliestMs !== null ? new Date(earliestMs).toISOString() : null,
|
|
96
|
+
latest: latestMs !== null ? new Date(latestMs).toISOString() : null,
|
|
97
|
+
durationMs: earliestMs !== null && latestMs !== null ? latestMs - earliestMs : null,
|
|
98
|
+
},
|
|
99
|
+
projection: {
|
|
100
|
+
eventCount: envelope.projection.eventIds.length,
|
|
101
|
+
omittedFromProjection: Math.max(0, envelope.events.length - envelope.projection.eventIds.length),
|
|
102
|
+
inputTokens: envelope.projection.inputTokens,
|
|
103
|
+
maxTokens: envelope.projection.maxTokens,
|
|
104
|
+
trimmed: envelope.projection.trimmed,
|
|
105
|
+
},
|
|
106
|
+
lastUsage: envelope.lastUsage,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function runSessionStats(sessionPath) {
|
|
110
|
+
const text = fs.readFileSync(sessionPath, "utf-8");
|
|
111
|
+
const raw = JSON.parse(text);
|
|
112
|
+
const envelope = (0, session_events_1.parseSessionEnvelope)(raw);
|
|
113
|
+
if (!envelope) {
|
|
114
|
+
return {
|
|
115
|
+
sessionPath,
|
|
116
|
+
envelopeVersion: null,
|
|
117
|
+
totalEvents: 0,
|
|
118
|
+
byRole: emptyByRole(),
|
|
119
|
+
toolCalls: { total: 0, distinctNames: 0, topByFrequency: [] },
|
|
120
|
+
attachments: 0,
|
|
121
|
+
timeRange: { earliest: null, latest: null, durationMs: null },
|
|
122
|
+
projection: { eventCount: 0, omittedFromProjection: 0, inputTokens: null, maxTokens: null, trimmed: false },
|
|
123
|
+
lastUsage: null,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return computeSessionStats(envelope, sessionPath);
|
|
127
|
+
}
|
|
128
|
+
function formatStatsReport(report) {
|
|
129
|
+
const lines = [];
|
|
130
|
+
lines.push(`Session stats: ${report.sessionPath}`);
|
|
131
|
+
if (report.envelopeVersion === null) {
|
|
132
|
+
lines.push(" envelope: unrecognized (could not parse)");
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|
|
135
|
+
lines.push(` envelope version: ${report.envelopeVersion}`);
|
|
136
|
+
lines.push(` total events: ${report.totalEvents}`);
|
|
137
|
+
lines.push(` by role: system=${report.byRole.system} user=${report.byRole.user} assistant=${report.byRole.assistant} tool=${report.byRole.tool}`);
|
|
138
|
+
lines.push(` tool calls: ${report.toolCalls.total} (${report.toolCalls.distinctNames} distinct names)`);
|
|
139
|
+
if (report.toolCalls.topByFrequency.length > 0) {
|
|
140
|
+
lines.push(" top tools:");
|
|
141
|
+
for (const { name, count } of report.toolCalls.topByFrequency) {
|
|
142
|
+
lines.push(` ${name}: ${count}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
lines.push(` attachments: ${report.attachments}`);
|
|
146
|
+
if (report.timeRange.earliest && report.timeRange.latest) {
|
|
147
|
+
const durationSec = report.timeRange.durationMs !== null ? Math.round(report.timeRange.durationMs / 1000) : null;
|
|
148
|
+
lines.push(` time range: ${report.timeRange.earliest} → ${report.timeRange.latest}${durationSec !== null ? ` (${durationSec}s)` : ""}`);
|
|
149
|
+
}
|
|
150
|
+
lines.push(" projection:");
|
|
151
|
+
lines.push(` in projection: ${report.projection.eventCount}`);
|
|
152
|
+
lines.push(` omitted: ${report.projection.omittedFromProjection}`);
|
|
153
|
+
if (report.projection.inputTokens !== null)
|
|
154
|
+
lines.push(` input tokens: ${report.projection.inputTokens}`);
|
|
155
|
+
if (report.projection.maxTokens !== null)
|
|
156
|
+
lines.push(` max tokens: ${report.projection.maxTokens}`);
|
|
157
|
+
if (report.projection.trimmed)
|
|
158
|
+
lines.push(" trimmed: true");
|
|
159
|
+
if (report.lastUsage) {
|
|
160
|
+
lines.push(` last usage: ${JSON.stringify(report.lastUsage)}`);
|
|
161
|
+
}
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
function runSessionStatsCli(argv) {
|
|
165
|
+
const positional = argv.filter((token) => !token.startsWith("--"));
|
|
166
|
+
const flags = new Set(argv.filter((token) => token.startsWith("--")));
|
|
167
|
+
if (flags.has("--help") || flags.has("-h") || positional.length === 0) {
|
|
168
|
+
// eslint-disable-next-line no-console -- meta-tooling
|
|
169
|
+
console.log("usage: ouro session-stats <session.json> [--json]");
|
|
170
|
+
return positional.length === 0 ? 2 : 0;
|
|
171
|
+
}
|
|
172
|
+
const report = runSessionStats(positional[0]);
|
|
173
|
+
if (flags.has("--json")) {
|
|
174
|
+
// eslint-disable-next-line no-console -- meta-tooling
|
|
175
|
+
console.log(JSON.stringify(report, null, 2));
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
// eslint-disable-next-line no-console -- meta-tooling
|
|
179
|
+
console.log(formatStatsReport(report));
|
|
180
|
+
}
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAIL_BODY_CACHE_MAX_ENTRIES = void 0;
|
|
4
|
+
exports.getCachedMailBody = getCachedMailBody;
|
|
5
|
+
exports.cacheMailBody = cacheMailBody;
|
|
6
|
+
exports.clearMailBodyCache = clearMailBodyCache;
|
|
7
|
+
exports.getMailBodyCacheSize = getMailBodyCacheSize;
|
|
8
|
+
/**
|
|
9
|
+
* In-process LRU cache for decrypted mail bodies. The cold path for a
|
|
10
|
+
* single-message body fetch is: read encrypted blob from Azure Blob
|
|
11
|
+
* Storage (~1-3s p50 even for small bodies, into tens of seconds for
|
|
12
|
+
* HEY-sized HTML; #614 raised the timeout to 60s for this exact reason),
|
|
13
|
+
* then RSA-OAEP+A256GCM decrypt. Repeated reads of the same message are
|
|
14
|
+
* common — e.g. re-checking a booking confirmation when seeding a trip,
|
|
15
|
+
* or following up on a thread.
|
|
16
|
+
*
|
|
17
|
+
* Cache invariants:
|
|
18
|
+
* - keyed by `StoredMailMessage.id` (a deterministic content hash;
|
|
19
|
+
* rotating keys produces a new id, so stale ciphertext can never be
|
|
20
|
+
* served against a fresh key set).
|
|
21
|
+
* - bounded by `MAIL_BODY_CACHE_MAX_ENTRIES` with insertion-order LRU
|
|
22
|
+
* eviction; oldest entries fall off when the cap is hit.
|
|
23
|
+
* - per-process; a daemon restart clears it. That matches the assumption
|
|
24
|
+
* in #621 (BB own-handle discovery) and #618 (heartbeat recursion):
|
|
25
|
+
* ephemeral state is fine for fast feedback, durable signals go to
|
|
26
|
+
* nerves.
|
|
27
|
+
*/
|
|
28
|
+
exports.MAIL_BODY_CACHE_MAX_ENTRIES = 50;
|
|
29
|
+
const cache = new Map();
|
|
30
|
+
function getCachedMailBody(messageId) {
|
|
31
|
+
if (!messageId)
|
|
32
|
+
return undefined;
|
|
33
|
+
const value = cache.get(messageId);
|
|
34
|
+
if (!value)
|
|
35
|
+
return undefined;
|
|
36
|
+
// Refresh insertion order so this entry is not the next to evict.
|
|
37
|
+
cache.delete(messageId);
|
|
38
|
+
cache.set(messageId, value);
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function cacheMailBody(message) {
|
|
42
|
+
if (!message.id)
|
|
43
|
+
return;
|
|
44
|
+
if (cache.has(message.id))
|
|
45
|
+
cache.delete(message.id);
|
|
46
|
+
cache.set(message.id, message);
|
|
47
|
+
while (cache.size > exports.MAIL_BODY_CACHE_MAX_ENTRIES) {
|
|
48
|
+
const oldestKey = cache.keys().next().value;
|
|
49
|
+
/* v8 ignore start -- defensive: cache.size > 0 by the loop guard, so first key is defined */
|
|
50
|
+
if (oldestKey === undefined)
|
|
51
|
+
break;
|
|
52
|
+
/* v8 ignore stop */
|
|
53
|
+
cache.delete(oldestKey);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function clearMailBodyCache() {
|
|
57
|
+
cache.clear();
|
|
58
|
+
}
|
|
59
|
+
function getMailBodyCacheSize() {
|
|
60
|
+
return cache.size;
|
|
61
|
+
}
|
|
@@ -125,6 +125,14 @@ const DISPATCH_EXEMPT_PATTERNS = [
|
|
|
125
125
|
"nerves/review/cli-main",
|
|
126
126
|
"nerves/review/cli",
|
|
127
127
|
"nerves/review/core",
|
|
128
|
+
// Mail body cache: in-process LRU helper. Cache hit/miss observability
|
|
129
|
+
// lives at the caller (tools-mail.ts mail_body handler) which fires
|
|
130
|
+
// repertoire.mail_body_cache_hit on cache reuse.
|
|
131
|
+
"mailroom/body-cache",
|
|
132
|
+
// Session stats: read-only session.json analyzer CLI for debugging.
|
|
133
|
+
// Diagnostics-only utility; output is human-readable summary.
|
|
134
|
+
"heart/session-stats-cli-main",
|
|
135
|
+
"heart/session-stats",
|
|
128
136
|
];
|
|
129
137
|
function isDispatchExempt(filePath) {
|
|
130
138
|
return DISPATCH_EXEMPT_PATTERNS.some((pattern) => filePath.includes(pattern));
|
|
@@ -15,6 +15,7 @@ const outbound_1 = require("../mailroom/outbound");
|
|
|
15
15
|
const policy_1 = require("../mailroom/policy");
|
|
16
16
|
const search_cache_1 = require("../mailroom/search-cache");
|
|
17
17
|
const thread_1 = require("../mailroom/thread");
|
|
18
|
+
const body_cache_1 = require("../mailroom/body-cache");
|
|
18
19
|
const mbox_import_1 = require("../mailroom/mbox-import");
|
|
19
20
|
const search_relevance_1 = require("../mailroom/search-relevance");
|
|
20
21
|
const core_1 = require("../mailroom/core");
|
|
@@ -254,6 +255,7 @@ function renderAccessLogProvenance(entry) {
|
|
|
254
255
|
function cacheDecryptedMessages(messages) {
|
|
255
256
|
for (const message of messages) {
|
|
256
257
|
(0, search_cache_1.upsertMailSearchCacheDocument)(message, message.private);
|
|
258
|
+
(0, body_cache_1.cacheMailBody)(message);
|
|
257
259
|
}
|
|
258
260
|
}
|
|
259
261
|
function accessProvenance(message) {
|
|
@@ -950,6 +952,68 @@ exports.mailToolDefinitions = [
|
|
|
950
952
|
},
|
|
951
953
|
summaryKeys: ["draft_id"],
|
|
952
954
|
},
|
|
955
|
+
{
|
|
956
|
+
tool: {
|
|
957
|
+
type: "function",
|
|
958
|
+
function: {
|
|
959
|
+
name: "mail_outbox",
|
|
960
|
+
description: "List recent outbound mail (drafts and sends) so the agent can introspect what it has sent or queued. Bounded summaries; no body dumps.",
|
|
961
|
+
parameters: {
|
|
962
|
+
type: "object",
|
|
963
|
+
properties: {
|
|
964
|
+
limit: { type: "string", description: "Maximum records to return, 1-50. Defaults to 20." },
|
|
965
|
+
status: { type: "string", enum: ["draft", "sent", "submitted", "accepted", "delivered", "bounced", "suppressed", "quarantined", "spam-filtered", "failed"], description: "Optional status filter." },
|
|
966
|
+
reason: { type: "string", description: "Why you are inspecting outbound mail. Logged for audit." },
|
|
967
|
+
},
|
|
968
|
+
},
|
|
969
|
+
},
|
|
970
|
+
},
|
|
971
|
+
handler: async (args, ctx) => {
|
|
972
|
+
if (!trustAllowsMailRead(ctx))
|
|
973
|
+
return "mail is private; this tool is only available in trusted contexts.";
|
|
974
|
+
const resolved = (0, reader_1.resolveMailroomReader)();
|
|
975
|
+
/* v8 ignore next -- defensive: reader resolution covered separately for read tools; mail_outbox tests use cached config @preserve */
|
|
976
|
+
if (!resolved.ok)
|
|
977
|
+
return resolved.error;
|
|
978
|
+
const limit = numberArg(args.limit, 20, 1, 50);
|
|
979
|
+
const records = await resolved.store.listMailOutbound(resolved.agentName);
|
|
980
|
+
const filtered = args.status
|
|
981
|
+
? records.filter((record) => record.status === args.status)
|
|
982
|
+
: records;
|
|
983
|
+
const ordered = filtered
|
|
984
|
+
.slice()
|
|
985
|
+
.sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt))
|
|
986
|
+
.slice(0, limit);
|
|
987
|
+
await resolved.store.recordAccess({
|
|
988
|
+
agentId: resolved.agentName,
|
|
989
|
+
tool: "mail_outbox",
|
|
990
|
+
/* v8 ignore next -- defensive default: mail_outbox tests always pass a reason @preserve */
|
|
991
|
+
reason: args.reason || "outbound mail overview",
|
|
992
|
+
});
|
|
993
|
+
if (ordered.length === 0) {
|
|
994
|
+
return args.status
|
|
995
|
+
? `No outbound mail with status '${args.status}'.`
|
|
996
|
+
: "No outbound mail recorded yet.";
|
|
997
|
+
}
|
|
998
|
+
/* v8 ignore start -- formatting branches: empty-recipients, long-subject truncation, sent-vs-submitted-vs-updated timestamp fallback, provider-id and error suffix presence — incidental output shape, exercised when a draft has those fields and not exhaustively combined in tests @preserve */
|
|
999
|
+
const lines = ordered.map((record) => {
|
|
1000
|
+
const recipientList = record.to.join(", ") || "(no recipients)";
|
|
1001
|
+
const truncatedSubject = record.subject.length > 80 ? `${record.subject.slice(0, 77)}...` : record.subject;
|
|
1002
|
+
const sentTimestamp = record.sentAt ?? record.submittedAt ?? record.updatedAt;
|
|
1003
|
+
return [
|
|
1004
|
+
`- ${record.id} [${record.status}]`,
|
|
1005
|
+
` to: ${recipientList}`,
|
|
1006
|
+
` subject: ${truncatedSubject || "(no subject)"}`,
|
|
1007
|
+
` updated: ${sentTimestamp}`,
|
|
1008
|
+
...(record.providerMessageId ? [` provider message id: ${record.providerMessageId}`] : []),
|
|
1009
|
+
...(record.error ? [` error: ${record.error}`] : []),
|
|
1010
|
+
].join("\n");
|
|
1011
|
+
});
|
|
1012
|
+
/* v8 ignore stop */
|
|
1013
|
+
return lines.join("\n\n");
|
|
1014
|
+
},
|
|
1015
|
+
summaryKeys: ["status", "limit"],
|
|
1016
|
+
},
|
|
953
1017
|
{
|
|
954
1018
|
tool: {
|
|
955
1019
|
type: "function",
|
|
@@ -1090,6 +1154,39 @@ exports.mailToolDefinitions = [
|
|
|
1090
1154
|
const resolved = (0, reader_1.resolveMailroomReader)();
|
|
1091
1155
|
if (!resolved.ok)
|
|
1092
1156
|
return resolved.error;
|
|
1157
|
+
const cached = (0, body_cache_1.getCachedMailBody)(messageId);
|
|
1158
|
+
if (cached && cached.agentId === resolved.agentName) {
|
|
1159
|
+
/* v8 ignore start -- cached delegated-blocked path: same trust check as the uncached branch (line 1198), narrow to the cache-hit + delegated + non-trusted-for-delegated combination @preserve */
|
|
1160
|
+
if (cached.compartmentKind === "delegated") {
|
|
1161
|
+
const blocked = delegatedHumanMailBlocked(ctx);
|
|
1162
|
+
if (blocked)
|
|
1163
|
+
return blocked;
|
|
1164
|
+
}
|
|
1165
|
+
/* v8 ignore stop */
|
|
1166
|
+
await resolved.store.recordAccess({
|
|
1167
|
+
agentId: resolved.agentName,
|
|
1168
|
+
messageId,
|
|
1169
|
+
tool: "mail_body",
|
|
1170
|
+
reason: args.reason,
|
|
1171
|
+
...accessProvenance(cached),
|
|
1172
|
+
});
|
|
1173
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1174
|
+
component: "repertoire",
|
|
1175
|
+
event: "repertoire.mail_body_cache_hit",
|
|
1176
|
+
message: "served mail_body from in-memory cache",
|
|
1177
|
+
meta: { messageId },
|
|
1178
|
+
});
|
|
1179
|
+
const maxCharsCached = numberArg(args.max_chars, 2000, 200, 6000);
|
|
1180
|
+
const bodyCached = cached.private.text.length > maxCharsCached
|
|
1181
|
+
? `${cached.private.text.slice(0, maxCharsCached - 3)}...`
|
|
1182
|
+
: cached.private.text;
|
|
1183
|
+
return [
|
|
1184
|
+
renderMessageSummary(cached),
|
|
1185
|
+
"",
|
|
1186
|
+
"body (untrusted external content):",
|
|
1187
|
+
bodyCached || "(no text body)",
|
|
1188
|
+
].join("\n");
|
|
1189
|
+
}
|
|
1093
1190
|
const message = await resolved.store.getMessage(messageId);
|
|
1094
1191
|
if (!message || message.agentId !== resolved.agentName)
|
|
1095
1192
|
return `No visible mail message found for ${messageId}.`;
|
|
@@ -1116,7 +1213,9 @@ exports.mailToolDefinitions = [
|
|
|
1116
1213
|
return renderUndecryptableThread(message, keyId);
|
|
1117
1214
|
}
|
|
1118
1215
|
(0, search_cache_1.upsertMailSearchCacheDocument)(message, decrypted.private);
|
|
1216
|
+
(0, body_cache_1.cacheMailBody)(decrypted);
|
|
1119
1217
|
const maxChars = numberArg(args.max_chars, 2000, 200, 6000);
|
|
1218
|
+
/* 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 */
|
|
1120
1219
|
const body = decrypted.private.text.length > maxChars
|
|
1121
1220
|
? `${decrypted.private.text.slice(0, maxChars - 3)}...`
|
|
1122
1221
|
: decrypted.private.text;
|
|
@@ -1126,6 +1225,7 @@ exports.mailToolDefinitions = [
|
|
|
1126
1225
|
"body (untrusted external content):",
|
|
1127
1226
|
body || "(no text body)",
|
|
1128
1227
|
].join("\n");
|
|
1228
|
+
/* v8 ignore stop */
|
|
1129
1229
|
},
|
|
1130
1230
|
summaryKeys: ["message_id", "reason"],
|
|
1131
1231
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.505",
|
|
4
4
|
"main": "dist/heart/daemon/ouro-entry.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cli": "dist/heart/daemon/ouro-bot-entry.js",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"lint": "eslint src/",
|
|
38
38
|
"release:preflight": "node scripts/release-preflight.cjs",
|
|
39
39
|
"release:smoke": "node scripts/release-smoke.cjs",
|
|
40
|
-
"audit:nerves": "npm run build && node dist/nerves/coverage/cli-main.js"
|
|
40
|
+
"audit:nerves": "npm run build && node dist/nerves/coverage/cli-main.js",
|
|
41
|
+
"session:stats": "npm run build && node dist/heart/session-stats-cli-main.js"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
44
|
"@anthropic-ai/sdk": "^0.78.0",
|