@indigoai-us/hq-cloud 6.11.14 → 6.11.15
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/.github/workflows/ci.yml +1 -0
- package/dist/bin/sync-runner-telemetry.d.ts +1 -0
- package/dist/bin/sync-runner-telemetry.d.ts.map +1 -1
- package/dist/bin/sync-runner-telemetry.js +8 -1
- package/dist/bin/sync-runner-telemetry.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +9 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +115 -9
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/cli/share.js +13 -9
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +24 -12
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +16 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/qmd-reindex.d.ts +7 -0
- package/dist/qmd-reindex.d.ts.map +1 -1
- package/dist/qmd-reindex.js +12 -1
- package/dist/qmd-reindex.js.map +1 -1
- package/package.json +3 -1
- package/src/bin/sync-runner-telemetry.ts +8 -1
- package/src/bin/sync-runner.ts +131 -7
- package/src/cli/share.ts +15 -9
- package/src/cli/sync.test.ts +17 -0
- package/src/cli/sync.ts +33 -12
- package/src/qmd-reindex.ts +18 -1
package/src/bin/sync-runner.ts
CHANGED
|
@@ -107,6 +107,7 @@ import { collectAndSendSkillTelemetry } from "../skill-telemetry.js";
|
|
|
107
107
|
import { reindexAfterSync } from "../qmd-reindex.js";
|
|
108
108
|
import { pruneConflictIndex } from "../lib/conflict-index.js";
|
|
109
109
|
import { getOrCreateMachineId } from "../lib/machine-id.js";
|
|
110
|
+
import { describeError } from "../lib/describe-error.js";
|
|
110
111
|
import type { Clock, TreeChangeBatch } from "../watcher.js";
|
|
111
112
|
import type { PushReceiver, SyncEngineFn } from "../sync/push-receiver.js";
|
|
112
113
|
import {
|
|
@@ -404,6 +405,53 @@ export interface VaultClientSurface {
|
|
|
404
405
|
listMyExplicitGrants?: (companyUid: string) => Promise<ExplicitGrant[]>;
|
|
405
406
|
}
|
|
406
407
|
|
|
408
|
+
interface RunnerDiagnostic {
|
|
409
|
+
component: string;
|
|
410
|
+
event: string;
|
|
411
|
+
message: string;
|
|
412
|
+
err?: unknown;
|
|
413
|
+
context?: Record<string, unknown>;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
type RunnerDiagnosticReporter = (diagnostic: RunnerDiagnostic) => void;
|
|
417
|
+
|
|
418
|
+
function errorDetails(err: unknown): Record<string, unknown> {
|
|
419
|
+
const details: Record<string, unknown> = { message: describeError(err) };
|
|
420
|
+
if (err instanceof Error) {
|
|
421
|
+
if (err.name) details.name = err.name;
|
|
422
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
423
|
+
if (code) details.code = code;
|
|
424
|
+
}
|
|
425
|
+
return details;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function createRunnerDiagnosticReporter(
|
|
429
|
+
stderr: { write: (chunk: string) => boolean | void },
|
|
430
|
+
): RunnerDiagnosticReporter {
|
|
431
|
+
return (diagnostic) => {
|
|
432
|
+
try {
|
|
433
|
+
const payload: Record<string, unknown> = {
|
|
434
|
+
type: "error",
|
|
435
|
+
diagnostic: true,
|
|
436
|
+
component: diagnostic.component,
|
|
437
|
+
event: diagnostic.event,
|
|
438
|
+
path: `(${diagnostic.component})`,
|
|
439
|
+
message:
|
|
440
|
+
diagnostic.err === undefined
|
|
441
|
+
? diagnostic.message
|
|
442
|
+
: `${diagnostic.message}: ${describeError(diagnostic.err)}`,
|
|
443
|
+
...(diagnostic.context ?? {}),
|
|
444
|
+
};
|
|
445
|
+
if (diagnostic.err !== undefined) {
|
|
446
|
+
payload.err = errorDetails(diagnostic.err);
|
|
447
|
+
}
|
|
448
|
+
stderr.write(`${JSON.stringify(payload)}\n`);
|
|
449
|
+
} catch {
|
|
450
|
+
/* diagnostics must never affect runner control flow */
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
407
455
|
// `resolvePullScope`, `readPinnedPrefixes`, and the `PullScope` type now live
|
|
408
456
|
// in `../sync/pull-scope.ts` so the menubar runner and `hq sync pull|now`
|
|
409
457
|
// (hq-cli) share ONE scope resolver — the drift between them was the root
|
|
@@ -769,6 +817,7 @@ export async function defaultCollectTelemetry(
|
|
|
769
817
|
client: VaultClientSurface,
|
|
770
818
|
clientIsStub: boolean,
|
|
771
819
|
hqRoot: string,
|
|
820
|
+
reportDiagnostic: RunnerDiagnosticReporter = () => undefined,
|
|
772
821
|
): Promise<void> {
|
|
773
822
|
if (clientIsStub) return;
|
|
774
823
|
|
|
@@ -797,7 +846,14 @@ export async function defaultCollectTelemetry(
|
|
|
797
846
|
try {
|
|
798
847
|
const persons = await client.entity.listByType("person");
|
|
799
848
|
if (pickCanonicalPersonEntity(persons) === null) return;
|
|
800
|
-
} catch {
|
|
849
|
+
} catch (err) {
|
|
850
|
+
reportDiagnostic({
|
|
851
|
+
component: "telemetry",
|
|
852
|
+
event: "runner.telemetry.person_probe_failed",
|
|
853
|
+
message: "telemetry person probe failed; continuing with telemetry",
|
|
854
|
+
err,
|
|
855
|
+
context: { hqRoot },
|
|
856
|
+
});
|
|
801
857
|
// Probe failed — preserve prior behavior and let telemetry run.
|
|
802
858
|
}
|
|
803
859
|
|
|
@@ -820,7 +876,14 @@ export async function defaultCollectTelemetry(
|
|
|
820
876
|
try {
|
|
821
877
|
machineId = getOrCreateMachineId(hqRoot);
|
|
822
878
|
installerVersion = process.env.HQ_INSTALLER_VERSION ?? "hq-cloud";
|
|
823
|
-
} catch {
|
|
879
|
+
} catch (err) {
|
|
880
|
+
reportDiagnostic({
|
|
881
|
+
component: "telemetry",
|
|
882
|
+
event: "runner.telemetry.identity_failed",
|
|
883
|
+
message: "telemetry identity resolution failed; skipping telemetry",
|
|
884
|
+
err,
|
|
885
|
+
context: { hqRoot },
|
|
886
|
+
});
|
|
824
887
|
return;
|
|
825
888
|
}
|
|
826
889
|
|
|
@@ -836,8 +899,22 @@ export async function defaultCollectTelemetry(
|
|
|
836
899
|
// manifest.yaml and stamp companyUid (US-002). Unmatched cwds stay
|
|
837
900
|
// unattributed (field omitted).
|
|
838
901
|
hqRoot,
|
|
902
|
+
log: (message) =>
|
|
903
|
+
reportDiagnostic({
|
|
904
|
+
component: "telemetry",
|
|
905
|
+
event: "runner.telemetry.usage_nonfatal",
|
|
906
|
+
message,
|
|
907
|
+
context: { hqRoot },
|
|
908
|
+
}),
|
|
909
|
+
});
|
|
910
|
+
} catch (err) {
|
|
911
|
+
reportDiagnostic({
|
|
912
|
+
component: "telemetry",
|
|
913
|
+
event: "runner.telemetry.usage_failed",
|
|
914
|
+
message: "usage telemetry collector threw",
|
|
915
|
+
err,
|
|
916
|
+
context: { hqRoot },
|
|
839
917
|
});
|
|
840
|
-
} catch {
|
|
841
918
|
// Fire-and-forget; nothing escapes the boundary.
|
|
842
919
|
}
|
|
843
920
|
|
|
@@ -851,8 +928,22 @@ export async function defaultCollectTelemetry(
|
|
|
851
928
|
// Scope skill capture to the HQ project — only invocations whose cwd is
|
|
852
929
|
// the HQ root are emitted; skill usage in unrelated repos is excluded.
|
|
853
930
|
hqRoot,
|
|
931
|
+
log: (message) =>
|
|
932
|
+
reportDiagnostic({
|
|
933
|
+
component: "telemetry",
|
|
934
|
+
event: "runner.telemetry.skill_nonfatal",
|
|
935
|
+
message,
|
|
936
|
+
context: { hqRoot },
|
|
937
|
+
}),
|
|
938
|
+
});
|
|
939
|
+
} catch (err) {
|
|
940
|
+
reportDiagnostic({
|
|
941
|
+
component: "telemetry",
|
|
942
|
+
event: "runner.telemetry.skill_failed",
|
|
943
|
+
message: "skill telemetry collector threw",
|
|
944
|
+
err,
|
|
945
|
+
context: { hqRoot },
|
|
854
946
|
});
|
|
855
|
-
} catch {
|
|
856
947
|
// Fire-and-forget; nothing escapes the boundary.
|
|
857
948
|
}
|
|
858
949
|
}
|
|
@@ -867,6 +958,7 @@ export async function runRunner(
|
|
|
867
958
|
): Promise<number> {
|
|
868
959
|
const stdout = deps.stdout ?? process.stdout;
|
|
869
960
|
const stderr = deps.stderr ?? process.stderr;
|
|
961
|
+
const reportDiagnostic = createRunnerDiagnosticReporter(stderr);
|
|
870
962
|
|
|
871
963
|
// ---- emit ---------------------------------------------------------------
|
|
872
964
|
// Error-class events go to stderr; everything else to stdout.
|
|
@@ -1068,7 +1160,16 @@ export async function runRunner(
|
|
|
1068
1160
|
client,
|
|
1069
1161
|
deps.createVaultClient !== undefined,
|
|
1070
1162
|
parsed.hqRoot,
|
|
1163
|
+
reportDiagnostic,
|
|
1071
1164
|
),
|
|
1165
|
+
onError: (err) =>
|
|
1166
|
+
reportDiagnostic({
|
|
1167
|
+
component: "telemetry",
|
|
1168
|
+
event: "runner.telemetry.collector_failed",
|
|
1169
|
+
message: "telemetry collector failed",
|
|
1170
|
+
err,
|
|
1171
|
+
context: { hqRoot: parsed.hqRoot },
|
|
1172
|
+
}),
|
|
1072
1173
|
});
|
|
1073
1174
|
|
|
1074
1175
|
emit({
|
|
@@ -1095,8 +1196,24 @@ export async function runRunner(
|
|
|
1095
1196
|
process.env.HQ_QMD_REINDEX_ON_SYNC !== "0"
|
|
1096
1197
|
) {
|
|
1097
1198
|
try {
|
|
1098
|
-
reindexAfterSync(parsed.hqRoot
|
|
1099
|
-
|
|
1199
|
+
reindexAfterSync(parsed.hqRoot, {
|
|
1200
|
+
log: ({ event, message, err, context }) =>
|
|
1201
|
+
reportDiagnostic({
|
|
1202
|
+
component: "qmd-reindex",
|
|
1203
|
+
event,
|
|
1204
|
+
message,
|
|
1205
|
+
err,
|
|
1206
|
+
context: { hqRoot: parsed.hqRoot, ...(context ?? {}) },
|
|
1207
|
+
}),
|
|
1208
|
+
});
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
reportDiagnostic({
|
|
1211
|
+
component: "qmd-reindex",
|
|
1212
|
+
event: "runner.qmd_reindex.failed",
|
|
1213
|
+
message: "post-sync qmd reindex threw",
|
|
1214
|
+
err,
|
|
1215
|
+
context: { hqRoot: parsed.hqRoot },
|
|
1216
|
+
});
|
|
1100
1217
|
// Defensive: reindexAfterSync already swallows internally.
|
|
1101
1218
|
}
|
|
1102
1219
|
}
|
|
@@ -1113,7 +1230,14 @@ export async function runRunner(
|
|
|
1113
1230
|
if (process.env.HQ_CONFLICT_PRUNE_ON_SYNC !== "0") {
|
|
1114
1231
|
try {
|
|
1115
1232
|
pruneConflictIndex(parsed.hqRoot);
|
|
1116
|
-
} catch {
|
|
1233
|
+
} catch (err) {
|
|
1234
|
+
reportDiagnostic({
|
|
1235
|
+
component: "conflict-index",
|
|
1236
|
+
event: "runner.conflict_index.prune_failed",
|
|
1237
|
+
message: "post-sync conflict index prune failed",
|
|
1238
|
+
err,
|
|
1239
|
+
context: { hqRoot: parsed.hqRoot },
|
|
1240
|
+
});
|
|
1117
1241
|
// Defensive: a prune failure must never affect the sync outcome.
|
|
1118
1242
|
}
|
|
1119
1243
|
}
|
package/src/cli/share.ts
CHANGED
|
@@ -888,15 +888,6 @@ async function createPushRunContext(options: ShareOptions): Promise<PushRunConte
|
|
|
888
888
|
let propagateDeletePolicy: DeletePolicy =
|
|
889
889
|
options.propagateDeletePolicy ?? "owned-only";
|
|
890
890
|
const baseEmit = options.onEvent ?? defaultConsoleLogger;
|
|
891
|
-
// Mirror push progress into the shared cross-process snapshot (see sync.ts).
|
|
892
|
-
const recordProgress = createSyncProgressRecorder({
|
|
893
|
-
company: company ?? null,
|
|
894
|
-
phase: "push",
|
|
895
|
-
});
|
|
896
|
-
const emit: ShareEmit = (event) => {
|
|
897
|
-
baseEmit(event);
|
|
898
|
-
recordProgress(event);
|
|
899
|
-
};
|
|
900
891
|
|
|
901
892
|
if (vaultConfig && entityContext) {
|
|
902
893
|
throw new Error(
|
|
@@ -928,6 +919,21 @@ async function createPushRunContext(options: ShareOptions): Promise<PushRunConte
|
|
|
928
919
|
propagateDeletePolicy = "currency-gated";
|
|
929
920
|
}
|
|
930
921
|
|
|
922
|
+
// Mirror push progress into the shared cross-process snapshot (see sync.ts).
|
|
923
|
+
// Record the friendly company slug, or null for the personal vault so the
|
|
924
|
+
// menubar shows "Personal" — never the raw prs_/cmp_ UID.
|
|
925
|
+
const recordProgress = createSyncProgressRecorder({
|
|
926
|
+
company:
|
|
927
|
+
ctx.uid.startsWith("prs_") || options.personalMode === true
|
|
928
|
+
? null
|
|
929
|
+
: ctx.slug,
|
|
930
|
+
phase: "push",
|
|
931
|
+
});
|
|
932
|
+
const emit: ShareEmit = (event) => {
|
|
933
|
+
baseEmit(event);
|
|
934
|
+
recordProgress(event);
|
|
935
|
+
};
|
|
936
|
+
|
|
931
937
|
const syncRoot = options.personalMode === true
|
|
932
938
|
? hqRoot
|
|
933
939
|
: path.join(hqRoot, "companies", ctx.slug);
|
package/src/cli/sync.test.ts
CHANGED
|
@@ -46,6 +46,7 @@ vi.mock("./reindex.js", () => ({
|
|
|
46
46
|
import { sync, reportNewFilesToNotify } from "./sync.js";
|
|
47
47
|
import * as s3Module from "../s3.js";
|
|
48
48
|
import { reindex } from "./reindex.js";
|
|
49
|
+
import { readSyncProgress } from "../sync-progress.js";
|
|
49
50
|
|
|
50
51
|
const mockConfig: VaultServiceConfig = {
|
|
51
52
|
apiUrl: "https://vault-api.test",
|
|
@@ -144,6 +145,22 @@ describe("sync", () => {
|
|
|
144
145
|
expect(reindex).toHaveBeenCalledWith({ repoRoot: tmpDir, skipLock: true });
|
|
145
146
|
});
|
|
146
147
|
|
|
148
|
+
it("records the company slug, not the input UID, in the shared progress snapshot", async () => {
|
|
149
|
+
// sync() is called with the company UID, but the menubar popover must show
|
|
150
|
+
// the friendly slug — so the producer records ctx.slug, never the raw UID.
|
|
151
|
+
vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
|
|
152
|
+
{ key: "docs/x.md", size: 10, lastModified: new Date(), etag: '"x"' },
|
|
153
|
+
]);
|
|
154
|
+
await sync({
|
|
155
|
+
company: "cmp_01ABCDEF",
|
|
156
|
+
vaultConfig: mockConfig,
|
|
157
|
+
hqRoot: tmpDir,
|
|
158
|
+
});
|
|
159
|
+
const snap = readSyncProgress();
|
|
160
|
+
expect(snap).not.toBeNull();
|
|
161
|
+
expect(snap!.company).toBe("acme"); // ctx.slug, not "cmp_01ABCDEF"
|
|
162
|
+
});
|
|
163
|
+
|
|
147
164
|
it("F15: public sync entrypoint refuses an already-held operation lock", async () => {
|
|
148
165
|
process.env.HQ_OP_LOCK_TIMEOUT = "0";
|
|
149
166
|
const lockPath = lockPathFor(tmpDir);
|
package/src/cli/sync.ts
CHANGED
|
@@ -658,6 +658,23 @@ function logNotifyFailure(err: unknown): void {
|
|
|
658
658
|
}
|
|
659
659
|
}
|
|
660
660
|
|
|
661
|
+
/** Log a non-fatal post-sync maintenance failure without changing sync state. */
|
|
662
|
+
function logPostSyncMaintenanceFailure(
|
|
663
|
+
task: string,
|
|
664
|
+
hqRoot: string,
|
|
665
|
+
err: unknown,
|
|
666
|
+
): void {
|
|
667
|
+
try {
|
|
668
|
+
console.error(
|
|
669
|
+
`[hq-sync] ${task} failed (non-fatal; hqRoot=${hqRoot}): ${
|
|
670
|
+
err instanceof Error ? err.message : String(err)
|
|
671
|
+
}`,
|
|
672
|
+
);
|
|
673
|
+
} catch {
|
|
674
|
+
// swallow — logging must never break sync
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
661
678
|
/**
|
|
662
679
|
* Sync (pull) all allowed files from the entity vault.
|
|
663
680
|
*/
|
|
@@ -710,17 +727,6 @@ async function syncWithOperationLockHeld(
|
|
|
710
727
|
async function buildPullContext(options: SyncOptions): Promise<PullRunContext> {
|
|
711
728
|
const { company, vaultConfig, hqRoot } = options;
|
|
712
729
|
const baseEmit = options.onEvent ?? defaultConsoleLogger;
|
|
713
|
-
// Mirror every progress event into the shared cross-process snapshot so the
|
|
714
|
-
// menubar shows live progress for THIS sync regardless of who launched it
|
|
715
|
-
// (auto-sync / Sync Now / CLI). Best-effort; never affects the sync.
|
|
716
|
-
const recordProgress = createSyncProgressRecorder({
|
|
717
|
-
company: company ?? null,
|
|
718
|
-
phase: "pull",
|
|
719
|
-
});
|
|
720
|
-
const emit: SyncEventEmitter = (event) => {
|
|
721
|
-
baseEmit(event);
|
|
722
|
-
recordProgress(event);
|
|
723
|
-
};
|
|
724
730
|
|
|
725
731
|
const companyRef = company ?? resolveActiveCompany(hqRoot);
|
|
726
732
|
if (!companyRef) {
|
|
@@ -738,6 +744,20 @@ async function buildPullContext(options: SyncOptions): Promise<PullRunContext> {
|
|
|
738
744
|
const journalSlug = options.journalSlug ?? ctx.slug;
|
|
739
745
|
const startedAt = new Date().toISOString();
|
|
740
746
|
|
|
747
|
+
// Mirror every progress event into the shared cross-process snapshot so the
|
|
748
|
+
// menubar shows live progress for THIS sync regardless of who launched it
|
|
749
|
+
// (auto-sync / Sync Now / CLI). Record the friendly company slug (e.g.
|
|
750
|
+
// "indigo") — or null for the personal vault so the menubar shows "Personal"
|
|
751
|
+
// — never the raw prs_/cmp_ UID. Best-effort; never affects the sync.
|
|
752
|
+
const recordProgress = createSyncProgressRecorder({
|
|
753
|
+
company: options.personalMode === true ? null : ctx.slug,
|
|
754
|
+
phase: "pull",
|
|
755
|
+
});
|
|
756
|
+
const emit: SyncEventEmitter = (event) => {
|
|
757
|
+
baseEmit(event);
|
|
758
|
+
recordProgress(event);
|
|
759
|
+
};
|
|
760
|
+
|
|
741
761
|
if (journalSlug === PERSONAL_VAULT_JOURNAL_SLUG) migratePersonalVaultJournal();
|
|
742
762
|
const journal = migrateToV2(readJournal(journalSlug));
|
|
743
763
|
gcTombstones(journal, Date.now());
|
|
@@ -1466,7 +1486,8 @@ function finalizePullRun(
|
|
|
1466
1486
|
if (!run.options.skipReindex && changedOnDisk) {
|
|
1467
1487
|
try {
|
|
1468
1488
|
reindex({ repoRoot: run.hqRoot, skipLock: true });
|
|
1469
|
-
} catch {
|
|
1489
|
+
} catch (err) {
|
|
1490
|
+
logPostSyncMaintenanceFailure("post-sync reindex", run.hqRoot, err);
|
|
1470
1491
|
// best-effort: a post-sync refresh failure never fails the sync
|
|
1471
1492
|
}
|
|
1472
1493
|
}
|
package/src/qmd-reindex.ts
CHANGED
|
@@ -56,6 +56,13 @@ export interface ReindexOptions {
|
|
|
56
56
|
readCompanies?: (companiesDir: string) => string[];
|
|
57
57
|
/** Returns true if the knowledge dir has at least one indexable .md file. */
|
|
58
58
|
hasIndexableMarkdown?: (knowledgeDir: string) => boolean;
|
|
59
|
+
/** Optional diagnostic sink for unexpected swallowed failures. */
|
|
60
|
+
log?: (diagnostic: {
|
|
61
|
+
event: string;
|
|
62
|
+
message: string;
|
|
63
|
+
err: unknown;
|
|
64
|
+
context?: Record<string, unknown>;
|
|
65
|
+
}) => void;
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
/**
|
|
@@ -110,7 +117,17 @@ export function reindexAfterSync(
|
|
|
110
117
|
const embed = exec(["embed"]);
|
|
111
118
|
result.embedded = embed.status === 0;
|
|
112
119
|
}
|
|
113
|
-
} catch {
|
|
120
|
+
} catch (err) {
|
|
121
|
+
try {
|
|
122
|
+
opts.log?.({
|
|
123
|
+
event: "runner.qmd_reindex.internal_failed",
|
|
124
|
+
message: "post-sync qmd reindex failed",
|
|
125
|
+
err,
|
|
126
|
+
context: { hqRoot },
|
|
127
|
+
});
|
|
128
|
+
} catch {
|
|
129
|
+
/* diagnostics must never affect sync */
|
|
130
|
+
}
|
|
114
131
|
// Reindex is best-effort; the sync result is authoritative. Swallow.
|
|
115
132
|
}
|
|
116
133
|
|