@indigoai-us/hq-cloud 6.15.6 → 6.15.7

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.
@@ -1152,6 +1152,108 @@ describe("fanout-plan", () => {
1152
1152
  expect(plan.companies).toEqual([{ uid: "cmp_empty", slug: "cmp_empty" }]);
1153
1153
  });
1154
1154
  });
1155
+ describe("automatic journal maintenance", () => {
1156
+ it("repairs every resolved journal shard before fanout and streams known heartbeat events", async () => {
1157
+ const order = [];
1158
+ const repairJournalStateIfNeeded = vi.fn().mockImplementation((slug, options) => {
1159
+ order.push(`repair:${slug}`);
1160
+ options.onProgress?.({ bytesRead: 13, totalBytes: 26 });
1161
+ options.onProgress?.({ bytesRead: 26, totalBytes: 26 });
1162
+ return { status: "repaired", slug };
1163
+ });
1164
+ const deps = makeDeps({
1165
+ createVaultClient: () => makeVaultStub({
1166
+ memberships: [{ companyUid: "cmp_indigo" }],
1167
+ entityGet: (uid) => Promise.resolve({ uid, slug: "indigo" }),
1168
+ listPersons: () => Promise.resolve([
1169
+ {
1170
+ uid: "prs_me",
1171
+ slug: "me",
1172
+ type: "person",
1173
+ status: "active",
1174
+ bucketName: "hq-vault-prs-me",
1175
+ createdAt: "2026-01-01T00:00:00Z",
1176
+ },
1177
+ ]),
1178
+ }),
1179
+ repairJournalStateIfNeeded,
1180
+ sync: vi.fn().mockImplementation(async (options) => {
1181
+ order.push(`sync:${options.journalSlug ?? "indigo"}`);
1182
+ return defaultSyncResult();
1183
+ }),
1184
+ });
1185
+ expect(await runRunner(["--companies"], deps)).toBe(0);
1186
+ expect(repairJournalStateIfNeeded.mock.calls.map(([slug]) => slug)).toEqual([
1187
+ "indigo",
1188
+ PERSONAL_VAULT_JOURNAL_SLUG,
1189
+ "personal",
1190
+ ]);
1191
+ expect(order.slice(0, 3)).toEqual([
1192
+ "repair:indigo",
1193
+ `repair:${PERSONAL_VAULT_JOURNAL_SLUG}`,
1194
+ "repair:personal",
1195
+ ]);
1196
+ expect(order[3]).toBe("sync:indigo");
1197
+ expect(deps.stdout.events().filter((event) => event.type === "maintenance-progress")).toEqual([
1198
+ {
1199
+ type: "maintenance-progress",
1200
+ company: "indigo",
1201
+ bytesProcessed: 13,
1202
+ totalBytes: 26,
1203
+ },
1204
+ {
1205
+ type: "maintenance-progress",
1206
+ company: "indigo",
1207
+ bytesProcessed: 26,
1208
+ totalBytes: 26,
1209
+ },
1210
+ {
1211
+ type: "maintenance-progress",
1212
+ company: "personal",
1213
+ bytesProcessed: 13,
1214
+ totalBytes: 26,
1215
+ },
1216
+ {
1217
+ type: "maintenance-progress",
1218
+ company: "personal",
1219
+ bytesProcessed: 26,
1220
+ totalBytes: 26,
1221
+ },
1222
+ {
1223
+ type: "maintenance-progress",
1224
+ company: "personal",
1225
+ bytesProcessed: 13,
1226
+ totalBytes: 26,
1227
+ },
1228
+ {
1229
+ type: "maintenance-progress",
1230
+ company: "personal",
1231
+ bytesProcessed: 26,
1232
+ totalBytes: 26,
1233
+ },
1234
+ ]);
1235
+ });
1236
+ it("fails before network work when authoritative local journal recovery fails", async () => {
1237
+ const sync = vi.fn().mockResolvedValue(defaultSyncResult());
1238
+ const deps = makeDeps({
1239
+ createVaultClient: () => makeVaultStub({
1240
+ memberships: [{ companyUid: "cmp_indigo" }],
1241
+ entityGet: (uid) => Promise.resolve({ uid, slug: "indigo" }),
1242
+ }),
1243
+ repairJournalStateIfNeeded: () => {
1244
+ throw new Error("newest journal generation is unreadable");
1245
+ },
1246
+ sync,
1247
+ });
1248
+ expect(await runRunner(["--companies"], deps)).toBe(1);
1249
+ expect(sync).not.toHaveBeenCalled();
1250
+ expect(deps.stderr.events()).toContainEqual({
1251
+ type: "error",
1252
+ path: "(local-state)",
1253
+ message: "newest journal generation is unreadable",
1254
+ });
1255
+ });
1256
+ });
1155
1257
  // ---------------------------------------------------------------------------
