@ouro.bot/cli 0.1.0-alpha.484 → 0.1.0-alpha.486

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,23 @@
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.486",
6
+ "changes": [
7
+ "Mail convergence pass 1-5 hardens hosted-mail truth surfaces under live HEY ingest: import truth + audit resilience, accurate archive freshness and identity surfaces, sharper recovery and archive truth, delegated search resilience, and natural anchor-list retrieval; imported archive content is now searched on parsed message text rather than raw archive bytes so quoted-printable / HTML-heavy booking mail is reachable.",
8
+ "`mail_search` now ranks by booking-aware relevance instead of pure recency. Score signals weight query-term hits by field (subject +6 / from +4 / body +2), booking-intent tokens (`booking confirmation`, `your stay`, e-ticket, etc.), confirmation-number-shaped tokens, currency amounts, and known travel-sender domains; recency stays as a tiebreaker. Recall is unchanged — noise still appears in results, just below the decisive message. Each rendered result also surfaces a `matched on:` line listing fields, booking signals, status (confirmed / cancelled / changed / refunded / etc.), confirmation token, amount, dates, attachment count, and sender hint, so the agent can triage without paying for a body open.",
9
+ "BlueBubbles sense no longer sticks in `error` status when a single message is permanently unrecoverable. `upstreamStatus` now tracks upstream health and pending work only — per-cycle recovery failures stay informational in `detail` for transparency without contradicting `ouro doctor`'s healthy verdict, so a malformed payload that fails repairEvent on every retry can no longer brick the visible sense state until operator intervention.",
10
+ "Heart streaming caps oversized Responses-API `function_call_output` history items both when rebuilding provider input from session history and when appending fresh tool output mid-turn, preventing a giant tool result on resume from blowing the model context."
11
+ ]
12
+ },
13
+ {
14
+ "version": "0.1.0-alpha.485",
15
+ "changes": [
16
+ "Session JSON storage no longer accumulates duplicate event ids when two writers race for the same session — `parseSessionEnvelope` now dedupes on read (last-occurrence-wins) so existing corrupted sessions self-heal on the next save, `buildCanonicalSessionEnvelope` assigns the next sequence as `max(existing) + 1` instead of `events.length + 1` so pruning gaps cannot collide, and `deferPostTurnPersist` serializes per-`sessPath` through an in-process queue so concurrent BlueBubbles webhooks for the same chat (or CLI postTurn racing the inner-dialog turn for the same MCP session) cannot interleave their writes.",
17
+ "Auto-created BlueBubbles group friends are now marked with a `notes.autoCreatedGroup` flag at resolver time, and the trust gate's family-member bypass surfaces a one-time inner-pending notice the first time messages route through an unacknowledged stranger-trust group so the agent can label, rename, or dismiss the relationship before activity accumulates invisibly.",
18
+ "Inner-dialog worker now caps consecutive `instinct` follow-on turns at `MAX_CONSECUTIVE_INSTINCT_TURNS = 3` to break self-sustaining loops where a tool that writes to the inner-dialog pending dir during a turn would otherwise re-fire the worker indefinitely; externally-queued messages reset the counter so legitimate cascading follow-ups still run, and a new `senses.inner_dialog_worker_instinct_loop_capped` event surfaces when the cap fires."
19
+ ]
20
+ },
4
21
  {
5
22
  "version": "0.1.0-alpha.484",
6
23
  "changes": [
@@ -239,6 +239,10 @@ function backgroundOperationPriority(operation) {
239
239
  case "succeeded": return 3;
240
240
  }
241
241
  }
242
+ function backgroundOperationSpecText(spec, key) {
243
+ const value = spec?.[key];
244
+ return typeof value === "string" ? value.trim() : "";
245
+ }
242
246
  function selectPrimaryBackgroundOperation(frame) {
243
247
  const operations = frame.backgroundOperations ?? [];
244
248
  if (operations.length === 0)
@@ -257,13 +261,34 @@ function formatBackgroundOperationNextAction(operation) {
257
261
  if (operation.kind === "mail.import-mbox") {
258
262
  switch (operation.status) {
259
263
  case "failed":
264
+ switch (operation.failure?.class?.trim()) {
265
+ case "transient-storage-read":
266
+ return "retry-safe once the transient storage/network issue clears";
267
+ case "mailroom-auth":
268
+ case "mailroom-config":
269
+ return "repair mail auth/config access, then retry";
270
+ case "source-grant-missing":
271
+ case "source-grant-ambiguous":
272
+ return "repair the delegated owner/source lane, then retry";
273
+ case "archive-discovery":
274
+ case "archive-ambiguity":
275
+ case "archive-missing":
276
+ case "archive-access":
277
+ return "materialize or point at the correct local archive, then retry";
278
+ }
279
+ if (operation.failure?.retryDisposition === "retry-safe") {
280
+ return "retry the failed mail import once the transient issue clears or the dependency answers again";
281
+ }
282
+ if (operation.failure?.retryDisposition === "investigate-first") {
283
+ return "inspect the failed mail import carefully, confirm the root cause, and then decide whether to retry";
284
+ }
260
285
  return "inspect the failed mail import, fix the issue, and retry it";
261
286
  case "queued":
262
- return "let the queued mail import start; failure or completion will wake me, so i only re-check if i need status or it looks stalled";
287
+ return "queued no action unless it stalls or i need live status";
263
288
  case "running":
264
- return "let the background mail import run; failure or completion will wake me, so i only check again if i need status or it looks stalled";
289
+ return "in flight no action unless it stalls or i need live status";
265
290
  case "succeeded":
266
- return "review the completed mail import and continue from the updated mailbox state; i only rerun if a newer archive appears";
291
+ return "caught up no rerun needed unless a newer archive appears";
267
292
  }
268
293
  }
269
294
  switch (operation.status) {
@@ -273,6 +298,66 @@ function formatBackgroundOperationNextAction(operation) {
273
298
  case "succeeded": return `review the completed ${operation.title} operation and continue`;
274
299
  }
275
300
  }
301
+ function formatMailImportRecoveryUniverse(operation) {
302
+ const failureClass = operation.failure?.class?.trim();
303
+ if (!failureClass)
304
+ return null;
305
+ switch (failureClass) {
306
+ case "transient-storage-read":
307
+ return "transient dependency/read issue — safe to retry once storage/network answers again";
308
+ case "mailroom-auth":
309
+ case "mailroom-config":
310
+ return "mail auth/config issue — repair mail access or runtime config before retrying";
311
+ case "source-grant-missing":
312
+ case "source-grant-ambiguous":
313
+ return "delegated lane/registry issue — inspect owner/source linking before retrying";
314
+ case "archive-discovery":
315
+ case "archive-ambiguity":
316
+ case "archive-missing":
317
+ case "archive-access":
318
+ return "local archive/file issue — materialize or point at the right archive before retrying";
319
+ default:
320
+ return "unclassified import issue — inspect the recorded error before retrying";
321
+ }
322
+ }
323
+ function formatBackgroundOperationMeta(operation) {
324
+ const lines = [`operation: ${operation.id}`];
325
+ const filePath = backgroundOperationSpecText(operation.spec, "filePath")
326
+ || backgroundOperationSpecText(operation.spec, "newestCandidatePath");
327
+ if (filePath)
328
+ lines.push(`file: ${filePath}`);
329
+ const originLabel = backgroundOperationSpecText(operation.spec, "fileOriginLabel")
330
+ || backgroundOperationSpecText(operation.spec, "newestCandidateOriginLabel");
331
+ if (originLabel)
332
+ lines.push(`origin: ${originLabel}`);
333
+ const ownerEmail = backgroundOperationSpecText(operation.spec, "ownerEmail");
334
+ const source = backgroundOperationSpecText(operation.spec, "source");
335
+ if (ownerEmail || source) {
336
+ lines.push(`owner/source: ${ownerEmail || "unknown"} / ${source || "unknown"}`);
337
+ }
338
+ if (operation.failure?.class?.trim())
339
+ lines.push(`failure class: ${operation.failure.class}`);
340
+ if (operation.failure?.retryDisposition?.trim())
341
+ lines.push(`retry: ${operation.failure.retryDisposition}`);
342
+ if (operation.failure?.hint?.trim())
343
+ lines.push(`recovery: ${operation.failure.hint}`);
344
+ const recoveryUniverse = operation.kind === "mail.import-mbox" ? formatMailImportRecoveryUniverse(operation) : null;
345
+ if (recoveryUniverse)
346
+ lines.push(`recovery universe: ${recoveryUniverse}`);
347
+ if (operation.startedAt?.trim())
348
+ lines.push(`started: ${operation.startedAt}`);
349
+ if (operation.finishedAt?.trim())
350
+ lines.push(`finished: ${operation.finishedAt}`);
351
+ else if (operation.updatedAt?.trim())
352
+ lines.push(`updated: ${operation.updatedAt}`);
353
+ const nextAction = formatBackgroundOperationNextAction(operation);
354
+ if (nextAction.trim().length > 0)
355
+ lines.push(`next: ${nextAction}`);
356
+ if (operation.remediation?.length) {
357
+ lines.push(...operation.remediation.map((step) => `remediation: ${step}`));
358
+ }
359
+ return lines;
360
+ }
276
361
  function formatNextAction(frame, obligation) {
277
362
  const obligationHasConcreteArtifact = Boolean(obligation?.currentArtifact?.trim())
278
363
  || obligation?.currentSurface?.kind === "merge";
@@ -696,6 +781,7 @@ function formatActiveWorkFrame(frame, options) {
696
781
  if (operation.summary.trim().length > 0) {
697
782
  line += `: ${operation.summary}`;
698
783
  }
784
+ line += `\n ${formatBackgroundOperationMeta(operation).join("\n ")}`;
699
785
  if (operation.detail?.trim()) {
700
786
  line += `\n ${operation.detail.trim()}`;
701
787
  }
@@ -112,6 +112,20 @@ function normalizeRecord(record) {
112
112
  if (record.error && typeof record.error.message === "string" && record.error.message.trim().length > 0) {
113
113
  normalized.error = { message: record.error.message.trim() };
114
114
  }
115
+ if (record.failure && typeof record.failure === "object" && !Array.isArray(record.failure)) {
116
+ const failureClass = typeof record.failure.class === "string" ? record.failure.class.trim() : "";
117
+ const retryDisposition = record.failure.retryDisposition;
118
+ const hint = typeof record.failure.hint === "string" ? record.failure.hint.trim() : "";
119
+ if (failureClass) {
120
+ normalized.failure = {
121
+ class: failureClass,
122
+ ...(retryDisposition === "retry-safe" || retryDisposition === "fix-before-retry" || retryDisposition === "investigate-first"
123
+ ? { retryDisposition }
124
+ : {}),
125
+ ...(hint ? { hint } : {}),
126
+ };
127
+ }
128
+ }
115
129
  if (Array.isArray(record.remediation)) {
116
130
  const remediation = record.remediation.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
117
131
  if (remediation.length > 0)
@@ -143,15 +157,19 @@ function visibleOperationKey(record) {
143
157
  return `${record.agentName.toLowerCase()}|${record.kind}|${filePath}|${ownerEmail}|${source}`;
144
158
  }
145
159
  function writeRecord(locator, record) {
160
+ const normalized = normalizeRecord(record);
161
+ if (!normalized) {
162
+ throw new Error(`invalid background operation record: ${locator.id}`);
163
+ }
146
164
  fs.mkdirSync(operationsDir(locator.agentName, locator.agentRoot), { recursive: true });
147
- fs.writeFileSync(operationPath(locator), `${JSON.stringify(record, null, 2)}\n`, "utf-8");
165
+ fs.writeFileSync(operationPath(locator), `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
148
166
  (0, runtime_1.emitNervesEvent)({
149
167
  component: "engine",
150
168
  event: "engine.background_operation_written",
151
169
  message: "background operation state written",
152
- meta: { agentName: locator.agentName, id: locator.id, kind: record.kind, status: record.status },
170
+ meta: { agentName: locator.agentName, id: locator.id, kind: normalized.kind, status: normalized.status },
153
171
  });
154
- return record;
172
+ return normalized;
155
173
  }
156
174
  function requireRecord(locator) {
157
175
  const record = readBackgroundOperation(locator);
@@ -226,6 +244,7 @@ function updateBackgroundOperation(input) {
226
244
  summary: input.summary ?? current.summary,
227
245
  ...(input.detail ? { detail: input.detail } : {}),
228
246
  ...(normalizeProgress(input.progress) ? { progress: normalizeProgress(input.progress) } : {}),
247
+ ...(input.spec ? { spec: input.spec } : {}),
229
248
  updatedAt: input.updatedAt ?? current.updatedAt,
230
249
  });
231
250
  }
@@ -238,6 +257,9 @@ function completeBackgroundOperation(input) {
238
257
  ...(input.detail ? { detail: input.detail } : {}),
239
258
  ...(normalizeProgress(input.progress) ? { progress: normalizeProgress(input.progress) } : {}),
240
259
  ...(input.result ? { result: input.result } : {}),
260
+ error: undefined,
261
+ remediation: undefined,
262
+ failure: undefined,
241
263
  finishedAt: input.finishedAt,
242
264
  updatedAt: input.updatedAt ?? input.finishedAt,
243
265
  });
@@ -251,6 +273,7 @@ function failBackgroundOperation(input) {
251
273
  ...(input.detail ? { detail: input.detail } : {}),
252
274
  ...(normalizeProgress(input.progress) ? { progress: normalizeProgress(input.progress) } : {}),
253
275
  error: { message: input.error },
276
+ ...(input.failure ? { failure: input.failure } : {}),
254
277
  ...(input.remediation && input.remediation.length > 0 ? { remediation: input.remediation } : {}),
255
278
  finishedAt: input.finishedAt,
256
279
  updatedAt: input.updatedAt ?? input.finishedAt,
@@ -3421,6 +3421,9 @@ async function notifyMailOperation(agentName, record, deps) {
3421
3421
  `status: ${record.status}`,
3422
3422
  `summary: ${record.summary}`,
3423
3423
  `detail: ${record.detail}`,
3424
+ ...(record.failure?.class ? [`failure class: ${record.failure.class}`] : []),
3425
+ ...(record.failure?.retryDisposition ? [`retry: ${record.failure.retryDisposition}`] : []),
3426
+ ...(record.failure?.hint ? [`recovery: ${record.failure.hint}`] : []),
3424
3427
  ...(record.error?.message ? [`error: ${record.error.message}`] : []),
3425
3428
  ...(record.remediation && record.remediation.length > 0 ? ["", "next steps:", ...record.remediation.map((step) => `- ${step}`)] : []),
3426
3429
  ...(record.kind === "mail.import-mbox" && record.status === "succeeded"
@@ -3518,7 +3521,7 @@ function ensureTrackedMailOperation(input) {
3518
3521
  ...(progress ? { progress } : {}),
3519
3522
  });
3520
3523
  },
3521
- update: (summary, detail, progress) => {
3524
+ update: (summary, detail, progress, spec) => {
3522
3525
  (0, background_operations_1.updateBackgroundOperation)({
3523
3526
  agentName: input.agentName,
3524
3527
  agentRoot,
@@ -3527,6 +3530,7 @@ function ensureTrackedMailOperation(input) {
3527
3530
  updatedAt: cliNowIso(input.deps),
3528
3531
  ...(detail ? { detail } : {}),
3529
3532
  ...(progress ? { progress } : {}),
3533
+ ...(spec ? { spec } : {}),
3530
3534
  });
3531
3535
  },
3532
3536
  succeed: async (summary, detail, result) => {
@@ -3541,7 +3545,7 @@ function ensureTrackedMailOperation(input) {
3541
3545
  });
3542
3546
  await notifyMailOperation(input.agentName, { ...completed, detail }, input.deps);
3543
3547
  },
3544
- fail: async (error, summary, detail, remediation) => {
3548
+ fail: async (error, summary, detail, remediation, failure) => {
3545
3549
  const failed = (0, background_operations_1.failBackgroundOperation)({
3546
3550
  agentName: input.agentName,
3547
3551
  agentRoot,
@@ -3550,6 +3554,7 @@ function ensureTrackedMailOperation(input) {
3550
3554
  summary,
3551
3555
  detail,
3552
3556
  error: error instanceof Error ? error.message : String(error),
3557
+ ...(failure ? { failure } : {}),
3553
3558
  remediation,
3554
3559
  });
3555
3560
  await notifyMailOperation(input.agentName, { ...failed, detail }, input.deps);
@@ -3608,6 +3613,143 @@ function buildMailImportOperationSpec(filePath, ownerEmail, source) {
3608
3613
  fileModifiedAt: new Date(stats.mtimeMs).toISOString(),
3609
3614
  };
3610
3615
  }
3616
+ function classifyMailImportFailure(error) {
3617
+ const message = error instanceof Error ? error.message : String(error);
3618
+ const normalized = message.toLowerCase();
3619
+ if (normalized.includes("auth_required:mailroom") || normalized.includes("cannot read mailroom config")) {
3620
+ return {
3621
+ failure: {
3622
+ class: "mailroom-auth",
3623
+ retryDisposition: "fix-before-retry",
3624
+ hint: "mailroom credentials or vault access are unavailable; unlock or repair mail auth first",
3625
+ },
3626
+ remediation: [
3627
+ "unlock or repair the owning agent vault/runtime config, then rerun the import",
3628
+ "use query_active_work to confirm the failed operation has settled before retrying",
3629
+ ],
3630
+ };
3631
+ }
3632
+ if (normalized.includes("missing mailroom config")
3633
+ || normalized.includes("missing registrypath/storepath")
3634
+ || normalized.includes("missing hosted registry coordinates")) {
3635
+ return {
3636
+ failure: {
3637
+ class: "mailroom-config",
3638
+ retryDisposition: "fix-before-retry",
3639
+ hint: "mailroom runtime config is incomplete for this agent",
3640
+ },
3641
+ remediation: [
3642
+ `rerun 'ouro connect mail --agent <agent>' or repair the stored mailroom runtime config`,
3643
+ "retry the import only after the config repair is complete",
3644
+ ],
3645
+ };
3646
+ }
3647
+ if (normalized.includes("no enabled mailroom source grant found")) {
3648
+ return {
3649
+ failure: {
3650
+ class: "source-grant-missing",
3651
+ retryDisposition: "fix-before-retry",
3652
+ hint: "the requested owner/source lane is not provisioned yet",
3653
+ },
3654
+ remediation: [
3655
+ "create or repair the delegated source grant for the intended owner/source, then rerun the import",
3656
+ "confirm the import is pointed at the intended owner/source before retrying",
3657
+ ],
3658
+ };
3659
+ }
3660
+ if (normalized.includes("multiple source grants found")) {
3661
+ return {
3662
+ failure: {
3663
+ class: "source-grant-ambiguous",
3664
+ retryDisposition: "fix-before-retry",
3665
+ hint: "more than one source grant matches; the import needs a narrower owner/source target",
3666
+ },
3667
+ remediation: [
3668
+ "rerun the import with explicit --owner-email and/or --source hints",
3669
+ "confirm which delegated lane should receive this archive before retrying",
3670
+ ],
3671
+ };
3672
+ }
3673
+ if (normalized.includes("could not discover an mbox file")) {
3674
+ return {
3675
+ failure: {
3676
+ class: "archive-discovery",
3677
+ retryDisposition: "fix-before-retry",
3678
+ hint: "no matching local archive was discovered in the known browser/download locations",
3679
+ },
3680
+ remediation: [
3681
+ "check the recent browser/download artifact locations or pass --file with the exact archive path",
3682
+ "retry only after the correct archive is visible locally",
3683
+ ],
3684
+ };
3685
+ }
3686
+ if (normalized.includes("multiple candidate mbox files found")) {
3687
+ return {
3688
+ failure: {
3689
+ class: "archive-ambiguity",
3690
+ retryDisposition: "fix-before-retry",
3691
+ hint: "more than one plausible archive matches this import request",
3692
+ },
3693
+ remediation: [
3694
+ "rerun with --file <path> or narrower owner/source hints so the archive choice is unambiguous",
3695
+ "retry after confirming which archive is the intended one",
3696
+ ],
3697
+ };
3698
+ }
3699
+ if (normalized.includes("no such file:") || normalized.includes("enoent")) {
3700
+ return {
3701
+ failure: {
3702
+ class: "archive-missing",
3703
+ retryDisposition: "fix-before-retry",
3704
+ hint: "the chosen archive path is not readable on disk anymore",
3705
+ },
3706
+ remediation: [
3707
+ "re-check the local archive path or re-download the export before retrying",
3708
+ "retry after the archive exists locally again",
3709
+ ],
3710
+ };
3711
+ }
3712
+ if (normalized.includes("permission denied") || normalized.includes("eacces") || normalized.includes("eperm")) {
3713
+ return {
3714
+ failure: {
3715
+ class: "archive-access",
3716
+ retryDisposition: "fix-before-retry",
3717
+ hint: "the archive or backing store could not be read with current filesystem permissions",
3718
+ },
3719
+ remediation: [
3720
+ "repair filesystem access to the archive or backing store, then rerun the import",
3721
+ "retry after confirming the path is readable by Ouro",
3722
+ ],
3723
+ };
3724
+ }
3725
+ if (normalized.includes("timed out")
3726
+ || normalized.includes("socket closed early")
3727
+ || normalized.includes("econnreset")
3728
+ || normalized.includes("eai_again")) {
3729
+ return {
3730
+ failure: {
3731
+ class: "transient-storage-read",
3732
+ retryDisposition: "retry-safe",
3733
+ hint: "likely transient hosted read failure",
3734
+ },
3735
+ remediation: [
3736
+ "retry the import from the same archive after the transient storage/network issue clears",
3737
+ "use query_active_work to confirm the failed operation has settled before retrying",
3738
+ ],
3739
+ };
3740
+ }
3741
+ return {
3742
+ failure: {
3743
+ class: "unknown-mail-import-failure",
3744
+ retryDisposition: "investigate-first",
3745
+ hint: "the import failed for a reason that does not match a known recovery class yet",
3746
+ },
3747
+ remediation: [
3748
+ "inspect the recorded error and surrounding Mailroom/runtime state before retrying",
3749
+ "retry only after the likely cause is understood or ruled out",
3750
+ ],
3751
+ };
3752
+ }
3611
3753
  function matchingMailImportOperation(record, command, filePath) {
3612
3754
  if (record.kind !== "mail.import-mbox")
3613
3755
  return false;
@@ -3683,8 +3825,8 @@ function resolveMailImportFilePath(command, deps) {
3683
3825
  });
3684
3826
  }
3685
3827
  async function executeMailImportMbox(command, deps) {
3686
- const filePath = resolveMailImportFilePath(command, deps);
3687
3828
  if (!command.foreground) {
3829
+ const filePath = resolveMailImportFilePath(command, deps);
3688
3830
  if (!fs.existsSync(filePath)) {
3689
3831
  throw new Error(`no such file: ${filePath}`);
3690
3832
  }
@@ -3707,7 +3849,7 @@ async function executeMailImportMbox(command, deps) {
3707
3849
  ], "mail import", "queued delegated mail import", spec);
3708
3850
  }
3709
3851
  const progress = createHumanCommandProgress(deps, "mail import");
3710
- const spec = buildMailImportOperationSpec(filePath, command.ownerEmail, command.source);
3852
+ let filePath = command.filePath ? path.resolve(command.filePath) : null;
3711
3853
  const trackedOperation = ensureTrackedMailOperation({
3712
3854
  agentName: command.agent,
3713
3855
  deps,
@@ -3715,13 +3857,28 @@ async function executeMailImportMbox(command, deps) {
3715
3857
  kind: "mail.import-mbox",
3716
3858
  title: "mail import",
3717
3859
  queuedSummary: "queued delegated mail import",
3718
- spec,
3860
+ spec: {
3861
+ ...(filePath ? { filePath } : {}),
3862
+ ...(command.ownerEmail ? { ownerEmail: command.ownerEmail } : {}),
3863
+ ...(command.source ? { source: command.source } : {}),
3864
+ ...(command.discover ? { discovery: true } : {}),
3865
+ },
3719
3866
  });
3720
3867
  try {
3868
+ trackedOperation?.running("resolving mail archive", filePath ? `file: ${filePath}` : "mode: discover", {
3869
+ current: 0,
3870
+ unit: "messages",
3871
+ });
3872
+ filePath = resolveMailImportFilePath(command, deps);
3873
+ const spec = buildMailImportOperationSpec(filePath, command.ownerEmail, command.source);
3721
3874
  trackedOperation?.running("reading Mailroom config", `file: ${filePath}`, {
3722
3875
  current: 0,
3723
3876
  unit: "messages",
3724
3877
  });
3878
+ trackedOperation?.update("reading Mailroom config", `file: ${filePath}`, {
3879
+ current: 0,
3880
+ unit: "messages",
3881
+ }, spec);
3725
3882
  progress.startPhase("reading Mailroom config");
3726
3883
  const runtime = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(command.agent, { preserveCachedOnFailure: true });
3727
3884
  if (!runtime.ok) {
@@ -3799,10 +3956,15 @@ async function executeMailImportMbox(command, deps) {
3799
3956
  }
3800
3957
  catch (error) {
3801
3958
  progress.end();
3802
- await trackedOperation?.fail(error, "delegated mail import failed", `file: ${filePath}`, [
3803
- "fix the reported Mailroom or MBOX problem, then rerun the import",
3804
- "use query_active_work to confirm whether the operation has cleared",
3805
- ]);
3959
+ const classifiedFailure = classifyMailImportFailure(error);
3960
+ const failureDetail = filePath
3961
+ ? `file: ${filePath}`
3962
+ : [
3963
+ "mode: discover",
3964
+ ...(command.ownerEmail ? [`owner: ${command.ownerEmail}`] : []),
3965
+ ...(command.source ? [`source: ${command.source}`] : []),
3966
+ ].join("; ");
3967
+ await trackedOperation?.fail(error, "delegated mail import failed", failureDetail, classifiedFailure.remediation, classifiedFailure.failure);
3806
3968
  throw error;
3807
3969
  }
3808
3970
  }
@@ -111,6 +111,26 @@ function listChildDirs(dir) {
111
111
  return [];
112
112
  }
113
113
  }
114
+ function classifyDiscoveredMboxOrigin(dir) {
115
+ const normalizedDir = path.resolve(dir);
116
+ const parts = normalizedDir.split(path.sep).filter(Boolean);
117
+ if (parts.includes(".playwright-mcp")) {
118
+ return {
119
+ originKind: "playwright-sandbox",
120
+ originLabel: "browser sandbox (.playwright-mcp)",
121
+ };
122
+ }
123
+ if (path.basename(normalizedDir) === "Downloads") {
124
+ return {
125
+ originKind: "downloads",
126
+ originLabel: "Downloads",
127
+ };
128
+ }
129
+ return {
130
+ originKind: "filesystem",
131
+ originLabel: "filesystem",
132
+ };
133
+ }
114
134
  function findWorktreePools(rootDir, maxDepth) {
115
135
  const seen = new Set();
116
136
  const found = [];
@@ -176,12 +196,19 @@ function listDiscoveredMboxCandidates(dir) {
176
196
  if (!fs.existsSync(dir))
177
197
  return [];
178
198
  try {
199
+ const origin = classifyDiscoveredMboxOrigin(dir);
179
200
  return fs.readdirSync(dir, { withFileTypes: true })
180
201
  .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".mbox"))
181
202
  .map((entry) => {
182
203
  const candidatePath = path.join(dir, entry.name);
183
204
  const stat = fs.statSync(candidatePath);
184
- return { path: candidatePath, name: entry.name, mtimeMs: stat.mtimeMs };
205
+ return {
206
+ path: candidatePath,
207
+ name: entry.name,
208
+ mtimeMs: stat.mtimeMs,
209
+ originKind: origin.originKind,
210
+ originLabel: origin.originLabel,
211
+ };
185
212
  });
186
213
  }
187
214
  catch {
@@ -244,7 +271,7 @@ function summarizeAmbientImportCandidates(candidates, candidateLimit) {
244
271
  : `${visibleCandidates.length} recent MBOX archives ready for import`;
245
272
  const detailLines = [
246
273
  "recent candidates:",
247
- ...visibleCandidates.map((candidate) => `- ${candidate.path}`),
274
+ ...visibleCandidates.map((candidate) => `- [${candidate.originLabel}] ${candidate.path}`),
248
275
  ...(hiddenCount > 0 ? [`- ...and ${hiddenCount} more recent archive${hiddenCount === 1 ? "" : "s"}`] : []),
249
276
  "next: if one matches an outstanding mail backfill, run `ouro mail import-mbox --discover` with owner/source hints so Ouro can select the right archive or report ambiguity.",
250
277
  ];
@@ -254,8 +281,16 @@ function summarizeAmbientImportCandidates(candidates, candidateLimit) {
254
281
  spec: {
255
282
  fingerprint: recentImportFingerprint(candidates),
256
283
  candidatePaths: visibleCandidates.map((candidate) => candidate.path),
284
+ candidateDescriptors: visibleCandidates.map((candidate) => ({
285
+ path: candidate.path,
286
+ originKind: candidate.originKind,
287
+ originLabel: candidate.originLabel,
288
+ modifiedAt: new Date(candidate.mtimeMs).toISOString(),
289
+ })),
257
290
  newestCandidatePath: visibleCandidates[0]?.path ?? null,
258
291
  newestCandidateMtime: visibleCandidates[0] ? new Date(visibleCandidates[0].mtimeMs).toISOString() : null,
292
+ newestCandidateOriginKind: visibleCandidates[0]?.originKind ?? null,
293
+ newestCandidateOriginLabel: visibleCandidates[0]?.originLabel ?? null,
259
294
  },
260
295
  };
261
296
  }
@@ -120,7 +120,7 @@ function createAzureProviderRuntime(model, azureConfig = (0, config_1.getAzureCo
120
120
  appendToolOutput(callId, output) {
121
121
  if (!nativeInput)
122
122
  return;
123
- nativeInput.push({ type: "function_call_output", call_id: callId, output });
123
+ nativeInput.push({ type: "function_call_output", call_id: callId, output: (0, streaming_1.truncateResponsesFunctionCallOutput)(output) });
124
124
  },
125
125
  async streamTurn(request) {
126
126
  if (!nativeInput)
@@ -102,7 +102,7 @@ function createGithubCopilotProviderRuntime(model, config = (0, config_1.getGith
102
102
  appendToolOutput(callId, output) {
103
103
  if (!nativeInput)
104
104
  return;
105
- nativeInput.push({ type: "function_call_output", call_id: callId, output });
105
+ nativeInput.push({ type: "function_call_output", call_id: callId, output: (0, streaming_1.truncateResponsesFunctionCallOutput)(output) });
106
106
  },
107
107
  async streamTurn(request) {
108
108
  if (!nativeInput)
@@ -154,7 +154,7 @@ function createOpenAICodexProviderRuntime(model, codexConfig = (0, config_1.getO
154
154
  appendToolOutput(callId, output) {
155
155
  if (!nativeInput)
156
156
  return;
157
- nativeInput.push({ type: "function_call_output", call_id: callId, output });
157
+ nativeInput.push({ type: "function_call_output", call_id: callId, output: (0, streaming_1.truncateResponsesFunctionCallOutput)(output) });
158
158
  },
159
159
  async streamTurn(request) {
160
160
  if (!nativeInput)
@@ -254,6 +254,35 @@ function messageFingerprint(message) {
254
254
  function makeEventId(sequence) {
255
255
  return `evt-${String(sequence).padStart(6, "0")}`;
256
256
  }
257
+ /**
258
+ * Collapse duplicate event ids to a single entry, last-occurrence-wins.
259
+ *
260
+ * Concurrent writers in older versions of postTurnPersist could each load the
261
+ * envelope, compute `events.length + 1` for the next sequence, and both write
262
+ * an event with the same id. The duplicates would persist in the saved JSON
263
+ * and confuse downstream replay (the same outbound message could appear to
264
+ * have been sent twice from the agent's perspective without the agent knowing
265
+ * it sent it). We dedupe defensively on every load so corrupted sessions
266
+ * self-heal on the next save and so any future race produces a consistent
267
+ * view.
268
+ */
269
+ function dedupeEventsByIdLastWins(events) {
270
+ // Index id → last position so we can preserve original order while
271
+ // collapsing duplicates to their final occurrence.
272
+ const lastIndexById = new Map();
273
+ for (let i = 0; i < events.length; i++) {
274
+ lastIndexById.set(events[i].id, i);
275
+ }
276
+ return events.filter((event, index) => lastIndexById.get(event.id) === index);
277
+ }
278
+ /**
279
+ * The next sequence to assign for a freshly-built event. Uses max(existing
280
+ * sequences) + 1 rather than `events.length + 1` so that gaps from earlier
281
+ * pruning, archive replay, or self-heal dedup never produce a colliding id.
282
+ */
283
+ function nextEventSequence(existing) {
284
+ return existing.reduce((max, event) => Math.max(max, event.sequence), 0) + 1;
285
+ }
257
286
  function validateSessionMessages(messages) {
258
287
  const violations = [];
259
288
  let prevNonToolRole = null;
@@ -665,7 +694,7 @@ function parseSessionEnvelope(raw, options = {}) {
665
694
  if (record.version !== 2 || !Array.isArray(record.events) || !record.projection || typeof record.projection !== "object") {
666
695
  return null;
667
696
  }
668
- const events = record.events
697
+ const rawEvents = record.events
669
698
  .filter((event) => event != null && typeof event === "object")
670
699
  .map((event, index) => {
671
700
  const role = normalizeRole(event.role);
@@ -705,6 +734,12 @@ function parseSessionEnvelope(raw, options = {}) {
705
734
  },
706
735
  };
707
736
  });
737
+ // Self-heal duplicate event ids that may have been written by concurrent
738
+ // writers in older harness versions. Last-occurrence-wins by id (later
739
+ // entries in the persisted file are the more recent state for that id).
740
+ // We preserve the original document order otherwise, so projection.eventIds
741
+ // still resolves predictably.
742
+ const events = dedupeEventsByIdLastWins(rawEvents);
708
743
  const projection = record.projection;
709
744
  return {
710
745
  version: 2,
@@ -821,8 +856,10 @@ function buildCanonicalSessionEnvelope(options) {
821
856
  else {
822
857
  if (!isSystem)
823
858
  nonSystemSeen++;
824
- // Create a new event
825
- const event = buildEventFromMessage(currentMessages[i], events.length + 1, options.recordedAt, "live", null, null, currentIngressTimes[i]);
859
+ // Create a new event. Use nextEventSequence(events) instead of
860
+ // `events.length + 1` so that any gap (from pruning, archive replay,
861
+ // or self-heal dedup) cannot collide with an existing id.
862
+ const event = buildEventFromMessage(currentMessages[i], nextEventSequence(events), options.recordedAt, "live", null, null, currentIngressTimes[i]);
826
863
  events.push(event);
827
864
  currentEventIds.push(event.id);
828
865
  }