@indigoai-us/hq-cloud 6.15.65 → 6.15.67

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.
@@ -1,4 +1,4 @@
1
- import { type TreeChange } from "../watcher.js";
1
+ import { type TreeChange, type TreeChangeBatch } from "../watcher.js";
2
2
  import { readJournal } from "../journal.js";
3
3
  import type { TelemetryClaims } from "../telemetry-events.js";
4
4
  import type { Direction, RunnerLoopDeps, RunnerPassOutcome } from "./sync-runner.js";
@@ -21,15 +21,37 @@ export declare const EVENT_BATCH_LIMIT_ENV = "HQ_SYNC_EVENT_BATCH_LIMIT";
21
21
  */
22
22
  export declare const FAST_LANE_MAX_FILES_ENV = "HQ_SYNC_FAST_LANE_MAX_FILES";
23
23
  export declare const FAST_LANE_MAX_BYTES_ENV = "HQ_SYNC_FAST_LANE_MAX_BYTES";
24
+ export declare const FAST_LANE_MAX_FILE_BYTES_ENV = "HQ_SYNC_FAST_LANE_MAX_FILE_BYTES";
24
25
  export declare const DEFAULT_FAST_LANE_MAX_FILES = 32;
25
26
  export declare const DEFAULT_FAST_LANE_MAX_BYTES: number;
27
+ /**
28
+ * A per-file ceiling keeps a single medium-sized transfer from consuming the
29
+ * realtime lane even when the batch aggregate is otherwise below its cap.
30
+ * 256 KiB comfortably covers ordinary notes and control-plane artifacts while
31
+ * leaving session logs and other transfer-heavy files in the slow lane.
32
+ */
33
+ export declare const DEFAULT_FAST_LANE_MAX_FILE_BYTES: number;
26
34
  export declare function resolveFastLaneLimits(): {
27
35
  maxFiles: number;
28
36
  maxBytes: number;
37
+ maxFileBytes: number;
38
+ };
39
+ /** One latest-per-path work item selected into the realtime or bulk lane. */
40
+ export interface LanePartition<T> {
41
+ fast: T[];
42
+ slow: T[];
43
+ }
44
+ /**
45
+ * Partition one selected watcher batch. The returned batches retain the same
46
+ * route/filter metadata, but no latest path can appear in both lane maps.
47
+ */
48
+ export declare function partitionLocalBatch(batch: TreeChangeBatch): {
49
+ fast: TreeChangeBatch | null;
50
+ slow: TreeChangeBatch | null;
29
51
  };
30
52
  /** A missing/stat-failed path is never allowed to bypass the slow lane. */
31
53
  export declare function isFastLocalBatch(paths: Iterable<string>, changes?: ReadonlyMap<string, Pick<TreeChange, "kind">>): boolean;
32
- /** Receiver events carry no content length, so only bounded event batches qualify. */
54
+ /** A receiver event needs a known small upsert size before it can go realtime. */
33
55
  export declare function isFastReceiverBatch(eventCount: number): boolean;
34
56
  export declare function resolveEventBatchLimit(env?: Record<string, string | undefined>): number;
35
57
  /**
@@ -54,6 +76,12 @@ export declare const WATCHER_REDISCOVERY_RETRY_MAX_MS: number;
54
76
  * can be unit-tested without a real host or a 10-minute wait.
55
77
  */
56
78
  export declare function adaptivePollMs(load1: number, cpuCount: number, floorMs?: number, ceilMs?: number): number;