1156
1258
  // per-company event tagging
1157
1259
  // ---------------------------------------------------------------------------
@@ -3334,6 +3436,9 @@ function makeWatcherStub() {
3334
3436
  };
3335
3437
  return stub;
3336
3438
  }
3439
+ function completedPassOutcome(exitCode = 0) {
3440
+ return { exitCode, result: { pushPathResults: [] } };
3441
+ }
3337
3442
  async function flushLoopMicrotasks(n = 8) {
3338
3443
  for (let i = 0; i < n; i++)
3339
3444
  await Promise.resolve();
@@ -3490,10 +3595,164 @@ describe("runRunnerWithLoop — adaptive poll interval (no --poll-remote-ms)", (
3490
3595
  });
3491
3596
  });
3492
3597
  describe("runRunnerWithLoop — event-push wiring", () => {
3598
+ it("does not start journal-reading watcher callbacks before the initial pass finishes", async () => {
3599
+ const watcher = makeWatcherStub();
3600
+ let maintenanceComplete = false;
3601
+ let callbackObservedMaintenance = false;
3602
+ let triggerShutdown = () => { };
3603
+ let releaseInitialPass = () => { };
3604
+ const initialPass = new Promise((resolve) => {
3605
+ releaseInitialPass = resolve;
3606
+ });
3607
+ const runPass = vi.fn().mockImplementation(async () => {
3608
+ await initialPass;
3609
+ maintenanceComplete = true;
3610
+ return completedPassOutcome();
3611
+ });
3612
+ const createWatcher = vi.fn((opts) => {
3613
+ watcher.start = () => {
3614
+ watcher.started = true;
3615
+ callbackObservedMaintenance = maintenanceComplete;
3616
+ opts.captureLocalDeleteSnapshots("companies/indigo/knowledge/deleted.md", "unlink");
3617
+ };
3618
+ return watcher;
3619
+ });
3620
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3621
+ runPass,
3622
+ createWatcher: createWatcher,
3623
+ sleep: () => new Promise(() => { }),
3624
+ onShutdownSignal: (handler) => {
3625
+ triggerShutdown = handler;
3626
+ return () => { };
3627
+ },
3628
+ });
3629
+ await flushLoopMicrotasks();
3630
+ expect(runPass).toHaveBeenCalledTimes(1);
3631
+ expect(createWatcher).not.toHaveBeenCalled();
3632
+ expect(watcher.started).toBe(false);
3633
+ releaseInitialPass();
3634
+ await flushLoopMicrotasks();
3635
+ expect(createWatcher).toHaveBeenCalledTimes(1);
3636
+ expect(watcher.started).toBe(true);
3637
+ expect(callbackObservedMaintenance).toBe(true);
3638
+ triggerShutdown();
3639
+ await loop;
3640
+ });
3641
+ it("keeps every event surface off until a result proves maintenance completed", async () => {
3642
+ const watcher = makeWatcherStub();
3643
+ const sleep = makeSteppableSleep();
3644
+ let triggerShutdown = () => { };
3645
+ const runPass = vi.fn()
3646
+ .mockResolvedValueOnce({ exitCode: TRANSIENT_NETWORK_EXIT })
3647
+ .mockResolvedValueOnce(completedPassOutcome());
3648
+ const createWatcher = vi.fn(() => watcher);
3649
+ const receiver = {
3650
+ connected: false,
3651
+ start: vi.fn().mockResolvedValue(undefined),
3652
+ dispose: vi.fn().mockResolvedValue(undefined),
3653
+ };
3654
+ const createReceiver = vi.fn(() => receiver);
3655
+ const scheduler = {
3656
+ signal: vi.fn(),
3657
+ checkHighWater: vi.fn().mockResolvedValue(undefined),
3658
+ dispose: vi.fn().mockResolvedValue(undefined),
3659
+ };
3660
+ const startRealtimeScheduler = vi.fn().mockResolvedValue(scheduler);
3661
+ const startEventSync = vi.fn().mockResolvedValue(null);
3662
+ const priorEventSyncOverride = process.env.HQ_SYNC_EVENT_SYNC;
3663
+ process.env.HQ_SYNC_EVENT_SYNC = "1";
3664
+ try {
3665
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3666
+ runPass,
3667
+ createWatcher,
3668
+ createReceiver,
3669
+ startRealtimeScheduler,
3670
+ startEventSync,
3671
+ getIdTokenClaims: () => null,
3672
+ getAccessToken: async () => "jwt-test",
3673
+ sleep: sleep.sleep,
3674
+ onShutdownSignal: (handler) => {
3675
+ triggerShutdown = handler;
3676
+ return () => { };
3677
+ },
3678
+ });
3679
+ await flushLoopMicrotasks();
3680
+ expect(runPass).toHaveBeenCalledTimes(1);
3681
+ expect(createWatcher).not.toHaveBeenCalled();
3682
+ expect(createReceiver).not.toHaveBeenCalled();
3683
+ expect(startRealtimeScheduler).not.toHaveBeenCalled();
3684
+ expect(startEventSync).not.toHaveBeenCalled();
3685
+ sleep.tick();
3686
+ await flushLoopMicrotasks();
3687
+ expect(runPass).toHaveBeenCalledTimes(2);
3688
+ expect(createWatcher).toHaveBeenCalledTimes(1);
3689
+ expect(createReceiver).toHaveBeenCalledTimes(1);
3690
+ expect(receiver.start).toHaveBeenCalledTimes(1);
3691
+ expect(startRealtimeScheduler).toHaveBeenCalledTimes(1);
3692
+ expect(startEventSync).toHaveBeenCalledTimes(1);
3693
+ expect(watcher.started).toBe(true);
3694
+ triggerShutdown();
3695
+ await loop;
3696
+ }
3697
+ finally {
3698
+ if (priorEventSyncOverride === undefined) {
3699
+ delete process.env.HQ_SYNC_EVENT_SYNC;
3700
+ }
3701
+ else {
3702
+ process.env.HQ_SYNC_EVENT_SYNC = priorEventSyncOverride;
3703
+ }
3704
+ }
3705
+ });
3706
+ it("rejects paused-company delete snapshots before opening their journal", async () => {
3707
+ const watcher = makeWatcherStub();
3708
+ let capture;
3709
+ let triggerShutdown = () => { };
3710
+ const priorSkipCompanies = process.env.HQ_SYNC_SKIP_COMPANIES;
3711
+ process.env.HQ_SYNC_SKIP_COMPANIES = "indigo";
3712
+ const stateDir = process.env.HQ_STATE_DIR;
3713
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3714
+ runPass: vi.fn().mockResolvedValue(completedPassOutcome()),
3715
+ createWatcher: (options) => {
3716
+ capture = options.captureLocalDeleteSnapshots;
3717
+ return watcher;
3718
+ },
3719
+ startEventSync: vi.fn().mockResolvedValue(null),
3720
+ getIdTokenClaims: () => null,
3721
+ getAccessToken: async () => "jwt-test",
3722
+ sleep: () => new Promise(() => { }),
3723
+ onShutdownSignal: (handler) => {
3724
+ triggerShutdown = handler;
3725
+ return () => { };
3726
+ },
3727
+ });
3728
+ try {
3729
+ await flushLoopMicrotasks();
3730
+ expect(watcher.started).toBe(true);
3731
+ expect(capture).toBeTypeOf("function");
3732
+ // Turn the configured state directory into a regular file. Any attempt
3733
+ // to open a journal beneath it now fails with ENOTDIR, so returning an
3734
+ // empty snapshot proves the pause check happens before journal I/O.
3735
+ fs.rmSync(stateDir, { recursive: true, force: true });
3736
+ fs.writeFileSync(stateDir, "not-a-directory");
3737
+ expect(capture("companies/indigo/knowledge/deleted.md", "unlink")).toEqual([]);
3738
+ }
3739
+ finally {
3740
+ triggerShutdown();
3741
+ await loop;
3742
+ fs.rmSync(stateDir, { force: true });
3743
+ fs.mkdirSync(stateDir, { recursive: true });
3744
+ if (priorSkipCompanies === undefined) {
3745
+ delete process.env.HQ_SYNC_SKIP_COMPANIES;
3746
+ }
3747
+ else {
3748
+ process.env.HQ_SYNC_SKIP_COMPANIES = priorSkipCompanies;
3749
+ }
3750
+ }
3751
+ });
3493
3752
  it("starts the watcher alongside the poll loop when --event-push is on", async () => {
3494
3753
  const watcher = makeWatcherStub();
3495
3754
  let triggerShutdown = () => { };
3496
- const runPass = vi.fn().mockResolvedValue(0);
3755
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
3497
3756
  const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3498
3757
  runPass,
3499
3758
  clock: new FakeClock(),
@@ -3518,7 +3777,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3518
3777
  const watcher = makeWatcherStub();
3519
3778
  const clock = new FakeClock();
3520
3779
  let triggerShutdown = () => { };
3521
- const runPass = vi.fn().mockResolvedValue(0);
3780
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
3522
3781
  const sleepCalls = [];
3523
3782
  const loop = runRunnerWithLoop([
3524
3783
  "--companies",
@@ -3556,7 +3815,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3556
3815
  const watcher = makeWatcherStub();
3557
3816
  const clock = new FakeClock();
3558
3817
  let triggerShutdown = () => { };
3559
- const runPass = vi.fn().mockResolvedValue(0);
3818
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
3560
3819
  const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3561
3820
  runPass,
3562
3821
  clock,
@@ -3596,7 +3855,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3596
3855
  if (callCount === 1)
3597
3856
  await firstGate; // hold the first (poll) pass
3598
3857
  active--;
3599
- return 0;
3858
+ return completedPassOutcome();
3600
3859
  });
3601
3860
  const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3602
3861
  runPass,
@@ -3631,7 +3890,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3631
3890
  triggerShutdown();
3632
3891
  await loop;
3633
3892
  });
