@indigoai-us/hq-cloud 6.12.5 → 6.13.1

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 (48) hide show
  1. package/dist/bin/sync-runner.d.ts +2 -0
  2. package/dist/bin/sync-runner.d.ts.map +1 -1
  3. package/dist/bin/sync-runner.js +35 -2
  4. package/dist/bin/sync-runner.js.map +1 -1
  5. package/dist/bin/sync-runner.test.js +49 -0
  6. package/dist/bin/sync-runner.test.js.map +1 -1
  7. package/dist/cli/reindex.d.ts.map +1 -1
  8. package/dist/cli/reindex.js +39 -4
  9. package/dist/cli/reindex.js.map +1 -1
  10. package/dist/cli/reindex.test.js +55 -0
  11. package/dist/cli/reindex.test.js.map +1 -1
  12. package/dist/cli/share.js +15 -0
  13. package/dist/cli/share.js.map +1 -1
  14. package/dist/cli/share.test.js +100 -0
  15. package/dist/cli/share.test.js.map +1 -1
  16. package/dist/cli/sync.d.ts +1 -1
  17. package/dist/cli/sync.d.ts.map +1 -1
  18. package/dist/cli/sync.js +87 -3
  19. package/dist/cli/sync.js.map +1 -1
  20. package/dist/cli/sync.test.js +204 -0
  21. package/dist/cli/sync.test.js.map +1 -1
  22. package/dist/journal.d.ts +8 -2
  23. package/dist/journal.d.ts.map +1 -1
  24. package/dist/journal.js +32 -1
  25. package/dist/journal.js.map +1 -1
  26. package/dist/journal.test.js +34 -1
  27. package/dist/journal.test.js.map +1 -1
  28. package/dist/skill-telemetry.d.ts.map +1 -1
  29. package/dist/skill-telemetry.js +7 -3
  30. package/dist/skill-telemetry.js.map +1 -1
  31. package/dist/skill-telemetry.test.js +12 -0
  32. package/dist/skill-telemetry.test.js.map +1 -1
  33. package/dist/types.d.ts +11 -1
  34. package/dist/types.d.ts.map +1 -1
  35. package/package.json +1 -1
  36. package/src/bin/sync-runner.test.ts +61 -0
  37. package/src/bin/sync-runner.ts +57 -14
  38. package/src/cli/reindex.test.ts +60 -0
  39. package/src/cli/reindex.ts +47 -8
  40. package/src/cli/share.test.ts +119 -0
  41. package/src/cli/share.ts +21 -1
  42. package/src/cli/sync.test.ts +256 -0
  43. package/src/cli/sync.ts +97 -3
  44. package/src/journal.test.ts +44 -0
  45. package/src/journal.ts +37 -2
  46. package/src/skill-telemetry.test.ts +24 -0
  47. package/src/skill-telemetry.ts +6 -3
  48. package/src/types.ts +11 -1
package/src/journal.ts CHANGED
@@ -26,6 +26,13 @@ export const TOMBSTONE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
26
26
  /** Current journal schema version written by all v2-aware writers. */
27
27
  export const JOURNAL_VERSION_CURRENT = "2" as const;
28
28
 
29
+ /**
30
+ * Retain bounded pull history per company. Scope-shrink logic only needs the
31
+ * newest record, but a small tail keeps diagnostics useful without letting
32
+ * long-running sync loops grow journals forever.
33
+ */
34
+ export const MAX_PULLS_PER_COMPANY = 50;
35
+
29
36
  const JOURNAL_FILE_PREFIX = "sync-journal.";
30
37
  const JOURNAL_FILE_SUFFIX = ".json";
31
38
  const JOURNAL_LAST_GOOD_SUFFIX = ".last-good";
@@ -517,13 +524,41 @@ export function lastPullRecord(
517
524
  return best;
518
525
  }
519
526
 
520
- /** Append a `PullRecord` (mutates `journal.pulls`). */
527
+ function trimPullRecords(journal: SyncJournal): void {
528
+ const pulls = journal.pulls;
529
+ if (!pulls || pulls.length === 0) return;
530
+
531
+ const byCompany = new Map<string, Array<{ record: PullRecord; index: number }>>();
532
+ pulls.forEach((record, index) => {
533
+ const entries = byCompany.get(record.companyUid);
534
+ if (entries) entries.push({ record, index });
535
+ else byCompany.set(record.companyUid, [{ record, index }]);
536
+ });
537
+
538
+ const keep = new Set<number>();
539
+ for (const entries of byCompany.values()) {
540
+ entries
541
+ .sort((a, b) => {
542
+ const byCompletedAt = b.record.completedAt.localeCompare(
543
+ a.record.completedAt,
544
+ );
545
+ return byCompletedAt === 0 ? b.index - a.index : byCompletedAt;
546
+ })
547
+ .slice(0, MAX_PULLS_PER_COMPANY)
548
+ .forEach((entry) => keep.add(entry.index));
549
+ }
550
+
551
+ journal.pulls = pulls.filter((_, index) => keep.has(index));
552
+ }
553
+
554
+ /** Append a `PullRecord` (mutates `journal.pulls`) and cap retained history. */
521
555
  export function appendPullRecord(
522
556
  journal: SyncJournal,
523
557
  record: PullRecord,
524
558
  ): void {
525
559
  migrateToV2(journal);
526
560
  journal.pulls!.push(record);
561
+ trimPullRecords(journal);
527
562
  }
