@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.
- package/dist/bin/sync-runner-company.d.ts +4 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +50 -1
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +3 -2
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +21 -7
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +6 -2
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +162 -11
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +155 -17
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/accept.d.ts.map +1 -1
- package/dist/cli/accept.js +29 -0
- package/dist/cli/accept.js.map +1 -1
- package/dist/cli/invite.d.ts.map +1 -1
- package/dist/cli/invite.js +33 -0
- package/dist/cli/invite.js.map +1 -1
- package/dist/cli/promote.d.ts.map +1 -1
- package/dist/cli/promote.js +33 -0
- package/dist/cli/promote.js.map +1 -1
- package/dist/cli/reindex.d.ts +23 -11
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +39 -10
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/reindex.test.js +40 -13
- package/dist/cli/reindex.test.js.map +1 -1
- package/dist/cli/share.js +47 -4
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +123 -2
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync-scope.test.js +84 -0
- package/dist/cli/sync-scope.test.js.map +1 -1
- package/dist/cli/sync.d.ts +44 -3
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +156 -7
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +133 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/company-resolver.d.ts +21 -0
- package/dist/company-resolver.d.ts.map +1 -1
- package/dist/company-resolver.js +31 -6
- package/dist/company-resolver.js.map +1 -1
- package/dist/company-resolver.test.js +44 -1
- package/dist/company-resolver.test.js.map +1 -1
- package/dist/ignore.d.ts +10 -0
- package/dist/ignore.d.ts.map +1 -1
- package/dist/ignore.js +23 -2
- package/dist/ignore.js.map +1 -1
- package/dist/ignore.test.js +13 -1
- package/dist/ignore.test.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/journal.d.ts +12 -0
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +12 -0
- package/dist/journal.js.map +1 -1
- package/dist/lib/exit-codes.d.ts +25 -0
- package/dist/lib/exit-codes.d.ts.map +1 -0
- package/dist/lib/exit-codes.js +25 -0
- package/dist/lib/exit-codes.js.map +1 -0
- package/dist/object-io.d.ts.map +1 -1
- package/dist/object-io.js +45 -14
- package/dist/object-io.js.map +1 -1
- package/dist/object-io.test.js +61 -1
- package/dist/object-io.test.js.map +1 -1
- package/dist/operation-lock.d.ts +36 -6
- package/dist/operation-lock.d.ts.map +1 -1
- package/dist/operation-lock.js +34 -11
- package/dist/operation-lock.js.map +1 -1
- package/dist/operation-lock.test.js +30 -0
- package/dist/operation-lock.test.js.map +1 -1
- package/dist/s3.d.ts +1 -0
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +75 -2
- package/dist/s3.js.map +1 -1
- package/dist/s3.test.js +56 -1
- package/dist/s3.test.js.map +1 -1
- package/dist/scope-shrink.d.ts +12 -0
- package/dist/scope-shrink.d.ts.map +1 -1
- package/dist/scope-shrink.js +12 -0
- package/dist/scope-shrink.js.map +1 -1
- package/dist/scope-shrink.test.js +31 -0
- package/dist/scope-shrink.test.js.map +1 -1
- package/dist/skill-telemetry.d.ts +41 -0
- package/dist/skill-telemetry.d.ts.map +1 -1
- package/dist/skill-telemetry.js +111 -7
- package/dist/skill-telemetry.js.map +1 -1
- package/dist/skill-telemetry.test.js +187 -5
- package/dist/skill-telemetry.test.js.map +1 -1
- package/dist/sync/event-sync.d.ts +3 -0
- package/dist/sync/event-sync.d.ts.map +1 -1
- package/dist/sync/event-sync.js +23 -0
- package/dist/sync/event-sync.js.map +1 -1
- package/dist/sync/pull-scope.d.ts +21 -0
- package/dist/sync/pull-scope.d.ts.map +1 -1
- package/dist/sync/pull-scope.js +46 -13
- package/dist/sync/pull-scope.js.map +1 -1
- package/dist/telemetry-events.d.ts +31 -0
- package/dist/telemetry-events.d.ts.map +1 -0
- package/dist/telemetry-events.js +140 -0
- package/dist/telemetry-events.js.map +1 -0
- package/dist/telemetry-events.test.d.ts +2 -0
- package/dist/telemetry-events.test.d.ts.map +1 -0
- package/dist/telemetry-events.test.js +73 -0
- package/dist/telemetry-events.test.js.map +1 -0
- package/dist/telemetry.js +1 -1
- package/dist/telemetry.js.map +1 -1
- package/dist/vault-client.d.ts +38 -5
- package/dist/vault-client.d.ts.map +1 -1
- package/dist/vault-client.js +69 -6
- package/dist/vault-client.js.map +1 -1
- package/dist/vault-client.test.js +31 -1
- package/dist/vault-client.test.js.map +1 -1
- package/dist/watcher.d.ts +7 -0
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +53 -1
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +57 -0
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
- package/pnpm-workspace.yaml +2 -0
- package/scripts/presign-transport-e2e.mjs +2 -2
- package/src/bin/sync-runner-company.ts +65 -1
- package/src/bin/sync-runner-watch-loop.ts +24 -8
- package/src/bin/sync-runner.test.ts +194 -16
- package/src/bin/sync-runner.ts +209 -11
- package/src/cli/accept.ts +27 -0
- package/src/cli/invite.ts +31 -0
- package/src/cli/promote.ts +31 -0
- package/src/cli/reindex.test.ts +44 -13
- package/src/cli/reindex.ts +74 -21
- package/src/cli/share.test.ts +139 -2
- package/src/cli/share.ts +48 -2
- package/src/cli/sync-scope.test.ts +90 -0
- package/src/cli/sync.test.ts +154 -0
- package/src/cli/sync.ts +224 -5
- package/src/company-resolver.test.ts +51 -0
- package/src/company-resolver.ts +40 -5
- package/src/ignore.test.ts +18 -1
- package/src/ignore.ts +24 -2
- package/src/index.ts +22 -1
- package/src/journal.ts +12 -0
- package/src/lib/exit-codes.ts +24 -0
- package/src/object-io.test.ts +66 -1
- package/src/object-io.ts +51 -14
- package/src/operation-lock.test.ts +33 -0
- package/src/operation-lock.ts +46 -11
- package/src/s3.test.ts +87 -0
- package/src/s3.ts +93 -2
- package/src/scope-shrink.test.ts +32 -0
- package/src/scope-shrink.ts +12 -0
- package/src/skill-telemetry.test.ts +247 -4
- package/src/skill-telemetry.ts +134 -6
- package/src/sync/event-sync.ts +37 -0
- package/src/sync/pull-scope.ts +62 -13
- package/src/telemetry-events.test.ts +88 -0
- package/src/telemetry-events.ts +205 -0
- package/src/telemetry.ts +1 -1
- package/src/vault-client.test.ts +39 -1
- package/src/vault-client.ts +115 -14
- package/src/watcher.test.ts +82 -0
- package/src/watcher.ts +69 -0
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
buildTargetedPushArgv,
|
|
22
22
|
resolvePullScope,
|
|
23
23
|
readPinnedPrefixes,
|
|
24
|
+
SESSIONS_SCOPE_PREFIX,
|
|
24
25
|
defaultCollectTelemetry,
|
|
25
26
|
} from "./sync-runner.js";
|
|
26
27
|
import { PresignObjectIO, S3SdkObjectIO } from "../object-io.js";
|
|
@@ -36,6 +37,7 @@ import { PERSONAL_VAULT_JOURNAL_SLUG } from "../journal.js";
|
|
|
36
37
|
import { HQ_CLOUD_VERSION } from "../version.js";
|
|
37
38
|
import { lockPathFor, OPERATION_LOCKED_EXIT } from "../operation-lock.js";
|
|
38
39
|
import { TRANSIENT_NETWORK_EXIT } from "../lib/net-errors.js";
|
|
40
|
+
import { PARTIAL_SYNC_EXIT } from "../lib/exit-codes.js";
|
|
39
41
|
import { isCoveredByAny } from "../prefix-coalesce.js";
|
|
40
42
|
import type { SyncResult, SyncOptions } from "../cli/sync.js";
|
|
41
43
|
import type { ShareResult, ShareOptions } from "../cli/share.js";
|
|
@@ -43,6 +45,7 @@ import type {
|
|
|
43
45
|
Membership,
|
|
44
46
|
EntityInfo,
|
|
45
47
|
PendingInviteByEmail,
|
|
48
|
+
TelemetryEventsBatch,
|
|
46
49
|
} from "../vault-client.js";
|
|
47
50
|
import { VaultAuthError } from "../vault-client.js";
|
|
48
51
|
import type { PushEvent } from "../sync/push-event.js";
|
|
@@ -1601,6 +1604,60 @@ describe("all-complete aggregate", () => {
|
|
|
1601
1604
|
expect(deps.stderr.raw()).toContain("runner.telemetry.timeout");
|
|
1602
1605
|
});
|
|
1603
1606
|
|
|
1607
|
+
it("emits action telemetry for completion and telemetry transport failure does not block completion", async () => {
|
|
1608
|
+
const posts: TelemetryEventsBatch[] = [];
|
|
1609
|
+
const client: VaultClientSurface = {
|
|
1610
|
+
...makeVaultStub({
|
|
1611
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
1612
|
+
entityGet: (uid: string) =>
|
|
1613
|
+
Promise.resolve({ uid, slug: "acme" } as unknown as EntityInfo),
|
|
1614
|
+
}),
|
|
1615
|
+
postTelemetryEvents: vi.fn(async (batch) => {
|
|
1616
|
+
posts.push(batch);
|
|
1617
|
+
throw new Error("telemetry transport down");
|
|
1618
|
+
}),
|
|
1619
|
+
};
|
|
1620
|
+
const deps = makeDeps({
|
|
1621
|
+
createVaultClient: () => client,
|
|
1622
|
+
collectTelemetry: vi.fn().mockResolvedValue(undefined),
|
|
1623
|
+
sync: vi
|
|
1624
|
+
.fn<(opts: SyncOptions) => Promise<SyncResult>>()
|
|
1625
|
+
.mockResolvedValue(
|
|
1626
|
+
defaultSyncResult({
|
|
1627
|
+
filesDownloaded: 2,
|
|
1628
|
+
bytesDownloaded: 300,
|
|
1629
|
+
}),
|
|
1630
|
+
),
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
const code = await runRunner(["--companies"], deps);
|
|
1634
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1635
|
+
|
|
1636
|
+
expect(code).toBe(0);
|
|
1637
|
+
expect(deps.stdout.events().some((e) => e.type === "all-complete")).toBe(
|
|
1638
|
+
true,
|
|
1639
|
+
);
|
|
1640
|
+
const completed = posts
|
|
1641
|
+
.flatMap((post) => post.events)
|
|
1642
|
+
.find((event) => event.eventName === "sync_runner_completed");
|
|
1643
|
+
expect(completed).toMatchObject({
|
|
1644
|
+
app: "hq-cloud",
|
|
1645
|
+
source: "sync-runner",
|
|
1646
|
+
schemaVersion: 1,
|
|
1647
|
+
properties: {
|
|
1648
|
+
result: "completed",
|
|
1649
|
+
attemptedCount: 1,
|
|
1650
|
+
companyCount: 1,
|
|
1651
|
+
errorCount: 0,
|
|
1652
|
+
filesDownloaded: 2,
|
|
1653
|
+
bytesDownloaded: 300,
|
|
1654
|
+
filesUploaded: 0,
|
|
1655
|
+
bytesUploaded: 0,
|
|
1656
|
+
},
|
|
1657
|
+
});
|
|
1658
|
+
expect(deps.stderr.raw()).toContain("runner.telemetry_events.nonfatal");
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1604
1661
|
it("sums filesDownloaded and bytesDownloaded across all companies", async () => {
|
|
1605
1662
|
const slugs: Record<string, string> = { cmp_a: "acme", cmp_b: "beta" };
|
|
1606
1663
|
const deps = makeDeps({
|
|
@@ -2407,6 +2464,13 @@ describe("personal slot fanout", () => {
|
|
|
2407
2464
|
const keys = Object.keys(args);
|
|
2408
2465
|
expect(keys).not.toContain("personalMode");
|
|
2409
2466
|
expect(keys).not.toContain("journalSlug");
|
|
2467
|
+
// Positive assertion on the runner→sync() glue (sync-runner-company.ts:278):
|
|
2468
|
+
// the push-only sessions exclusion IS threaded into the company pull leg.
|
|
2469
|
+
// The ABSENT-key checks above would still pass if a regression dropped the
|
|
2470
|
+
// `...(excludePrefixes.length > 0 ? { excludePrefixes } : {})` spread or
|
|
2471
|
+
// flipped its guard — silently re-enabling full session pulls. Pin the
|
|
2472
|
+
// exact leak against the exported constant, not a string literal.
|
|
2473
|
+
expect(args.excludePrefixes).toEqual([SESSIONS_SCOPE_PREFIX]);
|
|
2410
2474
|
}
|
|
2411
2475
|
});
|
|
2412
2476
|
|
|
@@ -3504,9 +3568,9 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3504
3568
|
expect(clock.pendingTimerCount()).toBe(0);
|
|
3505
3569
|
});
|
|
3506
3570
|
|
|
3507
|
-
it("a
|
|
3571
|
+
it("a hard-error (exit 1) poll pass returns immediately", async () => {
|
|
3508
3572
|
const watcher = makeWatcherStub();
|
|
3509
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
3573
|
+
const runPass = vi.fn().mockResolvedValue(1);
|
|
3510
3574
|
|
|
3511
3575
|
const code = await runRunnerWithLoop(
|
|
3512
3576
|
["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"],
|
|
@@ -3518,7 +3582,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3518
3582
|
onShutdownSignal: () => () => {},
|
|
3519
3583
|
},
|
|
3520
3584
|
);
|
|
3521
|
-
expect(code).toBe(
|
|
3585
|
+
expect(code).toBe(1);
|
|
3522
3586
|
expect(watcher.disposed).toBe(true);
|
|
3523
3587
|
});
|
|
3524
3588
|
|
|
@@ -3533,7 +3597,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3533
3597
|
n++;
|
|
3534
3598
|
// First pass: machine is offline (transient). Second pass: a real hard
|
|
3535
3599
|
// error, used here only to end the otherwise-infinite loop deterministically.
|
|
3536
|
-
return n === 1 ? TRANSIENT_NETWORK_EXIT :
|
|
3600
|
+
return n === 1 ? TRANSIENT_NETWORK_EXIT : 1;
|
|
3537
3601
|
});
|
|
3538
3602
|
|
|
3539
3603
|
const code = await runRunnerWithLoop(
|
|
@@ -3550,11 +3614,47 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3550
3614
|
// The transient pass did NOT end the loop — it ran again…
|
|
3551
3615
|
expect(runPass).toHaveBeenCalledTimes(2);
|
|
3552
3616
|
// …and the transient code was never surfaced as the watcher's exit code.
|
|
3553
|
-
expect(code).toBe(
|
|
3617
|
+
expect(code).toBe(1);
|
|
3554
3618
|
expect(code).not.toBe(TRANSIENT_NETWORK_EXIT);
|
|
3555
3619
|
expect(watcher.disposed).toBe(true);
|
|
3556
3620
|
});
|
|
3557
3621
|
|
|
3622
|
+
it("a PARTIAL_SYNC_EXIT poll pass does NOT surface as a crash — the loop retries", async () => {
|
|
3623
|
+
// A partial pass (some per-file transfers threw: oversized object on an
|
|
3624
|
+
// old runner, presign denial, expired token, corrupt vault key) must keep
|
|
3625
|
+
// the watcher alive. Exiting on it got the watcher hot-respawned by the
|
|
3626
|
+
// menubar supervisor and Sentry-captured on EVERY failing pass — the
|
|
3627
|
+
// July 2026 "watcher exited unexpectedly (code=Some(2))" alert flood. The
|
|
3628
|
+
// per-file errors still surface via ndjson `error` events; the loop logs
|
|
3629
|
+
// and retries next tick. A one-shot `hq sync` still exits 2.
|
|
3630
|
+
const watcher = makeWatcherStub();
|
|
3631
|
+
let n = 0;
|
|
3632
|
+
const runPass = vi.fn().mockImplementation(async () => {
|
|
3633
|
+
n++;
|
|
3634
|
+
// Two partial passes prove a DETERMINISTIC per-file failure doesn't kill
|
|
3635
|
+
// the loop either. A hard error then ends the otherwise-infinite loop.
|
|
3636
|
+
return n <= 2 ? PARTIAL_SYNC_EXIT : 1;
|
|
3637
|
+
});
|
|
3638
|
+
|
|
3639
|
+
const code = await runRunnerWithLoop(
|
|
3640
|
+
["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"],
|
|
3641
|
+
{
|
|
3642
|
+
runPass,
|
|
3643
|
+
clock: new FakeClock(),
|
|
3644
|
+
createWatcher: () => watcher,
|
|
3645
|
+
sleep: () => Promise.resolve(),
|
|
3646
|
+
onShutdownSignal: () => () => {},
|
|
3647
|
+
},
|
|
3648
|
+
);
|
|
3649
|
+
|
|
3650
|
+
// Both partial passes stayed alive — the loop ran a third time…
|
|
3651
|
+
expect(runPass).toHaveBeenCalledTimes(3);
|
|
3652
|
+
// …and the partial code was never surfaced as the watcher's exit code.
|
|
3653
|
+
expect(code).toBe(1);
|
|
3654
|
+
expect(code).not.toBe(PARTIAL_SYNC_EXIT);
|
|
3655
|
+
expect(watcher.disposed).toBe(true);
|
|
3656
|
+
});
|
|
3657
|
+
|
|
3558
3658
|
it("without --event-push, no watcher is created (poll-only safety net)", async () => {
|
|
3559
3659
|
let triggerShutdown = () => {};
|
|
3560
3660
|
const createWatcher = vi.fn(() => makeWatcherStub());
|
|
@@ -4208,7 +4308,85 @@ describe("resolvePullScope", () => {
|
|
|
4208
4308
|
|
|
4209
4309
|
it("returns all when the client has no getMembershipSyncConfig", async () => {
|
|
4210
4310
|
const scope = await resolvePullScope(stubClient({}), "cmp_a", "acme");
|
|
4211
|
-
expect(scope).toEqual({ syncMode: "all" });
|
|
4311
|
+
expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
4312
|
+
});
|
|
4313
|
+
|
|
4314
|
+
// US-006 AC4: the sessions prefix is push-only, so `resolvePullScope` stamps
|
|
4315
|
+
// `excludePrefixes: ["sessions/"]` on EVERY return — including `all`
|
|
4316
|
+
// (full-bucket), owner→all, agent→all, shared, and custom. A single missed
|
|
4317
|
+
// return site would silently regress to a full session pull; this test pins
|
|
4318
|
+
// the invariant across every mode the resolver can return.
|
|
4319
|
+
it("stamps the push-only sessions exclusion on EVERY resolved mode (AC4)", async () => {
|
|
4320
|
+
expect(SESSIONS_SCOPE_PREFIX).toBe("sessions/");
|
|
4321
|
+
|
|
4322
|
+
const modes = [
|
|
4323
|
+
// no-client early return → all
|
|
4324
|
+
await resolvePullScope(stubClient({}), "cmp_a", "acme"),
|
|
4325
|
+
// owner role-bypass → all
|
|
4326
|
+
await resolvePullScope(
|
|
4327
|
+
stubClient({
|
|
4328
|
+
listMyMemberships: async () => [ownerMembership("cmp_a", "mk_owner")],
|
|
4329
|
+
}),
|
|
4330
|
+
"cmp_a",
|
|
4331
|
+
"acme",
|
|
4332
|
+
),
|
|
4333
|
+
// agent membership → all (short-circuit before the always-403 endpoint)
|
|
4334
|
+
await resolvePullScope(
|
|
4335
|
+
stubClient({
|
|
4336
|
+
listMyMemberships: async () => [membership("cmp_a", "agt_9#cmp_a")],
|
|
4337
|
+
}),
|
|
4338
|
+
"cmp_a",
|
|
4339
|
+
"acme",
|
|
4340
|
+
),
|
|
4341
|
+
// config-all → all
|
|
4342
|
+
await resolvePullScope(
|
|
4343
|
+
stubClient({
|
|
4344
|
+
listMyMemberships: async () => [membership("cmp_a", "mk_a")],
|
|
4345
|
+
getMembershipSyncConfig: async () => ({
|
|
4346
|
+
membershipId: "mk_a",
|
|
4347
|
+
syncMode: "all",
|
|
4348
|
+
isDefault: true,
|
|
4349
|
+
}),
|
|
4350
|
+
}),
|
|
4351
|
+
"cmp_a",
|
|
4352
|
+
"acme",
|
|
4353
|
+
),
|
|
4354
|
+
// shared
|
|
4355
|
+
await resolvePullScope(
|
|
4356
|
+
stubClient({
|
|
4357
|
+
listMyMemberships: async () => [membership("cmp_a", "mk_a")],
|
|
4358
|
+
getMembershipSyncConfig: async () => ({
|
|
4359
|
+
membershipId: "mk_a",
|
|
4360
|
+
syncMode: "shared",
|
|
4361
|
+
isDefault: false,
|
|
4362
|
+
}),
|
|
4363
|
+
listMyExplicitGrants: async () =>
|
|
4364
|
+
[
|
|
4365
|
+
{ companyUid: "cmp_a", path: "knowledge/", permission: "read", source: "person" },
|
|
4366
|
+
] as never,
|
|
4367
|
+
}),
|
|
4368
|
+
"cmp_a",
|
|
4369
|
+
"acme",
|
|
4370
|
+
),
|
|
4371
|
+
// custom
|
|
4372
|
+
await resolvePullScope(
|
|
4373
|
+
stubClient({
|
|
4374
|
+
listMyMemberships: async () => [membership("cmp_a", "mk_a")],
|
|
4375
|
+
getMembershipSyncConfig: async () => ({
|
|
4376
|
+
membershipId: "mk_a",
|
|
4377
|
+
syncMode: "custom",
|
|
4378
|
+
customPaths: ["projects/x/"],
|
|
4379
|
+
isDefault: false,
|
|
4380
|
+
}),
|
|
4381
|
+
}),
|
|
4382
|
+
"cmp_a",
|
|
4383
|
+
"acme",
|
|
4384
|
+
),
|
|
4385
|
+
];
|
|
4386
|
+
|
|
4387
|
+
for (const scope of modes) {
|
|
4388
|
+
expect(scope.excludePrefixes).toEqual([SESSIONS_SCOPE_PREFIX]);
|
|
4389
|
+
}
|
|
4212
4390
|
});
|
|
4213
4391
|
|
|
4214
4392
|
// OWNER role-bypass regression (feedback_67bdb8a4 / feedback_a46c3b37).
|
|
@@ -4238,7 +4416,7 @@ describe("resolvePullScope", () => {
|
|
|
4238
4416
|
"cmp_a",
|
|
4239
4417
|
"acme",
|
|
4240
4418
|
);
|
|
4241
|
-
expect(scope).toEqual({ syncMode: "all" });
|
|
4419
|
+
expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
4242
4420
|
// The short-circuit must fire BEFORE any grant resolution.
|
|
4243
4421
|
expect(grantsCalled).toBe(false);
|
|
4244
4422
|
});
|
|
@@ -4264,7 +4442,7 @@ describe("resolvePullScope", () => {
|
|
|
4264
4442
|
// Even a `custom` row cannot scope an owner below their role on the sync
|
|
4265
4443
|
// path (the server would 403 their own writes), so `all` wins and the
|
|
4266
4444
|
// config fetch is never needed.
|
|
4267
|
-
expect(scope).toEqual({ syncMode: "all" });
|
|
4445
|
+
expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
4268
4446
|
expect(configCalled).toBe(false);
|
|
4269
4447
|
});
|
|
4270
4448
|
|
|
@@ -4285,7 +4463,7 @@ describe("resolvePullScope", () => {
|
|
|
4285
4463
|
"cmp_a",
|
|
4286
4464
|
"acme",
|
|
4287
4465
|
);
|
|
4288
|
-
expect(scope).toEqual({ syncMode: "shared", prefixSet: ["knowledge/"] });
|
|
4466
|
+
expect(scope).toEqual({ syncMode: "shared", prefixSet: ["knowledge/"], excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
4289
4467
|
});
|
|
4290
4468
|
|
|
4291
4469
|
it("returns all for an all-mode membership (no prefixSet)", async () => {
|
|
@@ -4301,7 +4479,7 @@ describe("resolvePullScope", () => {
|
|
|
4301
4479
|
"cmp_a",
|
|
4302
4480
|
"acme",
|
|
4303
4481
|
);
|
|
4304
|
-
expect(scope).toEqual({ syncMode: "all" });
|
|
4482
|
+
expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
4305
4483
|
});
|
|
4306
4484
|
|
|
4307
4485
|
it("returns all for an AGENT membership WITHOUT calling sync-config (HQ-1R: agents own no person entity → server 403s the person-gate)", async () => {
|
|
@@ -4325,7 +4503,7 @@ describe("resolvePullScope", () => {
|
|
|
4325
4503
|
"acme",
|
|
4326
4504
|
);
|
|
4327
4505
|
expect(called).toBe(false); // never hit the always-403 endpoint for an agent
|
|
4328
|
-
expect(scope).toEqual({ syncMode: "all" });
|
|
4506
|
+
expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
4329
4507
|
});
|
|
4330
4508
|
|
|
4331
4509
|
it("coalesces explicit grants for a shared-mode membership", async () => {
|
|
@@ -4445,7 +4623,7 @@ describe("resolvePullScope", () => {
|
|
|
4445
4623
|
// A `*` grant = everything. Because coalescePrefixes drops the empty
|
|
4446
4624
|
// prefix (which would otherwise collapse to "nothing" and prune the
|
|
4447
4625
|
// tree), an everything-scope resolves to full-access `all`.
|
|
4448
|
-
expect(scope).toEqual({ syncMode: "all" });
|
|
4626
|
+
expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
4449
4627
|
});
|
|
4450
4628
|
|
|
4451
4629
|
it("uses customPaths for a custom-mode membership", async () => {
|
|
@@ -4480,7 +4658,7 @@ describe("resolvePullScope", () => {
|
|
|
4480
4658
|
"cmp_a",
|
|
4481
4659
|
"acme",
|
|
4482
4660
|
);
|
|
4483
|
-
expect(scope).toEqual({ syncMode: "all" });
|
|
4661
|
+
expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
4484
4662
|
});
|
|
4485
4663
|
|
|
4486
4664
|
it("allows a genuinely-empty shared scope (grants method present, returns [])", async () => {
|
|
@@ -4497,7 +4675,7 @@ describe("resolvePullScope", () => {
|
|
|
4497
4675
|
"cmp_a",
|
|
4498
4676
|
"acme",
|
|
4499
4677
|
);
|
|
4500
|
-
expect(scope).toEqual({ syncMode: "shared", prefixSet: [] });
|
|
4678
|
+
expect(scope).toEqual({ syncMode: "shared", prefixSet: [], excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
4501
4679
|
});
|
|
4502
4680
|
|
|
4503
4681
|
it("degrades to all when the membership is not found", async () => {
|
|
@@ -4511,7 +4689,7 @@ describe("resolvePullScope", () => {
|
|
|
4511
4689
|
"cmp_a",
|
|
4512
4690
|
"acme",
|
|
4513
4691
|
);
|
|
4514
|
-
expect(scope).toEqual({ syncMode: "all" });
|
|
4692
|
+
expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
4515
4693
|
});
|
|
4516
4694
|
|
|
4517
4695
|
it("degrades to all when sync-config resolution throws (never prune on error)", async () => {
|
|
@@ -4525,7 +4703,7 @@ describe("resolvePullScope", () => {
|
|
|
4525
4703
|
"cmp_a",
|
|
4526
4704
|
"acme",
|
|
4527
4705
|
);
|
|
4528
|
-
expect(scope).toEqual({ syncMode: "all" });
|
|
4706
|
+
expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
4529
4707
|
});
|
|
4530
4708
|
|
|
4531
4709
|
// ── pin union (Phase C: hq files get) ──────────────────────────────────────
|
package/src/bin/sync-runner.ts
CHANGED
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
import * as os from "os";
|
|
60
60
|
import * as path from "path";
|
|
61
61
|
import * as fs from "fs";
|
|
62
|
+
import { randomUUID } from "node:crypto";
|
|
62
63
|
import { fileURLToPath } from "url";
|
|
63
64
|
import {
|
|
64
65
|
getValidAccessToken,
|
|
@@ -78,6 +79,7 @@ import { pickCanonicalPersonEntity } from "../vault-client.js";
|
|
|
78
79
|
import {
|
|
79
80
|
resolvePullScope,
|
|
80
81
|
readPinnedPrefixes,
|
|
82
|
+
SESSIONS_SCOPE_PREFIX,
|
|
81
83
|
type PullScope,
|
|
82
84
|
} from "../sync/pull-scope.js";
|
|
83
85
|
import {
|
|
@@ -103,10 +105,15 @@ import {
|
|
|
103
105
|
import { HQ_CLOUD_VERSION } from "../version.js";
|
|
104
106
|
import { collectAndSendTelemetry } from "../telemetry.js";
|
|
105
107
|
import { collectAndSendSkillTelemetry } from "../skill-telemetry.js";
|
|
108
|
+
import { emitCloudTelemetry } from "../telemetry-events.js";
|
|
106
109
|
import { reindexAfterSync } from "../qmd-reindex.js";
|
|
107
110
|
import type { ReindexOptions as QmdReindexOptions } from "../qmd-reindex.js";
|
|
108
111
|
import { reindex as defaultReindex } from "../cli/reindex.js";
|
|
109
112
|
import type { ReindexOptions, ReindexResult } from "../cli/reindex.js";
|
|
113
|
+
import type {
|
|
114
|
+
TelemetryEventsBatch,
|
|
115
|
+
TelemetryEventsIngestResult,
|
|
116
|
+
} from "../vault-client.js";
|
|
110
117
|
import { pruneConflictIndex } from "../lib/conflict-index.js";
|
|
111
118
|
import { materializeCodexAgents } from "../agent-codex-instructions.js";
|
|
112
119
|
import { getOrCreateMachineId } from "../lib/machine-id.js";
|
|
@@ -114,6 +121,7 @@ import {
|
|
|
114
121
|
isTransientNetworkError,
|
|
115
122
|
TRANSIENT_NETWORK_EXIT,
|
|
116
123
|
} from "../lib/net-errors.js";
|
|
124
|
+
import { PARTIAL_SYNC_EXIT } from "../lib/exit-codes.js";
|
|
117
125
|
import { describeError } from "../lib/describe-error.js";
|
|
118
126
|
import type { Clock, TreeChangeBatch } from "../watcher.js";
|
|
119
127
|
import type { PushReceiver, SyncEngineFn } from "../sync/push-receiver.js";
|
|
@@ -175,7 +183,7 @@ const DEFAULT_COGNITO: CognitoAuthConfig = {
|
|
|
175
183
|
};
|
|
176
184
|
|
|
177
185
|
const DEFAULT_VAULT_API_URL =
|
|
178
|
-
process.env.HQ_VAULT_API_URL ?? "https://hqapi.
|
|
186
|
+
process.env.HQ_VAULT_API_URL ?? "https://hqapi.hq.computer";
|
|
179
187
|
|
|
180
188
|
const DEFAULT_HQ_ROOT = path.join(os.homedir(), "hq");
|
|
181
189
|
const DEFAULT_TELEMETRY_TIMEOUT_MS = 120_000;
|
|
@@ -445,6 +453,10 @@ export interface VaultClientSurface {
|
|
|
445
453
|
// interface; when absent, `resolvePullScope` degrades to `all`.
|
|
446
454
|
getMembershipSyncConfig?: (membershipId: string) => Promise<MembershipSyncConfig>;
|
|
447
455
|
listMyExplicitGrants?: (companyUid: string) => Promise<ExplicitGrant[]>;
|
|
456
|
+
postTelemetryEvents?: (
|
|
457
|
+
batch: TelemetryEventsBatch,
|
|
458
|
+
options?: { timeoutMs?: number },
|
|
459
|
+
) => Promise<TelemetryEventsIngestResult>;
|
|
448
460
|
}
|
|
449
461
|
|
|
450
462
|
interface RunnerDiagnostic {
|
|
@@ -499,7 +511,7 @@ function createRunnerDiagnosticReporter(
|
|
|
499
511
|
// (hq-cli) share ONE scope resolver — the drift between them was the root
|
|
500
512
|
// cause of the all→shared scope-shrink wedge (DEV-1768). Re-exported here so
|
|
501
513
|
// existing importers (and the runner test suite) keep their import path.
|
|
502
|
-
export { resolvePullScope, readPinnedPrefixes };
|
|
514
|
+
export { resolvePullScope, readPinnedPrefixes, SESSIONS_SCOPE_PREFIX };
|
|
503
515
|
export type { PullScope };
|
|
504
516
|
|
|
505
517
|
/**
|
|
@@ -1123,6 +1135,36 @@ export async function runRunner(
|
|
|
1123
1135
|
claims?.sub && claims?.email
|
|
1124
1136
|
? { userSub: claims.sub, email: claims.email }
|
|
1125
1137
|
: undefined;
|
|
1138
|
+
const telemetrySessionId = `sync_${randomUUID()}`;
|
|
1139
|
+
void emitCloudTelemetry(
|
|
1140
|
+
client,
|
|
1141
|
+
{
|
|
1142
|
+
eventName: "sync_runner_session_started",
|
|
1143
|
+
source: "sync-runner",
|
|
1144
|
+
sessionId: telemetrySessionId,
|
|
1145
|
+
properties: {
|
|
1146
|
+
direction: parsed.direction,
|
|
1147
|
+
targetMode: parsed.personal
|
|
1148
|
+
? "personal"
|
|
1149
|
+
: parsed.company
|
|
1150
|
+
? "single_company"
|
|
1151
|
+
: "companies",
|
|
1152
|
+
watchState: parsed.watch ? "enabled" : "disabled",
|
|
1153
|
+
eventPushState: parsed.eventPush ? "enabled" : "disabled",
|
|
1154
|
+
scopePathCount: parsed.scopePaths?.length ?? 0,
|
|
1155
|
+
},
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
claims,
|
|
1159
|
+
log: (message) =>
|
|
1160
|
+
reportDiagnostic({
|
|
1161
|
+
component: "telemetry-events",
|
|
1162
|
+
event: "runner.telemetry_events.nonfatal",
|
|
1163
|
+
message,
|
|
1164
|
+
context: { source: "sync-runner" },
|
|
1165
|
+
}),
|
|
1166
|
+
},
|
|
1167
|
+
);
|
|
1126
1168
|
|
|
1127
1169
|
// ---- transport selection (presigned-URL for cmp_, direct-S3/STS for prs_) -
|
|
1128
1170
|
// Company vaults ALWAYS use the presigned-URL transport now (HQ-59): the
|
|
@@ -1204,6 +1246,31 @@ export async function runRunner(
|
|
|
1204
1246
|
}
|
|
1205
1247
|
const plan = targetPlan.plan;
|
|
1206
1248
|
|
|
1249
|
+
void emitCloudTelemetry(
|
|
1250
|
+
client,
|
|
1251
|
+
{
|
|
1252
|
+
eventName: "sync_runner_plan_built",
|
|
1253
|
+
source: "sync-runner",
|
|
1254
|
+
sessionId: telemetrySessionId,
|
|
1255
|
+
properties: {
|
|
1256
|
+
direction: parsed.direction,
|
|
1257
|
+
targetCount: plan.length,
|
|
1258
|
+
companyCount: plan.filter((target) => target.personalMode !== true).length,
|
|
1259
|
+
personalCount: plan.filter((target) => target.personalMode === true).length,
|
|
1260
|
+
},
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
claims,
|
|
1264
|
+
log: (message) =>
|
|
1265
|
+
reportDiagnostic({
|
|
1266
|
+
component: "telemetry-events",
|
|
1267
|
+
event: "runner.telemetry_events.nonfatal",
|
|
1268
|
+
message,
|
|
1269
|
+
context: { source: "sync-runner" },
|
|
1270
|
+
}),
|
|
1271
|
+
},
|
|
1272
|
+
);
|
|
1273
|
+
|
|
1207
1274
|
emitFanoutPlan(emit, plan);
|
|
1208
1275
|
|
|
1209
1276
|
// One-time seed of the reserved personal-vault journal from the legacy
|
|
@@ -1230,16 +1297,45 @@ export async function runRunner(
|
|
|
1230
1297
|
resolveDeletePolicy,
|
|
1231
1298
|
emit,
|
|
1232
1299
|
scopePaths: parsed.scopePaths,
|
|
1300
|
+
telemetryClient: client,
|
|
1301
|
+
telemetryClaims: claims,
|
|
1302
|
+
telemetrySessionId,
|
|
1233
1303
|
});
|
|
1234
1304
|
const { errors, allConflicts } = fanout;
|
|
1235
1305
|
const rollup = rollupAllComplete(plan, fanout.stateByCompany);
|
|
1236
1306
|
|
|
1237
1307
|
if (rollup.needsReindex) {
|
|
1238
1308
|
try {
|
|
1239
|
-
|
|
1309
|
+
const reindexOptions = {
|
|
1240
1310
|
repoRoot: parsed.hqRoot,
|
|
1241
1311
|
...(deps.operationLockAlreadyHeld === true ? { skipLock: true } : {}),
|
|
1242
|
-
}
|
|
1312
|
+
};
|
|
1313
|
+
const reindexResult = (deps.reindex ?? defaultReindex)(reindexOptions);
|
|
1314
|
+
void emitCloudTelemetry(
|
|
1315
|
+
client,
|
|
1316
|
+
{
|
|
1317
|
+
eventName: "reindex_run",
|
|
1318
|
+
source: "sync-runner",
|
|
1319
|
+
sessionId: telemetrySessionId,
|
|
1320
|
+
properties: {
|
|
1321
|
+
result: reindexResult.status === 0 ? "completed" : "failed",
|
|
1322
|
+
statusCode: reindexResult.status,
|
|
1323
|
+
changedCount: rollup.changedPaths.length,
|
|
1324
|
+
lockResult:
|
|
1325
|
+
deps.operationLockAlreadyHeld === true ? "skipped" : "attempted",
|
|
1326
|
+
},
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
claims,
|
|
1330
|
+
log: (message) =>
|
|
1331
|
+
reportDiagnostic({
|
|
1332
|
+
component: "telemetry-events",
|
|
1333
|
+
event: "runner.telemetry_events.nonfatal",
|
|
1334
|
+
message,
|
|
1335
|
+
context: { source: "sync-runner" },
|
|
1336
|
+
}),
|
|
1337
|
+
},
|
|
1338
|
+
);
|
|
1243
1339
|
} catch (err) {
|
|
1244
1340
|
reportDiagnostic({
|
|
1245
1341
|
component: "native-reindex",
|
|
@@ -1248,6 +1344,30 @@ export async function runRunner(
|
|
|
1248
1344
|
err,
|
|
1249
1345
|
context: { hqRoot: parsed.hqRoot },
|
|
1250
1346
|
});
|
|
1347
|
+
void emitCloudTelemetry(
|
|
1348
|
+
client,
|
|
1349
|
+
{
|
|
1350
|
+
eventName: "reindex_run",
|
|
1351
|
+
source: "sync-runner",
|
|
1352
|
+
sessionId: telemetrySessionId,
|
|
1353
|
+
properties: {
|
|
1354
|
+
result: "threw",
|
|
1355
|
+
changedCount: rollup.changedPaths.length,
|
|
1356
|
+
lockResult:
|
|
1357
|
+
deps.operationLockAlreadyHeld === true ? "skipped" : "attempted",
|
|
1358
|
+
},
|
|
1359
|
+
},
|
|
1360
|
+
{
|
|
1361
|
+
claims,
|
|
1362
|
+
log: (message) =>
|
|
1363
|
+
reportDiagnostic({
|
|
1364
|
+
component: "telemetry-events",
|
|
1365
|
+
event: "runner.telemetry_events.nonfatal",
|
|
1366
|
+
message,
|
|
1367
|
+
context: { source: "sync-runner" },
|
|
1368
|
+
}),
|
|
1369
|
+
},
|
|
1370
|
+
);
|
|
1251
1371
|
}
|
|
1252
1372
|
}
|
|
1253
1373
|
|
|
@@ -1294,6 +1414,34 @@ export async function runRunner(
|
|
|
1294
1414
|
partial: rollup.partial,
|
|
1295
1415
|
companies: rollup.companies,
|
|
1296
1416
|
});
|
|
1417
|
+
void emitCloudTelemetry(
|
|
1418
|
+
client,
|
|
1419
|
+
{
|
|
1420
|
+
eventName: "sync_runner_completed",
|
|
1421
|
+
source: "sync-runner",
|
|
1422
|
+
sessionId: telemetrySessionId,
|
|
1423
|
+
properties: {
|
|
1424
|
+
result: rollup.partial ? "partial" : "completed",
|
|
1425
|
+
attemptedCount: plan.length,
|
|
1426
|
+
companyCount: rollup.companies.length,
|
|
1427
|
+
errorCount: errors.length,
|
|
1428
|
+
filesDownloaded: rollup.totalDownloaded,
|
|
1429
|
+
bytesDownloaded: rollup.totalDownloadedBytes,
|
|
1430
|
+
filesUploaded: rollup.totalUploaded,
|
|
1431
|
+
bytesUploaded: rollup.totalUploadedBytes,
|
|
1432
|
+
},
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
claims,
|
|
1436
|
+
log: (message) =>
|
|
1437
|
+
reportDiagnostic({
|
|
1438
|
+
component: "telemetry-events",
|
|
1439
|
+
event: "runner.telemetry_events.nonfatal",
|
|
1440
|
+
message,
|
|
1441
|
+
context: { source: "sync-runner" },
|
|
1442
|
+
}),
|
|
1443
|
+
},
|
|
1444
|
+
);
|
|
1297
1445
|
|
|
1298
1446
|
// Post-sync qmd reindex — runs AFTER `all-complete` is emitted so the
|
|
1299
1447
|
// menubar/CLI already shows the sync as done; this is a best-effort tail
|
|
@@ -1304,7 +1452,7 @@ export async function runRunner(
|
|
|
1304
1452
|
const qmdChangedPaths = rollup.changedPaths.filter(isKnowledgeMarkdownPath);
|
|
1305
1453
|
if (qmdChangedPaths.length > 0 && process.env.HQ_QMD_REINDEX_ON_SYNC !== "0") {
|
|
1306
1454
|
try {
|
|
1307
|
-
(deps.qmdReindex ?? reindexAfterSync)(parsed.hqRoot, {
|
|
1455
|
+
const qmdResult = (deps.qmdReindex ?? reindexAfterSync)(parsed.hqRoot, {
|
|
1308
1456
|
changedPaths: qmdChangedPaths,
|
|
1309
1457
|
forceCollectionRefresh: true,
|
|
1310
1458
|
log: ({ event, message, err, context }) =>
|
|
@@ -1316,6 +1464,33 @@ export async function runRunner(
|
|
|
1316
1464
|
context: { hqRoot: parsed.hqRoot, ...(context ?? {}) },
|
|
1317
1465
|
}),
|
|
1318
1466
|
});
|
|
1467
|
+
void emitCloudTelemetry(
|
|
1468
|
+
client,
|
|
1469
|
+
{
|
|
1470
|
+
eventName: "qmd_reindex_run",
|
|
1471
|
+
source: "sync-runner",
|
|
1472
|
+
sessionId: telemetrySessionId,
|
|
1473
|
+
properties: {
|
|
1474
|
+
result: "completed",
|
|
1475
|
+
changedCount: qmdChangedPaths.length,
|
|
1476
|
+
qmdState: qmdResult.qmdAvailable ? "available" : "unavailable",
|
|
1477
|
+
collectionsAddedCount: qmdResult.collectionsAdded.length,
|
|
1478
|
+
updateState: qmdResult.updated ? "updated" : "unchanged",
|
|
1479
|
+
embedState: qmdResult.embedded ? "embedded" : "not_embedded",
|
|
1480
|
+
dirtyState: qmdResult.pendingDirty ? "pending_dirty" : "clean",
|
|
1481
|
+
},
|
|
1482
|
+
},
|
|
1483
|
+
{
|
|
1484
|
+
claims,
|
|
1485
|
+
log: (message) =>
|
|
1486
|
+
reportDiagnostic({
|
|
1487
|
+
component: "telemetry-events",
|
|
1488
|
+
event: "runner.telemetry_events.nonfatal",
|
|
1489
|
+
message,
|
|
1490
|
+
context: { source: "sync-runner" },
|
|
1491
|
+
}),
|
|
1492
|
+
},
|
|
1493
|
+
);
|
|
1319
1494
|
} catch (err) {
|
|
1320
1495
|
reportDiagnostic({
|
|
1321
1496
|
component: "qmd-reindex",
|
|
@@ -1325,6 +1500,28 @@ export async function runRunner(
|
|
|
1325
1500
|
context: { hqRoot: parsed.hqRoot },
|
|
1326
1501
|
});
|
|
1327
1502
|
// Defensive: reindexAfterSync already swallows internally.
|
|
1503
|
+
void emitCloudTelemetry(
|
|
1504
|
+
client,
|
|
1505
|
+
{
|
|
1506
|
+
eventName: "qmd_reindex_run",
|
|
1507
|
+
source: "sync-runner",
|
|
1508
|
+
sessionId: telemetrySessionId,
|
|
1509
|
+
properties: {
|
|
1510
|
+
result: "threw",
|
|
1511
|
+
changedCount: qmdChangedPaths.length,
|
|
1512
|
+
},
|
|
1513
|
+
},
|
|
1514
|
+
{
|
|
1515
|
+
claims,
|
|
1516
|
+
log: (message) =>
|
|
1517
|
+
reportDiagnostic({
|
|
1518
|
+
component: "telemetry-events",
|
|
1519
|
+
event: "runner.telemetry_events.nonfatal",
|
|
1520
|
+
message,
|
|
1521
|
+
context: { source: "sync-runner" },
|
|
1522
|
+
}),
|
|
1523
|
+
},
|
|
1524
|
+
);
|
|
1328
1525
|
}
|
|
1329
1526
|
}
|
|
1330
1527
|
|
|
@@ -1392,12 +1589,13 @@ export async function runRunner(
|
|
|
1392
1589
|
}
|
|
1393
1590
|
}
|
|
1394
1591
|
|
|
1395
|
-
// Exit 2 only when something actually threw
|
|
1396
|
-
// conflict-abort sets `partial: true` in the
|
|
1397
|
-
// menubar's non-zero-exit Sentry capture would
|
|
1398
|
-
// user-policy outcomes. Consumers that want to
|
|
1399
|
-
// (clean-abort + thrown-error) read `partial`
|
|
1400
|
-
|
|
1592
|
+
// Exit PARTIAL_SYNC_EXIT (2) only when something actually threw
|
|
1593
|
+
// (`errors.length > 0`). A clean conflict-abort sets `partial: true` in the
|
|
1594
|
+
// JSON but exits 0 — the Tauri menubar's non-zero-exit Sentry capture would
|
|
1595
|
+
// otherwise fire for normal user-policy outcomes. Consumers that want to
|
|
1596
|
+
// flag any non-clean outcome (clean-abort + thrown-error) read `partial`
|
|
1597
|
+
// from the JSON. The watch loop stays alive on this code (see exit-codes.ts).
|
|
1598
|
+
return errors.length > 0 ? PARTIAL_SYNC_EXIT : 0;
|
|
1401
1599
|
}
|
|
1402
1600
|
|
|
1403
1601
|
// ---------------------------------------------------------------------------
|