3634
- it("U07: retains a watcher event raised before the next wait registers, without guarded overlap", async () => {
3893
+ it("U07: drains the first watcher event after activation without guarded overlap", async () => {
3635
3894
  const watcher = makeWatcherStub();
3636
3895
  let triggerShutdown = () => { };
3637
3896
  let releaseFirstPass = () => { };
@@ -3647,7 +3906,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3647
3906
  if (passes === 1)
3648
3907
  await firstPassGate;
3649
3908
  span.exitedAt = process.hrtime.bigint();
3650
- return 0;
3909
+ return completedPassOutcome();
3651
3910
  });
3652
3911
  const loop = runRunnerWithLoop([
3653
3912
  "--companies",
@@ -3668,11 +3927,13 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3668
3927
  });
3669
3928
  await flushLoopMicrotasks();
3670
3929
  expect(spans).toHaveLength(1);
3671
- // This event arrives while the initial guarded pass owns the scheduler,
3672
- // before its next sleep/wait has been registered.
3673
- watcher.emit("companies/indigo/pre-registration.md");
3674
3930
  releaseFirstPass();
3675
3931
  await flushLoopMicrotasks();
3932
+ expect(watcher.started).toBe(true);
3933
+ // Activation happens synchronously after the maintenance-bearing pass,
3934
+ // before the next wait is registered. Its first event must still drain.
3935
+ watcher.emit("companies/indigo/pre-registration.md");
3936
+ await flushLoopMicrotasks();
3676
3937
  expect(spans).toHaveLength(2);
3677
3938
  expect(spans[1].enteredAt >= spans[0].exitedAt).toBe(true);
3678
3939
  triggerShutdown();
@@ -3696,7 +3957,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3696
3957
  if (calls === 1)
3697
3958
  await pollGate;
3698
3959
  active--;
3699
- return 0;
3960
+ return completedPassOutcome();
3700
3961
  });
