@indigoai-us/hq-cloud 6.18.35 → 6.18.38-alpha.0

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 (38) 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 +165 -34
  11. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  12. package/dist/bin/sync-runner-watch-loop.test.js +46 -0
  13. package/dist/bin/sync-runner-watch-loop.test.js.map +1 -1
  14. package/dist/bin/sync-runner.d.ts +11 -1
  15. package/dist/bin/sync-runner.d.ts.map +1 -1
  16. package/dist/bin/sync-runner.js +23 -4
  17. package/dist/bin/sync-runner.js.map +1 -1
  18. package/dist/bin/sync-runner.test.js +956 -4
  19. package/dist/bin/sync-runner.test.js.map +1 -1
  20. package/dist/lib/describe-error.d.ts +4 -0
  21. package/dist/lib/describe-error.d.ts.map +1 -1
  22. package/dist/lib/describe-error.js +37 -0
  23. package/dist/lib/describe-error.js.map +1 -1
  24. package/dist/lib/describe-error.test.js +18 -1
  25. package/dist/lib/describe-error.test.js.map +1 -1
  26. package/dist/outcome-telemetry.d.ts +16 -6
  27. package/dist/outcome-telemetry.d.ts.map +1 -1
  28. package/dist/outcome-telemetry.js +105 -11
  29. package/dist/outcome-telemetry.js.map +1 -1
  30. package/dist/outcome-telemetry.test.js +236 -0
  31. package/dist/outcome-telemetry.test.js.map +1 -1
  32. package/dist/sync/push-receiver.d.ts +5 -0
  33. package/dist/sync/push-receiver.d.ts.map +1 -1
  34. package/dist/sync/push-receiver.js +115 -52
  35. package/dist/sync/push-receiver.js.map +1 -1
  36. package/dist/sync/push-receiver.test.js +349 -2
  37. package/dist/sync/push-receiver.test.js.map +1 -1
  38. package/package.json +1 -1
@@ -6682,6 +6682,469 @@ describe("runRunnerWithLoop — event-push wiring", () => {
6682
6682
  process.env.HQ_SYNC_PARALLEL_COORDINATOR = previousCoordinator;
6683
6683
  }
6684
6684
  });
