@indigoai-us/hq-cloud 6.18.35 → 6.18.37

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 (36) hide show
  1. package/dist/bin/push-failure-backoff.d.ts +22 -0
  2. package/dist/bin/push-failure-backoff.d.ts.map +1 -0
  3. package/dist/bin/push-failure-backoff.js +27 -0
  4. package/dist/bin/push-failure-backoff.js.map +1 -0
  5. package/dist/bin/sync-runner-delete-snapshot-mixed.bench.test.js +9 -2
  6. package/dist/bin/sync-runner-delete-snapshot-mixed.bench.test.js.map +1 -1
  7. package/dist/bin/sync-runner-delete-snapshot.bench.test.js +3 -1
  8. package/dist/bin/sync-runner-delete-snapshot.bench.test.js.map +1 -1
  9. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  10. package/dist/bin/sync-runner-watch-loop.js +164 -34
  11. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  12. package/dist/bin/sync-runner.d.ts +11 -1
  13. package/dist/bin/sync-runner.d.ts.map +1 -1
  14. package/dist/bin/sync-runner.js +23 -4
  15. package/dist/bin/sync-runner.js.map +1 -1
  16. package/dist/bin/sync-runner.test.js +956 -4
  17. package/dist/bin/sync-runner.test.js.map +1 -1
  18. package/dist/lib/describe-error.d.ts +4 -0
  19. package/dist/lib/describe-error.d.ts.map +1 -1
  20. package/dist/lib/describe-error.js +37 -0
  21. package/dist/lib/describe-error.js.map +1 -1
  22. package/dist/lib/describe-error.test.js +18 -1
  23. package/dist/lib/describe-error.test.js.map +1 -1
  24. package/dist/outcome-telemetry.d.ts +16 -6
  25. package/dist/outcome-telemetry.d.ts.map +1 -1
  26. package/dist/outcome-telemetry.js +105 -11
  27. package/dist/outcome-telemetry.js.map +1 -1
  28. package/dist/outcome-telemetry.test.js +236 -0
  29. package/dist/outcome-telemetry.test.js.map +1 -1
  30. package/dist/sync/push-receiver.d.ts +5 -0
  31. package/dist/sync/push-receiver.d.ts.map +1 -1
  32. package/dist/sync/push-receiver.js +115 -52
  33. package/dist/sync/push-receiver.js.map +1 -1
  34. package/dist/sync/push-receiver.test.js +349 -2
  35. package/dist/sync/push-receiver.test.js.map +1 -1
  36. package/package.json +1 -1
@@ -9,7 +9,8 @@ import { targetedJournalSessionCensusSizes } from "../cli/sync.js";
9
9
  import { VaultClient } from "../index.js";
10
10
  import { pickCanonicalPersonEntity } from "../vault-client.js";
11
11
  import { acquireOperationLock, withOperationLock, withOperationLockSync, OperationLockedError, OPERATION_LOCKED_EXIT, } from "../operation-lock.js";
12
- import { describeError } from "../lib/describe-error.js";
12
+ import { describeError, pushFailureTelemetryLabel, sanitizePushFailureText, } from "../lib/describe-error.js";
13
+ import { RoutePushFailureBackoff, systemPushRetryScheduler, } from "./push-failure-backoff.js";
13
14
  import { TRANSIENT_NETWORK_EXIT } from "../lib/net-errors.js";
14
15
  import { PARTIAL_SYNC_EXIT, UNRESOLVED_COMPANY_EXIT, } from "../lib/exit-codes.js";
15
16
  import { getOrCreateMachineIdMetadata } from "../lib/machine-id.js";
@@ -1690,7 +1691,16 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1690
1691
  refresh.unref?.();
1691
1692
  }
1692
1693
  const passScheduler = new CooperativePassScheduler(monotonicNow, CHORE_STARVATION_CAP_MS, { caps: fullPassCaps });