3701
3962
  const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3702
3963
  runPass,
@@ -3737,7 +3998,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3737
3998
  const poll = { resolve: null };
3738
3999
  const runPass = vi.fn().mockImplementation(async () => {
3739
4000
  passes++;
3740
- return 0;
4001
+ return completedPassOutcome();
3741
4002
  });
3742
4003
  // A sleep we can step: resolve once to allow a second poll pass.
3743
4004
  const sleep = () => new Promise((resolve) => {
@@ -3771,7 +4032,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3771
4032
  const clock = new FakeClock();
3772
4033
  let triggerShutdown = () => { };
3773
4034
  let detached = false;
3774
- const runPass = vi.fn().mockResolvedValue(0);
4035
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
3775
4036
  const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3776
4037
  runPass,
3777
4038
  clock,
@@ -3806,7 +4067,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3806
4067
  onShutdownSignal: () => () => { },
3807
4068
  });
3808
4069
  expect(code).toBe(1);
3809
- expect(watcher.disposed).toBe(true);
4070
+ expect(watcher.disposed).toBe(false);
3810
4071
  });
3811
4072
  it("an exit-0 auth-required pass stops the unattended loop without reporting a crash", async () => {
3812
4073
  const watcher = makeWatcherStub();
@@ -3822,7 +4083,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3822
4083
  expect(code).toBe(0);
3823
4084
  expect(runPass).toHaveBeenCalledTimes(1);
3824
4085
  expect(sleep).not.toHaveBeenCalled();
3825
- expect(watcher.disposed).toBe(true);
4086
+ expect(watcher.disposed).toBe(false);
3826
4087
  });