79
+ /**
80
+ * A receiver that has observed messages owns a bounded drain, even if its
81
+ * slow lane is currently waiting for the operation lock. Do not stretch the
82
+ * cadence from that observed work to the CPU-derived backoff ceiling.
83
+ */
84
+ export declare function resolveWatchPollMs(explicitPollMs: number | undefined, receiverHasQueuedWork: boolean, loadAvg: number, cpuCount: number): number;
57
85
  /**
58
86
  * Advance the cadence from its previous monotonic deadline while the runner is
59
87
  * on time. When a pass overruns its slot, give the host one complete interval
@@ -1 +1 @@
1
- {"version":3,"file":"sync-runner-watch-loop.d.ts","sourceRoot":"","sources":["../../src/bin/sync-runner-watch-loop.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sync-runner-watch-loop.d.ts","sourceRoot":"","sources":["../../src/bin/sync-runner-watch-loop.ts"],"names":[],"mappings":"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"}
@@ -12,7 +12,7 @@ import { PARTIAL_SYNC_EXIT } from "../lib/exit-codes.js";
12
12
  import { getOrCreateMachineId } from "../lib/machine-id.js";
13
13
  import { localPathForVaultKey } from "../local-path-codec.js";
14
14
  import { TreeWatcher, resolveEventDebounceConfig, systemClock, } from "../watcher.js";
15
- import { clearLocalDeleteIntent, markLocalDeleteIntent, PERSONAL_VAULT_JOURNAL_SLUG, openJournalStoreSession, listJournals, readJournal, writeJournal, } from "../journal.js";
15
+ import { clearLocalDeleteIntent, getStateDir, markLocalDeleteIntent, PERSONAL_VAULT_JOURNAL_SLUG, openJournalStoreSession, listJournals, readJournal, writeJournal, } from "../journal.js";
16
16
  import { NoopPushReceiver, } from "../sync/push-receiver.js";
17
17
  import { resolveEventSync, startEventSync as defaultStartEventSync, } from "../sync/event-sync.js";
18
18
  import { buildRoutePullArgv, buildScopedPushArgv, buildScopedDrainPullArgv, buildTargetedPullArgv, isWatchRouteSelected, routeChangeToTarget, routeKey, } from "./sync-runner-watch-routes.js";
@@ -52,8 +52,16 @@ export const EVENT_BATCH_LIMIT_ENV = "HQ_SYNC_EVENT_BATCH_LIMIT";
52
52
  */
53
53
  export const FAST_LANE_MAX_FILES_ENV = "HQ_SYNC_FAST_LANE_MAX_FILES";
54
54
  export const FAST_LANE_MAX_BYTES_ENV = "HQ_SYNC_FAST_LANE_MAX_BYTES";
55
+ export const FAST_LANE_MAX_FILE_BYTES_ENV = "HQ_SYNC_FAST_LANE_MAX_FILE_BYTES";
55
56
  export const DEFAULT_FAST_LANE_MAX_FILES = 32;
56
57
  export const DEFAULT_FAST_LANE_MAX_BYTES = 4 * 1024 * 1024;
