@indigoai-us/hq-cloud 6.15.6 → 6.15.8

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.
Files changed (54) hide show
  1. package/dist/bin/journal-repair.d.ts +12 -0
  2. package/dist/bin/journal-repair.d.ts.map +1 -0
  3. package/dist/bin/journal-repair.js +46 -0
  4. package/dist/bin/journal-repair.js.map +1 -0
  5. package/dist/bin/journal-repair.test.d.ts +2 -0
  6. package/dist/bin/journal-repair.test.d.ts.map +1 -0
  7. package/dist/bin/journal-repair.test.js +53 -0
  8. package/dist/bin/journal-repair.test.js.map +1 -0
  9. package/dist/bin/sync-runner-company.d.ts +2 -0
  10. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  11. package/dist/bin/sync-runner-company.js +2 -0
  12. package/dist/bin/sync-runner-company.js.map +1 -1
  13. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  14. package/dist/bin/sync-runner-watch-loop.js +37 -4
  15. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  16. package/dist/bin/sync-runner.d.ts +12 -1
  17. package/dist/bin/sync-runner.d.ts.map +1 -1
  18. package/dist/bin/sync-runner.js +108 -32
  19. package/dist/bin/sync-runner.js.map +1 -1
  20. package/dist/bin/sync-runner.test.js +416 -66
  21. package/dist/bin/sync-runner.test.js.map +1 -1
  22. package/dist/cli/share.d.ts +7 -1
  23. package/dist/cli/share.d.ts.map +1 -1
  24. package/dist/cli/share.js +132 -11
  25. package/dist/cli/share.js.map +1 -1
  26. package/dist/cli/share.test.js +11 -2
  27. package/dist/cli/share.test.js.map +1 -1
  28. package/dist/cli/sync.d.ts +7 -1
  29. package/dist/cli/sync.d.ts.map +1 -1
  30. package/dist/cli/sync.js +5 -2
  31. package/dist/cli/sync.js.map +1 -1
  32. package/dist/cli/sync.test.js +11 -0
  33. package/dist/cli/sync.test.js.map +1 -1
  34. package/dist/journal.d.ts +34 -0
  35. package/dist/journal.d.ts.map +1 -1
  36. package/dist/journal.js +82 -13
  37. package/dist/journal.js.map +1 -1
  38. package/dist/journal.test.js +117 -1
  39. package/dist/journal.test.js.map +1 -1
  40. package/dist/s3.d.ts +9 -0
  41. package/dist/s3.d.ts.map +1 -1
  42. package/dist/s3.js +19 -0
  43. package/dist/s3.js.map +1 -1
  44. package/dist/s3.symlink-materialize.test.js +26 -1
  45. package/dist/s3.symlink-materialize.test.js.map +1 -1
  46. package/dist/sync/state-store.d.ts +52 -0
  47. package/dist/sync/state-store.d.ts.map +1 -1
  48. package/dist/sync/state-store.js +411 -99
  49. package/dist/sync/state-store.js.map +1 -1
  50. package/dist/sync/state-store.test.js +188 -1
  51. package/dist/sync/state-store.test.js.map +1 -1
  52. package/dist/types.d.ts +8 -0
  53. package/dist/types.d.ts.map +1 -1
  54. package/package.json +2 -1
