@indigoai-us/hq-cloud 6.14.0 → 6.14.2

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 (167) hide show
  1. package/dist/bin/sync-runner-company.d.ts +4 -0
  2. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  3. package/dist/bin/sync-runner-company.js +50 -1
  4. package/dist/bin/sync-runner-company.js.map +1 -1
  5. package/dist/bin/sync-runner-watch-loop.d.ts +3 -2
  6. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  7. package/dist/bin/sync-runner-watch-loop.js +21 -7
  8. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  9. package/dist/bin/sync-runner.d.ts +6 -2
  10. package/dist/bin/sync-runner.d.ts.map +1 -1
  11. package/dist/bin/sync-runner.js +162 -11
  12. package/dist/bin/sync-runner.js.map +1 -1
  13. package/dist/bin/sync-runner.test.js +155 -17
  14. package/dist/bin/sync-runner.test.js.map +1 -1
  15. package/dist/cli/accept.d.ts.map +1 -1
  16. package/dist/cli/accept.js +29 -0
  17. package/dist/cli/accept.js.map +1 -1
  18. package/dist/cli/invite.d.ts.map +1 -1
  19. package/dist/cli/invite.js +33 -0
  20. package/dist/cli/invite.js.map +1 -1
  21. package/dist/cli/promote.d.ts.map +1 -1
  22. package/dist/cli/promote.js +33 -0
  23. package/dist/cli/promote.js.map +1 -1
  24. package/dist/cli/reindex.d.ts +23 -11
  25. package/dist/cli/reindex.d.ts.map +1 -1
  26. package/dist/cli/reindex.js +39 -10
  27. package/dist/cli/reindex.js.map +1 -1
  28. package/dist/cli/reindex.test.js +40 -13
  29. package/dist/cli/reindex.test.js.map +1 -1
  30. package/dist/cli/share.js +47 -4
  31. package/dist/cli/share.js.map +1 -1
  32. package/dist/cli/share.test.js +123 -2
  33. package/dist/cli/share.test.js.map +1 -1
  34. package/dist/cli/sync-scope.test.js +84 -0
  35. package/dist/cli/sync-scope.test.js.map +1 -1
  36. package/dist/cli/sync.d.ts +44 -3
  37. package/dist/cli/sync.d.ts.map +1 -1
  38. package/dist/cli/sync.js +156 -7
  39. package/dist/cli/sync.js.map +1 -1
  40. package/dist/cli/sync.test.js +133 -0
  41. package/dist/cli/sync.test.js.map +1 -1
  42. package/dist/company-resolver.d.ts +21 -0
  43. package/dist/company-resolver.d.ts.map +1 -1
  44. package/dist/company-resolver.js +31 -6
  45. package/dist/company-resolver.js.map +1 -1
  46. package/dist/company-resolver.test.js +44 -1
  47. package/dist/company-resolver.test.js.map +1 -1
  48. package/dist/ignore.d.ts +10 -0
  49. package/dist/ignore.d.ts.map +1 -1
  50. package/dist/ignore.js +23 -2
  51. package/dist/ignore.js.map +1 -1
  52. package/dist/ignore.test.js +13 -1
  53. package/dist/ignore.test.js.map +1 -1
  54. package/dist/index.d.ts +4 -2
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +4 -1
  57. package/dist/index.js.map +1 -1
  58. package/dist/journal.d.ts +12 -0
  59. package/dist/journal.d.ts.map +1 -1
  60. package/dist/journal.js +12 -0
  61. package/dist/journal.js.map +1 -1
  62. package/dist/lib/exit-codes.d.ts +25 -0
  63. package/dist/lib/exit-codes.d.ts.map +1 -0
  64. package/dist/lib/exit-codes.js +25 -0
  65. package/dist/lib/exit-codes.js.map +1 -0
  66. package/dist/object-io.d.ts.map +1 -1
  67. package/dist/object-io.js +45 -14
  68. package/dist/object-io.js.map +1 -1
  69. package/dist/object-io.test.js +61 -1
  70. package/dist/object-io.test.js.map +1 -1
  71. package/dist/operation-lock.d.ts +36 -6
  72. package/dist/operation-lock.d.ts.map +1 -1
  73. package/dist/operation-lock.js +34 -11
  74. package/dist/operation-lock.js.map +1 -1
  75. package/dist/operation-lock.test.js +30 -0
  76. package/dist/operation-lock.test.js.map +1 -1
  77. package/dist/s3.d.ts +1 -0
  78. package/dist/s3.d.ts.map +1 -1
  79. package/dist/s3.js +75 -2
  80. package/dist/s3.js.map +1 -1
  81. package/dist/s3.test.js +56 -1
  82. package/dist/s3.test.js.map +1 -1
  83. package/dist/scope-shrink.d.ts +12 -0
  84. package/dist/scope-shrink.d.ts.map +1 -1
  85. package/dist/scope-shrink.js +12 -0
  86. package/dist/scope-shrink.js.map +1 -1
  87. package/dist/scope-shrink.test.js +31 -0
  88. package/dist/scope-shrink.test.js.map +1 -1
  89. package/dist/skill-telemetry.d.ts +41 -0
  90. package/dist/skill-telemetry.d.ts.map +1 -1
  91. package/dist/skill-telemetry.js +111 -7
  92. package/dist/skill-telemetry.js.map +1 -1
  93. package/dist/skill-telemetry.test.js +187 -5
  94. package/dist/skill-telemetry.test.js.map +1 -1
  95. package/dist/sync/event-sync.d.ts +3 -0
  96. package/dist/sync/event-sync.d.ts.map +1 -1
  97. package/dist/sync/event-sync.js +23 -0
  98. package/dist/sync/event-sync.js.map +1 -1
  99. package/dist/sync/pull-scope.d.ts +21 -0
  100. package/dist/sync/pull-scope.d.ts.map +1 -1
  101. package/dist/sync/pull-scope.js +46 -13
  102. package/dist/sync/pull-scope.js.map +1 -1
  103. package/dist/telemetry-events.d.ts +31 -0
  104. package/dist/telemetry-events.d.ts.map +1 -0
  105. package/dist/telemetry-events.js +140 -0
  106. package/dist/telemetry-events.js.map +1 -0
  107. package/dist/telemetry-events.test.d.ts +2 -0
  108. package/dist/telemetry-events.test.d.ts.map +1 -0
  109. package/dist/telemetry-events.test.js +73 -0
  110. package/dist/telemetry-events.test.js.map +1 -0
  111. package/dist/telemetry.js +1 -1
  112. package/dist/telemetry.js.map +1 -1
  113. package/dist/vault-client.d.ts +38 -5
  114. package/dist/vault-client.d.ts.map +1 -1
  115. package/dist/vault-client.js +69 -6
  116. package/dist/vault-client.js.map +1 -1
  117. package/dist/vault-client.test.js +31 -1
  118. package/dist/vault-client.test.js.map +1 -1
  119. package/dist/watcher.d.ts +7 -0
  120. package/dist/watcher.d.ts.map +1 -1
  121. package/dist/watcher.js +53 -1
  122. package/dist/watcher.js.map +1 -1
  123. package/dist/watcher.test.js +57 -0
  124. package/dist/watcher.test.js.map +1 -1
  125. package/package.json +1 -1
  126. package/pnpm-workspace.yaml +2 -0
  127. package/scripts/presign-transport-e2e.mjs +2 -2
  128. package/src/bin/sync-runner-company.ts +65 -1
  129. package/src/bin/sync-runner-watch-loop.ts +24 -8
  130. package/src/bin/sync-runner.test.ts +194 -16
  131. package/src/bin/sync-runner.ts +209 -11
  132. package/src/cli/accept.ts +27 -0
  133. package/src/cli/invite.ts +31 -0
  134. package/src/cli/promote.ts +31 -0
  135. package/src/cli/reindex.test.ts +44 -13
  136. package/src/cli/reindex.ts +74 -21
  137. package/src/cli/share.test.ts +139 -2
  138. package/src/cli/share.ts +48 -2
  139. package/src/cli/sync-scope.test.ts +90 -0
  140. package/src/cli/sync.test.ts +154 -0
  141. package/src/cli/sync.ts +224 -5
  142. package/src/company-resolver.test.ts +51 -0
  143. package/src/company-resolver.ts +40 -5
  144. package/src/ignore.test.ts +18 -1
  145. package/src/ignore.ts +24 -2
  146. package/src/index.ts +22 -1
  147. package/src/journal.ts +12 -0
  148. package/src/lib/exit-codes.ts +24 -0
  149. package/src/object-io.test.ts +66 -1
  150. package/src/object-io.ts +51 -14
  151. package/src/operation-lock.test.ts +33 -0
  152. package/src/operation-lock.ts +46 -11
  153. package/src/s3.test.ts +87 -0
  154. package/src/s3.ts +93 -2
  155. package/src/scope-shrink.test.ts +32 -0
  156. package/src/scope-shrink.ts +12 -0
  157. package/src/skill-telemetry.test.ts +247 -4
  158. package/src/skill-telemetry.ts +134 -6
  159. package/src/sync/event-sync.ts +37 -0
  160. package/src/sync/pull-scope.ts +62 -13
  161. package/src/telemetry-events.test.ts +88 -0
  162. package/src/telemetry-events.ts +205 -0
  163. package/src/telemetry.ts +1 -1
  164. package/src/vault-client.test.ts +39 -1
  165. package/src/vault-client.ts +115 -14
  166. package/src/watcher.test.ts +82 -0
  167. package/src/watcher.ts +69 -0