528
563
 
529
564
  /**
@@ -537,7 +572,7 @@ export function appendPullRecord(
537
572
  export function tombstoneEntry(
538
573
  journal: SyncJournal,
539
574
  relativePath: string,
540
- reason: "scope_shrink" | "narrow_apply" | "manual",
575
+ reason: "scope_shrink" | "narrow_apply" | "manual" | "local-delete",
541
576
  now: string = new Date().toISOString(),
542
577
  ): void {
543
578
  const entry = journal.files[relativePath];
@@ -226,6 +226,30 @@ describe("extractCodexSkillToolEvents (Codex model-driven SKILL.md reads)", () =
226
226
  expect(ev.source).toBe("model");
227
227
  });
228
228
 
229
+ it("matches normal Codex skill paths without backtracking ambiguity", () => {
230
+ expect(
231
+ extractCodexSkillToolEvents(
232
+ execEnd("cat /home/u/.claude/skills/hq/land/SKILL.md"),
233
+ ctx,
234
+ )[0].skill,
235
+ ).toBe("land");
236
+ expect(
237
+ extractCodexSkillToolEvents(
238
+ execEnd("cat /home/u/.claude/skills/a/b/c/SKILL.md"),
239
+ ctx,
240
+ )[0].skill,
241
+ ).toBe("c");
242
+ });
243
+
244
+ it("returns quickly for pathological non-matching skills paths", () => {
245
+ const cmd = "/home/u/.claude/skills/" + "a/".repeat(2000) + "z z";
246
+ const started = Date.now();
247
+ const events = extractCodexSkillToolEvents(execEnd(cmd), ctx);
248
+ const elapsedMs = Date.now() - started;
249
+ expect(events).toEqual([]);
250
+ expect(elapsedMs).toBeLessThan(200);
251
+ });
252
+
229
253
  it("absolute and relative SKILL.md paths both resolve", () => {
230
254
  expect(
231
255
  extractCodexSkillToolEvents(execEnd("cat /abs/path/.agents/skills/quiz/SKILL.md"), ctx)[0].skill,
@@ -342,10 +342,11 @@ export function extractCodexSkillEvents(
342
342
  * may be nested arbitrarily deep (`.agents/skills/…`, `.codex/skills/hq/…`),
343
343
  * and `<name>` is always the directory immediately above SKILL.md — captured
344
344
  * as the last segment so the bridge's `skills/hq/<name>/` layout resolves to
345
- * `<name>`, not `hq`. The leading `(?:…/)*` is non-greedy via the segment class
346
- * so it stops at the final directory boundary. */
345
+ * `<name>`, not `hq`. The segment classes deliberately exclude `/` so segment
346
+ * boundaries are unambiguous and non-matching `skills/...` paths cannot
347
+ * catastrophically backtrack. */
347
348
  const CODEX_SKILL_FILE =
348
- /(?:^|\/)skills\/(?:[^\s'"]+\/)*?([^/\s'"]+)\/SKILL\.md\b/;
349
+ /(?:^|\/)skills\/(?:[^/\s'"]+\/)*?([^/\s'"]+)\/SKILL\.md\b/;
349
350
 
350
351
  /** Pull the shell command string out of a Codex `exec_command_end` `command`,
351
352
  * which is `["/bin/zsh", "-lc", "<cmd>"]` (array) on the runtimes we see, but
@@ -488,6 +489,8 @@ export function extractCodexSkillToolEvents(
488
489
  const exec = codexExecParams(obj, payload, ctx);
489
490
  if (!exec) return [];
490
491
  const { cmd } = exec;
492
+ // Real exec commands are tiny; avoid running regexes over pathological rows.
493
+ if (cmd.length > 100_000) return [];
491
494
  const m = CODEX_SKILL_FILE.exec(cmd);
492
495
  if (!m) return [];
493
496
  // Confirm the exec is a read of the skill file, not a write to it. Codex's own
package/src/types.ts CHANGED
@@ -71,7 +71,7 @@ export interface JournalEntry {
71
71
  * the same paths as orphans, then garbage-collected.
72
72
  */
73
73
  removedAt?: string;
74
- removedReason?: "scope_shrink" | "narrow_apply" | "manual";
74
+ removedReason?: "scope_shrink" | "narrow_apply" | "manual" | "local-delete";
75
75
  /**
76
76
  * Durable automatic-pull retention marker. Set when a scope shrink keeps an
77
77
  * out-of-scope caller-authored or unknown-author entry on disk instead of
@@ -80,6 +80,16 @@ export interface JournalEntry {
80
80
  * the entry becomes in-scope again.
81
81
  */
82
82
  outOfScopeProtected?: boolean;
83
+ /**
84
+ * Journal-honesty marker. Set when a conflict was resolved by KEEPing a local
85
+ * copy that diverges from the remote: the entry still records the remote
86
+ * `remoteEtag` (so the conflict can't re-fire — the #137 invariant), but the
87
+ * local bytes never matched that remote. The currency-gated delete planner
88
+ * refuses to propagate a delete for such an entry — its currency would
89
+ * falsely "match" on HEAD, and deleting would destroy the divergent remote
90
+ * version. Cleared by any genuine download (which makes local == remote).
91
+ */
92
+ localDiverges?: boolean;
83
93
  }
84
94
 
85
95
  /**