@@ -1152,6 +1152,180 @@ 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("accepts direct liveness pulses from synchronous engine planning", async () => {
1157
+ vi.useFakeTimers();
1158
+ vi.setSystemTime(0);
1159
+ try {
1160
+ const sync = vi.fn().mockImplementation(async (options) => {
1161
+ // Moving wall time does not run a fake interval. The maintenance event
1162
+ // can only come from the direct planner callback wired into SyncOptions.
1163
+ vi.setSystemTime(30_000);
1164
+ options.onHeartbeat?.();
1165
+ return defaultSyncResult();
1166
+ });
1167
+ const deps = makeDeps({
1168
+ createVaultClient: () => makeVaultStub({
1169
+ memberships: [{ companyUid: "cmp_indigo" }],
1170
+ entityGet: (uid) => Promise.resolve({ uid, slug: "indigo" }),
1171
+ }),
1172
+ repairJournalStateIfNeeded: vi
1173
+ .fn()
1174
+ .mockReturnValue({ status: "not-needed", slug: "indigo" }),
1175
+ fanoutHeartbeatIntervalMs: 30_000,
1176
+ sync,
1177
+ });
1178
+ expect(await runRunner(["--companies", "--skip-personal"], deps)).toBe(0);
1179
+ expect(deps.stdout.events().filter((event) => event.type === "maintenance-progress")).toEqual([
1180
+ {
1181
+ type: "maintenance-progress",
1182
+ company: "indigo",
1183
+ bytesProcessed: 0,
1184
+ totalBytes: 0,
1185
+ },
1186
+ ]);
1187
+ }
1188
+ finally {
1189
+ vi.useRealTimers();
1190
+ }
1191
+ });
1192
+ it("keeps the desktop protocol alive while a long fanout is still reconciling", async () => {
1193
+ vi.useFakeTimers();
1194
+ try {
1195
+ let finishSync;
1196
+ const sync = vi.fn().mockImplementation(() => new Promise((resolve) => {
1197
+ finishSync = resolve;
1198
+ }));
1199
+ const deps = makeDeps({
1200
+ createVaultClient: () => makeVaultStub({
1201
+ memberships: [{ companyUid: "cmp_indigo" }],
1202
+ entityGet: (uid) => Promise.resolve({ uid, slug: "indigo" }),
1203
+ }),
1204
+ repairJournalStateIfNeeded: vi
1205
+ .fn()
1206
+ .mockReturnValue({ status: "not-needed", slug: "indigo" }),
1207
+ fanoutHeartbeatIntervalMs: 30_000,
1208
+ sync,
1209
+ });
1210
+ const running = runRunner(["--companies", "--skip-personal"], deps);
1211
+ await vi.waitFor(() => expect(sync).toHaveBeenCalledTimes(1));
1212
+ await vi.advanceTimersByTimeAsync(30_000);
1213
+ expect(deps.stdout.events().filter((event) => event.type === "maintenance-progress")).toContainEqual({
1214
+ type: "maintenance-progress",
1215
+ company: "indigo",
1216
+ bytesProcessed: 0,
1217
+ totalBytes: 0,
1218
+ });
1219
+ finishSync(defaultSyncResult());
1220
+ await expect(running).resolves.toBe(0);
1221
+ await vi.advanceTimersByTimeAsync(30_000);
1222
+ expect(deps.stdout.events().filter((event) => event.type === "maintenance-progress")).toHaveLength(1);
1223
+ }
1224
+ finally {
1225
+ vi.useRealTimers();
1226
+ }
1227
+ });
1228
+ it("repairs every resolved journal shard before fanout and streams known heartbeat events", async () => {
1229
+ const order = [];
1230
+ const repairJournalStateIfNeeded = vi.fn().mockImplementation((slug, options) => {
1231
+ order.push(`repair:${slug}`);
1232
+ options.onProgress?.({ bytesRead: 13, totalBytes: 26 });
1233
+ options.onProgress?.({ bytesRead: 26, totalBytes: 26 });
1234
+ return { status: "repaired", slug };
1235
+ });
1236
+ const deps = makeDeps({
1237
+ createVaultClient: () => makeVaultStub({
1238
+ memberships: [{ companyUid: "cmp_indigo" }],
1239
+ entityGet: (uid) => Promise.resolve({ uid, slug: "indigo" }),
1240
+ listPersons: () => Promise.resolve([
1241
+ {
1242
+ uid: "prs_me",
1243
+ slug: "me",
1244
+ type: "person",
1245
+ status: "active",
1246
+ bucketName: "hq-vault-prs-me",
1247
+ createdAt: "2026-01-01T00:00:00Z",
1248
+ },
1249
+ ]),
1250
+ }),
1251
+ repairJournalStateIfNeeded,
1252
+ sync: vi.fn().mockImplementation(async (options) => {
1253
+ order.push(`sync:${options.journalSlug ?? "indigo"}`);
1254
+ return defaultSyncResult();
1255
+ }),
1256
+ });
1257
+ expect(await runRunner(["--companies"], deps)).toBe(0);
1258
+ expect(repairJournalStateIfNeeded.mock.calls.map(([slug]) => slug)).toEqual([
1259
+ "indigo",
1260
+ PERSONAL_VAULT_JOURNAL_SLUG,
1261
+ "personal",
1262
+ ]);
1263
+ expect(order.slice(0, 3)).toEqual([
1264
+ "repair:indigo",
1265
+ `repair:${PERSONAL_VAULT_JOURNAL_SLUG}`,
1266
+ "repair:personal",
1267
+ ]);
1268
+ expect(order[3]).toBe("sync:indigo");
1269
+ expect(deps.stdout.events().filter((event) => event.type === "maintenance-progress")).toEqual([
1270
+ {
1271
+ type: "maintenance-progress",
1272
+ company: "indigo",
1273
+ bytesProcessed: 13,
1274
+ totalBytes: 26,
1275
+ },
1276
+ {
1277
+ type: "maintenance-progress",
1278
+ company: "indigo",
1279
+ bytesProcessed: 26,
1280
+ totalBytes: 26,
1281
+ },
1282
+ {
1283
+ type: "maintenance-progress",
1284
+ company: "personal",
1285
+ bytesProcessed: 13,
1286
+ totalBytes: 26,
1287
+ },
1288
+ {
1289
+ type: "maintenance-progress",
1290
+ company: "personal",
1291
+ bytesProcessed: 26,
1292
+ totalBytes: 26,
1293
+ },
1294
+ {
1295
+ type: "maintenance-progress",
1296
+ company: "personal",
1297
+ bytesProcessed: 13,
1298
+ totalBytes: 26,
1299
+ },
1300
+ {
1301
+ type: "maintenance-progress",
1302
+ company: "personal",
1303
+ bytesProcessed: 26,
1304
+ totalBytes: 26,
1305
+ },
1306
+ ]);
1307
+ });
1308
+ it("fails before network work when authoritative local journal recovery fails", async () => {
1309
+ const sync = vi.fn().mockResolvedValue(defaultSyncResult());
1310
+ const deps = makeDeps({
1311
+ createVaultClient: () => makeVaultStub({
1312
+ memberships: [{ companyUid: "cmp_indigo" }],
1313
+ entityGet: (uid) => Promise.resolve({ uid, slug: "indigo" }),
1314
+ }),
1315
+ repairJournalStateIfNeeded: () => {
1316
+ throw new Error("newest journal generation is unreadable");
1317
+ },
1318
+ sync,
1319
+ });
1320
+ expect(await runRunner(["--companies"], deps)).toBe(1);
1321
+ expect(sync).not.toHaveBeenCalled();
1322
+ expect(deps.stderr.events()).toContainEqual({
1323
+ type: "error",
1324
+ path: "(local-state)",
1325
+ message: "newest journal generation is unreadable",
1326
+ });
1327
+ });
1328
+ });
1155
1329
  // ---------------------------------------------------------------------------