6685
+ it("handles fake SQS A/B delivery through coordinator preemption", async () => {
6686
+ const previousCoordinator = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
6687
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
6688
+ const personalPath = "personal/notes/fake-sqs-preempt.md";
6689
+ const event = (relativePath, sequenceNumber) => ({
6690
+ kind: "upsert",
6691
+ relativePath,
6692
+ contentHash: `sha256:${"0".repeat(64)}`,
6693
+ mtime: "2026-10-02T12:00:00.000Z",
6694
+ originDeviceId: "peer-device",
6695
+ originTenantId: "tenant-indigo",
6696
+ sequenceNumber,
6697
+ eventTimestamp: "2026-10-02T12:00:00.000Z",
6698
+ });
6699
+ const queue = [
6700
+ { receipt: "rh-a", events: [event(personalPath, 1), event("companies/indigo/a-side.md", 1)] },
6701
+ { receipt: "rh-b", events: [event(personalPath, 2)] },
6702
+ ];
6703
+ const deleted = [];
6704
+ const released = [];
6705
+ let receiveCalls = 0;
6706
+ const receiveMessage = async () => {
6707
+ receiveCalls += 1;
6708
+ return queue.shift();
6709
+ };
6710
+ let receiverSync;
6711
+ let shutdown = () => { };
6712
+ let releaseFirst = () => { };
6713
+ let markFirstStarted = () => { };
6714
+ const firstStarted = new Promise((resolve) => { markFirstStarted = resolve; });
6715
+ const firstGate = new Promise((resolve) => { releaseFirst = resolve; });
6716
+ const appliedPersonalPaths = [];
6717
+ let personalPasses = 0;
6718
+ const runPass = vi.fn(async (argv, checkpoint) => {
6719
+ if (argv.includes(personalPath)) {
6720
+ personalPasses += 1;
6721
+ if (personalPasses === 1) {
6722
+ markFirstStarted();
6723
+ await firstGate;
6724
+ if (checkpoint?.shouldAbort?.())
6725
+ return completedPassOutcome();
6726
+ }
6727
+ appliedPersonalPaths.push(personalPath);
6728
+ }
6729
+ return completedPassOutcome();
6730
+ });
6731
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "pull", "--hq-root", "/tmp/hq"], {
6732
+ runPass,
6733
+ runPassSupportsCooperativeCheckpoint: true,
6734
+ createWatcher: () => makeBatchWatcherStub(),
6735
+ createReceiver: ({ syncBatchFn }) => {
6736
+ receiverSync = syncBatchFn;
6737
+ return { connected: true, start: async () => { }, dispose: async () => { } };
6738
+ },
6739
+ sleep: () => new Promise(() => { }),
6740
+ onShutdownSignal: (handler) => { shutdown = handler; return () => { }; },
6741
+ });
6742
+ const settle = async (receipt, delivery) => {
6743
+ try {
6744
+ await delivery;
6745
+ deleted.push(receipt);
6746
+ }
6747
+ catch {
6748
+ released.push(receipt);
6749
+ }
6750
+ };
6751
+ try {
6752
+ await flushLoopMicrotasks(30);
6753
+ const deliveryA = await receiveMessage();
6754
+ const handledA = receiverSync({ events: deliveryA.events, signal: new AbortController().signal });
6755
+ await firstStarted;
6756
+ const deliveryB = await receiveMessage();
6757
+ const handledB = receiverSync({ events: deliveryB.events, signal: new AbortController().signal });
6758
+ await flushLoopMicrotasks(30);
6759
+ releaseFirst();
6760
+ await Promise.all([settle(deliveryA.receipt, handledA), settle(deliveryB.receipt, handledB)]);
6761
+ expect(receiveCalls).toBe(2);
6762
+ expect(appliedPersonalPaths).toEqual([personalPath]);
6763
+ expect(personalPasses).toBe(2);
6764
+ expect(deleted).toContain("rh-b");
6765
+ expect(deleted.includes("rh-a") || released.includes("rh-a")).toBe(true);
6766
+ expect(receiveCalls).toBeLessThanOrEqual(2);
6767
+ }
6768
+ finally {
6769
+ releaseFirst();
6770
+ shutdown();
6771
+ await loop;
6772
+ if (previousCoordinator === undefined)
6773
+ delete process.env.HQ_SYNC_PARALLEL_COORDINATOR;
6774
+ else
6775
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = previousCoordinator;
6776
+ }
6777
+ });
6778
+ it("keeps fake SQS polling after A and B are admitted and applies B within bounded steps", async () => {
6779
+ const previousCoordinator = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
6780
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
6781
+ const personalPath = "personal/notes/fake-sqs-bounded-preempt.md";
6782
+ const event = (relativePath, sequenceNumber) => ({
6783
+ kind: "upsert",
6784
+ relativePath,
6785
+ contentHash: `sha256:${"0".repeat(64)}`,
6786
+ mtime: "2026-10-02T12:00:00.000Z",
6787
+ originDeviceId: "peer-device",
6788
+ originTenantId: "tenant-indigo",
6789
+ sequenceNumber,
6790
+ eventTimestamp: "2026-10-02T12:00:00.000Z",
6791
+ });
6792
+ const queue = [
6793
+ { receipt: "rh-bounded-a", events: [event(personalPath, 1), event("companies/indigo/bounded-a.md", 1)] },
6794
+ { receipt: "rh-bounded-b", events: [event(personalPath, 2)] },
6795
+ null,
6796
+ ];
6797
+ const deleted = [];
6798
+ const released = [];
6799
+ let receiveCalls = 0;
6800
+ const receiveMessage = async () => {
6801
+ receiveCalls += 1;
6802
+ return queue.shift() ?? null;
6803
+ };
6804
+ let receiverSync;
6805
+ let shutdown = () => { };
6806
+ let releaseFirst = () => { };
6807
+ let markFirstStarted = () => { };
6808
+ const firstStarted = new Promise((resolve) => { markFirstStarted = resolve; });
6809
+ const firstGate = new Promise((resolve) => { releaseFirst = resolve; });
6810
+ const appliedPersonalPaths = [];
6811
+ let personalPasses = 0;
6812
+ const runPass = vi.fn(async (argv, checkpoint) => {
6813
+ if (argv.includes(personalPath)) {
6814
+ personalPasses += 1;
6815
+ if (personalPasses === 1) {
6816
+ markFirstStarted();
6817
+ await firstGate;
6818
+ if (checkpoint?.shouldAbort?.())
6819
+ return completedPassOutcome();
6820
+ }
6821
+ appliedPersonalPaths.push(personalPath);
6822
+ }
6823
+ return completedPassOutcome();
6824
+ });
6825
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "pull", "--hq-root", "/tmp/hq"], {
6826
+ runPass,
6827
+ runPassSupportsCooperativeCheckpoint: true,
6828
+ createWatcher: () => makeBatchWatcherStub(),
6829
+ createReceiver: ({ syncBatchFn }) => {
6830
+ receiverSync = syncBatchFn;
6831
+ return { connected: true, start: async () => { }, dispose: async () => { } };
6832
+ },
6833
+ sleep: () => new Promise(() => { }),
6834
+ onShutdownSignal: (handler) => { shutdown = handler; return () => { }; },
6835
+ });
6836
+ const admit = (delivery) => {
6837
+ const handled = receiverSync({ events: delivery.events, signal: new AbortController().signal });
6838
+ return handled.then(() => { deleted.push(delivery.receipt); }, () => { released.push(delivery.receipt); });
6839
+ };
6840
+ let handledA;
6841
+ let handledB;
6842
+ try {
6843
+ await flushLoopMicrotasks(30);
6844
+ const deliveryA = await receiveMessage();
6845
+ expect(deliveryA).not.toBeNull();
6846
+ handledA = admit(deliveryA);
6847
+ await firstStarted;
6848
+ const deliveryB = await receiveMessage();
6849
+ expect(deliveryB).not.toBeNull();
6850
+ handledB = admit(deliveryB);
6851
+ // A third receive is issued after both long pulls have been admitted.
6852
+ expect(await receiveMessage()).toBeNull();
6853
+ expect(receiveCalls).toBe(3);
6854
+ releaseFirst();
6855
+ let schedulerSteps = 0;
6856
+ while (appliedPersonalPaths.length === 0 && schedulerSteps < 12) {
6857
+ schedulerSteps += 1;
6858
+ await flushLoopMicrotasks(1);
6859
+ }
6860
+ expect(personalPasses).toBe(2);
6861
+ expect(appliedPersonalPaths).toEqual([personalPath]);
6862
+ expect(schedulerSteps).toBeLessThanOrEqual(12);
6863
+ await Promise.all([handledA, handledB]);
6864
+ expect(deleted).toContain("rh-bounded-b");
6865
+ expect(deleted.includes("rh-bounded-a") || released.includes("rh-bounded-a")).toBe(true);
6866
+ }
6867
+ finally {
6868
+ releaseFirst();
6869
+ shutdown();
6870
+ await loop;
6871
+ if (previousCoordinator === undefined)
6872
+ delete process.env.HQ_SYNC_PARALLEL_COORDINATOR;
6873
+ else
6874
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = previousCoordinator;
6875
+ }
6876
+ });
6877
+ it("threads the receiver drain abort signal to the targeted pull checkpoint", async () => {
6878
+ const previousCoordinator = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
6879
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
6880
+ const targetPath = "personal/notes/receiver-timeout-abort.md";
6881
+ let receiverSync;
6882
+ let shutdown = () => { };
6883
+ let releasePull = () => { };
6884
+ let markPullStarted = () => { };
6885
+ const pullStarted = new Promise((resolve) => { markPullStarted = resolve; });
6886
+ const pullGate = new Promise((resolve) => { releasePull = resolve; });
6887
+ let abortObservedAtSafePoint = false;
6888
+ const runPass = vi.fn(async (argv, checkpoint) => {
6889
+ if (!argv.includes(targetPath))
6890
+ return completedPassOutcome();
6891
+ markPullStarted();
6892
+ await pullGate;
6893
+ abortObservedAtSafePoint = checkpoint?.shouldAbort?.() === true;
6894
+ return completedPassOutcome();
6895
+ });
6896
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "pull", "--hq-root", "/tmp/hq"], {
6897
+ runPass,
6898
+ runPassSupportsCooperativeCheckpoint: true,
6899
+ createWatcher: () => makeBatchWatcherStub(),
6900
+ createReceiver: ({ syncBatchFn }) => {
6901
+ receiverSync = syncBatchFn;
6902
+ return { connected: true, start: async () => { }, dispose: async () => { } };
6903
+ },
6904
+ sleep: () => new Promise(() => { }),
6905
+ onShutdownSignal: (handler) => { shutdown = handler; return () => { }; },
6906
+ });
6907
+ const event = {
6908
+ kind: "upsert",
6909
+ relativePath: targetPath,
6910
+ contentHash: `sha256:${"0".repeat(64)}`,
6911
+ mtime: "2026-10-02T12:00:00.000Z",
6912
+ originDeviceId: "peer-device",
6913
+ originTenantId: "tenant-indigo",
6914
+ sequenceNumber: 1,
6915
+ eventTimestamp: "2026-10-02T12:00:00.000Z",
6916
+ };
6917
+ const controller = new AbortController();
6918
+ try {
6919
+ await flushLoopMicrotasks(30);
6920
+ const delivery = receiverSync({ events: [event], signal: controller.signal });
6921
+ await pullStarted;
6922
+ controller.abort(new Error("test drain timeout"));
6923
+ releasePull();
6924
+ await delivery;
6925
+ expect(abortObservedAtSafePoint).toBe(true);
6926
+ }
6927
+ finally {
6928
+ releasePull();
6929
+ shutdown();
6930
+ await loop;
6931
+ if (previousCoordinator === undefined)
6932
+ delete process.env.HQ_SYNC_PARALLEL_COORDINATOR;
6933
+ else
6934
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = previousCoordinator;
6935
+ }
6936
+ });
6937
+ it("stops a preempted receiver pull at its next durable file boundary", async () => {
6938
+ const previousCoordinator = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
6939
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
6940
+ let receiverSync;
6941
+ let shutdown = () => { };
6942
+ let releaseFirst = () => { };
6943
+ let markFirstStarted = () => { };
6944
+ const firstStarted = new Promise((resolve) => { markFirstStarted = resolve; });
6945
+ const firstGate = new Promise((resolve) => { releaseFirst = resolve; });
6946
+ const appliedPersonalPaths = [];
6947
+ const localFiles = new Map();
6948
+ const journalRows = new Map();
6949
+ const runPass = vi.fn(async (argv, checkpoint) => {
6950
+ const personalPaths = [
6951
+ "personal/notes/preempted.md",
6952
+ "personal/notes/next-file.md",
6953
+ ].filter((path) => argv.includes(path));
6954
+ if (personalPaths.includes("personal/notes/preempted.md") && appliedPersonalPaths.length === 0) {
6955
+ markFirstStarted();
6956
+ await firstGate;
6957
+ if (checkpoint?.shouldAbort?.())
6958
+ return completedPassOutcome();
6959
+ }
6960
+ for (const path of personalPaths) {
6961
+ if (checkpoint?.shouldAbort?.())
6962
+ break;
6963
+ // Model the sync engine's durable safe point: the local replacement and
6964
+ // its journal row are committed together before the next abort check.
6965
+ localFiles.set(path, `delivery-${argv.includes("--sequence=2") ? 2 : 1}`);
6966
+ journalRows.set(path, localFiles.get(path));
6967
+ appliedPersonalPaths.push(path);
6968
+ }
6969
+ return completedPassOutcome();
6970
+ });
6971
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "pull", "--hq-root", "/tmp/hq"], {
6972
+ runPass,
6973
+ runPassSupportsCooperativeCheckpoint: true,
6974
+ createWatcher: () => makeBatchWatcherStub(),
6975
+ createReceiver: ({ syncBatchFn }) => {
6976
+ receiverSync = syncBatchFn;
6977
+ return { connected: true, start: async () => { }, dispose: async () => { } };
6978
+ },
6979
+ sleep: () => new Promise(() => { }),
6980
+ onShutdownSignal: (handler) => { shutdown = handler; return () => { }; },
6981
+ });
6982
+ const event = (relativePath, sequenceNumber) => ({
6983
+ kind: "upsert",
6984
+ relativePath,
6985
+ contentHash: "sha256:remote",
6986
+ mtime: "2026-10-02T12:00:00.000Z",
6987
+ originDeviceId: "peer-device",
6988
+ originTenantId: "tenant-indigo",
6989
+ sequenceNumber,
6990
+ eventTimestamp: "2026-10-02T12:00:00.000Z",
6991
+ });
6992
+ try {
6993
+ await flushLoopMicrotasks(30);
6994
+ const deliveryA = receiverSync({
6995
+ events: [event("personal/notes/preempted.md", 1), event("personal/notes/next-file.md", 1)],
6996
+ signal: new AbortController().signal,
6997
+ });
6998
+ await firstStarted;
6999
+ const deliveryB = receiverSync({
7000
+ events: [event("personal/notes/preempted.md", 2), event("personal/notes/next-file.md", 2)],
7001
+ signal: new AbortController().signal,
7002
+ });
7003
+ await flushLoopMicrotasks(30);
7004
+ releaseFirst();
7005
+ await deliveryA;
7006
+ await deliveryB;
7007
+ expect(appliedPersonalPaths).toEqual([
7008
+ "personal/notes/preempted.md",
7009
+ "personal/notes/next-file.md",
7010
+ ]);
7011
+ expect([...localFiles.keys()]).toEqual([...journalRows.keys()]);
7012
+ expect([...journalRows.entries()]).toEqual([...localFiles.entries()]);
7013
+ expect(localFiles.size).toBe(2);
7014
+ expect(journalRows.has("personal/notes/never-written.md")).toBe(false);
7015
+ }
7016
+ finally {
7017
+ releaseFirst();
7018
+ shutdown();
7019
+ await loop;
7020
+ if (previousCoordinator === undefined)
7021
+ delete process.env.HQ_SYNC_PARALLEL_COORDINATOR;
7022
+ else
7023
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = previousCoordinator;
7024
+ }
7025
+ });
7026
+ it("reconciles on-disk files and journal rows after a pull is preempted", async () => {
7027
+ const previousCoordinator = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
7028
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
7029
+ const journalSlug = "receiver-preempt-durable-boundary";
7030
+ const durablePath = "personal/notes/preempt-durable.md";
7031
+ const nextPath = "personal/notes/preempt-next.md";
7032
+ const localRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-preempt-durable-"));
7033
+ const journal = { version: "2", lastSync: "", pulls: [], files: {} };
7034
+ writeJournal(journalSlug, journal);
7035
+ let receiverSync;
7036
+ let shutdown = () => { };
7037
+ let releaseFirst = () => { };
7038
+ let markFirstStarted = () => { };
7039
+ const firstStarted = new Promise((resolve) => { markFirstStarted = resolve; });
7040
+ const firstGate = new Promise((resolve) => { releaseFirst = resolve; });
7041
+ const writesByPull = [];
7042
+ let targetedPulls = 0;
7043
+ const persist = (relativePath, body, writes) => {
7044
+ const localPath = path.join(localRoot, relativePath);
7045
+ fs.mkdirSync(path.dirname(localPath), { recursive: true });
7046
+ fs.writeFileSync(localPath, body);
7047
+ const stat = fs.statSync(localPath);
7048
+ const state = readJournal(journalSlug);
7049
+ state.files[relativePath] = {
7050
+ hash: body,
7051
+ size: stat.size,
7052
+ syncedAt: "2026-10-02T12:00:00.000Z",
7053
+ direction: "down",
7054
+ remoteEtag: body,
7055
+ mtimeMs: stat.mtimeMs,
7056
+ ctimeMs: stat.ctimeMs,
7057
+ };
7058
+ writeJournal(journalSlug, state);
7059
+ writes.push(relativePath);
7060
+ };
7061
+ const isDurable = (relativePath) => {
7062
+ const localPath = path.join(localRoot, relativePath);
7063
+ if (!fs.existsSync(localPath))
7064
+ return false;
7065
+ const state = readJournal(journalSlug);
7066
+ return state.files[relativePath]?.hash === fs.readFileSync(localPath, "utf8");
7067
+ };
7068
+ const runPass = vi.fn(async (argv, checkpoint) => {
7069
+ const paths = [durablePath, nextPath].filter((candidate) => argv.includes(candidate));
7070
+ if (paths.length === 0)
7071
+ return completedPassOutcome();
7072
+ const pullNumber = ++targetedPulls;
7073
+ const writes = [];
7074
+ writesByPull[pullNumber - 1] = writes;
7075
+ if (pullNumber === 1) {
7076
+ persist(durablePath, "durable-before-preemption", writes);
7077
+ markFirstStarted();
7078
+ await firstGate;
7079
+ if (checkpoint?.shouldAbort?.())
7080
+ return completedPassOutcome();
7081
+ }
7082
+ for (const relativePath of paths) {
7083
+ if (checkpoint?.shouldAbort?.())
7084
+ break;
7085
+ if (isDurable(relativePath))
7086
+ continue;
7087
+ persist(relativePath, `delivery-${pullNumber}-${relativePath}`, writes);
7088
+ }
7089
+ return completedPassOutcome();
7090
+ });
7091
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "pull", "--hq-root", "/tmp/hq"], {
7092
+ runPass,
7093
+ runPassSupportsCooperativeCheckpoint: true,
7094
+ createWatcher: () => makeBatchWatcherStub(),
7095
+ createReceiver: ({ syncBatchFn }) => {
7096
+ receiverSync = syncBatchFn;
7097
+ return { connected: true, start: async () => { }, dispose: async () => { } };
7098
+ },
7099
+ sleep: () => new Promise(() => { }),
7100
+ onShutdownSignal: (handler) => { shutdown = handler; return () => { }; },
7101
+ });
7102
+ const event = (relativePath, sequenceNumber) => ({
7103
+ kind: "upsert",
7104
+ relativePath,
7105
+ contentHash: `sha256:${"0".repeat(64)}`,
7106
+ mtime: "2026-10-02T12:00:00.000Z",
7107
+ originDeviceId: "peer-device",
7108
+ originTenantId: "tenant-indigo",
7109
+ sequenceNumber,
7110
+ eventTimestamp: "2026-10-02T12:00:00.000Z",
7111
+ });
7112
+ try {
7113
+ await flushLoopMicrotasks(30);
7114
+ const deliveryA = receiverSync({
7115
+ events: [event(durablePath, 1), event(nextPath, 1)],
7116
+ signal: new AbortController().signal,
7117
+ });
7118
+ await firstStarted;
7119
+ const deliveryB = receiverSync({
7120
+ events: [event(durablePath, 2), event(nextPath, 2)],
7121
+ signal: new AbortController().signal,
7122
+ });
7123
+ await flushLoopMicrotasks(30);
7124
+ releaseFirst();
7125
+ await Promise.all([deliveryA, deliveryB]);
7126
+ expect(writesByPull[0]).toEqual([durablePath]);
7127
+ expect(writesByPull[1]).toEqual([nextPath]);
7128
+ const expectedPaths = [durablePath, nextPath].sort();
7129
+ const localPaths = fs.readdirSync(path.join(localRoot, "personal", "notes"))
7130
+ .map((name) => `personal/notes/${name}`).sort();
7131
+ const finalJournal = readJournal(journalSlug);
7132
+ expect(localPaths).toEqual(expectedPaths);
7133
+ expect(Object.keys(finalJournal.files).sort()).toEqual(expectedPaths);
7134
+ for (const relativePath of expectedPaths)
7135
+ expect(isDurable(relativePath)).toBe(true);
7136
+ }
7137
+ finally {
7138
+ releaseFirst();
7139
+ shutdown();
7140
+ await loop;
7141
+ fs.rmSync(localRoot, { recursive: true, force: true });
7142
+ if (previousCoordinator === undefined)
7143
+ delete process.env.HQ_SYNC_PARALLEL_COORDINATOR;
7144
+ else
7145
+ process.env.HQ_SYNC_PARALLEL_COORDINATOR = previousCoordinator;
7146
+ }
7147
+ });
6685
7148
  it("acknowledges superseded receiver work and records refused telemetry", async () => {
6686
7149
  const previousCoordinator = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
6687
7150
  process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
@@ -10991,8 +11454,8 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
10991
11454
  "60000",
10992
11455
  ];
10993
11456
  const fullArgv = ["--companies", "--direction", "both", "--hq-root", "/tmp/hq"];
10994
- function startDrainLoop(runPass) {
10995
- return startDrainLoopWithArgv(runPass, watchArgv);
11457
+ function startDrainLoop(runPass, overrides = {}) {
11458
+ return startDrainLoopWithArgv(runPass, watchArgv, overrides);
10996
11459
  }
10997
11460
  function startBatchDrainLoop(runPass) {
10998
11461
  const watcher = makeBatchWatcherStub();
@@ -11016,7 +11479,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
11016
11479
  shutdown: () => triggerShutdown(),
11017
11480
  };
11018
11481
  }
11019
- function startDrainLoopWithArgv(runPass, argv) {
11482
+ function startDrainLoopWithArgv(runPass, argv, overrides = {}) {
11020
11483
  const watcher = makeWatcherStub();
11021
11484
  const sleep = makeSteppableSleep();
11022
11485
  let triggerShutdown = () => { };
@@ -11029,6 +11492,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
11029
11492
  triggerShutdown = handler;
11030
11493
  return () => { };
11031
11494
  },
11495
+ ...overrides,
11032
11496
  });