@@ -10,13 +10,14 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
10
10
  import * as fs from "fs";
11
11
  import * as os from "os";
12
12
  import * as path from "path";
13
- import { runRunner, runRunnerWithLoop, resolveDeletePolicy, resolveSkipPersonal, selectObjectIOFactory, routeChangeToTarget, buildTargetedPushArgv, resolvePullScope, readPinnedPrefixes, defaultCollectTelemetry, } from "./sync-runner.js";
13
+ import { runRunner, runRunnerWithLoop, resolveDeletePolicy, resolveSkipPersonal, selectObjectIOFactory, routeChangeToTarget, buildTargetedPushArgv, resolvePullScope, readPinnedPrefixes, SESSIONS_SCOPE_PREFIX, defaultCollectTelemetry, } from "./sync-runner.js";
14
14
  import { PresignObjectIO, S3SdkObjectIO } from "../object-io.js";
15
15
  import { FakeClock } from "../watcher.js";
16
16
  import { PERSONAL_VAULT_JOURNAL_SLUG } from "../journal.js";
17
17
  import { HQ_CLOUD_VERSION } from "../version.js";
18
18
  import { lockPathFor, OPERATION_LOCKED_EXIT } from "../operation-lock.js";
19
19
  import { TRANSIENT_NETWORK_EXIT } from "../lib/net-errors.js";