1156
1330
  // per-company event tagging
1157
1331
  // ---------------------------------------------------------------------------
@@ -3334,6 +3508,9 @@ function makeWatcherStub() {
3334
3508
  };
3335
3509
  return stub;
3336
3510
  }
3511
+ function completedPassOutcome(exitCode = 0) {
3512
+ return { exitCode, result: { pushPathResults: [] } };
3513
+ }
3337
3514
  async function flushLoopMicrotasks(n = 8) {
3338
3515
  for (let i = 0; i < n; i++)
3339
3516
  await Promise.resolve();
@@ -3490,10 +3667,164 @@ describe("runRunnerWithLoop — adaptive poll interval (no --poll-remote-ms)", (
3490
3667
  });
3491
3668
  });
3492
3669
  describe("runRunnerWithLoop — event-push wiring", () => {
3670
+ it("does not start journal-reading watcher callbacks before the initial pass finishes", async () => {
3671
+ const watcher = makeWatcherStub();
3672
+ let maintenanceComplete = false;
3673
+ let callbackObservedMaintenance = false;
3674
+ let triggerShutdown = () => { };
3675
+ let releaseInitialPass = () => { };
3676
+ const initialPass = new Promise((resolve) => {
3677
+ releaseInitialPass = resolve;
3678
+ });
3679
+ const runPass = vi.fn().mockImplementation(async () => {
3680
+ await initialPass;
3681
+ maintenanceComplete = true;
3682
+ return completedPassOutcome();
3683
+ });
3684
+ const createWatcher = vi.fn((opts) => {
3685
+ watcher.start = () => {
3686
+ watcher.started = true;
3687
+ callbackObservedMaintenance = maintenanceComplete;
3688
+ opts.captureLocalDeleteSnapshots("companies/indigo/knowledge/deleted.md", "unlink");
3689
+ };
3690
+ return watcher;
3691
+ });
3692
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3693
+ runPass,
3694
+ createWatcher: createWatcher,
3695
+ sleep: () => new Promise(() => { }),
3696
+ onShutdownSignal: (handler) => {
3697
+ triggerShutdown = handler;
3698
+ return () => { };
3699
+ },
3700
+ });
3701
+ await flushLoopMicrotasks();
3702
+ expect(runPass).toHaveBeenCalledTimes(1);
3703
+ expect(createWatcher).not.toHaveBeenCalled();
3704
+ expect(watcher.started).toBe(false);
3705
+ releaseInitialPass();
3706
+ await flushLoopMicrotasks();
3707
+ expect(createWatcher).toHaveBeenCalledTimes(1);
3708
+ expect(watcher.started).toBe(true);
3709
+ expect(callbackObservedMaintenance).toBe(true);
3710
+ triggerShutdown();
3711
+ await loop;
3712
+ });
3713
+ it("keeps every event surface off until a result proves maintenance completed", async () => {
3714
+ const watcher = makeWatcherStub();
3715
+ const sleep = makeSteppableSleep();
3716
+ let triggerShutdown = () => { };
3717
+ const runPass = vi.fn()
3718
+ .mockResolvedValueOnce({ exitCode: TRANSIENT_NETWORK_EXIT })
3719
+ .mockResolvedValueOnce(completedPassOutcome());
3720
+ const createWatcher = vi.fn(() => watcher);
3721
+ const receiver = {
3722
+ connected: false,
3723
+ start: vi.fn().mockResolvedValue(undefined),
3724
+ dispose: vi.fn().mockResolvedValue(undefined),
3725
+ };
3726
+ const createReceiver = vi.fn(() => receiver);
3727
+ const scheduler = {
3728
+ signal: vi.fn(),
3729
+ checkHighWater: vi.fn().mockResolvedValue(undefined),
3730
+ dispose: vi.fn().mockResolvedValue(undefined),
3731
+ };
3732
+ const startRealtimeScheduler = vi.fn().mockResolvedValue(scheduler);
3733
+ const startEventSync = vi.fn().mockResolvedValue(null);
3734
+ const priorEventSyncOverride = process.env.HQ_SYNC_EVENT_SYNC;
3735
+ process.env.HQ_SYNC_EVENT_SYNC = "1";
3736
+ try {
3737
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3738
+ runPass,
3739
+ createWatcher,
3740
+ createReceiver,
3741
+ startRealtimeScheduler,
3742
+ startEventSync,
3743
+ getIdTokenClaims: () => null,
3744
+ getAccessToken: async () => "jwt-test",
3745
+ sleep: sleep.sleep,
3746
+ onShutdownSignal: (handler) => {
3747
+ triggerShutdown = handler;
3748
+ return () => { };
3749
+ },
3750
+ });
3751
+ await flushLoopMicrotasks();
3752
+ expect(runPass).toHaveBeenCalledTimes(1);
3753
+ expect(createWatcher).not.toHaveBeenCalled();
3754
+ expect(createReceiver).not.toHaveBeenCalled();
3755
+ expect(startRealtimeScheduler).not.toHaveBeenCalled();
3756
+ expect(startEventSync).not.toHaveBeenCalled();
3757
+ sleep.tick();
3758
+ await flushLoopMicrotasks();
3759
+ expect(runPass).toHaveBeenCalledTimes(2);
3760
+ expect(createWatcher).toHaveBeenCalledTimes(1);
3761
+ expect(createReceiver).toHaveBeenCalledTimes(1);
3762
+ expect(receiver.start).toHaveBeenCalledTimes(1);
3763
+ expect(startRealtimeScheduler).toHaveBeenCalledTimes(1);
3764
+ expect(startEventSync).toHaveBeenCalledTimes(1);
3765
+ expect(watcher.started).toBe(true);
3766
+ triggerShutdown();
3767
+ await loop;
3768
+ }
3769
+ finally {
3770
+ if (priorEventSyncOverride === undefined) {
3771
+ delete process.env.HQ_SYNC_EVENT_SYNC;
3772
+ }
3773
+ else {
3774
+ process.env.HQ_SYNC_EVENT_SYNC = priorEventSyncOverride;
3775
+ }
3776
+ }
3777
+ });
3778
+ it("rejects paused-company delete snapshots before opening their journal", async () => {
3779
+ const watcher = makeWatcherStub();
3780
+ let capture;
3781
+ let triggerShutdown = () => { };
3782
+ const priorSkipCompanies = process.env.HQ_SYNC_SKIP_COMPANIES;
3783
+ process.env.HQ_SYNC_SKIP_COMPANIES = "indigo";
3784
+ const stateDir = process.env.HQ_STATE_DIR;
3785
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3786
+ runPass: vi.fn().mockResolvedValue(completedPassOutcome()),
3787
+ createWatcher: (options) => {
3788
+ capture = options.captureLocalDeleteSnapshots;
3789
+ return watcher;
3790
+ },
3791
+ startEventSync: vi.fn().mockResolvedValue(null),
3792
+ getIdTokenClaims: () => null,
3793
+ getAccessToken: async () => "jwt-test",
3794
+ sleep: () => new Promise(() => { }),
3795
+ onShutdownSignal: (handler) => {
3796
+ triggerShutdown = handler;
3797
+ return () => { };
3798
+ },
3799
+ });
3800
+ try {
3801
+ await flushLoopMicrotasks();
3802
+ expect(watcher.started).toBe(true);
3803
+ expect(capture).toBeTypeOf("function");
3804
+ // Turn the configured state directory into a regular file. Any attempt
3805
+ // to open a journal beneath it now fails with ENOTDIR, so returning an
3806
+ // empty snapshot proves the pause check happens before journal I/O.
3807
+ fs.rmSync(stateDir, { recursive: true, force: true });
3808
+ fs.writeFileSync(stateDir, "not-a-directory");
3809
+ expect(capture("companies/indigo/knowledge/deleted.md", "unlink")).toEqual([]);
3810
+ }
3811
+ finally {
3812
+ triggerShutdown();
3813
+ await loop;
3814
+ fs.rmSync(stateDir, { force: true });
3815
+ fs.mkdirSync(stateDir, { recursive: true });
3816
+ if (priorSkipCompanies === undefined) {
3817
+ delete process.env.HQ_SYNC_SKIP_COMPANIES;
3818
+ }
3819
+ else {
3820
+ process.env.HQ_SYNC_SKIP_COMPANIES = priorSkipCompanies;
3821
+ }
3822
+ }
3823
+ });
3493
3824
  it("starts the watcher alongside the poll loop when --event-push is on", async () => {
3494
3825
  const watcher = makeWatcherStub();
3495
3826
  let triggerShutdown = () => { };
3496
- const runPass = vi.fn().mockResolvedValue(0);
3827
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
3497
3828
  const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3498
3829
  runPass,
3499
3830
  clock: new FakeClock(),
@@ -3518,7 +3849,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3518
3849
  const watcher = makeWatcherStub();
3519
3850
  const clock = new FakeClock();
3520
3851
  let triggerShutdown = () => { };
3521
- const runPass = vi.fn().mockResolvedValue(0);
3852
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
3522
3853
  const sleepCalls = [];
3523
3854
  const loop = runRunnerWithLoop([
3524
3855
  "--companies",
@@ -3556,7 +3887,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3556
3887
  const watcher = makeWatcherStub();
3557
3888
  const clock = new FakeClock();
3558
3889
  let triggerShutdown = () => { };
3559
- const runPass = vi.fn().mockResolvedValue(0);
3890
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
3560
3891
  const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3561
3892
  runPass,
3562
3893
  clock,
@@ -3596,7 +3927,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3596
3927
  if (callCount === 1)
3597
3928
  await firstGate; // hold the first (poll) pass
3598
3929
  active--;
3599
- return 0;
3930
+ return completedPassOutcome();
3600
3931
  });
3601
3932
  const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3602
3933
  runPass,
@@ -3631,7 +3962,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3631
3962
  triggerShutdown();
3632
3963
  await loop;
3633
3964
  });