3827
4088
  it("a TRANSIENT_NETWORK_EXIT poll pass does NOT surface as a crash — the loop retries (HQ-SYNC-1W)", async () => {
3828
4089
  // A transient offline blip must keep the watcher alive: the loop logs and
@@ -3849,7 +4110,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3849
4110
  // …and the transient code was never surfaced as the watcher's exit code.
3850
4111
  expect(code).toBe(1);
3851
4112
  expect(code).not.toBe(TRANSIENT_NETWORK_EXIT);
3852
- expect(watcher.disposed).toBe(true);
4113
+ expect(watcher.disposed).toBe(false);
3853
4114
  });
3854
4115
  it("keeps the watch loop alive when a company leg returns transient exit 75 (HQ-SYNC-X)", async () => {
3855
4116
  const watcher = makeWatcherStub();
@@ -3883,7 +4144,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3883
4144
  expect(transientPass.stderr
3884
4145
  .events()
3885
4146
  .some((event) => event.type === "error" && event.path === "(company)")).toBe(false);
3886
- expect(watcher.disposed).toBe(true);
4147
+ expect(watcher.disposed).toBe(false);
3887
4148
  });
3888
4149
  it("a PARTIAL_SYNC_EXIT poll pass does NOT surface as a crash — the loop retries", async () => {
3889
4150
  // A partial pass (some per-file transfers threw: oversized object on an
@@ -3913,7 +4174,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3913
4174
  // …and the partial code was never surfaced as the watcher's exit code.
3914
4175
  expect(code).toBe(1);
3915
4176
  expect(code).not.toBe(PARTIAL_SYNC_EXIT);
3916
- expect(watcher.disposed).toBe(true);
4177
+ expect(watcher.disposed).toBe(false);
3917
4178
  });