58
+ /**
59
+ * A per-file ceiling keeps a single medium-sized transfer from consuming the
60
+ * realtime lane even when the batch aggregate is otherwise below its cap.
61
+ * 256 KiB comfortably covers ordinary notes and control-plane artifacts while
62
+ * leaving session logs and other transfer-heavy files in the slow lane.
63
+ */
64
+ export const DEFAULT_FAST_LANE_MAX_FILE_BYTES = 256 * 1024;
57
65
  function positiveEnvInt(name, fallback) {
58
66
  const value = Number.parseInt(process.env[name] ?? "", 10);
59
67
  return Number.isFinite(value) && value > 0 ? value : fallback;
@@ -62,7 +70,68 @@ export function resolveFastLaneLimits() {
62
70
  return {
63
71
  maxFiles: positiveEnvInt(FAST_LANE_MAX_FILES_ENV, DEFAULT_FAST_LANE_MAX_FILES),
64
72
  maxBytes: positiveEnvInt(FAST_LANE_MAX_BYTES_ENV, DEFAULT_FAST_LANE_MAX_BYTES),
73
+ maxFileBytes: positiveEnvInt(FAST_LANE_MAX_FILE_BYTES_ENV, DEFAULT_FAST_LANE_MAX_FILE_BYTES),
74
+ };
75
+ }
76
+ /**
77
+ * Select a bounded realtime prefix from already coalesced work. A path appears
78
+ * exactly once in this input, so a later change to the same path has already
79
+ * replaced its earlier revision and cannot straddle lanes.
80
+ */
81
+ function partitionByKnownSizes(items, sizeOf) {
82
+ const { maxFiles, maxBytes, maxFileBytes } = resolveFastLaneLimits();
83
+ const fast = [];
84
+ const slow = [];
85
+ let bytes = 0;
86
+ for (const item of items) {
87
+ const size = sizeOf(item);
88
+ if (size === null ||
89
+ size > maxFileBytes ||
90
+ fast.length >= maxFiles ||
91
+ bytes + size > maxBytes) {
92
+ slow.push(item);
93
+ continue;
94
+ }
95
+ fast.push(item);
96
+ bytes += size;
97
+ }
98
+ return { fast, slow };
99
+ }
100
+ function localPathSize(absolutePath, changes) {
101
+ // `unlinkDir` has an unbounded journal expansion; never guess its payload.
102
+ if (changes?.get(absolutePath)?.kind === "unlinkDir")
103
+ return null;
104
+ try {
105
+ const stat = fs.lstatSync(absolutePath);
106
+ if (stat.isDirectory())
107
+ return null;
108
+ return stat.isFile() ? stat.size : 0;
109
+ }
110
+ catch (err) {
111
+ // A missing file is an exact delete and has no content transfer. Any other
112
+ // stat error leaves its payload unknown and therefore slow.
113
+ return err.code === "ENOENT" ? 0 : null;
114
+ }
115
+ }
116
+ /**
117
+ * Partition one selected watcher batch. The returned batches retain the same
118
+ * route/filter metadata, but no latest path can appear in both lane maps.
119
+ */
120
+ export function partitionLocalBatch(batch) {
121
+ const partition = partitionByKnownSizes([...batch.paths.entries()], ([absolutePath]) => localPathSize(absolutePath, batch.changes));
122
+ const makeBatch = (entries) => {
123
+ if (entries.length === 0)
124
+ return null;
125
+ const paths = new Map(entries);
126
+ const changes = new Map();
127
+ for (const [absolutePath] of entries) {
128
+ const change = batch.changes?.get(absolutePath);
129
+ if (change)
130
+ changes.set(absolutePath, change);
131
+ }
132
+ return { ...batch, paths, ...(changes.size > 0 ? { changes } : {}) };
65
133
  };
134
+ return { fast: makeBatch(partition.fast), slow: makeBatch(partition.slow) };
66
135
  }
67
136
  /** A missing/stat-failed path is never allowed to bypass the slow lane. */
68
137
  export function isFastLocalBatch(paths, changes) {
@@ -97,7 +166,7 @@ export function isFastLocalBatch(paths, changes) {
97
166
  }
98
167
  return files > 0;
99
168
  }
100
- /** Receiver events carry no content length, so only bounded event batches qualify. */
169
+ /** A receiver event needs a known small upsert size before it can go realtime. */
101
170
  export function isFastReceiverBatch(eventCount) {
102
171
  return eventCount > 0 && eventCount <= resolveFastLaneLimits().maxFiles;
103
172
  }
@@ -161,6 +230,18 @@ export function adaptivePollMs(load1, cpuCount, floorMs = ADAPTIVE_POLL_FLOOR_MS
161
230
  const ratio = Math.max(0, Math.min(1, saturation));
162
231
  return Math.round(floorMs + ratio * (ceilMs - floorMs));
163
232
  }
233
+ /**
234
+ * A receiver that has observed messages owns a bounded drain, even if its
235
+ * slow lane is currently waiting for the operation lock. Do not stretch the
236
+ * cadence from that observed work to the CPU-derived backoff ceiling.
237
+ */
238
+ export function resolveWatchPollMs(explicitPollMs, receiverHasQueuedWork, loadAvg, cpuCount) {
239
+ if (explicitPollMs !== undefined)
240
+ return explicitPollMs;
241
+ return receiverHasQueuedWork
242
+ ? 60_000
243
+ : adaptivePollMs(loadAvg, cpuCount);
244
+ }
164
245
  /**
165
246
  * Advance the cadence from its previous monotonic deadline while the runner is
166
247
  * on time. When a pass overruns its slot, give the host one complete interval
@@ -557,6 +638,19 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
557
638
  resolveStoppedQueue();
558
639
  return;
559
640
  }
641
+ // A queued slow pass is the bulk-drain progress boundary. Admit it before
642
+ // refilling the realtime queue; otherwise a steady stream of fast work
643
+ // perpetually leapfrogs a receiver's bulk batch and its SQS poll loop
644
+ // cannot issue another receive.
645
+ if (activeSlowPass === null &&
646
+ activeFastPass === null &&
647
+ pendingSlowPasses.length > 0) {
648
+ const slow = pendingSlowPasses.shift();
649
+ if (slow) {
650
+ const current = startGuardedTask(slow.task, "slow");
651
+ void current.then(slow.resolve, slow.reject);
652
+ }
653
+ }
560
654
  if (activeFastPass === null) {
561
655
  const fast = pendingFastPasses.shift();
562
656
  if (fast) {
@@ -905,6 +999,22 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
905
999
  : relativePath.slice(prefix.length + 1),
906
1000
  };
907
1001
  };
1002
+ /**
1003
+ * A receiver event is eligible only when its upsert bytes are known small.
1004
+ * Current publishers do not require a size field, so a locally journaled
1005
+ * version is the backwards-compatible HEAD-size source. An event with no
1006
+ * declared or journal-known size is bulk: guessing would let a large legacy
1007
+ * transfer consume the realtime lane.
1008
+ */
1009
+ const receiverEventSize = (event) => {
1010
+ if (event.kind !== "upsert")
1011
+ return null;
1012
+ const coordinates = journalCoordinates(event.relativePath);
1013
+ if (!coordinates)
1014
+ return null;
1015
+ const size = readJournal(coordinates.slug).files[coordinates.key]?.size;
1016
+ return Number.isSafeInteger(size) && size >= 0 ? size : null;
1017
+ };
908
1018
  const captureLocalDeleteSnapshots = (relativePath, kind) => {
909
1019
  const coordinates = journalCoordinates(relativePath);
910
1020
  const route = routeChangeToTarget(relativePath);
@@ -1030,6 +1140,27 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1030
1140
  }
1031
1141
  return paths;
1032
1142
  };