3634
- it("U07: retains a watcher event raised before the next wait registers, without guarded overlap", async () => {
3965
+ it("U07: drains the first watcher event after activation without guarded overlap", async () => {
3635
3966
  const watcher = makeWatcherStub();
3636
3967
  let triggerShutdown = () => { };
3637
3968
  let releaseFirstPass = () => { };
@@ -3647,7 +3978,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3647
3978
  if (passes === 1)
3648
3979
  await firstPassGate;
3649
3980
  span.exitedAt = process.hrtime.bigint();
3650
- return 0;
3981
+ return completedPassOutcome();
3651
3982
  });
3652
3983
  const loop = runRunnerWithLoop([
3653
3984
  "--companies",
@@ -3668,11 +3999,13 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3668
3999
  });
3669
4000
  await flushLoopMicrotasks();
3670
4001
  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
4002
  releaseFirstPass();
3675
4003
  await flushLoopMicrotasks();
4004
+ expect(watcher.started).toBe(true);
4005
+ // Activation happens synchronously after the maintenance-bearing pass,
4006
+ // before the next wait is registered. Its first event must still drain.
4007
+ watcher.emit("companies/indigo/pre-registration.md");
4008
+ await flushLoopMicrotasks();
3676
4009
  expect(spans).toHaveLength(2);
3677
4010
  expect(spans[1].enteredAt >= spans[0].exitedAt).toBe(true);
3678
4011
  triggerShutdown();
@@ -3696,7 +4029,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3696
4029
  if (calls === 1)
3697
4030
  await pollGate;
3698
4031
  active--;
3699
- return 0;
4032
+ return completedPassOutcome();
3700
4033
  });