11033
11497
  return {
11034
11498
  loop,
@@ -11498,6 +11962,405 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
11498
11962
  await loop;
11499
11963
  capture.restore();
11500
11964
  });
11965
+ it("backs off watcher pushes after consecutive route failures", async () => {
11966
+ const clockState = { now: 0 };
11967
+ const timers = new Map();
11968
+ let nextTimerId = 0;
11969
+ const retryScheduler = {
11970
+ setTimeout(callback, delayMs) {
11971
+ const id = ++nextTimerId;
11972
+ timers.set(id, { dueAt: clockState.now + delayMs, callback });
11973
+ return id;
11974
+ },
11975
+ clearTimeout(handle) {
11976
+ timers.delete(handle);
11977
+ },
11978
+ advanceTo(now) {
11979
+ clockState.now = now;
11980
+ while (true) {
11981
+ const due = [...timers.entries()]
11982
+ .filter(([, timer]) => timer.dueAt <= now)
11983
+ .sort((left, right) => left[1].dueAt - right[1].dueAt)[0];
11984
+ if (!due)
11985
+ return;
11986
+ timers.delete(due[0]);
11987
+ due[1].callback();
11988
+ }
11989
+ },
11990
+ };
11991
+ const watcher = makeWatcherStub();
11992
+ const sleep = makeSteppableSleep();
11993
+ const scopedPushAttempts = [];
11994
+ const otherRouteAttempts = [];
11995
+ const diagnostics = [];
11996
+ const telemetry = [];
11997
+ let indigoPushCount = 0;
11998
+ let shutdown = () => { };
11999
+ const runPass = vi.fn(async (argv) => {
12000
+ if (argv.includes("--scope-path") && argv.includes("push")) {
12001
+ if (argv[argv.indexOf("--company") + 1] === "indigo") {
12002
+ scopedPushAttempts.push(clockState.now);
12003
+ indigoPushCount += 1;
12004
+ if (indigoPushCount === 3)
12005
+ return completedPassOutcome();
12006
+ if (indigoPushCount === 2) {
12007
+ return {
12008
+ exitCode: 1,
12009
+ result: {
12010
+ transient: [{ company: "indigo", message: "ECONNRESET host=sync.example.test" }],
12011
+ },
12012
+ };
12013
+ }
12014
+ return {
12015
+ exitCode: 1,
12016
+ result: {
12017
+ companyFailures: [{
12018
+ company: "indigo",
12019
+ message: "AccessDenied token=ghp_0123456789abcdefghijklmnopqrstuv https://bucket.example.test/key?X-Amz-Signature=secret /Users/alice/private.txt /tmp/hq/companies/indigo/knowledge/note.md",
12020
+ }],
12021
+ },
12022
+ };
12023
+ }
12024
+ otherRouteAttempts.push(clockState.now);
12025
+ }
12026
+ return completedPassOutcome();
12027
+ });
12028
+ const loop = runRunnerWithLoop(watchArgv, {
12029
+ runPass: runPass,
12030
+ clock: new FakeClock(),
12031
+ monotonicNow: () => clockState.now,
12032
+ pushRetryScheduler: retryScheduler,
12033
+ stderr: { write: (line) => { diagnostics.push(line); } },
12034
+ emitScopedPushFailureTelemetry: (event) => { telemetry.push(event); },
12035
+ createWatcher: () => watcher,
12036
+ sleep: sleep.sleep,
12037
+ onShutdownSignal: (handler) => {
12038
+ shutdown = handler;
12039
+ return () => { };
12040
+ },
12041
+ });
12042
+ try {
12043
+ await flushLoopMicrotasks();
12044
+ runPass.mockClear();
12045
+ watcher.emit("companies/indigo/knowledge/failing.md");
12046
+ await flushLoopMicrotasks(24);
12047
+ expect(scopedPushAttempts).toEqual([0]);
12048
+ expect(telemetry).toEqual([
12049
+ expect.objectContaining({ route: "company:indigo", consecutiveFailures: 1, nextRetryDelayMs: 5_000 }),
12050
+ ]);
12051
+ clockState.now = 1_000;
12052
+ watcher.emit("companies/other/knowledge/available.md");
12053
+ await flushLoopMicrotasks(24);
12054
+ expect(otherRouteAttempts).toEqual([1_000]);
12055
+ sleep.tick();
12056
+ await flushLoopMicrotasks(24);
12057
+ expect(scopedPushAttempts).toEqual([0]);
12058
+ retryScheduler.advanceTo(4_999);
12059
+ await flushLoopMicrotasks(24);
12060
+ expect(scopedPushAttempts).toEqual([0]);
12061
+ retryScheduler.advanceTo(5_000);
12062
+ await flushLoopMicrotasks(24);
12063
+ expect(scopedPushAttempts).toEqual([0, 5_000]);
12064
+ retryScheduler.advanceTo(14_999);
12065
+ await flushLoopMicrotasks(24);
12066
+ expect(scopedPushAttempts).toEqual([0, 5_000]);
12067
+ retryScheduler.advanceTo(15_000);
12068
+ await flushLoopMicrotasks(24);
12069
+ expect(scopedPushAttempts).toEqual([0, 5_000, 15_000]);
12070
+ watcher.emit("companies/indigo/knowledge/failing.md");
12071
+ await flushLoopMicrotasks(24);
12072
+ expect(scopedPushAttempts).toEqual([0, 5_000, 15_000, 15_000]);
12073
+ retryScheduler.advanceTo(19_999);
12074
+ await flushLoopMicrotasks(24);
12075
+ expect(scopedPushAttempts).toEqual([0, 5_000, 15_000, 15_000]);
12076
+ retryScheduler.advanceTo(20_000);
12077
+ await flushLoopMicrotasks(24);
12078
+ expect(scopedPushAttempts).toEqual([0, 5_000, 15_000, 15_000, 20_000]);
12079
+ for (const [dueAt, count] of [
12080
+ [30_000, 6],
12081
+ [50_000, 7],
12082
+ [90_000, 8],
12083
+ [170_000, 9],
12084
+ [330_000, 10],
12085
+ [630_000, 11],
12086
+ [930_000, 12],
12087
+ ]) {
12088
+ retryScheduler.advanceTo(dueAt - 1);
12089
+ await flushLoopMicrotasks(24);
12090
+ expect(scopedPushAttempts).toHaveLength(count - 1);
12091
+ retryScheduler.advanceTo(dueAt);
12092
+ await flushLoopMicrotasks(24);
12093
+ expect(scopedPushAttempts).toHaveLength(count);
12094
+ }
12095
+ expect(telemetry.map(({ consecutiveFailures, nextRetryDelayMs }) => [consecutiveFailures, nextRetryDelayMs]))
12096
+ .toEqual([
12097
+ [1, 5_000], [2, 10_000], [1, 5_000], [2, 10_000],
12098
+ [3, 20_000], [4, 40_000], [5, 80_000], [6, 160_000], [7, 300_000],
12099
+ ]);
12100
+ expect(diagnostics.join("")).toContain("sync.example.test");
12101
+ expect(telemetry.some(({ errorSummary }) => errorSummary.includes("sync.example.test"))).toBe(true);
12102
+ expect(diagnostics.join("")).toContain('"type":"scoped-push-failure"');
12103
+ const emittedText = diagnostics.join("") + JSON.stringify(telemetry);
12104
+ expect(emittedText).toContain("bucket.example.test");
12105
+ expect(emittedText).toContain("knowledge/note.md");
12106
+ expect(emittedText).not.toContain("ghp_0123456789");
12107
+ expect(emittedText).not.toContain("X-Amz-Signature");
12108
+ expect(emittedText).not.toContain("/Users/alice/private.txt");
12109
+ expect(emittedText).not.toContain("secret");
12110
+ }
12111
+ finally {
12112
+ shutdown();
12113
+ await flushLoopMicrotasks();
12114
+ await loop;
12115
+ }
12116
+ });
12117
+ it("includes transient push failures in the route error summary", async () => {
12118
+ const clockState = { now: 0 };
12119
+ const timers = new Map();
12120
+ let nextTimerId = 0;
12121
+ const retryScheduler = {
12122
+ setTimeout(callback, delayMs) {
12123
+ const id = ++nextTimerId;
12124
+ timers.set(id, { dueAt: clockState.now + delayMs, callback });
12125
+ return id;
12126
+ },
12127
+ clearTimeout(handle) {
12128
+ timers.delete(handle);
12129
+ },
12130
+ };
12131
+ const watcher = makeWatcherStub();
12132
+ const sleep = makeSteppableSleep();
12133
+ const diagnostics = [];
12134
+ const telemetry = [];
12135
+ let shutdown = () => { };
12136
+ const runPass = vi.fn(async (argv) => {
12137
+ if (argv.includes("--scope-path") &&
12138
+ argv.includes("--direction") &&
12139
+ argv[argv.indexOf("--direction") + 1] === "push" &&
12140
+ argv[argv.indexOf("--company") + 1] === "indigo") {
12141
+ return {
12142
+ exitCode: 1,
12143
+ result: {
12144
+ transient: [{ company: "indigo", message: "ECONNRESET host=sync.example.test" }],
12145
+ },
12146
+ };
12147
+ }
12148
+ return completedPassOutcome();
12149
+ });
12150
+ const loop = runRunnerWithLoop(watchArgv, {
12151
+ runPass: runPass,
12152
+ clock: new FakeClock(),
12153
+ monotonicNow: () => clockState.now,
12154
+ pushRetryScheduler: retryScheduler,
12155
+ stderr: { write: (line) => { diagnostics.push(line); } },
12156
+ emitScopedPushFailureTelemetry: (event) => { telemetry.push(event); },
12157
+ createWatcher: () => watcher,
12158
+ sleep: sleep.sleep,
12159
+ onShutdownSignal: (handler) => {
12160
+ shutdown = handler;
12161
+ return () => { };
12162
+ },
12163
+ });
12164
+ try {
12165
+ await flushLoopMicrotasks();
12166
+ runPass.mockClear();
12167
+ watcher.emit("companies/indigo/knowledge/transient.md");
12168
+ await flushLoopMicrotasks(24);
12169
+ expect(diagnostics.join("")).toContain("sync.example.test");
12170
+ expect(telemetry).toEqual([
12171
+ expect.objectContaining({ errorSummary: expect.stringContaining("sync.example.test") }),
12172
+ ]);
12173
+ }
12174
+ finally {
12175
+ shutdown();
12176
+ await flushLoopMicrotasks();
12177
+ await loop;
12178
+ }
12179
+ });
12180
+ it("preserves captured delete metadata while a backed-off route is deferred", async () => {
12181
+ const clockState = { now: 0 };
12182
+ const timers = new Map();
12183
+ let nextTimerId = 0;
12184
+ const retryScheduler = {
12185
+ setTimeout(callback, delayMs) {
12186
+ const id = ++nextTimerId;
12187
+ timers.set(id, { dueAt: clockState.now + delayMs, callback });
12188
+ return id;
12189
+ },
12190
+ clearTimeout(handle) {
12191
+ timers.delete(handle);
12192
+ },
12193
+ advanceTo(now) {
12194
+ clockState.now = now;
12195
+ for (const [id, timer] of [...timers]) {
12196
+ if (timer.dueAt > now)
12197
+ continue;
12198
+ timers.delete(id);
12199
+ timer.callback();
12200
+ }
12201
+ },
12202
+ };
12203
+ const watcher = makeBatchWatcherStub();
12204
+ const sleep = makeSteppableSleep();
12205
+ const pushCalls = [];
12206
+ let indigoPushCount = 0;
12207
+ let shutdown = () => { };
12208
+ const runPass = vi.fn(async (argv) => {
12209
+ if (argv.includes("--direction") && argv[argv.indexOf("--direction") + 1] === "push" && argv.includes("--company")) {
12210
+ pushCalls.push(argv);
12211
+ indigoPushCount += 1;
12212
+ if (indigoPushCount === 1) {
12213
+ return { exitCode: 1, result: { companyFailures: [{ company: "indigo", message: "temporary push failure" }] } };
12214
+ }
12215
+ }
12216
+ return completedPassOutcome();
12217
+ });
12218
+ const loop = runRunnerWithLoop(watchArgv, {
12219
+ runPass: runPass,
12220
+ clock: new FakeClock(),
12221
+ monotonicNow: () => clockState.now,
12222
+ pushRetryScheduler: retryScheduler,
12223
+ createWatcher: () => watcher,
12224
+ sleep: sleep.sleep,
12225
+ onShutdownSignal: (handler) => {
12226
+ shutdown = handler;
12227
+ return () => { };
12228
+ },
12229
+ });
12230
+ try {
12231
+ await flushLoopMicrotasks();
12232
+ runPass.mockClear();
12233
+ watcher.emit("companies/indigo/knowledge/first.md", {
12234
+ paths: new Map([["/tmp/hq/companies/indigo/knowledge/first.md", "companies/indigo/knowledge/first.md"]]),
12235
+ });
12236
+ if (sleep.pendingCount() > 0)
12237
+ sleep.tick();
12238
+ await flushLoopMicrotasks(40);
12239
+ expect(pushCalls).toHaveLength(1);
12240
+ expect([...timers.values()].map(({ dueAt }) => dueAt)).toEqual([5_000]);
12241
+ clockState.now = 1_000;
12242
+ const deletedPath = "/tmp/hq/companies/indigo/knowledge/gone.md";
12243
+ watcher.emit("companies/indigo/knowledge/gone.md", {
12244
+ paths: new Map([[deletedPath, "companies/indigo/knowledge/gone.md"]]),
12245
+ changes: new Map([[deletedPath, {
12246
+ kind: "unlink",
12247
+ deleteSnapshots: [{
12248
+ journalSlug: "indigo",
12249
+ journalPath: "knowledge/gone.md",
12250
+ deleteScopeRoot: "knowledge",
12251
+ absolutePath: deletedPath,
12252
+ remoteEtag: "etag",
12253
+ localHash: "sha256:gone",
12254
+ localKind: "file",
12255
+ }],
12256
+ }]]),
12257
+ });
12258
+ if (sleep.pendingCount() > 0)
12259
+ sleep.tick();
12260
+ await flushLoopMicrotasks(40);
12261
+ expect(pushCalls).toHaveLength(1);
12262
+ retryScheduler.advanceTo(5_000);
12263
+ if (sleep.pendingCount() > 0)
12264
+ sleep.tick();
12265
+ await waitForCondition("retry after backed-off unlink", () => pushCalls.length === 2);
12266
+ expect(pushCalls).toHaveLength(2);
12267
+ expect(pushCalls[1]).toContain("--delete-scope-path");
12268
+ expect(pushCalls[1]).toContain("knowledge");
12269
+ }
12270
+ finally {
12271
+ shutdown();
12272
+ await flushLoopMicrotasks();
12273
+ await loop;
12274
+ }
12275
+ });
12276
+ it("resets route backoff after a successful whole-route overflow push", async () => {
12277
+ const clockState = { now: 0 };
12278
+ const timers = new Map();
12279
+ let nextTimerId = 0;
12280
+ const retryScheduler = {
12281
+ setTimeout(callback, delayMs) {
12282
+ const id = ++nextTimerId;
12283
+ timers.set(id, { dueAt: clockState.now + delayMs, callback });
12284
+ return id;
12285
+ },
12286
+ clearTimeout(handle) {
12287
+ timers.delete(handle);
12288
+ },
12289
+ advanceTo(now) {
12290
+ clockState.now = now;
12291
+ for (const [id, timer] of [...timers]) {
12292
+ if (timer.dueAt > now)
12293
+ continue;
12294
+ timers.delete(id);
12295
+ timer.callback();
12296
+ }
12297
+ },
12298
+ };
12299
+ const watcher = makeBatchWatcherStub();
12300
+ const sleep = makeSteppableSleep();
12301
+ const pushAttemptTimes = [];
12302
+ let pushCount = 0;
12303
+ let shutdown = () => { };
12304
+ const runPass = vi.fn(async (argv) => {
12305
+ if (argv.includes("--direction") && argv[argv.indexOf("--direction") + 1] === "push" && argv.includes("--company")) {
12306
+ pushAttemptTimes.push(clockState.now);
12307
+ pushCount += 1;
12308
+ if (pushCount === 1 || pushCount === 3) {
12309
+ return { exitCode: 1, result: { companyFailures: [{ company: "indigo", message: "temporary push failure" }] } };
12310
+ }
12311
+ }
12312
+ return completedPassOutcome();
12313
+ });
12314
+ const loop = runRunnerWithLoop(watchArgv, {
12315
+ runPass: runPass,
12316
+ clock: new FakeClock(),
12317
+ monotonicNow: () => clockState.now,
12318
+ pushRetryScheduler: retryScheduler,
12319
+ createWatcher: () => watcher,
12320
+ sleep: sleep.sleep,
12321
+ onShutdownSignal: (handler) => {
12322
+ shutdown = handler;
12323
+ return () => { };
12324
+ },
12325
+ });
12326
+ try {
12327
+ await flushLoopMicrotasks();
12328
+ runPass.mockClear();
12329
+ watcher.emit("companies/indigo/knowledge/a.md", {
12330
+ paths: new Map([["/tmp/hq/companies/indigo/knowledge/a.md", "companies/indigo/knowledge/a.md"]]),
12331
+ });
12332
+ if (sleep.pendingCount() > 0)
12333
+ sleep.tick();
12334
+ await flushLoopMicrotasks(40);
12335
+ expect(pushAttemptTimes).toEqual([0]);
12336
+ expect([...timers.values()].map(({ dueAt }) => dueAt)).toEqual([5_000]);
12337
+ clockState.now = 1_000;
12338
+ watcher.emit("companies/indigo/knowledge/a.md", {
12339
+ paths: new Map([["/tmp/hq/companies/indigo/knowledge/a.md", "companies/indigo/knowledge/a.md"]]),
12340
+ overflowed: true,
12341
+ droppedRouteHints: ["companies/indigo/knowledge/a.md"],
12342
+ });
12343
+ if (sleep.pendingCount() > 0)
12344
+ sleep.tick();
12345
+ await flushLoopMicrotasks(40);
12346
+ expect(pushAttemptTimes).toEqual([0, 1_000]);
12347
+ expect(timers.size).toBe(0);
12348
+ clockState.now = 2_000;
12349
+ watcher.emit("companies/indigo/knowledge/b.md", {
12350
+ paths: new Map([["/tmp/hq/companies/indigo/knowledge/b.md", "companies/indigo/knowledge/b.md"]]),
12351
+ });
12352
+ if (sleep.pendingCount() > 0)
12353
+ sleep.tick();
12354
+ await flushLoopMicrotasks(40);
12355
+ expect(pushAttemptTimes).toEqual([0, 1_000, 2_000]);
12356
+ expect([...timers.values()].map(({ dueAt }) => dueAt)).toEqual([7_000]);
12357
+ }
12358
+ finally {
12359
+ shutdown();
12360
+ await flushLoopMicrotasks();
12361
+ await loop;
12362
+ }
12363
+ });
11501
12364
  it("reports interval diagnostics while the configured interval has not elapsed", async () => {
11502
12365
  const saved = process.env.HQ_SYNC_FULL_RECONCILE_MS;
11503
12366
  process.env.HQ_SYNC_FULL_RECONCILE_MS = "1000";
@@ -11817,7 +12680,29 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
11817
12680
  .mockResolvedValueOnce(0)
11818
12681
  .mockResolvedValueOnce(0)
11819
12682
  .mockResolvedValueOnce(0);
11820
- const { loop, watcher, tick, waitForPoll, shutdown } = startDrainLoop(runPass);
12683
+ const retryClock = { now: 0 };
12684
+ const retryTimers = new Map();
12685
+ let nextRetryTimerId = 0;
12686
+ const retryScheduler = {
12687
+ setTimeout(callback) {
12688
+ const id = ++nextRetryTimerId;
12689
+ retryTimers.set(id, callback);
12690
+ return id;
12691
+ },
12692
+ clearTimeout(handle) {
12693
+ retryTimers.delete(handle);
12694
+ },
12695
+ fire() {
12696
+ const callbacks = [...retryTimers.values()];
12697
+ retryTimers.clear();
12698
+ for (const callback of callbacks)
12699
+ callback();
12700
+ },
12701
+ };
12702
+ const { loop, watcher, tick, waitForPoll, shutdown } = startDrainLoop(runPass, {
12703
+ monotonicNow: () => retryClock.now,
12704
+ pushRetryScheduler: retryScheduler,
12705
+ });
11821
12706
  await waitForPoll();
11822
12707
  runPass.mockClear();
11823
12708
  const pushCallCount = () => runPass.mock.calls
@@ -11830,6 +12715,8 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
11830
12715
  await waitForCondition("first scoped push attempt", () => pushCallCount() >= 1);
11831
12716
  await waitForPoll();
11832
12717
  tick();
12718
+ retryClock.now = 5_000;
12719
+ retryScheduler.fire();
11833
12720
  await waitForCondition("retry scoped push attempt", () => pushCallCount() >= 2);
11834
12721
  const pushCalls = runPass.mock.calls
11835
12722
  .map((call) => call[0])
@@ -15019,6 +15906,71 @@ describe("defaultCollectTelemetry person-entity gate", () => {
15019
15906
  fs.rmSync(hqRoot, { recursive: true, force: true });
15020
15907
  }
15021
15908
  });
15909
+ it("uses the company hq-flags decision for done-state outcomes on forceCollect agent syncs", async () => {
15910
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-tele-agent-outcome-"));
15911
+ const previous = process.env.HQ_FLAG_CLOUD_OUTCOME_STORY_DONE_STATE;
15912
+ const previousHome = process.env.HOME;
15913
+ process.env.HOME = path.join(hqRoot, "home");
15914
+ process.env.HQ_FLAG_CLOUD_OUTCOME_STORY_DONE_STATE = "true";
15915
+ const posts = [];
15916
+ const diagnostics = [];
15917
+ try {
15918
+ const companyUid = "cmp_01TESTCOMPANY0000000000000";
15919
+ const agentUid = "agt_01KUSAGEAGENTTEST00000000";
15920
+ fs.mkdirSync(path.join(hqRoot, "companies", "acme", "projects", "p"), { recursive: true });
15921
+ fs.writeFileSync(path.join(hqRoot, "companies", "manifest.yaml"), [
15922
+ "companies:",
15923
+ " acme:",
15924
+ " name: Acme",
15925
+ " path: companies/acme",
15926
+ " repos: []",
15927
+ ` cloud_uid: ${companyUid}`,
15928
+ "",
15929
+ ].join("\n"));
15930
+ fs.writeFileSync(path.join(hqRoot, "companies", "acme", "projects", "p", "prd.json"), JSON.stringify({ name: "p", userStories: [{ id: "US-AGENT", state: "done" }] }));
15931
+ const client = {
15932
+ entity: {
15933
+ get: async () => ({}),
15934
+ listByType: async (type) => type === "agent"
15935
+ ? [{
15936
+ uid: agentUid,
15937
+ type: "agent",
15938
+ slug: "agent",
15939
+ status: "active",
15940
+ bucketName: `hq-vault-${agentUid}`,
15941
+ createdAt: "2026-06-01T00:00:00Z",
15942
+ }]
15943
+ : [],
15944
+ },
15945
+ getTelemetryOptIn: async () => ({ enabled: false }),
15946
+ postUsage: async () => ({ ok: true }),
15947
+ postSkillInvocations: async () => ({ written: 0, skipped: [] }),
15948
+ postOutcomeEvents: async (batch) => {
15949
+ posts.push(batch);
15950
+ return { ok: true, written: batch.events.length, deduped: 0 };
15951
+ },
15952
+ };
15953
+ await defaultCollectTelemetry(client, false, hqRoot, (diagnostic) => diagnostics.push(diagnostic), undefined, agentUid, async () => "test-token");
15954
+ expect(fs.existsSync(path.join(hqRoot, ".hq", "machine-id"))).toBe(true);
15955
+ expect(posts, JSON.stringify(diagnostics)).toHaveLength(1);
15956
+ expect(posts[0].events[0]).toMatchObject({
15957
+ type: "story-completed",
15958
+ companyUid,
15959
+ dedupeKey: `${companyUid}#p#US-AGENT`,
15960
+ });
15961
+ }
15962
+ finally {
15963
+ if (previous === undefined)
15964
+ delete process.env.HQ_FLAG_CLOUD_OUTCOME_STORY_DONE_STATE;
15965
+ else
15966
+ process.env.HQ_FLAG_CLOUD_OUTCOME_STORY_DONE_STATE = previous;
15967
+ if (previousHome === undefined)
15968
+ delete process.env.HOME;
15969
+ else
15970
+ process.env.HOME = previousHome;
15971
+ fs.rmSync(hqRoot, { recursive: true, force: true });
15972
+ }
15973
+ });
15022
15974
  it("does not force-collect when a claimed agent uid is absent from the self-listing", async () => {
15023
15975
  const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-tele-agent-miss-"));
15024
15976
  try {