20
+ import { PARTIAL_SYNC_EXIT } from "../lib/exit-codes.js";
20
21
  import { isCoveredByAny } from "../prefix-coalesce.js";
21
22
  import { VaultAuthError } from "../vault-client.js";
22
23
  // ---------------------------------------------------------------------------
@@ -1335,6 +1336,52 @@ describe("all-complete aggregate", () => {
1335
1336
  expect(deps.stdout.events().some((e) => e.type === "all-complete")).toBe(true);
1336
1337
  expect(deps.stderr.raw()).toContain("runner.telemetry.timeout");
1337
1338
  });
1339
+ it("emits action telemetry for completion and telemetry transport failure does not block completion", async () => {
1340
+ const posts = [];
1341
+ const client = {
1342
+ ...makeVaultStub({
1343
+ memberships: [{ companyUid: "cmp_a" }],
1344
+ entityGet: (uid) => Promise.resolve({ uid, slug: "acme" }),
1345
+ }),
1346
+ postTelemetryEvents: vi.fn(async (batch) => {
1347
+ posts.push(batch);
1348
+ throw new Error("telemetry transport down");
1349
+ }),
1350
+ };
1351
+ const deps = makeDeps({
1352
+ createVaultClient: () => client,
1353
+ collectTelemetry: vi.fn().mockResolvedValue(undefined),
1354
+ sync: vi
1355
+ .fn()
1356
+ .mockResolvedValue(defaultSyncResult({
1357
+ filesDownloaded: 2,
1358
+ bytesDownloaded: 300,
1359
+ })),
1360
+ });
1361
+ const code = await runRunner(["--companies"], deps);
1362
+ await new Promise((resolve) => setTimeout(resolve, 0));
1363
+ expect(code).toBe(0);
1364
+ expect(deps.stdout.events().some((e) => e.type === "all-complete")).toBe(true);
1365
+ const completed = posts
1366
+ .flatMap((post) => post.events)
1367
+ .find((event) => event.eventName === "sync_runner_completed");
1368
+ expect(completed).toMatchObject({
1369
+ app: "hq-cloud",
1370
+ source: "sync-runner",
1371
+ schemaVersion: 1,
1372
+ properties: {
1373
+ result: "completed",
1374
+ attemptedCount: 1,
1375
+ companyCount: 1,
1376
+ errorCount: 0,
1377
+ filesDownloaded: 2,
1378
+ bytesDownloaded: 300,
1379
+ filesUploaded: 0,
1380
+ bytesUploaded: 0,
1381
+ },
1382
+ });
1383
+ expect(deps.stderr.raw()).toContain("runner.telemetry_events.nonfatal");
1384
+ });
1338
1385
  it("sums filesDownloaded and bytesDownloaded across all companies", async () => {
1339
1386
  const slugs = { cmp_a: "acme", cmp_b: "beta" };
1340
1387
  const deps = makeDeps({
@@ -1974,6 +2021,13 @@ describe("personal slot fanout", () => {
1974
2021
  const keys = Object.keys(args);
1975
2022
  expect(keys).not.toContain("personalMode");
1976
2023
  expect(keys).not.toContain("journalSlug");
2024
+ // Positive assertion on the runner→sync() glue (sync-runner-company.ts:278):
2025
+ // the push-only sessions exclusion IS threaded into the company pull leg.
2026
+ // The ABSENT-key checks above would still pass if a regression dropped the
2027
+ // `...(excludePrefixes.length > 0 ? { excludePrefixes } : {})` spread or
2028
+ // flipped its guard — silently re-enabling full session pulls. Pin the
2029
+ // exact leak against the exported constant, not a string literal.
2030
+ expect(args.excludePrefixes).toEqual([SESSIONS_SCOPE_PREFIX]);
1977
2031
  }
1978
2032
  });
1979
2033
  // ── Push-side personal-vault contract (Slice 2 / personal-vault scope) ────
@@ -2822,9 +2876,9 @@ describe("runRunnerWithLoop — event-push wiring", () => {
2822
2876
  // No pending FakeClock timers left behind by the driver's debounce.
2823
2877
  expect(clock.pendingTimerCount()).toBe(0);
2824
2878
  });
2825
- it("a non-zero poll pass exit returns immediately (hard error surfaces)", async () => {
2879
+ it("a hard-error (exit 1) poll pass returns immediately", async () => {
2826
2880
  const watcher = makeWatcherStub();
2827
- const runPass = vi.fn().mockResolvedValue(2);
2881
+ const runPass = vi.fn().mockResolvedValue(1);
2828
2882
  const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
2829
2883
  runPass,
2830
2884
  clock: new FakeClock(),
@@ -2832,7 +2886,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
2832
2886
  sleep: () => Promise.resolve(),
2833
2887
  onShutdownSignal: () => () => { },
2834
2888
  });
2835
- expect(code).toBe(2);
2889
+ expect(code).toBe(1);
2836
2890
  expect(watcher.disposed).toBe(true);
2837
2891
  });
2838
2892
  it("a TRANSIENT_NETWORK_EXIT poll pass does NOT surface as a crash — the loop retries (HQ-SYNC-1W)", async () => {
@@ -2846,7 +2900,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
2846
2900
  n++;
2847
2901
  // First pass: machine is offline (transient). Second pass: a real hard
2848
2902
  // error, used here only to end the otherwise-infinite loop deterministically.
2849
- return n === 1 ? TRANSIENT_NETWORK_EXIT : 2;
2903
+ return n === 1 ? TRANSIENT_NETWORK_EXIT : 1;
2850
2904
  });
2851
2905
  const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
2852
2906
  runPass,
@@ -2858,10 +2912,40 @@ describe("runRunnerWithLoop — event-push wiring", () => {
2858
2912
  // The transient pass did NOT end the loop — it ran again…
2859
2913
  expect(runPass).toHaveBeenCalledTimes(2);
2860
2914
  // …and the transient code was never surfaced as the watcher's exit code.
2861
- expect(code).toBe(2);
2915
+ expect(code).toBe(1);
2862
2916
  expect(code).not.toBe(TRANSIENT_NETWORK_EXIT);
2863
2917
  expect(watcher.disposed).toBe(true);
2864
2918
  });
2919
+ it("a PARTIAL_SYNC_EXIT poll pass does NOT surface as a crash — the loop retries", async () => {
2920
+ // A partial pass (some per-file transfers threw: oversized object on an
2921
+ // old runner, presign denial, expired token, corrupt vault key) must keep
2922
+ // the watcher alive. Exiting on it got the watcher hot-respawned by the
2923
+ // menubar supervisor and Sentry-captured on EVERY failing pass — the
2924
+ // July 2026 "watcher exited unexpectedly (code=Some(2))" alert flood. The
2925
+ // per-file errors still surface via ndjson `error` events; the loop logs
2926
+ // and retries next tick. A one-shot `hq sync` still exits 2.
2927
+ const watcher = makeWatcherStub();
2928
+ let n = 0;
2929
+ const runPass = vi.fn().mockImplementation(async () => {
2930
+ n++;
2931
+ // Two partial passes prove a DETERMINISTIC per-file failure doesn't kill
2932
+ // the loop either. A hard error then ends the otherwise-infinite loop.
2933
+ return n <= 2 ? PARTIAL_SYNC_EXIT : 1;
2934
+ });
2935
+ const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
2936
+ runPass,
2937
+ clock: new FakeClock(),
2938
+ createWatcher: () => watcher,
2939
+ sleep: () => Promise.resolve(),
2940
+ onShutdownSignal: () => () => { },
2941
+ });
2942
+ // Both partial passes stayed alive — the loop ran a third time…
2943
+ expect(runPass).toHaveBeenCalledTimes(3);
2944
+ // …and the partial code was never surfaced as the watcher's exit code.
2945
+ expect(code).toBe(1);
2946
+ expect(code).not.toBe(PARTIAL_SYNC_EXIT);
2947
+ expect(watcher.disposed).toBe(true);
2948
+ });
2865
2949
  it("without --event-push, no watcher is created (poll-only safety net)", async () => {
2866
2950
  let triggerShutdown = () => { };
2867
2951
  const createWatcher = vi.fn(() => makeWatcherStub());
@@ -3409,7 +3493,61 @@ describe("resolvePullScope", () => {
3409
3493
  });
3410
3494
  it("returns all when the client has no getMembershipSyncConfig", async () => {
3411
3495
  const scope = await resolvePullScope(stubClient({}), "cmp_a", "acme");
3412
- expect(scope).toEqual({ syncMode: "all" });
3496
+ expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
3497
+ });
3498
+ // US-006 AC4: the sessions prefix is push-only, so `resolvePullScope` stamps
3499
+ // `excludePrefixes: ["sessions/"]` on EVERY return — including `all`
3500
+ // (full-bucket), owner→all, agent→all, shared, and custom. A single missed
3501
+ // return site would silently regress to a full session pull; this test pins
3502
+ // the invariant across every mode the resolver can return.
3503
+ it("stamps the push-only sessions exclusion on EVERY resolved mode (AC4)", async () => {
3504
+ expect(SESSIONS_SCOPE_PREFIX).toBe("sessions/");
3505
+ const modes = [
3506
+ // no-client early return → all
3507
+ await resolvePullScope(stubClient({}), "cmp_a", "acme"),
3508
+ // owner role-bypass → all
3509
+ await resolvePullScope(stubClient({
3510
+ listMyMemberships: async () => [ownerMembership("cmp_a", "mk_owner")],
3511
+ }), "cmp_a", "acme"),
3512
+ // agent membership → all (short-circuit before the always-403 endpoint)
3513
+ await resolvePullScope(stubClient({
3514
+ listMyMemberships: async () => [membership("cmp_a", "agt_9#cmp_a")],
3515
+ }), "cmp_a", "acme"),
3516
+ // config-all → all
3517
+ await resolvePullScope(stubClient({
3518
+ listMyMemberships: async () => [membership("cmp_a", "mk_a")],
3519
+ getMembershipSyncConfig: async () => ({
3520
+ membershipId: "mk_a",
3521
+ syncMode: "all",
3522
+ isDefault: true,
3523
+ }),
3524
+ }), "cmp_a", "acme"),
3525
+ // shared
3526
+ await resolvePullScope(stubClient({
3527
+ listMyMemberships: async () => [membership("cmp_a", "mk_a")],
3528
+ getMembershipSyncConfig: async () => ({
3529
+ membershipId: "mk_a",
3530
+ syncMode: "shared",
3531
+ isDefault: false,
3532
+ }),
3533
+ listMyExplicitGrants: async () => [
3534
+ { companyUid: "cmp_a", path: "knowledge/", permission: "read", source: "person" },
3535
+ ],
3536
+ }), "cmp_a", "acme"),
3537
+ // custom
3538
+ await resolvePullScope(stubClient({
3539
+ listMyMemberships: async () => [membership("cmp_a", "mk_a")],
3540
+ getMembershipSyncConfig: async () => ({
3541
+ membershipId: "mk_a",
3542
+ syncMode: "custom",
3543
+ customPaths: ["projects/x/"],
3544
+ isDefault: false,
3545
+ }),
3546
+ }), "cmp_a", "acme"),
3547
+ ];
3548
+ for (const scope of modes) {
3549
+ expect(scope.excludePrefixes).toEqual([SESSIONS_SCOPE_PREFIX]);
3550
+ }
3413
3551
  });
3414
3552
  // OWNER role-bypass regression (feedback_67bdb8a4 / feedback_a46c3b37).
3415
3553
  // An owner owns the whole vault by ROLE; the grants endpoint deliberately
@@ -3434,7 +3572,7 @@ describe("resolvePullScope", () => {
3434
3572
  return [];
3435
3573
  },
3436
3574
  }), "cmp_a", "acme");
3437
- expect(scope).toEqual({ syncMode: "all" });
3575
+ expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
3438
3576
  // The short-circuit must fire BEFORE any grant resolution.
3439
3577
  expect(grantsCalled).toBe(false);
3440
3578
  });