3701
4034
  const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3702
4035
  runPass,
@@ -3737,7 +4070,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3737
4070
  const poll = { resolve: null };
3738
4071
  const runPass = vi.fn().mockImplementation(async () => {
3739
4072
  passes++;
3740
- return 0;
4073
+ return completedPassOutcome();
3741
4074
  });
3742
4075
  // A sleep we can step: resolve once to allow a second poll pass.
3743
4076
  const sleep = () => new Promise((resolve) => {
@@ -3771,7 +4104,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3771
4104
  const clock = new FakeClock();
3772
4105
  let triggerShutdown = () => { };
3773
4106
  let detached = false;
3774
- const runPass = vi.fn().mockResolvedValue(0);
4107
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
3775
4108
  const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3776
4109
  runPass,
3777
4110
  clock,
@@ -3806,7 +4139,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3806
4139
  onShutdownSignal: () => () => { },
3807
4140
  });
3808
4141
  expect(code).toBe(1);
3809
- expect(watcher.disposed).toBe(true);
4142
+ expect(watcher.disposed).toBe(false);
3810
4143
  });
3811
4144
  it("an exit-0 auth-required pass stops the unattended loop without reporting a crash", async () => {
3812
4145
  const watcher = makeWatcherStub();
@@ -3822,7 +4155,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3822
4155
  expect(code).toBe(0);
3823
4156
  expect(runPass).toHaveBeenCalledTimes(1);
3824
4157
  expect(sleep).not.toHaveBeenCalled();
3825
- expect(watcher.disposed).toBe(true);
4158
+ expect(watcher.disposed).toBe(false);
3826
4159
  });
3827
4160
  it("a TRANSIENT_NETWORK_EXIT poll pass does NOT surface as a crash — the loop retries (HQ-SYNC-1W)", async () => {
3828
4161
  // A transient offline blip must keep the watcher alive: the loop logs and
@@ -3849,7 +4182,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3849
4182
  // …and the transient code was never surfaced as the watcher's exit code.
3850
4183
  expect(code).toBe(1);
3851
4184
  expect(code).not.toBe(TRANSIENT_NETWORK_EXIT);
3852
- expect(watcher.disposed).toBe(true);
4185
+ expect(watcher.disposed).toBe(false);
3853
4186
  });
3854
4187
  it("keeps the watch loop alive when a company leg returns transient exit 75 (HQ-SYNC-X)", async () => {
3855
4188
  const watcher = makeWatcherStub();
@@ -3883,7 +4216,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3883
4216
  expect(transientPass.stderr
3884
4217
  .events()
3885
4218
  .some((event) => event.type === "error" && event.path === "(company)")).toBe(false);
3886
- expect(watcher.disposed).toBe(true);
4219
+ expect(watcher.disposed).toBe(false);
3887
4220
  });
3888
4221
  it("a PARTIAL_SYNC_EXIT poll pass does NOT surface as a crash — the loop retries", async () => {
3889
4222
  // A partial pass (some per-file transfers threw: oversized object on an
@@ -3913,7 +4246,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3913
4246
  // …and the partial code was never surfaced as the watcher's exit code.
3914
4247
  expect(code).toBe(1);
3915
4248
  expect(code).not.toBe(PARTIAL_SYNC_EXIT);
3916
- expect(watcher.disposed).toBe(true);
4249
+ expect(watcher.disposed).toBe(false);
3917
4250
  });