1693
- const runPassWithLock = async (passArgvForRun, prepare, lane = "slow", workClass = "chore", checkpoint, telemetry) => {
1694
+ const runPassWithLock = async (passArgvForRun, prepare, lane = "slow", workClass = "chore", checkpoint, telemetry, pullSignal, receiverSignal) => {
1695
+ const passCheckpoint = pullSignal || receiverSignal
1696
+ ? Object.assign(async () => checkpoint?.(), {
1697
+ shouldYieldToRealtime: checkpoint?.shouldYieldToRealtime,
1698
+ shouldAbort: () => pullSignal?.aborted === true ||
1699
+ receiverSignal?.aborted === true ||
1700
+ checkpoint?.shouldAbort?.() === true,
1701
+ runChoreNetworkTransfer: checkpoint?.runChoreNetworkTransfer,
1702
+ })
1703
+ : checkpoint;
1694
1704
  const reportedAppliedUpserts = new Set();
1695
1705
  const reportedAppliedDeletes = new Set();
1696
1706
  const recordDurableAppliedUpserts = (upserts) => {
@@ -1722,9 +1732,9 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1722
1732
  // existing scheduler's cooperative hand-off checkpoint.
1723
1733
  const onJournalsReady = () => activateEventPushSurfaces();
1724
1734
  if (!runPassSupportsQmdLifecycle) {
1725
- return runPass(passArgvForRun, checkpoint, onJournalsReady, recordDurableAppliedUpserts, workClass);
1735
+ return runPass(passArgvForRun, passCheckpoint, onJournalsReady, recordDurableAppliedUpserts, workClass);
1726
1736
  }
1727
- return runPass(passArgvForRun, checkpoint, onJournalsReady, recordDurableAppliedUpserts, workClass, qmdShutdownController.signal, writeMaintenanceProgress, idleHeartbeatIntervalMs, recordDurableAppliedDeletes);
1737
+ return runPass(passArgvForRun, passCheckpoint, onJournalsReady, recordDurableAppliedUpserts, workClass, qmdShutdownController.signal, writeMaintenanceProgress, idleHeartbeatIntervalMs, recordDurableAppliedDeletes);
1728
1738
  };
1729
1739
  // Observe the heap at pass boundaries — a gated checkpoint immediately before
1730
1740
  // admitting a pass and immediately after it returns. A pass that runs a long
@@ -1987,13 +1997,16 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1987
1997
  // chore ownership. Realtime normally waits for a chore hand-off; the
1988
1998
  // watcher fast path explicitly opts into its established scoped exception.
1989
1999
  passScheduler.submit(task, workClass, lane, allowFastRealtimeOvertake, signal, resourceCap, allowConcurrentHandoff, transferDirection, transferSizeClass);
1990
- const runGuarded = (passArgvForRun, prepare, lane = "slow", workClass = "chore", allowFastRealtimeOvertake = false, signal, resourceCap = false, telemetry, allowConcurrentHandoff = false) => {
2000
+ const runGuarded = (passArgvForRun, prepare, lane = "slow", workClass = "chore", allowFastRealtimeOvertake = false, signal, resourceCap = false, telemetry, allowConcurrentHandoff = false, receiverSignal) => {
1991
2001
  telemetry?.mark("operationGate");
2002
+ const admissionSignal = signal && receiverSignal
2003
+ ? AbortSignal.any([signal, receiverSignal])
2004
+ : signal ?? receiverSignal;
1992
2005
  return runGuardedTask((context) => {
1993
2006
  telemetry?.mark("appendLock");
1994
2007
  telemetry?.setOccupancy(passScheduler.activeRealtimeWorkerCount);
1995
- return runPassWithLock(passArgvForRun, prepare, lane, workClass, workClass === "chore" ? context.checkpoint : undefined, telemetry);
1996
- }, lane, workClass, allowFastRealtimeOvertake, signal, resourceCap, allowConcurrentHandoff, telemetry?.direction, telemetry?.sizeClass);
2008
+ return runPassWithLock(passArgvForRun, prepare, lane, workClass, workClass === "chore" ? context.checkpoint : undefined, telemetry, context.signal, receiverSignal);
2009
+ }, lane, workClass, allowFastRealtimeOvertake, admissionSignal, resourceCap, allowConcurrentHandoff, telemetry?.direction, telemetry?.sizeClass);
1997
2010
  };
1998
2011
  /*
1999
2012
  * Area storage and the parallel coordinator default to AUTO. AUTO turns
@@ -2465,15 +2478,43 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
2465
2478
  pendingWatcherBatchGeneration = null;
2466
2479
  return { batch, bare, warmupCatchup, warmupCatchupFailed, generation };
2467
2480
  };
2468
- const restorePendingWatcherPaths = (paths, generation) => {
2481
+ const restorePendingWatcherPaths = (paths, generation, changes) => {
2469
2482
  pendingWatcherBatchGeneration ??=
2470
2483
  generation ?? ++nextWatcherBatchGeneration;
2471
2484
  pendingWatcherOriginalBatch = null;
2472
2485
  for (const [absolutePath, relativePath] of paths.entries()) {
2486
+ // A newer watcher event may already be queued for this path. Keep its
2487
+ // change record instead of restoring stale delete metadata from the
2488
+ // batch that just finished.
2489
+ if (pendingWatcherChangeSequences.has(absolutePath))
2490
+ continue;
2473
2491
  pendingWatcherPaths.set(absolutePath, relativePath);
2474
2492
  pendingWatcherChangeSequences.set(absolutePath, ++nextPendingWatcherChangeSequence);
2493
+ const change = changes?.get(absolutePath);
2494
+ if (change)
2495
+ pendingWatcherChanges.set(absolutePath, change);
2475
2496
  }
2476
2497
  };
2498
+ const routePushFailureBackoff = new RoutePushFailureBackoff();
2499
+ const pushRetryScheduler = deps.pushRetryScheduler ?? systemPushRetryScheduler;
2500
+ const routePushRetryTimerHandles = new Map();
2501
+ const clearRoutePushRetry = (route) => {
2502
+ const handle = routePushRetryTimerHandles.get(route);
2503
+ if (handle === undefined)
2504
+ return;
2505
+ pushRetryScheduler.clearTimeout(handle);
2506
+ routePushRetryTimerHandles.delete(route);
2507
+ };
2508
+ const scheduleRoutePushRetry = (route, delayMs) => {
2509
+ if (routePushRetryTimerHandles.has(route))
2510
+ return;
2511
+ const handle = pushRetryScheduler.setTimeout(() => {
2512
+ routePushRetryTimerHandles.delete(route);
2513
+ if (!stopped)
2514
+ interruptWait();
2515
+ }, delayMs);
2516
+ routePushRetryTimerHandles.set(route, handle);
2517
+ };
2477
2518
  /**
2478
2519
  * Journal roots the bulk-intent quarantine has refused for the current
2479
2520
  * delete episode, keyed `slug\0journalKey`.
@@ -3332,7 +3373,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
3332
3373
  }
3333
3374
  return false;
3334
3375
  };
3335
- const runScopedDrain = async (batch, watcherBatchGeneration, wakeOnPushFailure = false, allowFastRealtimeOvertake = false) => {
3376
+ let emitScopedPushFailureTelemetry = null;
3377
+ const runScopedDrain = async (batch, watcherBatchGeneration, _wakeOnPushFailure = false, allowFastRealtimeOvertake = false) => {
3336
3378
  if (batch)
3337
3379
  await resolveDeleteSnapshotCaptures(batch);
3338
3380
  const grouped = batch && batch.paths.size > 0 ? groupBatchByRoute(batch) : null;
@@ -3343,6 +3385,52 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
3343
3385
  const relativePath = group.paths.get(absolutePath);
3344
3386
  return relativePath !== undefined && admittedPaths.some((scope) => relativePath === scope || relativePath.startsWith(`${scope}/`));
3345
3387
  }));
3388
+ const pushFailureText = (result, route, caughtError) => {
3389
+ if (caughtError !== undefined)
3390
+ return describeError(caughtError);
3391
+ if (typeof result === "number")
3392
+ return `sync pass exited ${result}`;
3393
+ const failures = [
3394
+ ...(result.result?.companyFailures ?? []),
3395
+ ...(result.result?.errors ?? []),
3396
+ ...(result.result?.transient ?? []),
3397
+ ];
3398
+ const routeFailure = route.kind === "company"
3399
+ ? failures.find((failure) => failure.company.toLowerCase() === route.slug.toLowerCase())
3400
+ : undefined;
3401
+ return routeFailure?.message ?? failures[0]?.message ??
3402
+ `sync pass exited ${result.exitCode}`;
3403
+ };
3404
+ const recordPushFailure = (route, exitCode, message) => {
3405
+ const routeId = routeKey(route);
3406
+ const companyRoot = route.kind === "company"
3407
+ ? path.join(hqRoot, "companies", route.slug)
3408
+ : hqRoot;
3409
+ const errorText = sanitizePushFailureText(message, companyRoot);
3410
+ const state = routePushFailureBackoff.failed(routeId, monotonicNow());
3411
+ scheduleRoutePushRetry(routeId, state.delayMs);
3412
+ if (!state.shouldReport)
3413
+ return;
3414
+ const event = {
3415
+ route: routeId,
3416
+ exitCode,
3417
+ consecutiveFailures: state.consecutiveFailures,
3418
+ nextRetryDelayMs: state.delayMs,
3419
+ errorSummary: pushFailureTelemetryLabel(errorText),
3420
+ };
3421
+ const diagnostic = {
3422
+ type: "scoped-push-failure",
3423
+ ...event,
3424
+ error: errorText,
3425
+ };
3426
+ (deps.stderr ?? process.stderr).write(`${JSON.stringify(diagnostic)}\n`);
3427
+ if (deps.emitScopedPushFailureTelemetry) {
3428
+ deps.emitScopedPushFailureTelemetry(event);
3429
+ }
3430
+ else {
3431
+ emitScopedPushFailureTelemetry?.(event);
3432
+ }
3433
+ };
3346
3434
  if (watcherDrainRunsPush && grouped) {
3347
3435
  for (const group of grouped.values()) {
3348
3436
  // Paused workspaces: drop the scoped push without uploading or
@@ -3350,6 +3438,14 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
3350
3438
  if (isPausedRoute(group.route)) {
3351
3439
  continue;
3352
3440
  }
3441
+ const routeId = routeKey(group.route);
3442
+ const retryState = routePushFailureBackoff.state(routeId);
3443
+ if (retryState && retryState.retryAt > monotonicNow()) {
3444
+ scheduleRoutePushRetry(routeId, retryState.retryAt - monotonicNow());
3445
+ restorePendingWatcherPaths(group.paths, watcherBatchGeneration, group.changes);
3446
+ continue;
3447
+ }
3448
+ clearRoutePushRetry(routeId);
3353
3449
  try {
3354
3450
  const sessionLogBackup = group.relPaths.every(isSessionLogsPushOnlyPath);
3355
3451
  const lane = sessionLogBackup
@@ -3384,6 +3480,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
3384
3480
  rememberUploadedPaths(result);
3385
3481
  const pushExit = passExitCode(result);
3386
3482
  if (execution.accepted && pushExit === 0) {
3483
+ routePushFailureBackoff.succeeded(routeId);
3484
+ clearRoutePushRetry(routeId);
3387
3485
  const pushPathResults = typeof result === "number" ? [] : (result.result?.pushPathResults ?? []);
3388
3486
  const refusedPaths = new Set(pushPathResults
3389
3487
  .filter((pathResult) => pathResult.status === "refused")
@@ -3431,21 +3529,25 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
3431
3529
  }
3432
3530
  }
3433
3531
  else if (pushExit === UNRESOLVED_COMPANY_EXIT) {
3532
+ routePushFailureBackoff.succeeded(routeId);
3533
+ clearRoutePushRetry(routeId);
3434
3534
  if (group.route.kind === "company") {
3435
3535
  logDroppedUnresolvedWatcherRoute(group.route.slug);
3436
3536
  }
3437
3537
  }
3438
3538
  else {
3439
- restorePendingWatcherPaths(group.paths, watcherBatchGeneration);
3440
- if (wakeOnPushFailure)
3539
+ restorePendingWatcherPaths(group.paths, watcherBatchGeneration, group.changes);
3540
+ if (pushExit !== 0) {
3541
+ recordPushFailure(group.route, pushExit, pushFailureText(result, group.route));
3542
+ }
3543
+ else if (_wakeOnPushFailure) {
3441
3544
  interruptWait();
3545
+ }
3442
3546
  }
3443
3547
  }
3444
3548
  catch (err) {
3445
- restorePendingWatcherPaths(group.paths, watcherBatchGeneration);
3446
- process.stderr.write(`watch scoped push failed, will retry next tick: ${describeError(err)}\n`);
3447
- if (wakeOnPushFailure)
3448
- interruptWait();
3549
+ recordPushFailure(group.route, 1, pushFailureText(1, group.route, err));
3550
+ restorePendingWatcherPaths(group.paths, watcherBatchGeneration, group.changes);
3449
3551
  }
3450
3552
  }
3451
3553
  }
@@ -3534,6 +3636,11 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
3534
3636
  if (watcherDrainRunsPush) {
3535
3637
  const push = await runGuarded(buildScopedPushArgv(route, [], passArgv));
3536
3638
  const pushExit = scopedRouteDrainExit({ result: push, accepted: true });
3639
+ if (pushExit === 0) {
3640
+ const routeId = routeKey(route);
3641
+ routePushFailureBackoff.succeeded(routeId);
3642
+ clearRoutePushRetry(routeId);
3643
+ }
3537
3644
  if (pushExit !== 0)
3538
3645
  return push;
3539
3646
  }
@@ -3562,10 +3669,16 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
3562
3669
  if (watcherDrainRunsPush) {
3563
3670
  // Whole-route push (no --scope-path): the dropped paths are unknown,
3564
3671
  // so the route's full walk is the narrowest sound scope.
3672
+ const push = await runGuarded(buildScopedPushArgv(route, [], passArgv));
3565
3673
  const exit = scopedRouteDrainExit({
3566
- result: await runGuarded(buildScopedPushArgv(route, [], passArgv)),
3674
+ result: push,
3567
3675
  accepted: true,
3568
3676
  });
3677
+ if (exit === 0) {
3678
+ const routeId = routeKey(route);
3679
+ routePushFailureBackoff.succeeded(routeId);
3680
+ clearRoutePushRetry(routeId);
3681
+ }
3569
3682
  if (exit !== 0 && worstExit === 0)
3570
3683
  worstExit = exit;
3571
3684
  }
@@ -3631,6 +3744,16 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
3631
3744
  authToken: getAccessToken,
3632
3745
  region: runtime.region,
3633
3746
  });
3747
+ emitScopedPushFailureTelemetry = (event) => {
3748
+ void emitCloudTelemetry(watcherTelemetryClient, {
3749
+ eventName: "sync_runner_scoped_push_failed",
3750
+ source: "sync_runner_watch_loop",
3751
+ properties: event,
3752
+ }, {
3753
+ claims,
3754
+ log: (message) => (deps.stderr ?? process.stderr).write(`hq-sync-runner: ${message}\n`),
3755
+ });
3756
+ };
3634
3757
  const createWatcher = deps.createWatcher ??
3635
3758
  ((opts) => new TreeWatcher({
3636
3759
  hqRoot: opts.hqRoot,
@@ -3862,7 +3985,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
3862
3985
  // a known-small event can be slow solely because an earlier event exhausted
3863
3986
  // the delivery-wide fast budget.
3864
3987
  const receiverLaneByBatch = new WeakMap();
3865
- const drainParallelReceiverLane = async (lane, laneEvents, receiveCountsByPath) => {
3988
+ const drainParallelReceiverLane = async (lane, laneEvents, receiveCountsByPath, settleEvents, receiverSignal) => {
3866
3989
  const eventsByRoute = new Map();
3867
3990
  for (const event of laneEvents) {
3868
3991
  const route = routeChangeToTarget(event.relativePath);
@@ -3877,15 +4000,12 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
3877
4000
  // particular, a later route's fast receipt must never wait for an
3878
4001
  // earlier route's slow pull to finish.
3879
4002
  const routeGroups = [...eventsByRoute.values()];
3880
- const completed = await Promise.all(routeGroups.map(({ route, events }) => {
3881
- return runRealtimePass("receiver", "pull", events.map((event) => event.relativePath), lane, (slice, telemetry, coordinatorFenced) => {
4003
+ const settlements = await Promise.allSettled(routeGroups.map(async ({ route, events }) => {
4004
+ const execution = await runRealtimePass("receiver", "pull", events.map((event) => event.relativePath), lane, (slice, telemetry, coordinatorFenced) => {
3882
4005
  const slicePaths = slice.paths.map((path) => path.canonicalPath);
3883
4006
  const exactFileUpserts = exactUpsertPathsForCoordinatorSlice(events, slicePaths);
3884
- return withScopeContentHashesFile(route, events, slicePaths, (contentHashesFile) => runGuarded(buildTargetedPullArgv(route, passArgv, slicePaths, exactFileUpserts, contentHashesFile), undefined, lane, "realtime", lane === "fast", slice.signal, false, telemetry, coordinatorFenced));
4007
+ return withScopeContentHashesFile(route, events, slicePaths, (contentHashesFile) => runGuarded(buildTargetedPullArgv(route, passArgv, slicePaths, exactFileUpserts, contentHashesFile), undefined, lane, "realtime", lane === "fast", slice.signal, false, telemetry, coordinatorFenced, receiverSignal));
3885
4008
  });
3886
- }));
3887
- for (const [index, execution] of completed.entries()) {
3888
- const { route, events } = routeGroups[index];
3889
4009
  const pathCount = events.length;
3890
4010
  const exitCode = passExitCode(execution.result);
3891
4011
  if (shouldAcknowledgeParallelUnresolvedCompany(execution.accepted, exitCode)) {
@@ -3893,14 +4013,19 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
3893
4013
  throw targetedReceiverDispatchError(execution.result, execution.accepted, runtime.authRequiredPassExit);
3894
4014
  }
3895
4015
  warnUnresolvedReceiverCompany(route, pathCount);
3896
- continue;
3897
4016
  }
3898
- if (!execution.accepted || exitCode !== 0) {
4017
+ else if (!execution.accepted || exitCode !== 0) {
3899
4018
  throw targetedReceiverDispatchError(execution.result, execution.accepted, runtime.authRequiredPassExit);
3900
4019
  }
3901
- }
4020
+ // The receiver can acknowledge this tenant's receipts now, while
4021
+ // independent route groups continue to settle.
4022
+ await settleEvents?.(events);
4023
+ }));
4024
+ const failedSettlement = settlements.find((settlement) => settlement.status === "rejected");
4025
+ if (failedSettlement)
4026
+ throw failedSettlement.reason;
3902
4027
  };
3903
- const receiverSyncBatchFn = async ({ events, approximateReceiveCountsByPath, }) => {
4028
+ const receiverSyncBatchFn = async ({ events, signal, approximateReceiveCountsByPath, settleEvents, }) => {
3904
4029
  if (stopped)
3905
4030
  return;
3906
4031
  // The injectable receiver seam and the production SQS receiver both
@@ -3918,19 +4043,19 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
3918
4043
  const lane = receiverLaneByBatch.get(events);
3919
4044
  if (lane) {
3920
4045
  receiverLaneByBatch.delete(events);
3921
- await drainParallelReceiverLane(lane, events, approximateReceiveCountsByPath);
4046
+ await drainParallelReceiverLane(lane, events, approximateReceiveCountsByPath, settleEvents, signal);
3922
4047
  return;
3923
4048
  }
3924
4049
  // Direct embedders may invoke the engine without first going through
3925
4050
  // SqsPushReceiver. Preserve that seam while applying the same
3926
4051
  // delivery-wide partition the receiver would expose.
3927
4052
  const lanes = partitionByKnownSizes(events, receiverEventSize);
3928
- await drainParallelReceiverLane("fast", lanes.fast, approximateReceiveCountsByPath);
3929
- await drainParallelReceiverLane("slow", lanes.slow, approximateReceiveCountsByPath);
4053
+ await drainParallelReceiverLane("fast", lanes.fast, approximateReceiveCountsByPath, settleEvents, signal);
4054
+ await drainParallelReceiverLane("slow", lanes.slow, approximateReceiveCountsByPath, settleEvents, signal);
3930
4055
  return;
3931
4056
  }
3932
4057
  const lanes = partitionByKnownSizes(events, receiverEventSize);
3933
- const drainLane = async (lane, laneEvents, receiveCountsByPath) => {
4058
+ const drainLane = async (lane, laneEvents, receiveCountsByPath, receiverSignal) => {
3934
4059
  const eventsByRoute = new Map();
3935
4060
  for (const event of laneEvents) {
3936
4061
  const route = routeChangeToTarget(event.relativePath);
@@ -3949,26 +4074,28 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
3949
4074
  }
3950
4075
  for (const { route, events: routeEvents, upsertPaths } of eventsByRoute.values()) {
3951
4076
  const paths = routeEvents.map((event) => event.relativePath);
3952
- const result = await withScopeContentHashesFile(route, routeEvents, paths, (contentHashesFile) => runGuarded(buildTargetedPullArgv(route, passArgv, paths, upsertPaths, contentHashesFile), undefined, lane, "realtime"));
4077
+ const result = await withScopeContentHashesFile(route, routeEvents, paths, (contentHashesFile) => runGuarded(buildTargetedPullArgv(route, passArgv, paths, upsertPaths, contentHashesFile), undefined, lane, "realtime", false, receiverSignal));
3953
4078
  const exitCode = passExitCode(result);
3954
4079
  if (exitCode === UNRESOLVED_COMPANY_EXIT) {
3955
4080
  if (!unresolvedReceiverMessagesWereRedelivered(routeEvents, receiveCountsByPath)) {
3956
4081
  throw targetedReceiverDispatchError(result, true, runtime.authRequiredPassExit);
3957
4082
  }
3958
4083
  warnUnresolvedReceiverCompany(route, routeEvents.length);
4084
+ await settleEvents?.(routeEvents);
3959
4085
  continue;
3960
4086
  }
3961
4087
  if (exitCode !== 0) {
3962
4088
  throw targetedReceiverDispatchError(result, true, runtime.authRequiredPassExit);
3963
4089
  }
4090
+ await settleEvents?.(routeEvents);
3964
4091
  }
3965
4092
  };
3966
4093
  // Dispatch realtime work before awaiting its bulk residue. This lets an
3967
4094
  // established slow pass continue while the small scoped pull uses its
3968
4095
  // ref-counted lock carve-out; the residue remains serialized and cannot
3969
4096
  // starve the slow lane.
3970
- await drainLane("fast", lanes.fast, approximateReceiveCountsByPath);
3971
- await drainLane("slow", lanes.slow, approximateReceiveCountsByPath);
4097
+ await drainLane("fast", lanes.fast, approximateReceiveCountsByPath, signal);
4098
+ await drainLane("slow", lanes.slow, approximateReceiveCountsByPath, signal);
3972
4099
  };
3973
4100
  receiverSyncBatchFn.partition = (events, maxPaths) => {
3974
4101
  const lanes = partitionByKnownSizes(events, receiverEventSize);
@@ -4110,6 +4237,9 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
4110
4237
  if (stopped)
4111
4238
  return shutdownPromise ?? Promise.resolve();
4112
4239
  stopped = true;
4240
+ for (const route of routePushRetryTimerHandles.keys()) {
4241
+ clearRoutePushRetry(route);
4242
+ }
4113
4243
  qmdShutdownController.abort();
4114
4244
  eventSyncShutdownDeadlineMs =
4115
4245
  Date.now() + EVENT_SYNC_STARTUP_SHUTDOWN_WAIT_MS;