1143
+ const publishedContentHashBaseline = () => {
1144
+ const hashes = {};
1145
+ for (const { slug, journal } of listJournals()) {
1146
+ const isPersonal = slug === PERSONAL_VAULT_JOURNAL_SLUG || slug === "personal";
1147
+ const route = isPersonal
1148
+ ? { kind: "personal" }
1149
+ : { kind: "company", slug };
1150
+ if (!isWatchRouteSelected(route, watchRouteSelection))
1151
+ continue;
1152
+ for (const [key, entry] of Object.entries(journal.files)) {
1153
+ // Tombstones and link records have no live regular-file body for the
1154
+ // PushEventEmitter's SHA-256 comparison, so they must not seed it.
1155
+ if (entry.removedAt !== undefined ||
1156
+ entry.kind === "symlink" ||
1157
+ entry.localDiverges)
1158
+ continue;
1159
+ hashes[isPersonal ? key : `companies/${slug}/${key}`] = `sha256:${entry.hash}`;
1160
+ }
1161
+ }
1162
+ return hashes;
1163
+ };
1033
1164
  const prepareWatcherChanges = (changes) => {
1034
1165
  const journals = new Map();
1035
1166
  const dirty = new Set();
@@ -1266,6 +1397,12 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1266
1397
  return worstExit;
1267
1398
  };
1268
1399
  let receiver = null;
1400
+ let receiverHasQueuedWork = false;
1401
+ const onReceiveActivity = (hasMessages) => {
1402
+ receiverHasQueuedWork = hasMessages;
1403
+ if (hasMessages)
1404
+ interruptWait();
1405
+ };
1269
1406
  let eventSync = null;
1270
1407
  const realtimeState = { scheduler: null, nextHighWaterAt: null };