3918
4251
  it("alerts when a partial pass failed before a company's download leg", async () => {
3919
4252
  const watcher = makeWatcherStub();
@@ -3961,7 +4294,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3961
4294
  it("without --event-push, no watcher is created (poll-only safety net)", async () => {
3962
4295
  let triggerShutdown = () => { };
3963
4296
  const createWatcher = vi.fn(() => makeWatcherStub());
3964
- const runPass = vi.fn().mockResolvedValue(0);
4297
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
3965
4298
  const loop = runRunnerWithLoop(["--companies", "--watch"], {
3966
4299
  runPass,
3967
4300
  createWatcher,
@@ -3992,17 +4325,17 @@ describe("runRunnerWithLoop — U58 realtime scheduler guard", () => {
3992
4325
  active += 1;
3993
4326
  maximumActive = Math.max(maximumActive, active);
3994
4327
  passes += 1;
3995
- if (passes === 1)
4328
+ if (passes === 2)
3996
4329
  await pollGate;
3997
4330
  active -= 1;
3998
- return 0;
4331
+ return completedPassOutcome();
3999
4332
  });
4000
4333
  let signal = null;
4001
4334
  const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
4002
4335
  runPass,
4003
4336
  clock: new FakeClock(),
4004
4337
  createWatcher: () => watcher,
4005
- sleep: () => new Promise(() => { }),
4338
+ sleep: makeSteppableSleep().sleep,
4006
4339
  onShutdownSignal: (handler) => {
4007
4340
  triggerShutdown = handler;
4008
4341
  return () => { };
@@ -4022,7 +4355,12 @@ describe("runRunnerWithLoop — U58 realtime scheduler guard", () => {
4022
4355
  dispose: async () => undefined,
4023
4356
  }),
4024
4357
  });
4025
- for (let i = 0; i < 6; i++)
4358
+ for (let i = 0; i < 12; i++)
4359
+ await Promise.resolve();
4360
+ expect(watcher.started).toBe(true);
4361
+ // A second legacy pass owns the guard after activation.
4362
+ watcher.emit("companies/indigo/legacy.md");
4363
+ for (let i = 0; i < 8; i++)
4026
4364
  await Promise.resolve();
4027
4365
  expect(active).toBe(1);
4028
4366
  watcher.emit("companies/indigo/v2.md");
@@ -4054,7 +4392,7 @@ describe("runRunnerWithLoop — U58 realtime scheduler guard", () => {
4054
4392
  passes += 1;
4055
4393
  if (passes === 1)
4056
4394
  await initial;
4057
- return 0;
4395
+ return completedPassOutcome();
4058
4396
  },
4059
4397
  clock: new FakeClock(),
4060
4398
  createWatcher: () => watcher,
@@ -4073,11 +4411,10 @@ describe("runRunnerWithLoop — U58 realtime scheduler guard", () => {
4073
4411
  dispose: async () => undefined,
4074
4412
  }),
4075
4413
  });
4076
- for (let i = 0; i < 6; i++)
4414
+ for (let i = 0; i < 12; i++)
4077
4415
  await Promise.resolve();
4078
4416
  releaseInitial();
4079
- for (let i = 0; i < 6; i++)
4080
- await Promise.resolve();
4417
+ await flushLoopMicrotasks(20);
4081
4418
  expect(sleepState.delays[0]).toBe(300_000);
4082
4419
  clockState.now = 300_000;
4083
4420
  sleepState.resolve?.();
@@ -4133,7 +4470,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4133
4470
  };
4134
4471
  }
4135
4472
  it("startup runs one full reconcile, then watcher edits interrupt the pending wait", async () => {
4136
- const runPass = vi.fn().mockResolvedValue(0);
4473
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4137
4474
  const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
4138
4475
  await flushLoopMicrotasks();
4139
4476
  expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
@@ -4158,7 +4495,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4158
4495
  await loop;
4159
4496
  });
4160
4497
  it("coalesces repeated edits to the same path into one scoped push", async () => {
4161
- const runPass = vi.fn().mockResolvedValue(0);
4498
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4162
4499
  const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
4163
4500
  await flushLoopMicrotasks();
4164
4501
  runPass.mockClear();
@@ -4186,7 +4523,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4186
4523
  await loop;
4187
4524
  });
4188
4525
  it("U08: --companies --skip-personal drops a personal watcher edit before it queues work", async () => {
4189
- const runPass = vi.fn().mockResolvedValue(0);
4526
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4190
4527
  const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
4191
4528
  ...watchArgv,
4192
4529
  "--skip-personal",
@@ -4204,7 +4541,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4204
4541
  await loop;
4205
4542
  });
4206
4543
  it("drains an unlink path through the same scoped tombstone push path", async () => {
4207
- const runPass = vi.fn().mockResolvedValue(0);
4544
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4208
4545
  const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
4209
4546
  await flushLoopMicrotasks();
4210
4547
  runPass.mockClear();
@@ -4226,7 +4563,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4226
4563
  await loop;
4227
4564
  });
4228
4565
  it("omitted --direction is pull-only (parser default): a scoped tick never pushes", async () => {
4229
- const runPass = vi.fn().mockResolvedValue(0);
4566
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4230
4567
  // No --direction flag → parseArgs defaults the run to pull-only. The drain
4231
4568
  // must honor that and NOT run a scoped push. Regression: the loop used to
4232
4569
  // re-parse argv and wrongly default an omitted direction to "both", so a
@@ -4260,7 +4597,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4260
4597
  it("restores failed scoped push paths so the next tick retries them", async () => {
4261
4598
  const runPass = vi
4262
4599
  .fn()
4263
- .mockResolvedValueOnce(0)
4600
+ .mockResolvedValueOnce(completedPassOutcome())
4264
4601
  .mockResolvedValueOnce(1)
4265
4602
  .mockResolvedValueOnce(0)
4266
4603
  .mockResolvedValueOnce(0)
@@ -4303,7 +4640,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4303
4640
  await loop;
4304
4641
  });
4305
4642
  it("empty scoped ticks run only the pull leg", async () => {
4306
- const runPass = vi.fn().mockResolvedValue(0);
4643
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4307
4644
  const { loop, tick, shutdown } = startDrainLoop(runPass);
4308
4645
  await flushLoopMicrotasks();
4309
4646
  runPass.mockClear();
@@ -4315,7 +4652,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4315
4652
  await loop;
4316
4653
  });
4317
4654
  it("personal scoped ticks preserve personal mode for the pull leg", async () => {
4318
- const runPass = vi.fn().mockResolvedValue(0);
4655
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4319
4656
  const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
4320
4657
  "--personal",
4321
4658
  "--watch",
@@ -4349,7 +4686,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4349
4686
  await loop;
4350
4687
  });
4351
4688
  it("single-company scoped ticks preserve company mode for the pull leg", async () => {
4352
- const runPass = vi.fn().mockResolvedValue(0);
4689
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4353
4690
  const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
4354
4691
  "--company",
4355
4692
  "acme",
@@ -4385,7 +4722,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4385
4722
  await loop;
4386
4723
  });
4387
4724
  it("push-only scoped ticks run the scoped push and no pull leg", async () => {
4388
- const runPass = vi.fn().mockResolvedValue(0);
4725
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4389
4726
  const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
4390
4727
  "--companies",
4391
4728
  "--watch",
@@ -4422,7 +4759,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4422
4759
  const watcher = makeBatchWatcherStub();
4423
4760
  const sleep = makeSteppableSleep();
4424
4761
  let triggerShutdown = () => { };
4425
- const runPass = vi.fn().mockResolvedValue(0);
4762
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
4426
4763
  const loop = runRunnerWithLoop(watchArgv, {
4427
4764
  runPass,
4428
4765
  clock: new FakeClock(),
@@ -5210,7 +5547,8 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5210
5547
  const UNENROLLED_CLAIMS = { email: "someone@getindigo.ai" };
5211
5548
  function runLoop(opts) {
5212
5549
  const watcher = opts.watcher ?? makeBatchWatcherStub();
5213
- const runPass = opts.runPass ?? vi.fn().mockResolvedValue(0);
5550
+ const runPass = opts.runPass ??
5551
+ vi.fn().mockResolvedValue(completedPassOutcome());
5214
5552
  const clock = new FakeClock();
5215
5553
  const sleep = makeSteppableSleep();
5216
5554
  let triggerShutdown = () => { };
@@ -5304,7 +5642,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5304
5642
  });
5305
5643
  const runPass = vi
5306
5644
  .fn()
5307
- .mockResolvedValueOnce(0)
5645
+ .mockResolvedValueOnce(completedPassOutcome())
5308
5646
  .mockImplementation((passArgv) => passArgv.includes("--company") ? targetedPush : Promise.resolve(0));
5309
5647
  const { loop, watcher, clock, shutdown } = runLoop({
5310
5648
  claims: EMAIL_CLAIMS,
@@ -5349,7 +5687,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5349
5687
  // Initial poll pass succeeds; the targeted pass fails.
5350
5688
  const runPass = vi
5351
5689
  .fn()
5352
- .mockResolvedValueOnce(0)
5690
+ .mockResolvedValueOnce(completedPassOutcome())
5353
5691
  .mockResolvedValue(1);
5354
5692
  const { loop, watcher, clock, tick, shutdown } = runLoop({
5355
5693
  claims: EMAIL_CLAIMS,
@@ -5381,7 +5719,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5381
5719
  ownDeviceId: "dev-test",
5382
5720
  dispose: vi.fn().mockResolvedValue(undefined),
5383
5721
  });
5384
- const runPass = vi.fn().mockResolvedValue(0);
5722
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
5385
5723
  const { loop, watcher, clock, tick, shutdown } = runLoop({
5386
5724
  claims: EMAIL_CLAIMS,
5387
5725
  startEventSync,
@@ -5421,7 +5759,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5421
5759
  });
5422
5760
  const runPass = vi
5423
5761
  .fn()
5424
- .mockResolvedValueOnce(0)
5762
+ .mockResolvedValueOnce(completedPassOutcome())
5425
5763
  .mockResolvedValueOnce({
5426
5764
  exitCode: 0,
5427
5765
  result: {
@@ -5462,7 +5800,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5462
5800
  });
5463
5801
  const runPass = vi
5464
5802
  .fn()
5465
- .mockResolvedValueOnce(0)
5803
+ .mockResolvedValueOnce(completedPassOutcome())
5466
5804
  .mockResolvedValueOnce({
5467
5805
  exitCode: 0,
5468
5806
  result: {
@@ -5503,7 +5841,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5503
5841
  ownDeviceId: "dev-test",
5504
5842
  dispose: vi.fn().mockResolvedValue(undefined),
5505
5843
  });
5506
- const runPass = vi.fn().mockResolvedValue(0);
5844
+ const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
5507
5845
  const { loop, watcher, clock, tick, shutdown } = runLoop({
5508
5846
  claims: EMAIL_CLAIMS,
5509
5847
  startEventSync,
@@ -5580,9 +5918,9 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5580
5918
  let passCount = 0;
5581
5919
  const runPass = vi.fn().mockImplementation(async () => {
5582
5920
  passCount += 1;
5583
- if (passCount === 1)
5921
+ if (passCount === 2)
5584
5922
  await pollGate;
5585
- return 0;
5923
+ return completedPassOutcome();
5586
5924
  });
5587
5925
  const loop = runRunnerWithLoop(
5588
5926
  // `--direction both` so the queued watcher pushes actually run (an omitted
@@ -5619,6 +5957,21 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5619
5957
  await microtasks();
5620
5958
  expect(passCount).toBe(1);
5621
5959
  expect(receiverSync).toBeTruthy();
5960
+ const remotePull = receiverSync({
5961
+ event: {
5962
+ kind: "upsert",
5963
+ relativePath: "companies/acme/remote.md",
5964
+ contentHash: "sha256:remote",
5965
+ mtime: "2026-06-18T12:00:00.000Z",
5966
+ originDeviceId: "peer-device",
5967
+ originTenantId: "tenant-acme",
5968
+ sequenceNumber: 7,
5969
+ eventTimestamp: "2026-06-18T12:00:00.000Z",
5970
+ },
5971
+ signal: new AbortController().signal,
5972
+ });
5973
+ await microtasks();
5974
+ expect(passCount).toBe(2);
5622
5975
  const indigoBatch = {
5623
5976
  paths: new Map([
5624
5977
  ["/tmp/hq/companies/indigo/a.md", "companies/indigo/a.md"],
@@ -5635,20 +5988,6 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5635
5988
  watcher.emit("companies/beta/b.md", betaBatch);
5636
5989
  clock.advance(0);
5637
5990
  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
5991
  releasePoll();
5653
5992
  await remotePull;
5654
5993
  await microtasks();
@@ -6622,7 +6961,7 @@ describe("watcher delete intents without pre-seeded authorization", () => {
6622
6961
  const initialPullGate = new Promise((resolve) => {
6623
6962
  releaseInitialPull = resolve;
6624
6963
  });
6625
- let initialPullGated = options.gateInitialPull === true;
6964
+ let pullPasses = 0;
6626
6965
  const runPass = vi.fn(async (argv) => {
6627
6966
  // Lets a test hold a pass mid-flight and act while the loop is awaiting
6628
6967
  // it, which is where producer-side state that outlives a pass is decided.
@@ -6673,8 +7012,8 @@ describe("watcher delete intents without pre-seeded authorization", () => {
6673
7012
  return { exitCode: 0, result: { pushPathResults: pathResults } };
6674
7013
  }
6675
7014
  if (direction === "pull" || direction === "both") {
6676
- if (initialPullGated) {
6677
- initialPullGated = false;
7015
+ pullPasses += 1;
7016
+ if (options.gateNextPull === true && pullPasses === 2) {
6678
7017
  await initialPullGate;
6679
7018
  }
6680
7019
  const journal = readJournal("indigo");
@@ -6694,7 +7033,7 @@ describe("watcher delete intents without pre-seeded authorization", () => {
6694
7033
  downloads++;
6695
7034
  }
6696
7035
  }
6697
- return 0;
7036
+ return completedPassOutcome();
6698
7037
  });
6699
7038
  const loop = runRunnerWithLoop([
6700
7039
  "--companies", "--watch", "--event-push",
@@ -6761,7 +7100,9 @@ describe("watcher delete intents without pre-seeded authorization", () => {
6761
7100
  await stopHarness(h);
6762
7101
  });
6763
7102
  it("persists the intent before an in-flight pull plans the missing path", async () => {
6764
- const h = startDeleteHarness(["knowledge/in-flight.md"], { gateInitialPull: true });
7103
+ const h = startDeleteHarness(["knowledge/in-flight.md"], { gateNextPull: true });
7104
+ await settle();
7105
+ h.tick();
6765
7106
  await settle();
6766
7107
  const relativePath = "companies/indigo/knowledge/in-flight.md";
6767
7108
  const absolutePath = path.join(h.companyRoot, "knowledge/in-flight.md");
@@ -6907,7 +7248,16 @@ describe("watcher delete intents without pre-seeded authorization", () => {
6907
7248
  "--poll-remote-ms",
6908
7249
  "60000",
6909
7250
  ], {
6910
- runPass: (passArgv) => runRunner(passArgv, runnerDeps),
7251
+ runPass: async (passArgv) => {
7252
+ let result;
7253
+ const exitCode = await runRunner(passArgv, {
7254
+ ...runnerDeps,
7255
+ onPassResult: (passResult) => {
7256
+ result = passResult;
7257
+ },
7258
+ });
7259
+ return { exitCode, ...(result ? { result } : {}) };
7260
+ },
6911
7261
  createWatcher: (options) => {
6912
7262
  capture = options.captureLocalDeleteSnapshots;
6913
7263
  return watcher;