@ouro.bot/cli 0.1.0-alpha.479 → 0.1.0-alpha.480

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,14 @@
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.480",
6
+ "changes": [
7
+ "`ouro mail import-mbox` and `ouro mail backfill-indexes` now launch as durable background operations by default, so large delegated-mail imports and hosted index repairs no longer block ordinary conversation.",
8
+ "Mail background jobs now persist queued/running/succeeded/failed state with progress, show up in `query_active_work`, and wake the agent immediately on completion or failure with actionable remediation context.",
9
+ "The background-operation runtime and CLI lifecycle are covered back to a restored 100% statements/branches/functions/lines gate, including detached background spawn defaults, malformed persisted records, and non-Error failure paths."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.479",
6
14
  "changes": [
@@ -513,6 +513,7 @@ function buildActiveWorkFrame(input) {
513
513
  allOtherLiveSessions,
514
514
  },
515
515
  codingSessions: liveCodingSessions,
516
+ backgroundOperations: input.backgroundOperations ?? [],
516
517
  otherCodingSessions,
517
518
  pendingObligations,
518
519
  targetCandidates: input.targetCandidates ?? [],
@@ -640,6 +641,30 @@ function formatActiveWorkFrame(frame, options) {
640
641
  /* v8 ignore stop */
641
642
  }
642
643
  }
644
+ const backgroundOperations = frame.backgroundOperations ?? [];
645
+ if (backgroundOperations.length > 0) {
646
+ lines.push("");
647
+ lines.push("## background operations");
648
+ for (const operation of backgroundOperations) {
649
+ let line = `- [${operation.status}] ${operation.title}`;
650
+ if (operation.summary.trim().length > 0) {
651
+ line += `: ${operation.summary}`;
652
+ }
653
+ if (operation.detail?.trim()) {
654
+ line += `\n ${operation.detail.trim()}`;
655
+ }
656
+ if (operation.progress) {
657
+ const current = typeof operation.progress.current === "number" ? String(operation.progress.current) : "?";
658
+ const total = typeof operation.progress.total === "number" ? String(operation.progress.total) : null;
659
+ const unit = operation.progress.unit?.trim() ? ` ${operation.progress.unit.trim()}` : "";
660
+ line += `\n progress: ${total ? `${current}/${total}` : current}${unit}`;
661
+ }
662
+ if (operation.error?.message?.trim()) {
663
+ line += `\n error: ${operation.error.message.trim()}`;
664
+ }
665
+ lines.push(line);
666
+ }
667
+ }
643
668
  if (otherActiveSessions.length > 0) {
644
669
  lines.push("");
645
670
  lines.push("## other active sessions");
@@ -0,0 +1,234 @@
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.readBackgroundOperation = readBackgroundOperation;
37
+ exports.listBackgroundOperations = listBackgroundOperations;
38
+ exports.startBackgroundOperation = startBackgroundOperation;
39
+ exports.markBackgroundOperationRunning = markBackgroundOperationRunning;
40
+ exports.updateBackgroundOperation = updateBackgroundOperation;
41
+ exports.completeBackgroundOperation = completeBackgroundOperation;
42
+ exports.failBackgroundOperation = failBackgroundOperation;
43
+ const fs = __importStar(require("node:fs"));
44
+ const path = __importStar(require("node:path"));
45
+ const runtime_1 = require("../nerves/runtime");
46
+ const identity_1 = require("./identity");
47
+ function operationsDir(agentName, agentRoot = (0, identity_1.getAgentRoot)(agentName)) {
48
+ return path.join(agentRoot, "state", "background-operations");
49
+ }
50
+ function operationPath(locator) {
51
+ return path.join(operationsDir(locator.agentName, locator.agentRoot), `${locator.id}.json`);
52
+ }
53
+ function normalizeProgress(progress) {
54
+ if (!progress)
55
+ return undefined;
56
+ const normalized = {};
57
+ if (typeof progress.current === "number" && Number.isFinite(progress.current))
58
+ normalized.current = progress.current;
59
+ if (typeof progress.total === "number" && Number.isFinite(progress.total))
60
+ normalized.total = progress.total;
61
+ if (typeof progress.unit === "string" && progress.unit.trim().length > 0)
62
+ normalized.unit = progress.unit.trim();
63
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
64
+ }
65
+ function normalizeRecord(record) {
66
+ if (record.schemaVersion !== 1)
67
+ return null;
68
+ if (typeof record.id !== "string" || record.id.trim().length === 0)
69
+ return null;
70
+ if (typeof record.agentName !== "string" || record.agentName.trim().length === 0)
71
+ return null;
72
+ if (typeof record.kind !== "string" || record.kind.trim().length === 0)
73
+ return null;
74
+ if (typeof record.title !== "string" || record.title.trim().length === 0)
75
+ return null;
76
+ if (typeof record.summary !== "string" || record.summary.trim().length === 0)
77
+ return null;
78
+ if (typeof record.createdAt !== "string" || record.createdAt.trim().length === 0)
79
+ return null;
80
+ if (typeof record.updatedAt !== "string" || record.updatedAt.trim().length === 0)
81
+ return null;
82
+ if (record.status !== "queued"
83
+ && record.status !== "running"
84
+ && record.status !== "succeeded"
85
+ && record.status !== "failed") {
86
+ return null;
87
+ }
88
+ const normalized = {
89
+ schemaVersion: 1,
90
+ id: record.id,
91
+ agentName: record.agentName,
92
+ kind: record.kind,
93
+ title: record.title,
94
+ status: record.status,
95
+ summary: record.summary,
96
+ createdAt: record.createdAt,
97
+ updatedAt: record.updatedAt,
98
+ };
99
+ if (typeof record.detail === "string" && record.detail.trim().length > 0)
100
+ normalized.detail = record.detail.trim();
101
+ if (typeof record.startedAt === "string" && record.startedAt.trim().length > 0)
102
+ normalized.startedAt = record.startedAt;
103
+ if (typeof record.finishedAt === "string" && record.finishedAt.trim().length > 0)
104
+ normalized.finishedAt = record.finishedAt;
105
+ const progress = normalizeProgress(record.progress);
106
+ if (progress)
107
+ normalized.progress = progress;
108
+ if (record.spec && typeof record.spec === "object" && !Array.isArray(record.spec))
109
+ normalized.spec = { ...record.spec };
110
+ if (record.result && typeof record.result === "object" && !Array.isArray(record.result))
111
+ normalized.result = { ...record.result };
112
+ if (record.error && typeof record.error.message === "string" && record.error.message.trim().length > 0) {
113
+ normalized.error = { message: record.error.message.trim() };
114
+ }
115
+ if (Array.isArray(record.remediation)) {
116
+ const remediation = record.remediation.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
117
+ if (remediation.length > 0)
118
+ normalized.remediation = remediation;
119
+ }
120
+ return normalized;
121
+ }
122
+ function readRecord(filePath) {
123
+ try {
124
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
125
+ return normalizeRecord(parsed);
126
+ }
127
+ catch {
128
+ return null;
129
+ }
130
+ }
131
+ function writeRecord(locator, record) {
132
+ fs.mkdirSync(operationsDir(locator.agentName, locator.agentRoot), { recursive: true });
133
+ fs.writeFileSync(operationPath(locator), `${JSON.stringify(record, null, 2)}\n`, "utf-8");
134
+ (0, runtime_1.emitNervesEvent)({
135
+ component: "engine",
136
+ event: "engine.background_operation_written",
137
+ message: "background operation state written",
138
+ meta: { agentName: locator.agentName, id: locator.id, kind: record.kind, status: record.status },
139
+ });
140
+ return record;
141
+ }
142
+ function requireRecord(locator) {
143
+ const record = readBackgroundOperation(locator);
144
+ if (!record) {
145
+ throw new Error(`background operation not found: ${locator.id}`);
146
+ }
147
+ return record;
148
+ }
149
+ function readBackgroundOperation(locator) {
150
+ return readRecord(operationPath(locator));
151
+ }
152
+ function listBackgroundOperations(input) {
153
+ const dir = operationsDir(input.agentName, input.agentRoot);
154
+ if (!fs.existsSync(dir))
155
+ return [];
156
+ let entries;
157
+ try {
158
+ entries = fs.readdirSync(dir);
159
+ }
160
+ catch {
161
+ return [];
162
+ }
163
+ return entries
164
+ .filter((entry) => entry.endsWith(".json"))
165
+ .map((entry) => readRecord(path.join(dir, entry)))
166
+ .filter((entry) => entry !== null)
167
+ .sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt))
168
+ .slice(0, input.limit ?? 10);
169
+ }
170
+ function startBackgroundOperation(input) {
171
+ return writeRecord(input, {
172
+ schemaVersion: 1,
173
+ id: input.id,
174
+ agentName: input.agentName,
175
+ kind: input.kind,
176
+ title: input.title,
177
+ status: "queued",
178
+ summary: input.summary,
179
+ createdAt: input.createdAt,
180
+ updatedAt: input.createdAt,
181
+ ...(input.spec ? { spec: input.spec } : {}),
182
+ });
183
+ }
184
+ function markBackgroundOperationRunning(input) {
185
+ const current = requireRecord(input);
186
+ const updatedAt = input.updatedAt ?? input.startedAt;
187
+ return writeRecord(input, {
188
+ ...current,
189
+ status: "running",
190
+ summary: input.summary ?? current.summary,
191
+ ...(input.detail ? { detail: input.detail } : {}),
192
+ ...(normalizeProgress(input.progress) ? { progress: normalizeProgress(input.progress) } : {}),
193
+ startedAt: input.startedAt,
194
+ updatedAt,
195
+ ...(current.finishedAt ? { finishedAt: current.finishedAt } : {}),
196
+ });
197
+ }
198
+ function updateBackgroundOperation(input) {
199
+ const current = requireRecord(input);
200
+ return writeRecord(input, {
201
+ ...current,
202
+ summary: input.summary ?? current.summary,
203
+ ...(input.detail ? { detail: input.detail } : {}),
204
+ ...(normalizeProgress(input.progress) ? { progress: normalizeProgress(input.progress) } : {}),
205
+ updatedAt: input.updatedAt ?? current.updatedAt,
206
+ });
207
+ }
208
+ function completeBackgroundOperation(input) {
209
+ const current = requireRecord(input);
210
+ return writeRecord(input, {
211
+ ...current,
212
+ status: "succeeded",
213
+ summary: input.summary ?? current.summary,
214
+ ...(input.detail ? { detail: input.detail } : {}),
215
+ ...(normalizeProgress(input.progress) ? { progress: normalizeProgress(input.progress) } : {}),
216
+ ...(input.result ? { result: input.result } : {}),
217
+ finishedAt: input.finishedAt,
218
+ updatedAt: input.updatedAt ?? input.finishedAt,
219
+ });
220
+ }
221
+ function failBackgroundOperation(input) {
222
+ const current = requireRecord(input);
223
+ return writeRecord(input, {
224
+ ...current,
225
+ status: "failed",
226
+ summary: input.summary ?? current.summary,
227
+ ...(input.detail ? { detail: input.detail } : {}),
228
+ ...(normalizeProgress(input.progress) ? { progress: normalizeProgress(input.progress) } : {}),
229
+ error: { message: input.error },
230
+ ...(input.remediation && input.remediation.length > 0 ? { remediation: input.remediation } : {}),
231
+ finishedAt: input.finishedAt,
232
+ updatedAt: input.updatedAt ?? input.finishedAt,
233
+ });
234
+ }
@@ -171,6 +171,14 @@ async function defaultFetchCliRegistryJson(timeoutMs) {
171
171
  function defaultSleep(ms) {
172
172
  return new Promise((resolve) => setTimeout(resolve, ms));
173
173
  }
174
+ function defaultSpawnBackgroundCli(argv) {
175
+ const child = (0, child_process_1.spawn)(process.execPath, argv, {
176
+ detached: true,
177
+ stdio: "ignore",
178
+ });
179
+ child.unref();
180
+ return Promise.resolve({ pid: child.pid ?? null });
181
+ }
174
182
  function defaultFallbackPendingMessage(command) {
175
183
  const inboxDir = path.join((0, identity_1.getAgentBundlesRoot)(), `${command.to}.ouro`, "inbox");
176
184
  const pendingPath = path.join(inboxDir, "pending.jsonl");
@@ -529,6 +537,7 @@ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SO
529
537
  readHealthUpdatedAt: defaultReadHealthUpdatedAt,
530
538
  readRecentDaemonLogLines: defaultReadRecentDaemonLogLines,
531
539
  sleep: defaultSleep,
540
+ spawnBackgroundCli: defaultSpawnBackgroundCli,
532
541
  now: () => Date.now(),
533
542
  updateCheckTimeoutMs: update_checker_1.CLI_UPDATE_CHECK_TIMEOUT_MS,
534
543
  startupPollIntervalMs: 250,
@@ -82,6 +82,8 @@ const sync_1 = require("../sync");
82
82
  const core_1 = require("../../mailroom/core");
83
83
  const mbox_import_1 = require("../../mailroom/mbox-import");
84
84
  const reader_1 = require("../../mailroom/reader");
85
+ const pending_1 = require("../../mind/pending");
86
+ const background_operations_1 = require("../background-operations");
85
87
  const cli_parse_1 = require("./cli-parse");
86
88
  const cli_parse_2 = require("./cli-parse");
87
89
  const cli_help_1 = require("./cli-help");
@@ -3400,9 +3402,192 @@ function stringField(record, key) {
3400
3402
  const value = record[key];
3401
3403
  return typeof value === "string" ? value.trim() : "";
3402
3404
  }
3405
+ function cliNowIso(deps) {
3406
+ return new Date(deps.now?.() ?? Date.now()).toISOString();
3407
+ }
3408
+ function makeBackgroundOperationId(kind) {
3409
+ const prefix = kind === "mail.import-mbox" ? "op_mail_import" : "op_mail_backfill";
3410
+ return `${prefix}_${(0, crypto_1.randomUUID)().slice(0, 8)}`;
3411
+ }
3412
+ function notifyMailOperation(agentName, record, deps) {
3413
+ const lines = [
3414
+ "[Background Operation]",
3415
+ `${record.title} ${record.status === "failed" ? "failed" : "finished"}.`,
3416
+ `operation: ${record.id}`,
3417
+ `status: ${record.status}`,
3418
+ `summary: ${record.summary}`,
3419
+ `detail: ${record.detail}`,
3420
+ ...(record.error?.message ? [`error: ${record.error.message}`] : []),
3421
+ ...(record.remediation && record.remediation.length > 0 ? ["", "next steps:", ...record.remediation.map((step) => `- ${step}`)] : []),
3422
+ "",
3423
+ "Use query_active_work for the live background-work view before drilling into mail details.",
3424
+ ];
3425
+ (0, pending_1.queuePendingMessage)((0, pending_1.getInnerDialogPendingDir)(agentName), {
3426
+ from: "mailroom",
3427
+ friendId: "self",
3428
+ channel: "inner",
3429
+ key: "dialog",
3430
+ content: lines.join("\n"),
3431
+ timestamp: deps.now?.() ?? Date.now(),
3432
+ mode: "reflect",
3433
+ });
3434
+ return deps.sendCommand(deps.socketPath, { kind: "inner.wake", agent: agentName })
3435
+ .then(() => undefined)
3436
+ .catch(() => undefined);
3437
+ }
3438
+ function ensureTrackedMailOperation(input) {
3439
+ if (!input.operationId)
3440
+ return null;
3441
+ const operationId = input.operationId;
3442
+ const agentRoot = providerCliAgentRoot({ agent: input.agentName }, input.deps);
3443
+ const existing = (0, background_operations_1.readBackgroundOperation)({
3444
+ agentName: input.agentName,
3445
+ agentRoot,
3446
+ id: operationId,
3447
+ });
3448
+ const record = existing ?? (0, background_operations_1.startBackgroundOperation)({
3449
+ agentName: input.agentName,
3450
+ agentRoot,
3451
+ id: operationId,
3452
+ kind: input.kind,
3453
+ title: input.title,
3454
+ summary: input.queuedSummary,
3455
+ createdAt: cliNowIso(input.deps),
3456
+ ...(input.spec ? { spec: input.spec } : {}),
3457
+ });
3458
+ return {
3459
+ record,
3460
+ running: (summary, detail, progress) => {
3461
+ (0, background_operations_1.markBackgroundOperationRunning)({
3462
+ agentName: input.agentName,
3463
+ agentRoot,
3464
+ id: operationId,
3465
+ startedAt: cliNowIso(input.deps),
3466
+ summary,
3467
+ ...(detail ? { detail } : {}),
3468
+ ...(progress ? { progress } : {}),
3469
+ });
3470
+ },
3471
+ update: (summary, detail, progress) => {
3472
+ (0, background_operations_1.updateBackgroundOperation)({
3473
+ agentName: input.agentName,
3474
+ agentRoot,
3475
+ id: operationId,
3476
+ summary,
3477
+ updatedAt: cliNowIso(input.deps),
3478
+ ...(detail ? { detail } : {}),
3479
+ ...(progress ? { progress } : {}),
3480
+ });
3481
+ },
3482
+ succeed: async (summary, detail, result) => {
3483
+ const completed = (0, background_operations_1.completeBackgroundOperation)({
3484
+ agentName: input.agentName,
3485
+ agentRoot,
3486
+ id: operationId,
3487
+ finishedAt: cliNowIso(input.deps),
3488
+ summary,
3489
+ detail,
3490
+ result,
3491
+ });
3492
+ await notifyMailOperation(input.agentName, { ...completed, detail }, input.deps);
3493
+ },
3494
+ fail: async (error, summary, detail, remediation) => {
3495
+ const failed = (0, background_operations_1.failBackgroundOperation)({
3496
+ agentName: input.agentName,
3497
+ agentRoot,
3498
+ id: operationId,
3499
+ finishedAt: cliNowIso(input.deps),
3500
+ summary,
3501
+ detail,
3502
+ error: error instanceof Error ? error.message : String(error),
3503
+ remediation,
3504
+ });
3505
+ await notifyMailOperation(input.agentName, { ...failed, detail }, input.deps);
3506
+ },
3507
+ };
3508
+ }
3509
+ async function launchBackgroundMailCommand(command, deps, args, title, queuedSummary, spec) {
3510
+ const spawnBackgroundCli = deps.spawnBackgroundCli;
3511
+ if (!spawnBackgroundCli) {
3512
+ throw new Error("background CLI launch is not available in this runtime");
3513
+ }
3514
+ const operationId = makeBackgroundOperationId(command.kind);
3515
+ const agentRoot = providerCliAgentRoot({ agent: command.agent }, deps);
3516
+ (0, background_operations_1.startBackgroundOperation)({
3517
+ agentName: command.agent,
3518
+ agentRoot,
3519
+ id: operationId,
3520
+ kind: command.kind,
3521
+ title,
3522
+ summary: queuedSummary,
3523
+ createdAt: cliNowIso(deps),
3524
+ ...(spec ? { spec } : {}),
3525
+ });
3526
+ try {
3527
+ await spawnBackgroundCli([process.argv[1], ...args, "--foreground", "--operation-id", operationId]);
3528
+ }
3529
+ catch (error) {
3530
+ (0, background_operations_1.failBackgroundOperation)({
3531
+ agentName: command.agent,
3532
+ agentRoot,
3533
+ id: operationId,
3534
+ finishedAt: cliNowIso(deps),
3535
+ summary: `${title} could not be started`,
3536
+ error: error instanceof Error ? error.message : String(error),
3537
+ remediation: ["retry the command once the local runtime is healthy"],
3538
+ });
3539
+ throw error;
3540
+ }
3541
+ const message = [
3542
+ `Started background ${title} for ${command.agent}`,
3543
+ `operation: ${operationId}`,
3544
+ ...(spec ? Object.entries(spec).map(([key, value]) => `${key}: ${String(value)}`) : []),
3545
+ `${command.agent} will be woken on failure or completion.`,
3546
+ "Use query_active_work to check live progress.",
3547
+ ].join("\n");
3548
+ deps.writeStdout(message);
3549
+ return message;
3550
+ }
3403
3551
  async function executeMailImportMbox(command, deps) {
3552
+ const filePath = path.resolve(command.filePath);
3553
+ if (!command.foreground) {
3554
+ if (!fs.existsSync(filePath)) {
3555
+ throw new Error(`no such file: ${filePath}`);
3556
+ }
3557
+ return launchBackgroundMailCommand(command, deps, [
3558
+ "mail",
3559
+ "import-mbox",
3560
+ "--agent",
3561
+ command.agent,
3562
+ "--file",
3563
+ filePath,
3564
+ ...(command.ownerEmail ? ["--owner-email", command.ownerEmail] : []),
3565
+ ...(command.source ? ["--source", command.source] : []),
3566
+ ], "mail import", "queued delegated mail import", {
3567
+ filePath,
3568
+ ...(command.ownerEmail ? { ownerEmail: command.ownerEmail } : {}),
3569
+ ...(command.source ? { source: command.source } : {}),
3570
+ });
3571
+ }
3404
3572
  const progress = createHumanCommandProgress(deps, "mail import");
3573
+ const trackedOperation = ensureTrackedMailOperation({
3574
+ agentName: command.agent,
3575
+ deps,
3576
+ operationId: command.operationId,
3577
+ kind: "mail.import-mbox",
3578
+ title: "mail import",
3579
+ queuedSummary: "queued delegated mail import",
3580
+ spec: {
3581
+ filePath,
3582
+ ...(command.ownerEmail ? { ownerEmail: command.ownerEmail } : {}),
3583
+ ...(command.source ? { source: command.source } : {}),
3584
+ },
3585
+ });
3405
3586
  try {
3587
+ trackedOperation?.running("reading Mailroom config", `file: ${filePath}`, {
3588
+ current: 0,
3589
+ unit: "messages",
3590
+ });
3406
3591
  progress.startPhase("reading Mailroom config");
3407
3592
  const runtime = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(command.agent, { preserveCachedOnFailure: true });
3408
3593
  if (!runtime.ok) {
@@ -3437,9 +3622,10 @@ async function executeMailImportMbox(command, deps) {
3437
3622
  throw new Error(resolved.error);
3438
3623
  }
3439
3624
  progress.updateDetail("reading registry and MBOX");
3625
+ trackedOperation?.update("reading registry and MBOX", `file: ${filePath}`);
3440
3626
  const registry = await (0, reader_1.readMailroomRegistry)(resolved.config);
3441
- const filePath = path.resolve(command.filePath);
3442
3627
  progress.updateDetail("importing delegated mail");
3628
+ trackedOperation?.update("importing delegated mail", `file: ${filePath}`);
3443
3629
  const result = await (0, mbox_import_1.importMboxFileToStore)({
3444
3630
  registry,
3445
3631
  store: resolved.store,
@@ -3447,6 +3633,12 @@ async function executeMailImportMbox(command, deps) {
3447
3633
  filePath,
3448
3634
  ownerEmail: command.ownerEmail,
3449
3635
  source: command.source,
3636
+ onProgress: (importProgress) => {
3637
+ trackedOperation?.update("importing delegated mail", `scanned ${importProgress.scanned} messages`, {
3638
+ current: importProgress.scanned,
3639
+ unit: "messages",
3640
+ });
3641
+ },
3450
3642
  });
3451
3643
  progress.completePhase("reading Mailroom config", "imported");
3452
3644
  progress.end();
@@ -3461,17 +3653,44 @@ async function executeMailImportMbox(command, deps) {
3461
3653
  "archive imports are historical; they do not create Screener wakeups.",
3462
3654
  "body reads remain explicit through mail_recent/mail_search/mail_thread and are access-logged.",
3463
3655
  ].join("\n");
3656
+ await trackedOperation?.succeed("imported delegated mail archive", `scanned ${result.scanned}; imported ${result.imported}; duplicates ${result.duplicates}`, {
3657
+ scanned: result.scanned,
3658
+ imported: result.imported,
3659
+ duplicates: result.duplicates,
3660
+ sourceFreshThrough: result.sourceFreshThrough,
3661
+ });
3464
3662
  deps.writeStdout(message);
3465
3663
  return message;
3466
3664
  }
3467
3665
  catch (error) {
3468
3666
  progress.end();
3667
+ await trackedOperation?.fail(error, "delegated mail import failed", `file: ${filePath}`, [
3668
+ "fix the reported Mailroom or MBOX problem, then rerun the import",
3669
+ "use query_active_work to confirm whether the operation has cleared",
3670
+ ]);
3469
3671
  throw error;
3470
3672
  }
3471
3673
  }
3472
3674
  async function executeMailBackfillIndexes(command, deps) {
3675
+ if (!command.foreground) {
3676
+ return launchBackgroundMailCommand(command, deps, [
3677
+ "mail",
3678
+ "backfill-indexes",
3679
+ "--agent",
3680
+ command.agent,
3681
+ ], "mail index repair", "queued hosted mail index repair");
3682
+ }
3473
3683
  const progress = createHumanCommandProgress(deps, "mail index repair");
3684
+ const trackedOperation = ensureTrackedMailOperation({
3685
+ agentName: command.agent,
3686
+ deps,
3687
+ operationId: command.operationId,
3688
+ kind: "mail.backfill-indexes",
3689
+ title: "mail index repair",
3690
+ queuedSummary: "queued hosted mail index repair",
3691
+ });
3474
3692
  try {
3693
+ trackedOperation?.running("resolving Mailroom store");
3475
3694
  progress.startPhase("resolving Mailroom store");
3476
3695
  const runtime = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(command.agent, { preserveCachedOnFailure: true });
3477
3696
  if (!runtime.ok) {
@@ -3491,6 +3710,7 @@ async function executeMailBackfillIndexes(command, deps) {
3491
3710
  `store: ${resolved.storeLabel}`,
3492
3711
  "This agent is using the local file Mailroom store, so recent mail reads do not depend on hosted blob indexes.",
3493
3712
  ].join("\n");
3713
+ await trackedOperation?.succeed("hosted mail index repair not needed", `store: ${resolved.storeLabel}`, { indexed: 0, store: resolved.storeLabel, reason: "local-file-store" });
3494
3714
  deps.writeStdout(message);
3495
3715
  return message;
3496
3716
  }
@@ -3500,7 +3720,14 @@ async function executeMailBackfillIndexes(command, deps) {
3500
3720
  throw new Error(`hosted Mailroom store for ${command.agent} does not expose index backfill`);
3501
3721
  }
3502
3722
  progress.updateDetail("backfilling hosted message indexes");
3503
- const indexed = await store.backfillMessageIndexes(command.agent);
3723
+ trackedOperation?.update("backfilling hosted message indexes");
3724
+ const indexed = await store.backfillMessageIndexes(command.agent, (backfillProgress) => {
3725
+ trackedOperation?.update("backfilling hosted message indexes", `indexed ${backfillProgress.indexed} of ${backfillProgress.total}`, {
3726
+ current: backfillProgress.scanned,
3727
+ total: backfillProgress.total,
3728
+ unit: "blobs",
3729
+ });
3730
+ });
3504
3731
  progress.completePhase("resolving Mailroom store", "backfilled");
3505
3732
  progress.end();
3506
3733
  const message = [
@@ -3509,11 +3736,16 @@ async function executeMailBackfillIndexes(command, deps) {
3509
3736
  `indexed: ${indexed}`,
3510
3737
  "Safe to rerun. Existing index entries are rewritten in place.",
3511
3738
  ].join("\n");
3739
+ await trackedOperation?.succeed("hosted mail index repair finished", `indexed ${indexed} message indexes`, { indexed, store: resolved.storeLabel });
3512
3740
  deps.writeStdout(message);
3513
3741
  return message;
3514
3742
  }
3515
3743
  catch (error) {
3516
3744
  progress.end();
3745
+ await trackedOperation?.fail(error, "hosted mail index repair failed", "store resolution or blob backfill failed", [
3746
+ "rerun the hosted index repair to retry remaining blobs",
3747
+ "if the same residue repeats, inspect the failed blob ids and timeouts",
3748
+ ]);
3517
3749
  throw error;
3518
3750
  }
3519
3751
  }
@@ -323,12 +323,12 @@ const SUBCOMMAND_HELP = {
323
323
  },
324
324
  "mail import-mbox": {
325
325
  description: "Import a HEY or other MBOX export into an existing delegated Mailroom source grant",
326
- usage: "ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]",
326
+ usage: "ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>] [--foreground]",
327
327
  example: "ouro mail import-mbox --file ~/Downloads/hey.mbox --owner-email ari@mendelow.me --source hey --agent slugger",
328
328
  },
329
329
  "mail backfill-indexes": {
330
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>]",
331
+ usage: "ouro mail backfill-indexes [--agent <name>] [--foreground]",
332
332
  example: "ouro mail backfill-indexes --agent slugger",
333
333
  },
334
334
  "provider refresh": {
@@ -86,8 +86,8 @@ function usage() {
86
86
  " ouro auth [--agent <name>] [--provider <provider>]",
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
- " ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>]",
90
- " ouro mail backfill-indexes [--agent <name>]",
89
+ " ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>] [--foreground]",
90
+ " ouro mail backfill-indexes [--agent <name>] [--foreground]",
91
91
  " ouro auth verify [--agent <name>] [--provider <provider>]",
92
92
  " ouro auth switch [--agent <name>] --provider <provider>",
93
93
  " ouro vault create [--agent <name>] --email <email> [--server <url>] [--store <store>]",
@@ -885,14 +885,28 @@ function parseConnectCommand(args) {
885
885
  }
886
886
  function parseMailCommand(args) {
887
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>]";
888
+ const usageText = "Usage: ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>] [--foreground]\n ouro mail backfill-indexes [--agent <name>] [--foreground]";
889
889
  if (sub === "backfill-indexes") {
890
890
  const { agent, rest } = extractAgentFlag(subArgs);
891
- if (rest.length > 0)
891
+ let foreground = false;
892
+ let operationId;
893
+ for (let i = 0; i < rest.length; i += 1) {
894
+ const token = rest[i];
895
+ if (token === "--foreground") {
896
+ foreground = true;
897
+ continue;
898
+ }
899
+ if (token === "--operation-id" && rest[i + 1]) {
900
+ operationId = rest[++i];
901
+ continue;
902
+ }
892
903
  throw new Error(usageText);
904
+ }
893
905
  return {
894
906
  kind: "mail.backfill-indexes",
895
907
  ...(agent ? { agent } : {}),
908
+ ...(foreground ? { foreground: true } : {}),
909
+ ...(operationId ? { operationId } : {}),
896
910
  };
897
911
  }
898
912
  if (sub !== "import-mbox") {
@@ -902,6 +916,8 @@ function parseMailCommand(args) {
902
916
  let filePath;
903
917
  let ownerEmail;
904
918
  let source;
919
+ let foreground = false;
920
+ let operationId;
905
921
  for (let i = 0; i < rest.length; i += 1) {
906
922
  const token = rest[i];
907
923
  if (token === "--file" && rest[i + 1]) {
@@ -916,6 +932,14 @@ function parseMailCommand(args) {
916
932
  source = rest[++i];
917
933
  continue;
918
934
  }
935
+ if (token === "--foreground") {
936
+ foreground = true;
937
+ continue;
938
+ }
939
+ if (token === "--operation-id" && rest[i + 1]) {
940
+ operationId = rest[++i];
941
+ continue;
942
+ }
919
943
  throw new Error(usageText);
920
944
  }
921
945
  if (!filePath) {
@@ -927,6 +951,8 @@ function parseMailCommand(args) {
927
951
  filePath,
928
952
  ...(ownerEmail ? { ownerEmail } : {}),
929
953
  ...(source ? { source } : {}),
954
+ ...(foreground ? { foreground: true } : {}),
955
+ ...(operationId ? { operationId } : {}),
930
956
  };
931
957
  }
932
958
  function parseAccountCommand(args) {
@@ -273,7 +273,7 @@ class AzureBlobMailroomStore {
273
273
  .sort(compareNewestFirst)
274
274
  .slice(0, filters.limit ?? 20);
275
275
  }
276
- async backfillMessageIndexes(agentId) {
276
+ async backfillMessageIndexes(agentId, onProgress) {
277
277
  await this.ensureContainer();
278
278
  const messageBlobNames = [];
279
279
  for await (const item of this.container.listBlobsFlat({ prefix: "messages/" })) {
@@ -281,6 +281,7 @@ class AzureBlobMailroomStore {
281
281
  }
282
282
  let indexed = 0;
283
283
  const failures = [];
284
+ let scanned = 0;
284
285
  let nextIndex = 0;
285
286
  const worker = async () => {
286
287
  while (nextIndex < messageBlobNames.length) {
@@ -298,6 +299,10 @@ class AzureBlobMailroomStore {
298
299
  catch (error) {
299
300
  failures.push(error instanceof Error ? error.message : String(error));
300
301
  }
302
+ finally {
303
+ scanned += 1;
304
+ onProgress?.({ scanned, indexed, failures: failures.length, total: messageBlobNames.length });
305
+ }
301
306
  }
302
307
  };
303
308
  await Promise.all(Array.from({ length: Math.min(this.backfillConcurrency, Math.max(messageBlobNames.length, 1)) }, async () => worker()));
@@ -246,6 +246,7 @@ async function importParsedMessagesToStore(input) {
246
246
  imported += 1;
247
247
  else
248
248
  duplicates += 1;
249
+ input.onProgress?.({ scanned, imported, duplicates });
249
250
  })().finally(() => {
250
251
  inFlight.delete(task);
251
252
  });
@@ -254,6 +255,7 @@ async function importParsedMessagesToStore(input) {
254
255
  };
255
256
  for await (const parsedMessage of input.parsedMessages) {
256
257
  scanned += 1;
258
+ input.onProgress?.({ scanned, imported, duplicates });
257
259
  enqueue(parsedMessage);
258
260
  if (inFlight.size >= maxConcurrency) {
259
261
  await Promise.race(inFlight);
@@ -334,5 +336,6 @@ async function importMboxFileToStore(input) {
334
336
  collectMessages: false,
335
337
  sourceFreshThrough,
336
338
  maxConcurrency: 8,
339
+ onProgress: input.onProgress,
337
340
  });
338
341
  }
@@ -46,6 +46,7 @@ const manager_1 = require("../heart/bridges/manager");
46
46
  const session_transcript_1 = require("../heart/session-transcript");
47
47
  const session_activity_1 = require("../heart/session-activity");
48
48
  const active_work_1 = require("../heart/active-work");
49
+ const background_operations_1 = require("../heart/background-operations");
49
50
  const coding_1 = require("./coding");
50
51
  const tasks_1 = require("./tasks");
51
52
  const pending_1 = require("../mind/pending");
@@ -263,6 +264,11 @@ async function buildToolActiveWorkFrame(ctx) {
263
264
  && obligation.origin.channel === currentSession.channel
264
265
  && obligation.origin.key === currentSession.key)?.content ?? null
265
266
  : null;
267
+ const backgroundOperations = (0, background_operations_1.listBackgroundOperations)({
268
+ agentName: (0, identity_1.getAgentName)(),
269
+ agentRoot,
270
+ limit: 5,
271
+ });
266
272
  return (0, active_work_1.buildActiveWorkFrame)({
267
273
  currentSession,
268
274
  currentObligation,
@@ -270,6 +276,7 @@ async function buildToolActiveWorkFrame(ctx) {
270
276
  inner: readActiveWorkInnerState(),
271
277
  bridges,
272
278
  codingSessions,
279
+ backgroundOperations,
273
280
  otherCodingSessions,
274
281
  pendingObligations,
275
282
  taskBoard: (() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.479",
3
+ "version": "0.1.0-alpha.480",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",