3918
4179
  it("alerts when a partial pass failed before a company's download leg", async () => {
3919
4180
  const watcher = makeWatcherStub();
@@ -3961,7 +4222,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3961
4222
  it("without --event-push, no watcher is created (poll-only safety net)", async () => {
3962
4223
  let triggerShutdown = () => { };
3963
4224
  const createWatcher = vi.fn(() => makeWatcherStub());
3964
- const runPass = vi.fn().mockResolvedValue(0);
4225
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
3965
4226
  const loop = runRunnerWithLoop(["--companies", "--watch"], {
3966
4227
  runPass,
3967
4228
  createWatcher,
@@ -3992,17 +4253,17 @@ describe("runRunnerWithLoop — U58 realtime scheduler guard", () => {
3992
4253
  active += 1;
3993
4254
  maximumActive = Math.max(maximumActive, active);
3994
4255
  passes += 1;
3995
- if (passes === 1)
4256
+ if (passes === 2)
3996
4257
  await pollGate;
3997
4258
  active -= 1;
3998
- return 0;
4259
+ return completedPassOutcome();
3999
4260
  });
4000
4261
  let signal = null;
4001
4262
  const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
4002
4263
  runPass,
4003
4264
  clock: new FakeClock(),
4004
4265
  createWatcher: () => watcher,
4005
- sleep: () => new Promise(() => { }),
4266
+ sleep: makeSteppableSleep().sleep,
4006
4267
  onShutdownSignal: (handler) => {
4007
4268
  triggerShutdown = handler;
4008
4269
  return () => { };
@@ -4022,7 +4283,12 @@ describe("runRunnerWithLoop — U58 realtime scheduler guard", () => {
4022
4283
  dispose: async () => undefined,
4023
4284
  }),
4024
4285
  });
4025
- for (let i = 0; i < 6; i++)
4286
+ for (let i = 0; i < 12; i++)
4287
+ await Promise.resolve();
4288
+ expect(watcher.started).toBe(true);
4289
+ // A second legacy pass owns the guard after activation.
4290
+ watcher.emit("companies/indigo/legacy.md");
4291
+ for (let i = 0; i < 8; i++)
4026
4292
  await Promise.resolve();
4027
4293
  expect(active).toBe(1);
4028
4294
  watcher.emit("companies/indigo/v2.md");
@@ -4054,7 +4320,7 @@ describe("runRunnerWithLoop — U58 realtime scheduler guard", () => {
4054
4320
  passes += 1;
4055
4321
  if (passes === 1)
4056
4322
  await initial;
4057
- return 0;
4323
+ return completedPassOutcome();
4058
4324
  },
4059
4325
  clock: new FakeClock(),
4060
4326
  createWatcher: () => watcher,
@@ -4073,11 +4339,10 @@ describe("runRunnerWithLoop — U58 realtime scheduler guard", () => {
4073
4339
  dispose: async () => undefined,
4074
4340
  }),
4075
4341
  });
4076
- for (let i = 0; i < 6; i++)
4342
+ for (let i = 0; i < 12; i++)
4077
4343
  await Promise.resolve();
4078
4344
  releaseInitial();
4079
- for (let i = 0; i < 6; i++)
4080
- await Promise.resolve();
4345
+ await flushLoopMicrotasks(20);
4081
4346
  expect(sleepState.delays[0]).toBe(300_000);
4082
4347
  clockState.now = 300_000;
4083
4348
  sleepState.resolve?.();
@@ -4133,7 +4398,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4133
4398
  };
4134
4399
  }
4135
4400
  it("startup runs one full reconcile, then watcher edits interrupt the pending wait", async () => {
4136
- const runPass = vi.fn().mockResolvedValue(0);
4401
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4137
4402
  const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
4138
4403
  await flushLoopMicrotasks();
4139
4404
  expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
@@ -4158,7 +4423,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4158
4423
  await loop;
4159
4424
  });
4160
4425
  it("coalesces repeated edits to the same path into one scoped push", async () => {
4161
- const runPass = vi.fn().mockResolvedValue(0);
4426
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4162
4427
  const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
4163
4428
  await flushLoopMicrotasks();
4164
4429
  runPass.mockClear();
@@ -4186,7 +4451,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4186
4451
  await loop;
4187
4452
  });
4188
4453
  it("U08: --companies --skip-personal drops a personal watcher edit before it queues work", async () => {
4189
- const runPass = vi.fn().mockResolvedValue(0);
4454
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4190
4455
  const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
4191
4456
  ...watchArgv,
4192
4457
  "--skip-personal",
@@ -4204,7 +4469,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4204
4469
  await loop;
4205
4470
  });
4206
4471
  it("drains an unlink path through the same scoped tombstone push path", async () => {
4207
- const runPass = vi.fn().mockResolvedValue(0);
4472
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4208
4473
  const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
4209
4474
  await flushLoopMicrotasks();
4210
4475
  runPass.mockClear();
@@ -4226,7 +4491,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4226
4491
  await loop;
4227
4492
  });
4228
4493
  it("omitted --direction is pull-only (parser default): a scoped tick never pushes", async () => {
4229
- const runPass = vi.fn().mockResolvedValue(0);
4494
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4230
4495
  // No --direction flag → parseArgs defaults the run to pull-only. The drain
4231
4496
  // must honor that and NOT run a scoped push. Regression: the loop used to
4232
4497
  // re-parse argv and wrongly default an omitted direction to "both", so a
@@ -4260,7 +4525,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4260
4525
  it("restores failed scoped push paths so the next tick retries them", async () => {
4261
4526
  const runPass = vi
4262
4527
  .fn()
4263
- .mockResolvedValueOnce(0)
4528
+ .mockResolvedValueOnce(completedPassOutcome())
4264
4529
  .mockResolvedValueOnce(1)
4265
4530
  .mockResolvedValueOnce(0)
4266
4531
  .mockResolvedValueOnce(0)
@@ -4303,7 +4568,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4303
4568
  await loop;
4304
4569
  });
4305
4570
  it("empty scoped ticks run only the pull leg", async () => {
4306
- const runPass = vi.fn().mockResolvedValue(0);
4571
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4307
4572
  const { loop, tick, shutdown } = startDrainLoop(runPass);
4308
4573
  await flushLoopMicrotasks();
4309
4574
  runPass.mockClear();
@@ -4315,7 +4580,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4315
4580
  await loop;
4316
4581
  });
4317
4582
  it("personal scoped ticks preserve personal mode for the pull leg", async () => {
4318
- const runPass = vi.fn().mockResolvedValue(0);
4583
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4319
4584
  const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
4320
4585
  "--personal",
4321
4586
  "--watch",
@@ -4349,7 +4614,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4349
4614
  await loop;
4350
4615
  });
4351
4616
  it("single-company scoped ticks preserve company mode for the pull leg", async () => {
4352
- const runPass = vi.fn().mockResolvedValue(0);
4617
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4353
4618
  const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
4354
4619
  "--company",
4355
4620
  "acme",
@@ -4385,7 +4650,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4385
4650
  await loop;
4386
4651
  });
4387
4652
  it("push-only scoped ticks run the scoped push and no pull leg", async () => {
4388
- const runPass = vi.fn().mockResolvedValue(0);
4653
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4389
4654
  const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
4390
4655
  "--companies",
4391
4656
  "--watch",
@@ -4422,7 +4687,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4422
4687
  const watcher = makeBatchWatcherStub();
4423
4688
  const sleep = makeSteppableSleep();
4424
4689
  let triggerShutdown = () => { };
4425
- const runPass = vi.fn().mockResolvedValue(0);
4690
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4426
4691
  const loop = runRunnerWithLoop(watchArgv, {
4427
4692
  runPass,
4428
4693
  clock: new FakeClock(),
@@ -5210,7 +5475,8 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5210
5475
  const UNENROLLED_CLAIMS = { email: "someone@getindigo.ai" };
5211
5476
  function runLoop(opts) {
5212
5477
  const watcher = opts.watcher ?? makeBatchWatcherStub();
5213
- const runPass = opts.runPass ?? vi.fn().mockResolvedValue(0);
5478
+ const runPass = opts.runPass ??
5479
+ vi.fn().mockResolvedValue(completedPassOutcome());
5214
5480
  const clock = new FakeClock();
5215
5481
  const sleep = makeSteppableSleep();
5216
5482
  let triggerShutdown = () => { };
@@ -5304,7 +5570,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5304
5570
  });
5305
5571
  const runPass = vi
5306
5572
  .fn()
5307
- .mockResolvedValueOnce(0)
5573
+ .mockResolvedValueOnce(completedPassOutcome())
5308
5574
  .mockImplementation((passArgv) => passArgv.includes("--company") ? targetedPush : Promise.resolve(0));
5309
5575
  const { loop, watcher, clock, shutdown } = runLoop({
5310
5576
  claims: EMAIL_CLAIMS,
@@ -5349,7 +5615,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5349
5615
  // Initial poll pass succeeds; the targeted pass fails.
5350
5616
  const runPass = vi
5351
5617
  .fn()
5352
- .mockResolvedValueOnce(0)
5618
+ .mockResolvedValueOnce(completedPassOutcome())
5353
5619
  .mockResolvedValue(1);
5354
5620
  const { loop, watcher, clock, tick, shutdown } = runLoop({
5355
5621
  claims: EMAIL_CLAIMS,
@@ -5381,7 +5647,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5381
5647
  ownDeviceId: "dev-test",
5382
5648
  dispose: vi.fn().mockResolvedValue(undefined),
5383
5649
  });
5384
- const runPass = vi.fn().mockResolvedValue(0);
5650
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
5385
5651
  const { loop, watcher, clock, tick, shutdown } = runLoop({
5386
5652
  claims: EMAIL_CLAIMS,
5387
5653
  startEventSync,
@@ -5421,7 +5687,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5421
5687
  });
5422
5688
  const runPass = vi
5423
5689
  .fn()
5424
- .mockResolvedValueOnce(0)
5690
+ .mockResolvedValueOnce(completedPassOutcome())
5425
5691
  .mockResolvedValueOnce({
5426
5692
  exitCode: 0,
5427
5693
  result: {
@@ -5462,7 +5728,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5462
5728
  });
5463
5729
  const runPass = vi
5464
5730
  .fn()
5465
- .mockResolvedValueOnce(0)
5731
+ .mockResolvedValueOnce(completedPassOutcome())
5466
5732
  .mockResolvedValueOnce({
5467
5733
  exitCode: 0,
5468
5734
  result: {
@@ -5503,7 +5769,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5503
5769
  ownDeviceId: "dev-test",
5504
5770
  dispose: vi.fn().mockResolvedValue(undefined),
5505
5771
  });
5506
- const runPass = vi.fn().mockResolvedValue(0);
5772
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
5507
5773
  const { loop, watcher, clock, tick, shutdown } = runLoop({
5508
5774
  claims: EMAIL_CLAIMS,
5509
5775
  startEventSync,
@@ -5580,9 +5846,9 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5580
5846
  let passCount = 0;
5581
5847
  const runPass = vi.fn().mockImplementation(async () => {
5582
5848
  passCount += 1;
5583
- if (passCount === 1)
5849
+ if (passCount === 2)
5584
5850
  await pollGate;
5585
- return 0;
5851
+ return completedPassOutcome();
5586
5852
  });
5587
5853
  const loop = runRunnerWithLoop(
5588
5854
  // `--direction both` so the queued watcher pushes actually run (an omitted
@@ -5619,6 +5885,21 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5619
5885
  await microtasks();
5620
5886
  expect(passCount).toBe(1);
5621
5887
  expect(receiverSync).toBeTruthy();
5888
+ const remotePull = receiverSync({
5889
+ event: {
5890
+ kind: "upsert",
5891
+ relativePath: "companies/acme/remote.md",
5892
+ contentHash: "sha256:remote",
5893
+ mtime: "2026-06-18T12:00:00.000Z",
5894
+ originDeviceId: "peer-device",
5895
+ originTenantId: "tenant-acme",
5896
+ sequenceNumber: 7,
5897
+ eventTimestamp: "2026-06-18T12:00:00.000Z",
5898
+ },
5899
+ signal: new AbortController().signal,
5900
+ });
5901
+ await microtasks();
5902
+ expect(passCount).toBe(2);
5622
5903
  const indigoBatch = {
5623
5904
  paths: new Map([
5624
5905
  ["/tmp/hq/companies/indigo/a.md", "companies/indigo/a.md"],
@@ -5635,20 +5916,6 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5635
5916
  watcher.emit("companies/beta/b.md", betaBatch);
5636
5917
  clock.advance(0);
5637
5918
  await microtasks();
5638
- const remotePull = receiverSync({
5639
- event: {
5640
- kind: "upsert",
5641
- relativePath: "companies/acme/remote.md",
5642
- contentHash: "sha256:remote",
5643
- mtime: "2026-06-18T12:00:00.000Z",
5644
- originDeviceId: "peer-device",
5645
- originTenantId: "tenant-acme",
5646
- sequenceNumber: 7,
5647
- eventTimestamp: "2026-06-18T12:00:00.000Z",
5648
- },
5649
- signal: new AbortController().signal,
5650
- });
5651
- await microtasks();
5652
5919
  releasePoll();
5653
5920
  await remotePull;
5654
5921
  await microtasks();
@@ -6622,7 +6889,7 @@ describe("watcher delete intents without pre-seeded authorization", () => {
6622
6889
  const initialPullGate = new Promise((resolve) => {
6623
6890
  releaseInitialPull = resolve;
6624
6891
  });
6625
- let initialPullGated = options.gateInitialPull === true;
6892
+ let pullPasses = 0;
6626
6893
  const runPass = vi.fn(async (argv) => {
6627
6894
  // Lets a test hold a pass mid-flight and act while the loop is awaiting
6628
6895
  // it, which is where producer-side state that outlives a pass is decided.
@@ -6673,8 +6940,8 @@ describe("watcher delete intents without pre-seeded authorization", () => {
6673
6940
  return { exitCode: 0, result: { pushPathResults: pathResults } };
6674
6941
  }
6675
6942
  if (direction === "pull" || direction === "both") {
6676
- if (initialPullGated) {
6677
- initialPullGated = false;
6943
+ pullPasses += 1;
6944
+ if (options.gateNextPull === true && pullPasses === 2) {
6678
6945
  await initialPullGate;
6679
6946
  }
6680
6947
  const journal = readJournal("indigo");
@@ -6694,7 +6961,7 @@ describe("watcher delete intents without pre-seeded authorization", () => {
6694
6961
  downloads++;
6695
6962
  }
6696
6963
  }
6697
- return 0;
6964
+ return completedPassOutcome();
6698
6965
  });
6699
6966
  const loop = runRunnerWithLoop([
6700
6967
  "--companies", "--watch", "--event-push",
@@ -6761,7 +7028,9 @@ describe("watcher delete intents without pre-seeded authorization", () => {
6761
7028
  await stopHarness(h);
6762
7029
  });
6763
7030
  it("persists the intent before an in-flight pull plans the missing path", async () => {
6764
- const h = startDeleteHarness(["knowledge/in-flight.md"], { gateInitialPull: true });
7031
+ const h = startDeleteHarness(["knowledge/in-flight.md"], { gateNextPull: true });
7032
+ await settle();
7033
+ h.tick();
6765
7034
  await settle();
6766
7035
  const relativePath = "companies/indigo/knowledge/in-flight.md";
6767
7036
  const absolutePath = path.join(h.companyRoot, "knowledge/in-flight.md");
@@ -6907,7 +7176,16 @@ describe("watcher delete intents without pre-seeded authorization", () => {
6907
7176
  "--poll-remote-ms",
6908
7177
  "60000",
6909
7178
  ], {
6910
- runPass: (passArgv) => runRunner(passArgv, runnerDeps),
7179
+ runPass: async (passArgv) => {
7180
+ let result;
7181
+ const exitCode = await runRunner(passArgv, {
7182
+ ...runnerDeps,
7183
+ onPassResult: (passResult) => {
7184
+ result = passResult;
7185
+ },
7186
+ });
7187
+ return { exitCode, ...(result ? { result } : {}) };
7188
+ },
6911
7189
  createWatcher: (options) => {
6912
7190
  capture = options.captureLocalDeleteSnapshots;
6913
7191
  return watcher;