@ouro.bot/cli 0.1.0-alpha.476 → 0.1.0-alpha.478

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,22 @@
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.478",
6
+ "changes": [
7
+ "`ouro mail backfill-indexes` now refreshes vault runtime credentials before resolving the hosted Mailroom reader, so the one-shot index repair works in a fresh CLI process instead of falsely reporting `runtime/config` missing.",
8
+ "Backfill CLI coverage now locks both the runtime-refresh path and the real failure shape, preventing future hosted-mail repair releases from depending on warmed in-process config caches.",
9
+ "`@ouro.bot/cli` and the `ouro.bot` wrapper stay version-synced for the hosted mail index-repair follow-up."
10
+ ]
11
+ },
12
+ {
13
+ "version": "0.1.0-alpha.477",
14
+ "changes": [
15
+ "Hosted Mailroom reads now use per-agent message index blobs, so MCP mail tools and Outlook stop crawling the entire hosted mailbox just to load recent mail.",
16
+ "Legacy hosted mailboxes can rebuild missing message indexes without reimporting mail, and duplicate or placement-update paths now repair index drift automatically.",
17
+ "Mail source filtering is now case-insensitive across blob and file mail stores, which keeps delegated-source reads resilient to older mixed-case source labels while the runtime and `ouro.bot` wrapper stay version-synced for the hosted-mail read repair."
18
+ ]
19
+ },
4
20
  {
5
21
  "version": "0.1.0-alpha.476",
6
22
  "changes": [
@@ -3469,6 +3469,54 @@ async function executeMailImportMbox(command, deps) {
3469
3469
  throw error;
3470
3470
  }
3471
3471
  }
3472
+ async function executeMailBackfillIndexes(command, deps) {
3473
+ const progress = createHumanCommandProgress(deps, "mail index repair");
3474
+ try {
3475
+ progress.startPhase("resolving Mailroom store");
3476
+ const runtime = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(command.agent, { preserveCachedOnFailure: true });
3477
+ if (!runtime.ok) {
3478
+ progress.end();
3479
+ throw new Error(`cannot read Mailroom config from ${runtime.itemPath}: ${runtime.error}`);
3480
+ }
3481
+ const resolved = (0, reader_1.resolveMailroomReader)(command.agent);
3482
+ if (!resolved.ok) {
3483
+ progress.end();
3484
+ throw new Error(resolved.error);
3485
+ }
3486
+ if (resolved.storeKind !== "azure-blob") {
3487
+ progress.completePhase("resolving Mailroom store", "not needed");
3488
+ progress.end();
3489
+ const message = [
3490
+ `Hosted mail index backfill not needed for ${command.agent}`,
3491
+ `store: ${resolved.storeLabel}`,
3492
+ "This agent is using the local file Mailroom store, so recent mail reads do not depend on hosted blob indexes.",
3493
+ ].join("\n");
3494
+ deps.writeStdout(message);
3495
+ return message;
3496
+ }
3497
+ const store = resolved.store;
3498
+ if (typeof store.backfillMessageIndexes !== "function") {
3499
+ progress.end();
3500
+ throw new Error(`hosted Mailroom store for ${command.agent} does not expose index backfill`);
3501
+ }
3502
+ progress.updateDetail("backfilling hosted message indexes");
3503
+ const indexed = await store.backfillMessageIndexes(command.agent);
3504
+ progress.completePhase("resolving Mailroom store", "backfilled");
3505
+ progress.end();
3506
+ const message = [
3507
+ `Backfilled hosted mail indexes for ${command.agent}`,
3508
+ `store: ${resolved.storeLabel}`,
3509
+ `indexed: ${indexed}`,
3510
+ "Safe to rerun. Existing index entries are rewritten in place.",
3511
+ ].join("\n");
3512
+ deps.writeStdout(message);
3513
+ return message;
3514
+ }
3515
+ catch (error) {
3516
+ progress.end();
3517
+ throw error;
3518
+ }
3519
+ }
3472
3520
  async function executeConnectProviders(agent, deps) {
3473
3521
  const promptInput = deps.promptInput;
3474
3522
  if (!promptInput) {
@@ -4889,6 +4937,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
4889
4937
  if (command.kind === "mail.import-mbox") {
4890
4938
  return executeMailImportMbox(command, deps);
4891
4939
  }
4940
+ if (command.kind === "mail.backfill-indexes") {
4941
+ return executeMailBackfillIndexes(command, deps);
4942
+ }
4892
4943
  if (command.kind === "daemon.up") {
4893
4944
  // ── dev mode cleanup: delete dev-config.json so the wrapper stops dispatching to dev repo ──
4894
4945
  /* v8 ignore start -- dev-config cleanup: requires real filesystem state @preserve */
@@ -179,10 +179,10 @@ exports.COMMAND_REGISTRY = {
179
179
  },
180
180
  mail: {
181
181
  category: "Auth",
182
- description: "Import delegated mail into the agent Mailroom substrate",
183
- usage: "ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]",
182
+ description: "Import delegated mail and repair hosted Mailroom mailbox indexes",
183
+ usage: "ouro mail <import-mbox|backfill-indexes> [--agent <name>]",
184
184
  example: "ouro mail import-mbox --file ~/Downloads/hey.mbox --owner-email ari@mendelow.me --source hey --agent slugger",
185
- subcommands: ["import-mbox"],
185
+ subcommands: ["import-mbox", "backfill-indexes"],
186
186
  },
187
187
  use: {
188
188
  category: "Auth",
@@ -326,6 +326,11 @@ const SUBCOMMAND_HELP = {
326
326
  usage: "ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]",
327
327
  example: "ouro mail import-mbox --file ~/Downloads/hey.mbox --owner-email ari@mendelow.me --source hey --agent slugger",
328
328
  },
329
+ "mail backfill-indexes": {
330
+ description: "Rebuild hosted blob mailbox indexes for faster recent-mail reads after large legacy imports or drift repair.",
331
+ usage: "ouro mail backfill-indexes [--agent <name>]",
332
+ example: "ouro mail backfill-indexes --agent slugger",
333
+ },
329
334
  "provider refresh": {
330
335
  description: "Reload this agent's provider credentials from its vault into daemon memory",
331
336
  usage: "ouro provider refresh [--agent <name>]",
@@ -87,6 +87,7 @@ function usage() {
87
87
  " ouro account ensure [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
88
88
  " ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
89
89
  " ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]",
90
+ " ouro mail backfill-indexes [--agent <name>]",
90
91
  " ouro auth verify [--agent <name>] [--provider <provider>]",
91
92
  " ouro auth switch [--agent <name>] --provider <provider>",
92
93
  " ouro vault create [--agent <name>] --email <email> [--server <url>] [--store <store>]",
@@ -884,8 +885,18 @@ function parseConnectCommand(args) {
884
885
  }
885
886
  function parseMailCommand(args) {
886
887
  const [sub, ...subArgs] = args;
888
+ const usageText = "Usage: ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]\n ouro mail backfill-indexes [--agent <name>]";
889
+ if (sub === "backfill-indexes") {
890
+ const { agent, rest } = extractAgentFlag(subArgs);
891
+ if (rest.length > 0)
892
+ throw new Error(usageText);
893
+ return {
894
+ kind: "mail.backfill-indexes",
895
+ ...(agent ? { agent } : {}),
896
+ };
897
+ }
887
898
  if (sub !== "import-mbox") {
888
- throw new Error("Usage: ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]");
899
+ throw new Error(usageText);
889
900
  }
890
901
  const { agent, rest } = extractAgentFlag(subArgs);
891
902
  let filePath;
@@ -905,10 +916,10 @@ function parseMailCommand(args) {
905
916
  source = rest[++i];
906
917
  continue;
907
918
  }
908
- throw new Error("Usage: ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]");
919
+ throw new Error(usageText);
909
920
  }
910
921
  if (!filePath) {
911
- throw new Error("Usage: ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]");
922
+ throw new Error(usageText);
912
923
  }
913
924
  return {
914
925
  kind: "mail.import-mbox",
@@ -4,6 +4,12 @@ exports.AzureBlobMailroomStore = void 0;
4
4
  exports.decryptBlobMessages = decryptBlobMessages;
5
5
  const runtime_1 = require("../nerves/runtime");
6
6
  const core_1 = require("./core");
7
+ const MESSAGE_INDEX_PREFIX = "message-index";
8
+ const MESSAGE_INDEX_SORT_MAX_MS = 9_999_999_999_999;
9
+ const MESSAGE_INDEX_SORT_WIDTH = 13;
10
+ const MESSAGE_INDEX_NO_SOURCE = "~";
11
+ const MESSAGE_INDEX_BACKFILL_CONCURRENCY = 16;
12
+ const MESSAGE_LIST_SCAN_CONCURRENCY = 32;
7
13
  function compareNewestFirst(left, right) {
8
14
  return Date.parse(right.receivedAt) - Date.parse(left.receivedAt);
9
15
  }
@@ -18,6 +24,73 @@ async function downloadJson(blob) {
18
24
  return null;
19
25
  return JSON.parse((await blob.downloadToBuffer()).toString("utf-8"));
20
26
  }
27
+ function encodeSourceToken(source) {
28
+ return source ? encodeURIComponent(source.toLowerCase()) : MESSAGE_INDEX_NO_SOURCE;
29
+ }
30
+ function decodeSourceToken(token) {
31
+ return token === MESSAGE_INDEX_NO_SOURCE ? undefined : decodeURIComponent(token);
32
+ }
33
+ function parseSortMs(receivedAt) {
34
+ const parsed = Date.parse(receivedAt);
35
+ if (!Number.isFinite(parsed))
36
+ return 0;
37
+ return Math.max(0, Math.min(MESSAGE_INDEX_SORT_MAX_MS, parsed));
38
+ }
39
+ function messageIndexPrefix(agentId) {
40
+ return `${MESSAGE_INDEX_PREFIX}/${agentId}/`;
41
+ }
42
+ function messageIndexBlobName(message) {
43
+ const sortKey = String(MESSAGE_INDEX_SORT_MAX_MS - parseSortMs(message.receivedAt)).padStart(MESSAGE_INDEX_SORT_WIDTH, "0");
44
+ return `${messageIndexPrefix(message.agentId)}${sortKey}__${message.compartmentKind}__${message.placement}__${encodeSourceToken(message.source)}__${message.id}.json`;
45
+ }
46
+ function messageIndexRecord(message) {
47
+ return {
48
+ schemaVersion: 1,
49
+ id: message.id,
50
+ agentId: message.agentId,
51
+ compartmentKind: message.compartmentKind,
52
+ placement: message.placement,
53
+ ...(message.source ? { source: message.source } : {}),
54
+ receivedAt: message.receivedAt,
55
+ };
56
+ }
57
+ function parseMessageIndexBlobName(name) {
58
+ if (!name.startsWith(`${MESSAGE_INDEX_PREFIX}/`) || !name.endsWith(".json"))
59
+ return null;
60
+ const parts = name.split("/");
61
+ if (parts.length !== 3)
62
+ return null;
63
+ const agentId = parts[1];
64
+ const stem = parts[2].slice(0, -5);
65
+ const [sortKey, compartmentKind, placement, sourceToken, ...idParts] = stem.split("__");
66
+ if (!sortKey || !compartmentKind || !placement || !sourceToken || idParts.length === 0)
67
+ return null;
68
+ if (compartmentKind !== "native" && compartmentKind !== "delegated")
69
+ return null;
70
+ const receivedAtMs = MESSAGE_INDEX_SORT_MAX_MS - Number.parseInt(sortKey, 10);
71
+ return {
72
+ schemaVersion: 1,
73
+ id: idParts.join("__"),
74
+ agentId,
75
+ compartmentKind,
76
+ placement: placement,
77
+ ...(decodeSourceToken(sourceToken) ? { source: decodeSourceToken(sourceToken) } : {}),
78
+ receivedAt: Number.isFinite(receivedAtMs) ? new Date(receivedAtMs).toISOString() : new Date(0).toISOString(),
79
+ };
80
+ }
81
+ function sourceMatchesFilter(source, filter) {
82
+ if (!filter)
83
+ return true;
84
+ if (!source)
85
+ return false;
86
+ return source.toLowerCase() === filter.toLowerCase();
87
+ }
88
+ function messageMatchesFilters(message, filters) {
89
+ return message.agentId === filters.agentId &&
90
+ (filters.placement ? message.placement === filters.placement : true) &&
91
+ (filters.compartmentKind ? message.compartmentKind === filters.compartmentKind : true) &&
92
+ sourceMatchesFilter(message.source, filters.source);
93
+ }
21
94
  class AzureBlobMailroomStore {
22
95
  serviceClient;
23
96
  containerName;
@@ -44,6 +117,9 @@ class AzureBlobMailroomStore {
44
117
  messageBlob(id) {
45
118
  return this.container.getBlockBlobClient(`messages/${id}.json`);
46
119
  }
120
+ messageIndexBlob(name) {
121
+ return this.container.getBlockBlobClient(name);
122
+ }
47
123
  candidateBlob(id) {
48
124
  return this.container.getBlockBlobClient(`candidates/${id}.json`);
49
125
  }
@@ -59,11 +135,92 @@ class AzureBlobMailroomStore {
59
135
  outboundBlob(id) {
60
136
  return this.container.getBlockBlobClient(`outbound/${id}.json`);
61
137
  }
138
+ async putMessageIndex(message) {
139
+ await this.messageIndexBlob(messageIndexBlobName(message)).uploadData(blobText(messageIndexRecord(message)));
140
+ }
141
+ async removeMessageIndex(message) {
142
+ await this.messageIndexBlob(messageIndexBlobName(message)).deleteIfExists();
143
+ }
144
+ async listMessagesLegacy(filters) {
145
+ const messageBlobNames = [];
146
+ for await (const item of this.container.listBlobsFlat({ prefix: "messages/" })) {
147
+ messageBlobNames.push(item.name);
148
+ }
149
+ const matches = [];
150
+ const limit = filters.limit ?? 20;
151
+ let nextIndex = 0;
152
+ const worker = async () => {
153
+ while (nextIndex < messageBlobNames.length) {
154
+ const current = messageBlobNames[nextIndex];
155
+ nextIndex += 1;
156
+ const message = await downloadJson(this.container.getBlockBlobClient(current));
157
+ if (!message || !messageMatchesFilters(message, filters))
158
+ continue;
159
+ matches.push(message);
160
+ matches.sort(compareNewestFirst);
161
+ if (matches.length > limit)
162
+ matches.length = limit;
163
+ }
164
+ };
165
+ await Promise.all(Array.from({ length: Math.min(MESSAGE_LIST_SCAN_CONCURRENCY, Math.max(messageBlobNames.length, 1)) }, async () => worker()));
166
+ return matches.sort(compareNewestFirst).slice(0, limit);
167
+ }
168
+ async listMessagesFromIndexes(filters) {
169
+ const messageIds = [];
170
+ let sawIndex = false;
171
+ for await (const item of this.container.listBlobsFlat({ prefix: messageIndexPrefix(filters.agentId) })) {
172
+ sawIndex = true;
173
+ const parsed = parseMessageIndexBlobName(item.name);
174
+ if (!parsed || !messageMatchesFilters(parsed, filters))
175
+ continue;
176
+ messageIds.push(parsed.id);
177
+ if (messageIds.length >= (filters.limit ?? 20))
178
+ break;
179
+ }
180
+ if (!sawIndex)
181
+ return null;
182
+ return (await Promise.all(messageIds.map(async (id) => downloadJson(this.messageBlob(id)))))
183
+ .filter((message) => message !== null)
184
+ .filter((message) => messageMatchesFilters(message, filters))
185
+ .sort(compareNewestFirst)
186
+ .slice(0, filters.limit ?? 20);
187
+ }
188
+ async backfillMessageIndexes(agentId) {
189
+ await this.ensureContainer();
190
+ const messageBlobNames = [];
191
+ for await (const item of this.container.listBlobsFlat({ prefix: "messages/" })) {
192
+ messageBlobNames.push(item.name);
193
+ }
194
+ let indexed = 0;
195
+ let nextIndex = 0;
196
+ const worker = async () => {
197
+ while (nextIndex < messageBlobNames.length) {
198
+ const current = messageBlobNames[nextIndex];
199
+ nextIndex += 1;
200
+ const message = await downloadJson(this.container.getBlockBlobClient(current));
201
+ if (!message)
202
+ continue;
203
+ if (agentId && message.agentId !== agentId)
204
+ continue;
205
+ await this.messageIndexBlob(messageIndexBlobName(message)).uploadData(blobText(messageIndexRecord(message)));
206
+ indexed += 1;
207
+ }
208
+ };
209
+ await Promise.all(Array.from({ length: Math.min(MESSAGE_INDEX_BACKFILL_CONCURRENCY, Math.max(messageBlobNames.length, 1)) }, async () => worker()));
210
+ (0, runtime_1.emitNervesEvent)({
211
+ component: "senses",
212
+ event: "senses.mail_blob_index_backfilled",
213
+ message: "azure blob mailroom message indexes backfilled",
214
+ meta: { agentId: agentId ?? null, indexed },
215
+ });
216
+ return indexed;
217
+ }
62
218
  async putRawMessage(input) {
63
219
  await this.ensureContainer();
64
220
  const { message, rawPayload, candidate } = await (0, core_1.buildStoredMailMessage)(input);
65
221
  const existing = await downloadJson(this.messageBlob(message.id));
66
222
  if (existing) {
223
+ await this.putMessageIndex(existing);
67
224
  (0, runtime_1.emitNervesEvent)({
68
225
  component: "senses",
69
226
  event: "senses.mail_blob_store_dedupe",
@@ -74,6 +231,7 @@ class AzureBlobMailroomStore {
74
231
  }
75
232
  await this.rawBlob(message.rawObject).uploadData(blobText(rawPayload));
76
233
  await this.messageBlob(message.id).uploadData(blobText(message));
234
+ await this.putMessageIndex(message);
77
235
  if (candidate) {
78
236
  await this.candidateBlob(candidate.id).uploadData(blobText(candidate));
79
237
  }
@@ -98,24 +256,17 @@ class AzureBlobMailroomStore {
98
256
  }
99
257
  async listMessages(filters) {
100
258
  await this.ensureContainer();
101
- const messages = [];
102
- for await (const item of this.container.listBlobsFlat({ prefix: "messages/" })) {
103
- const message = await downloadJson(this.container.getBlockBlobClient(item.name));
104
- if (message)
105
- messages.push(message);
259
+ let filtered = await this.listMessagesFromIndexes(filters);
260
+ let source = "index";
261
+ if (filtered === null) {
262
+ filtered = await this.listMessagesLegacy(filters);
263
+ source = "legacy";
106
264
  }
107
- const filtered = messages
108
- .filter((message) => message.agentId === filters.agentId)
109
- .filter((message) => filters.placement ? message.placement === filters.placement : true)
110
- .filter((message) => filters.compartmentKind ? message.compartmentKind === filters.compartmentKind : true)
111
- .filter((message) => filters.source ? message.source === filters.source : true)
112
- .sort(compareNewestFirst)
113
- .slice(0, filters.limit ?? 20);
114
265
  (0, runtime_1.emitNervesEvent)({
115
266
  component: "senses",
116
267
  event: "senses.mail_blob_store_messages_listed",
117
268
  message: "azure blob mailroom store listed messages",
118
- meta: { agentId: filters.agentId, count: filtered.length },
269
+ meta: { agentId: filters.agentId, count: filtered.length, source },
119
270
  });
120
271
  return filtered;
121
272
  }
@@ -134,6 +285,8 @@ class AzureBlobMailroomStore {
134
285
  }
135
286
  const updated = { ...message, placement };
136
287
  await blob.uploadData(blobText(updated));
288
+ await this.removeMessageIndex(message);
289
+ await this.putMessageIndex(updated);
137
290
  (0, runtime_1.emitNervesEvent)({
138
291
  component: "senses",
139
292
  event: "senses.mail_blob_store_message_placement_updated",
@@ -61,6 +61,13 @@ function compareNewestFirst(left, right) {
61
61
  function compareCandidatesNewestFirst(left, right) {
62
62
  return Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt);
63
63
  }
64
+ function sourceMatchesFilter(source, filter) {
65
+ if (!filter)
66
+ return true;
67
+ if (!source)
68
+ return false;
69
+ return source.toLowerCase() === filter.toLowerCase();
70
+ }
64
71
  class FileMailroomStore {
65
72
  rootDir;
66
73
  constructor(options) {
@@ -157,7 +164,7 @@ class FileMailroomStore {
157
164
  .filter((message) => message.agentId === filters.agentId)
158
165
  .filter((message) => filters.placement ? message.placement === filters.placement : true)
159
166
  .filter((message) => filters.compartmentKind ? message.compartmentKind === filters.compartmentKind : true)
160
- .filter((message) => filters.source ? message.source === filters.source : true)
167
+ .filter((message) => sourceMatchesFilter(message.source, filters.source))
161
168
  .sort(compareNewestFirst)
162
169
  .slice(0, filters.limit ?? 20);
163
170
  (0, runtime_1.emitNervesEvent)({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.476",
3
+ "version": "0.1.0-alpha.478",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",