@@ -3455,7 +3593,7 @@ describe("resolvePullScope", () => {
3455
3593
  // Even a `custom` row cannot scope an owner below their role on the sync
3456
3594
  // path (the server would 403 their own writes), so `all` wins and the
3457
3595
  // config fetch is never needed.
3458
- expect(scope).toEqual({ syncMode: "all" });
3596
+ expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
3459
3597
  expect(configCalled).toBe(false);
3460
3598
  });
3461
3599
  it("still narrows a NON-owner (member) to explicit grants — the fix is owner-scoped", async () => {
@@ -3470,7 +3608,7 @@ describe("resolvePullScope", () => {
3470
3608
  { companyUid: "cmp_a", path: "knowledge/", permission: "read", source: "person" },
3471
3609
  ],
3472
3610
  }), "cmp_a", "acme");
3473
- expect(scope).toEqual({ syncMode: "shared", prefixSet: ["knowledge/"] });
3611
+ expect(scope).toEqual({ syncMode: "shared", prefixSet: ["knowledge/"], excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
3474
3612
  });
3475
3613
  it("returns all for an all-mode membership (no prefixSet)", async () => {
3476
3614
  const scope = await resolvePullScope(stubClient({
@@ -3481,7 +3619,7 @@ describe("resolvePullScope", () => {
3481
3619
  isDefault: true,
3482
3620
  }),
3483
3621
  }), "cmp_a", "acme");
3484
- expect(scope).toEqual({ syncMode: "all" });
3622
+ expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
3485
3623
  });