1271
1408
  function activateEventPushSurfaces(rediscovery = false) {
@@ -1342,29 +1479,34 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1342
1479
  watcher.onChange((changedRelPath, batch) => {
1343
1480
  if (stopped)
1344
1481
  return;
1345
- // A complete, small watcher batch is dispatched immediately while a
1346
- // bulk pass is in flight. Leaving it in the next poll iteration would
1347
- // still make it wait for that pass even though the fast lane exists.
1482
+ // Peel a bounded realtime sub-batch from a mixed watcher batch while a
1483
+ // bulk pass is in flight. The residue enters the ordinary pending drain
1484
+ // immediately; a latest path appears in exactly one of these maps.
1348
1485
  const selectedBatch = batch ? selectWatchBatch(batch) : null;
1349
- if (selectedBatch &&
1350
- !selectedBatch.overflowed &&
1351
- activeSlowPass !== null &&
1352
- isFastLocalBatch(selectedBatch.paths.keys(), selectedBatch.changes)) {
1486
+ const lanes = selectedBatch && !selectedBatch.overflowed
1487
+ ? partitionLocalBatch(selectedBatch)
1488
+ : null;
1489
+ if (lanes?.fast &&
1490
+ activeSlowPass !== null) {
1353
1491
  const generation = ++nextWatcherBatchGeneration;
1354
1492
  // Fast dispatch bypasses the pending-batch ingress, so preserve both
1355
1493
  // of its contracts here: only selected routes may run, and V2 sees
1356
1494
  // the same dirty signal as the compatibility drain.
1357
1495
  realtimeState.scheduler?.signal("watcher");
1358
- void runScopedDrain(selectedBatch, generation, true).then((exit) => {
1496
+ void runScopedDrain(lanes.fast, generation, true).then((exit) => {
1359
1497
  if (exit === 0)
1360
1498
  return;
1361
- restorePendingWatcherPaths(selectedBatch.paths, generation);
1499
+ restorePendingWatcherPaths(lanes.fast.paths, generation);
1362
1500
  interruptWait();
1363
1501
  }).catch((err) => {
1364
- restorePendingWatcherPaths(selectedBatch.paths, generation);
1502
+ restorePendingWatcherPaths(lanes.fast.paths, generation);
1365
1503
  process.stderr.write(`watch fast-lane scoped push failed, will retry next tick: ${describeError(err)}\n`);
1366
1504
  interruptWait();
1367
1505
  });
1506
+ if (lanes.slow && addPendingWatcherChange(undefined, lanes.slow)) {
1507
+ realtimeState.scheduler?.signal("watcher");
1508
+ interruptWait();
1509
+ }
1368
1510
  return;
1369
1511
  }
1370
1512
  if (addPendingWatcherChange(changedRelPath, batch)) {
@@ -1454,40 +1596,54 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1454
1596
  const receiverSyncBatchFn = async ({ events }) => {
1455
1597
  if (stopped)
1456
1598
  return;
1599
+ // The injectable receiver seam and the production SQS receiver both
1600
+ // arrive here only after a successful receive. Keep the cadence tight
1601
+ // until the receiver later reports an empty outer long-poll.
1602
+ onReceiveActivity(true);
1457
1603
  // A received wake/high-water signal means there is bounded work now.
1458
1604
  // It wakes the scheduler but is still funneled through runGuarded below,
1459
1605
  // so it cannot overlap the pass that may currently own the operation
1460
1606
  // lock.
1461
- interruptWait();
1462
- const pathsByRoute = new Map();
1463
- let eligibleEventCount = 0;
1464
- for (const event of events) {
1465
- const route = routeChangeToTarget(event.relativePath);
1466
- if (!route)
1467
- continue;
1468
- eligibleEventCount += 1;
1469
- const key = routeKey(route);
1470
- const group = pathsByRoute.get(key) ?? { route, paths: [] };
1471
- group.paths.push(event.relativePath);
1472
- pathsByRoute.set(key, group);
1473
- }
1474
- // One receiver delivery can fan out to many routes. Apply the advertised
1475
- // cap to its whole eligible payload, not independently to each route.
1476
- const lane = isFastReceiverBatch(eligibleEventCount)
1477
- ? "fast"
1478
- : "slow";
1479
- for (const { route, paths } of pathsByRoute.values()) {
1480
- // One guarded targeted pass per affected route, with every coalesced
1481
- // path for that route expressed through the watcher's existing argv seam.
1482
- const targetedArgv = buildTargetedPullArgv(route, passArgv, paths);
1483
- const result = await runGuarded(targetedArgv, undefined, lane);
1484
- if (passExitCode(result) !== 0) {
1485
- throw new Error(`targeted pull failed with exit code ${passExitCode(result)}`);
1607
+ // Apply the caps once across the whole delivery, then retain the residue
1608
+ // in the normal lane. The receiver already coalesces to one latest event
1609
+ // per path, so the partition cannot split revisions of one path.
1610
+ const lanes = partitionByKnownSizes(events, receiverEventSize);
1611
+ const drainLane = async (lane, laneEvents) => {
1612
+ const pathsByRoute = new Map();
1613
+ for (const event of laneEvents) {
1614
+ const route = routeChangeToTarget(event.relativePath);
1615
+ if (!route)
1616
+ continue;
1617
+ const key = routeKey(route);
1618
+ const group = pathsByRoute.get(key) ?? { route, paths: [] };
1619
+ group.paths.push(event.relativePath);
1620
+ pathsByRoute.set(key, group);
1486
1621
  }
1487
- }
1622
+ for (const { route, paths } of pathsByRoute.values()) {
1623
+ const targetedArgv = buildTargetedPullArgv(route, passArgv, paths);
1624
+ const result = await runGuarded(targetedArgv, undefined, lane);
1625
+ if (passExitCode(result) !== 0) {
1626
+ throw new Error(`targeted pull failed with exit code ${passExitCode(result)}`);
1627
+ }
1628
+ }
1629
+ };
1630
+ // Dispatch realtime work before awaiting its bulk residue. This lets an
1631
+ // established slow pass continue while the small scoped pull uses its
1632
+ // ref-counted lock carve-out; the residue remains serialized and cannot
1633
+ // starve the slow lane.
1634
+ await drainLane("fast", lanes.fast);
1635
+ await drainLane("slow", lanes.slow);
1636
+ };
1637
+ receiverSyncBatchFn.partition = (events) => {
1638
+ const lanes = partitionByKnownSizes(events, receiverEventSize);
1639
+ return [lanes.fast, lanes.slow].filter((lane) => lane.length > 0);
1488
1640
  };
1489
1641
  const createReceiver = deps.createReceiver ?? (() => new NoopPushReceiver());
1490
- receiver = createReceiver({ syncBatchFn: receiverSyncBatchFn, hqRoot });
1642
+ receiver = createReceiver({
1643
+ syncBatchFn: receiverSyncBatchFn,
1644
+ hqRoot,
1645
+ onReceiveActivity,
1646
+ });
1491
1647
  void Promise.resolve(receiver.start()).catch(() => undefined);
1492
1648
  if (deps.startRealtimeScheduler) {
1493
1649
  // Bound the very first post-maintenance wait while the async scheduler
@@ -1562,6 +1718,9 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1562
1718
  log: (m) => process.stderr.write(`${m}\n`),
1563
1719
  telemetryClient: eventSyncClient,
1564
1720
  telemetryClaims: claims,
1721
+ publishedContentHashStateDir: getStateDir(),
1722
+ initialPublishedContentHashes: publishedContentHashBaseline(),
1723
+ onReceiveActivity,
1565
1724
  });
1566
1725
  if (!handles)
1567
1726
  return;
@@ -1807,7 +1966,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1807
1966
  process.stderr.write(`hq-sync-runner: backup retention dry-run failed — ${error instanceof Error ? error.message : String(error)}\n`);
1808
1967
  }
1809
1968
  }
1810
- const nextPollMs = explicitPollMs ?? adaptivePollMs(sampleLoadAvg(), cpuCount);
1969
+ const nextPollMs = explicitPollMs ?? resolveWatchPollMs(undefined, receiverHasQueuedWork, receiverHasQueuedWork ? 0 : sampleLoadAvg(), cpuCount);
1811
1970
  const activeRealtimeScheduler = realtimeState.scheduler;
1812
1971
  let nextRealtimeHighWaterAt = realtimeState.nextHighWaterAt;
1813
1972
  if (activeRealtimeScheduler !== null && nextRealtimeHighWaterAt !== null && monotonicNow() >= nextRealtimeHighWaterAt) {
@@ -1820,7 +1979,13 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
1820
1979
  } while (nextRealtimeHighWaterAt <= monotonicNow());
1821
1980
  realtimeState.nextHighWaterAt = nextRealtimeHighWaterAt;
1822
1981
  }
1823
- const schedule = advanceMonotonicDeadline(nextDueAt, monotonicNow(), nextPollMs);
1982
+ const now = monotonicNow();
1983
+ // Queued receiver work shortens an already scheduled adaptive poll from
1984
+ // as long as ten minutes to one minute. Its deadline must start at this
1985
+ // receive-driven cycle rather than extending the old, still-future slot.
1986
+ const schedule = receiverHasQueuedWork && explicitPollMs === undefined
1987
+ ? { nextDueAt: now + nextPollMs, delayMs: nextPollMs }
1988
+ : advanceMonotonicDeadline(nextDueAt, now, nextPollMs);
1824
1989
  nextDueAt = schedule.nextDueAt;
1825
1990
  const untilHighWaterMs = realtimeState.nextHighWaterAt === null
1826
1991
  ? schedule.delayMs