3486
3624
  it("returns all for an AGENT membership WITHOUT calling sync-config (HQ-1R: agents own no person entity → server 403s the person-gate)", async () => {
3487
3625
  let called = false;
@@ -3500,7 +3638,7 @@ describe("resolvePullScope", () => {
3500
3638
  },
3501
3639
  }), "cmp_a", "acme");
3502
3640
  expect(called).toBe(false); // never hit the always-403 endpoint for an agent
3503
- expect(scope).toEqual({ syncMode: "all" });
3641
+ expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
3504
3642
  });
3505
3643
  it("coalesces explicit grants for a shared-mode membership", async () => {
3506
3644
  const scope = await resolvePullScope(stubClient({
@@ -3597,7 +3735,7 @@ describe("resolvePullScope", () => {
3597
3735
  // A `*` grant = everything. Because coalescePrefixes drops the empty
3598
3736
  // prefix (which would otherwise collapse to "nothing" and prune the
3599
3737
  // tree), an everything-scope resolves to full-access `all`.
3600
- expect(scope).toEqual({ syncMode: "all" });
3738
+ expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
3601
3739
  });
3602
3740
  it("uses customPaths for a custom-mode membership", async () => {
3603
3741
  const scope = await resolvePullScope(stubClient({
@@ -3622,7 +3760,7 @@ describe("resolvePullScope", () => {
3622
3760
  }),
3623
3761
  // listMyExplicitGrants intentionally absent.
3624
3762
  }), "cmp_a", "acme");
3625
- expect(scope).toEqual({ syncMode: "all" });
3763
+ expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
3626
3764
  });
3627
3765
  it("allows a genuinely-empty shared scope (grants method present, returns [])", async () => {
3628
3766
  const scope = await resolvePullScope(stubClient({
@@ -3634,7 +3772,7 @@ describe("resolvePullScope", () => {
3634
3772
  }),
3635
3773
  listMyExplicitGrants: async () => [],
3636
3774
  }), "cmp_a", "acme");
3637
- expect(scope).toEqual({ syncMode: "shared", prefixSet: [] });
3775
+ expect(scope).toEqual({ syncMode: "shared", prefixSet: [], excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
3638
3776
  });
3639
3777
  it("degrades to all when the membership is not found", async () => {
3640
3778
  const scope = await resolvePullScope(stubClient({
@@ -3643,7 +3781,7 @@ describe("resolvePullScope", () => {
3643
3781
  throw new Error("should not be called");
3644
3782
  },
3645
3783
  }), "cmp_a", "acme");
3646
- expect(scope).toEqual({ syncMode: "all" });
3784
+ expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
3647
3785
  });
3648
3786
  it("degrades to all when sync-config resolution throws (never prune on error)", async () => {
3649
3787
  const scope = await resolvePullScope(stubClient({
@@ -3652,7 +3790,7 @@ describe("resolvePullScope", () => {
3652
3790
  throw new Error("network blip");
3653
3791
  },
3654
3792
  }), "cmp_a", "acme");
3655
- expect(scope).toEqual({ syncMode: "all" });
3793
+ expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
3656
3794
  });
3657
3795
  // ── pin union (Phase C: hq files get) ──────────────────────────────────────
3658
3796
  it("unions pinned prefixes into a shared-mode scope", async () => {