@spool-lab/core 0.4.12 → 0.4.20

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 (63) hide show
  1. package/dist/db/db.d.ts +10 -2
  2. package/dist/db/db.d.ts.map +1 -1
  3. package/dist/db/db.js +111 -3
  4. package/dist/db/db.js.map +1 -1
  5. package/dist/db/queries.d.ts +1 -2
  6. package/dist/db/queries.d.ts.map +1 -1
  7. package/dist/db/queries.js +23 -9
  8. package/dist/db/queries.js.map +1 -1
  9. package/dist/index.d.ts +2 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/projects/sessions.d.ts +29 -1
  14. package/dist/projects/sessions.d.ts.map +1 -1
  15. package/dist/projects/sessions.js +115 -11
  16. package/dist/projects/sessions.js.map +1 -1
  17. package/dist/security/index.d.ts +14 -0
  18. package/dist/security/index.d.ts.map +1 -0
  19. package/dist/security/index.js +7 -0
  20. package/dist/security/index.js.map +1 -0
  21. package/dist/security/maintenance.d.ts +15 -0
  22. package/dist/security/maintenance.d.ts.map +1 -0
  23. package/dist/security/maintenance.js +68 -0
  24. package/dist/security/maintenance.js.map +1 -0
  25. package/dist/security/profile.d.ts +47 -0
  26. package/dist/security/profile.d.ts.map +1 -0
  27. package/dist/security/profile.js +122 -0
  28. package/dist/security/profile.js.map +1 -0
  29. package/dist/security/purge.d.ts +49 -0
  30. package/dist/security/purge.d.ts.map +1 -0
  31. package/dist/security/purge.js +159 -0
  32. package/dist/security/purge.js.map +1 -0
  33. package/dist/security/repo.d.ts +141 -0
  34. package/dist/security/repo.d.ts.map +1 -0
  35. package/dist/security/repo.js +469 -0
  36. package/dist/security/repo.js.map +1 -0
  37. package/dist/security/scan.d.ts +50 -0
  38. package/dist/security/scan.d.ts.map +1 -0
  39. package/dist/security/scan.js +120 -0
  40. package/dist/security/scan.js.map +1 -0
  41. package/dist/security/types.d.ts +128 -0
  42. package/dist/security/types.d.ts.map +1 -0
  43. package/dist/security/types.js +10 -0
  44. package/dist/security/types.js.map +1 -0
  45. package/dist/security/worker.d.ts +53 -0
  46. package/dist/security/worker.d.ts.map +1 -0
  47. package/dist/security/worker.js +180 -0
  48. package/dist/security/worker.js.map +1 -0
  49. package/dist/sync/source-paths.d.ts +1 -0
  50. package/dist/sync/source-paths.d.ts.map +1 -1
  51. package/dist/sync/source-paths.js +13 -3
  52. package/dist/sync/source-paths.js.map +1 -1
  53. package/dist/sync/syncer.d.ts +8 -1
  54. package/dist/sync/syncer.d.ts.map +1 -1
  55. package/dist/sync/syncer.js +26 -14
  56. package/dist/sync/syncer.js.map +1 -1
  57. package/dist/types.d.ts +13 -0
  58. package/dist/types.d.ts.map +1 -1
  59. package/dist/util/resolve-bin.d.ts +34 -0
  60. package/dist/util/resolve-bin.d.ts.map +1 -1
  61. package/dist/util/resolve-bin.js +175 -20
  62. package/dist/util/resolve-bin.js.map +1 -1
  63. package/package.json +5 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.js","sourceRoot":"","sources":["../../src/security/scan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,128 @@
1
+ import type { SensitiveKind, Severity } from '@spool-lab/redact';
2
+ /** A finding's lifecycle state.
3
+ *
4
+ * active — surfaced in Security page and session strip
5
+ * dismissed — user opted to ignore; suppressed from default views
6
+ * purged — raw value rewritten with a mask in
7
+ * messages.content_text; row remains as an audit record
8
+ */
9
+ export type FindingState = 'active' | 'dismissed' | 'purged';
10
+ /** One row of the `findings` table, mapped to camelCase for the
11
+ * renderer. The raw sensitive value is NEVER part of this payload —
12
+ * consumers that need to render the original text fetch it via
13
+ * `getFindingValue(findingId)`, which reads
14
+ * `messages.content_text[startOffset..endOffset]` at request time. */
15
+ export interface FindingRow {
16
+ id: number;
17
+ sessionId: number;
18
+ messageId: number | null;
19
+ kind: SensitiveKind;
20
+ valueHash: string;
21
+ confidence: number;
22
+ provider: string;
23
+ startOffset: number;
24
+ endOffset: number;
25
+ state: FindingState;
26
+ detectedAt: string;
27
+ stateChangedAt: string | null;
28
+ }
29
+ /** A session row joined with its denormalised scan counts. Used by
30
+ * the Security page list and the Library row badge. */
31
+ export interface SessionWithFindingCounts {
32
+ id: number;
33
+ sessionUuid: string;
34
+ title: string | null;
35
+ startedAt: string;
36
+ /** Active findings of any severity. */
37
+ findingCount: number;
38
+ /** Active findings whose kind is in `HIGH_SEVERITY_KINDS`. */
39
+ highCount: number;
40
+ /** Lifetime count of purged findings on this session. Combined with
41
+ * scan_completed_at + a zero findingCount, lets the Library row
42
+ * show an "all-resolved" check rather than a stale red badge. */
43
+ purgedCount: number;
44
+ scanCompletedAt: string | null;
45
+ /** Sources name ('claude' / 'codex' / 'gemini'). */
46
+ source: string;
47
+ /** Number of non-sidechain messages in the session. */
48
+ messageCount: number;
49
+ /** Model identifier as captured at sync time. May be null for
50
+ * sessions where the parser couldn't infer it. */
51
+ model: string | null;
52
+ /** Working directory captured when the session started; used to
53
+ * build the resume-CLI command and the "Copy terminal command"
54
+ * menu action. Null when the parser couldn't infer it. */
55
+ cwd: string | null;
56
+ /** Human-readable project name, e.g. "spool". Surfaced at the head
57
+ * of the session card's meta row so users can locate the session
58
+ * in the Library at a glance. */
59
+ projectDisplayName: string | null;
60
+ }
61
+ /** Row in the Security page's Risk-by-category panel. One per kind
62
+ * that has ≥ 1 active finding; categories with zero counts are not
63
+ * emitted. */
64
+ export interface RiskByCategoryRow {
65
+ kind: SensitiveKind;
66
+ severity: Severity;
67
+ count: number;
68
+ /** Distinct sessions containing ≥ 1 active finding of this kind. */
69
+ sessions: number;
70
+ }
71
+ /** Scope of a Dismiss action.
72
+ * session — add to allowlist_session for this finding's session
73
+ * global — add to allowlist_global; all matching findings across
74
+ * every session auto-dismiss on next scan */
75
+ export interface DismissScope {
76
+ scope: 'session' | 'global';
77
+ /** Required when scope === 'session'. */
78
+ sessionId?: number;
79
+ }
80
+ /** Change events emitted on the worker's PubSub stream and forwarded
81
+ * to renderers via IPC. Consumers should treat unknown `type` values
82
+ * as no-ops (forward-compat). */
83
+ export type FindingsChange = {
84
+ type: 'inserted';
85
+ sessionId: number;
86
+ } | {
87
+ type: 'state-changed';
88
+ sessionId: number;
89
+ findingId: number;
90
+ state: FindingState;
91
+ } | {
92
+ type: 'session-rescanned';
93
+ sessionId: number;
94
+ };
95
+ /** Snapshot of the scan worker's transient state. The DB stores
96
+ * scan_profile / scan_completed_at; queued and scanning are
97
+ * worker-memory only — they reset on restart. */
98
+ export interface ScanStatus {
99
+ /** Sessions waiting for a slot in the worker queue. */
100
+ queued: number;
101
+ /** Session currently being scanned, or null when idle. */
102
+ scanning: number | null;
103
+ /** When in backfill mode, sessions still to process; 0 otherwise. */
104
+ backfillRemaining: number;
105
+ /** High-water mark for the active scan burst — the total session
106
+ * count this burst has set out to scan. Held until the worker
107
+ * goes fully idle (queued + scanning + backfillRemaining all 0),
108
+ * then resets to 0. Lets the renderer's progress bar survive
109
+ * navigation away from + back to the Security page mid-scan
110
+ * without "restarting" at the remaining-now count. */
111
+ backfillTotal: number;
112
+ /** True for the lifetime of a user-initiated `Rescan all` burst
113
+ * (set by `worker.rescanAll()`; cleared automatically when the
114
+ * worker goes fully idle). The renderer reads this directly
115
+ * instead of tracking the click locally — a renderer-side flag
116
+ * would race against any auto sync-driven enqueue that completed
117
+ * between the click and the IPC reaching the worker, causing
118
+ * the manual ACK banner to be hijacked + cleared by the auto
119
+ * burst's idle edge. Tying it to the worker keeps a single
120
+ * source of truth. */
121
+ manualBurstInFlight: boolean;
122
+ /** Composite identifier for the active provider set, e.g.
123
+ * 'regex@3' or 'regex@3,pf@1.5b-q4'. Persisted on
124
+ * sessions.scan_profile to detect rescan candidates after
125
+ * provider configuration changes. */
126
+ currentProfile: string;
127
+ }
128
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/security/types.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAEhE;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,WAAW,GAAG,QAAQ,CAAA;AAE5D;;;;uEAIuE;AACvE,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,IAAI,EAAE,aAAa,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,YAAY,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED;wDACwD;AACxD,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,MAAM,CAAA;IACV,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,YAAY,EAAE,MAAM,CAAA;IACpB,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAA;IACjB;;sEAEkE;IAClE,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAA;IACd,uDAAuD;IACvD,YAAY,EAAE,MAAM,CAAA;IACpB;uDACmD;IACnD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;+DAE2D;IAC3D,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;sCAEkC;IAClC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;CAClC;AAED;;eAEe;AACf,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,aAAa,CAAA;IACnB,QAAQ,EAAE,QAAQ,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;yDAGyD;AACzD,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAA;IAC3B,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;kCAEkC;AAClC,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE,GACpF;IAAE,IAAI,EAAE,mBAAmB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAA;AAEpD;;kDAEkD;AAClD,MAAM,WAAW,UAAU;IACzB,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,qEAAqE;IACrE,iBAAiB,EAAE,MAAM,CAAA;IACzB;;;;;2DAKuD;IACvD,aAAa,EAAE,MAAM,CAAA;IACrB;;;;;;;;2BAQuB;IACvB,mBAAmB,EAAE,OAAO,CAAA;IAC5B;;;0CAGsC;IACtC,cAAc,EAAE,MAAM,CAAA;CACvB"}
@@ -0,0 +1,10 @@
1
+ // Shared types for the Security Scan subsystem.
2
+ //
3
+ // These are the cross-boundary payloads — worker → IPC → renderer.
4
+ // They live in one module so:
5
+ // 1. The worker (main process), IPC adapters, and renderer hooks
6
+ // can import a single source of truth.
7
+ // 2. Promoting them to `Schema.Struct` later (for a Scope-B Effect
8
+ // RPC migration) is a local refactor, not a consumer rewrite.
9
+ export {};
10
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/security/types.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,EAAE;AACF,mEAAmE;AACnE,8BAA8B;AAC9B,mEAAmE;AACnE,4CAA4C;AAC5C,qEAAqE;AACrE,mEAAmE"}
@@ -0,0 +1,53 @@
1
+ import { Effect, Fiber, Stream, Scope } from 'effect';
2
+ import type Database from 'better-sqlite3';
3
+ import type { RedactProvider } from '@spool-lab/redact';
4
+ import type { FindingsChange, ScanStatus } from './types.js';
5
+ export interface WorkerConfig {
6
+ db: Database.Database;
7
+ providers: readonly RedactProvider[];
8
+ /** Profile string for `providers`. The worker rejects mismatched
9
+ * pairs (you can't claim profile = 'regex@3,pf@1.5b' with only
10
+ * regexProvider in `providers`).
11
+ *
12
+ * Pass either a static string (test fixtures) or a thunk (prod —
13
+ * lets the profile track pref changes like `kindAllowlist` so
14
+ * toggling a kind triggers backfill via `listSessionsNeedingScan`
15
+ * on the next call). The worker resolves the thunk on every read
16
+ * rather than caching, so a pref save → SET_PREFS handler can
17
+ * invalidate sessions and immediately see them as stale. */
18
+ currentProfile: string | (() => string);
19
+ /** Names of providers active in this profile. */
20
+ providerNames: readonly string[];
21
+ /** Live accessor for the kind-level allowlist. Read on every scan
22
+ * (cheap — a Set lookup per match), so changing the preference
23
+ * takes effect on the next rescan without restarting the worker. */
24
+ getKindAllowlist?: () => ReadonlySet<string>;
25
+ }
26
+ export interface ScanWorker {
27
+ /** Drop a session at the back of the queue. */
28
+ enqueue: (sessionId: number) => Effect.Effect<void>;
29
+ /** Set every session's profile to NULL and enqueue them — used by
30
+ * "Rescan all" and by provider-set changes. */
31
+ rescanAll: () => Effect.Effect<number>;
32
+ /** Enqueue every session whose scan_profile != currentProfile.
33
+ * Called once on boot. */
34
+ backfill: () => Effect.Effect<number>;
35
+ /** Stream of finding change events. Translator in main process
36
+ * subscribes and forwards over IPC. */
37
+ changes: Stream.Stream<FindingsChange>;
38
+ /** Stream of status updates — emitted on every queued / scanning /
39
+ * backfillRemaining mutation. Lets the UI replace polling with a
40
+ * push subscription. */
41
+ statusChanges: Stream.Stream<ScanStatus>;
42
+ /** Current snapshot. UI uses this for the first read; subsequent
43
+ * updates land via `statusChanges`. */
44
+ getStatus: Effect.Effect<ScanStatus>;
45
+ }
46
+ /** Build a worker as a Scoped effect: when the scope closes, the
47
+ * draining fiber is interrupted cleanly and the PubSub shuts down. */
48
+ export declare function makeScanWorker(config: WorkerConfig): Effect.Effect<ScanWorker, never, Scope.Scope>;
49
+ /** Helper for tests / callers that want a complete drain promise
50
+ * (e.g. wait for the queue to empty after a backfill). */
51
+ export declare function waitForIdle(worker: ScanWorker): Effect.Effect<void>;
52
+ export { Fiber };
53
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/security/worker.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAsB,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AACzE,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAC1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAGvD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5D,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAA;IACrB,SAAS,EAAE,SAAS,cAAc,EAAE,CAAA;IACpC;;;;;;;;;iEAS6D;IAC7D,cAAc,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAA;IACvC,iDAAiD;IACjD,aAAa,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC;;yEAEqE;IACrE,gBAAgB,CAAC,EAAE,MAAM,WAAW,CAAC,MAAM,CAAC,CAAA;CAC7C;AAQD,MAAM,WAAW,UAAU;IACzB,+CAA+C;IAC/C,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACnD;oDACgD;IAChD,SAAS,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACtC;+BAC2B;IAC3B,QAAQ,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACrC;4CACwC;IACxC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAA;IACtC;;6BAEyB;IACzB,aAAa,EAAE,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;IACxC;4CACwC;IACxC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;CACrC;AAED;uEACuE;AACvE,wBAAgB,cAAc,CAC5B,MAAM,EAAE,YAAY,GACnB,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAiL/C;AAED;2DAC2D;AAC3D,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAQnE;AAGD,OAAO,EAAE,KAAK,EAAE,CAAA"}
@@ -0,0 +1,180 @@
1
+ // Scan worker — the long-lived coordinator that drains a session
2
+ // queue, runs `scanSession` per item, and publishes change events.
3
+ //
4
+ // Implementation: Effect Scope A. Inside this module is Effect-shaped
5
+ // (Queue, Stream, PubSub, Ref, Scope). The boundary — IPC handlers
6
+ // in `packages/app/src/main/ipc/security.ts` — wraps `runFiber` /
7
+ // `runPromise` so renderers see plain Promise-shaped channels.
8
+ //
9
+ // Single-fiber by design (`concurrency: 1` on the queue stream).
10
+ // Better-sqlite3 is synchronous, so multiple parallel fibers compete
11
+ // for the DB lock and gain nothing. Backfill UX surfaces "session N
12
+ // of M" progress for the user.
13
+ import { Effect, Fiber, PubSub, Queue, Ref, Stream } from 'effect';
14
+ import { scanSession } from './scan.js';
15
+ import { listSessionsNeedingScan } from './repo.js';
16
+ function resolveProfile(config) {
17
+ return typeof config.currentProfile === 'function'
18
+ ? config.currentProfile()
19
+ : config.currentProfile;
20
+ }
21
+ /** Build a worker as a Scoped effect: when the scope closes, the
22
+ * draining fiber is interrupted cleanly and the PubSub shuts down. */
23
+ export function makeScanWorker(config) {
24
+ return Effect.gen(function* () {
25
+ const queue = yield* Queue.unbounded();
26
+ const events = yield* PubSub.unbounded();
27
+ const statusEvents = yield* PubSub.unbounded();
28
+ const statusRef = yield* Ref.make({
29
+ queued: 0,
30
+ scanning: null,
31
+ backfillRemaining: 0,
32
+ backfillTotal: 0,
33
+ manualBurstInFlight: false,
34
+ currentProfile: resolveProfile(config),
35
+ });
36
+ const publish = (c) => PubSub.publish(events, c).pipe(Effect.asVoid);
37
+ // Update statusRef AND publish the new snapshot so subscribers get
38
+ // a push event for every mutation. One helper at the seam avoids
39
+ // forgetting to publish from a future Ref.update site.
40
+ //
41
+ // `backfillTotal` is derived here so individual mutation sites
42
+ // (enqueue / rescanAll / backfill / scanOne) don't have to
43
+ // remember to bump it. The renderer's progress bar reads
44
+ // `backfillRemaining + (scanning ? 1 : 0)` as the active count;
45
+ // we track the high-water mark of that quantity since the last
46
+ // full-idle state, and reset to 0 once everything drains. Survives
47
+ // navigation: a user who switches tabs mid-burst and comes back
48
+ // gets the true original total via `getStatus`, not the
49
+ // remaining-at-remount-time approximation.
50
+ const updateStatus = (f) => Ref.update(statusRef, (prev) => {
51
+ const after = f(prev);
52
+ // High-water mark of *backfillRemaining alone*. Including the
53
+ // scanning slot (br + 1 when scanning != null) bumped the
54
+ // total by one every time a scanOne started, and dropped it
55
+ // again when scanning went back to null between sessions —
56
+ // turning the renderer's `(total - remaining)/total` into a
57
+ // non-monotonic value that visibly rewound the progress bar
58
+ // on every cross-session tick. backfillRemaining itself is
59
+ // strictly monotonic (only decrements at scanOne end), so
60
+ // anchoring the total to it makes the progress bar strictly
61
+ // forward.
62
+ const fullyIdle = after.backfillRemaining === 0 &&
63
+ after.queued === 0 &&
64
+ after.scanning === null;
65
+ const backfillTotal = fullyIdle
66
+ ? 0
67
+ : Math.max(prev.backfillTotal, after.backfillRemaining);
68
+ // `manualBurstInFlight` rides along with the burst. The
69
+ // worker.rescanAll() mutation flips it on; this wrapper
70
+ // flips it off the instant the worker drains, so the
71
+ // renderer's "your manual scan finished" detection never
72
+ // sees a stale truth across burst boundaries.
73
+ const manualBurstInFlight = fullyIdle ? false : after.manualBurstInFlight;
74
+ return { ...after, backfillTotal, manualBurstInFlight };
75
+ }).pipe(Effect.zipRight(Ref.get(statusRef)), Effect.flatMap((s) => PubSub.publish(statusEvents, s)), Effect.asVoid);
76
+ const scanOne = (sessionId) => Effect.gen(function* () {
77
+ yield* updateStatus((s) => ({
78
+ ...s,
79
+ scanning: sessionId,
80
+ queued: Math.max(0, s.queued - 1),
81
+ }));
82
+ const result = yield* scanSession(sessionId, {
83
+ db: config.db,
84
+ providers: config.providers,
85
+ currentProfile: resolveProfile(config),
86
+ providerNames: config.providerNames,
87
+ publish,
88
+ ...(config.getKindAllowlist ? { kindAllowlist: config.getKindAllowlist() } : {}),
89
+ }).pipe(Effect.catchAll((err) => Effect.logError(`scanSession failed for ${sessionId}: ${err.reason}`, err).pipe(Effect.as(null))));
90
+ yield* updateStatus((s) => ({
91
+ ...s,
92
+ scanning: null,
93
+ backfillRemaining: Math.max(0, s.backfillRemaining - 1),
94
+ }));
95
+ return result;
96
+ });
97
+ // The drain fiber runs for the worker's lifetime; Scope.addFinalizer
98
+ // interrupts it when the runtime tears down.
99
+ //
100
+ // Throttle: yield 50ms between scans. better-sqlite3 is synchronous
101
+ // so the DB lock is held for the full scanOne duration (~50-100ms
102
+ // per session). Without a yield, a backfill of N sessions becomes
103
+ // ~N*scanTime of uninterrupted lock contention — foreground reads
104
+ // (listSessions, FTS search) get queued behind every scan. The
105
+ // sleep gives the event loop a window to flush foreground IPC
106
+ // handlers before grabbing the next session. Doubles backfill
107
+ // duration but keeps the app responsive.
108
+ const drain = Stream.fromQueue(queue).pipe(Stream.mapEffect((sessionId) => scanOne(sessionId).pipe(Effect.tap(() => Effect.sleep('50 millis'))), { concurrency: 1 }), Stream.runDrain);
109
+ yield* Effect.forkScoped(drain);
110
+ const enqueue = (sessionId) => Effect.gen(function* () {
111
+ yield* Queue.offer(queue, sessionId);
112
+ yield* updateStatus((s) => ({ ...s, queued: s.queued + 1 }));
113
+ });
114
+ const rescanAll = () => Effect.gen(function* () {
115
+ // SELECT and UPDATE share one transaction so a concurrent sync
116
+ // that inserts a new session can't slip between them — the
117
+ // new row would otherwise miss the profile NULL pass AND be
118
+ // omitted from `all`, leaving it silently skipped by the
119
+ // drain fiber even though the user pressed "Rescan all". With
120
+ // a transaction the worker either sees every session present
121
+ // at the moment of the click, or sees the post-insert state
122
+ // (in which case the new session participates).
123
+ const all = yield* Effect.sync(() => config.db.transaction(() => {
124
+ const rows = config.db
125
+ .prepare('SELECT id FROM sessions ORDER BY started_at DESC')
126
+ .all();
127
+ config.db.prepare('UPDATE sessions SET scan_profile = NULL').run();
128
+ return rows;
129
+ })());
130
+ yield* updateStatus((s) => ({
131
+ ...s,
132
+ backfillRemaining: all.length,
133
+ // Flag this as a user-initiated burst — the renderer
134
+ // hangs its result-banner ACK off this field instead of
135
+ // a click-local flag.
136
+ manualBurstInFlight: true,
137
+ }));
138
+ for (const r of all) {
139
+ yield* enqueue(r.id);
140
+ }
141
+ return all.length;
142
+ });
143
+ const backfill = () => Effect.gen(function* () {
144
+ // Re-resolve the profile each call so a SET_PREFS handler that
145
+ // changed kindAllowlist sees the updated hash before invoking
146
+ // backfill. Keep the per-call status snapshot in sync too.
147
+ const profile = resolveProfile(config);
148
+ yield* updateStatus((s) => ({ ...s, currentProfile: profile }));
149
+ const stale = yield* Effect.sync(() => listSessionsNeedingScan(config.db, profile));
150
+ yield* updateStatus((s) => ({ ...s, backfillRemaining: stale.length }));
151
+ for (const id of stale) {
152
+ yield* enqueue(id);
153
+ }
154
+ return stale.length;
155
+ });
156
+ return {
157
+ enqueue,
158
+ rescanAll,
159
+ backfill,
160
+ changes: Stream.fromPubSub(events),
161
+ statusChanges: Stream.fromPubSub(statusEvents),
162
+ getStatus: Ref.get(statusRef),
163
+ };
164
+ });
165
+ }
166
+ /** Helper for tests / callers that want a complete drain promise
167
+ * (e.g. wait for the queue to empty after a backfill). */
168
+ export function waitForIdle(worker) {
169
+ return Effect.gen(function* () {
170
+ while (true) {
171
+ const s = yield* worker.getStatus;
172
+ if (s.queued === 0 && s.scanning === null && s.backfillRemaining === 0)
173
+ return;
174
+ yield* Effect.sleep('25 millis');
175
+ }
176
+ });
177
+ }
178
+ // Re-export Fiber for callers that want to manage runtime lifecycle.
179
+ export { Fiber };
180
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../../src/security/worker.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,mEAAmE;AACnE,EAAE;AACF,sEAAsE;AACtE,mEAAmE;AACnE,kEAAkE;AAClE,+DAA+D;AAC/D,EAAE;AACF,iEAAiE;AACjE,qEAAqE;AACrE,oEAAoE;AACpE,+BAA+B;AAE/B,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAS,MAAM,QAAQ,CAAA;AAGzE,OAAO,EAAE,WAAW,EAAmB,MAAM,WAAW,CAAA;AACxD,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAA;AAyBnD,SAAS,cAAc,CAAC,MAAoB;IAC1C,OAAO,OAAO,MAAM,CAAC,cAAc,KAAK,UAAU;QAChD,CAAC,CAAC,MAAM,CAAC,cAAc,EAAE;QACzB,CAAC,CAAC,MAAM,CAAC,cAAc,CAAA;AAC3B,CAAC;AAuBD;uEACuE;AACvE,MAAM,UAAU,cAAc,CAC5B,MAAoB;IAEpB,OAAO,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACzB,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,SAAS,EAAU,CAAA;QAC9C,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,EAAkB,CAAA;QACxD,MAAM,YAAY,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,EAAc,CAAA;QAC1D,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAa;YAC5C,MAAM,EAAE,CAAC;YACT,QAAQ,EAAE,IAAI;YACd,iBAAiB,EAAE,CAAC;YACpB,aAAa,EAAE,CAAC;YAChB,mBAAmB,EAAE,KAAK;YAC1B,cAAc,EAAE,cAAc,CAAC,MAAM,CAAC;SACvC,CAAC,CAAA;QAEF,MAAM,OAAO,GAAG,CAAC,CAAiB,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QAEpF,mEAAmE;QACnE,iEAAiE;QACjE,uDAAuD;QACvD,EAAE;QACF,+DAA+D;QAC/D,2DAA2D;QAC3D,yDAAyD;QACzD,gEAAgE;QAChE,+DAA+D;QAC/D,mEAAmE;QACnE,gEAAgE;QAChE,wDAAwD;QACxD,2CAA2C;QAC3C,MAAM,YAAY,GAAG,CAAC,CAAgC,EAAE,EAAE,CACxD,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE;YAC7B,MAAM,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,CAAA;YACrB,8DAA8D;YAC9D,0DAA0D;YAC1D,4DAA4D;YAC5D,2DAA2D;YAC3D,4DAA4D;YAC5D,4DAA4D;YAC5D,2DAA2D;YAC3D,0DAA0D;YAC1D,4DAA4D;YAC5D,WAAW;YACX,MAAM,SAAS,GACb,KAAK,CAAC,iBAAiB,KAAK,CAAC;gBAC7B,KAAK,CAAC,MAAM,KAAK,CAAC;gBAClB,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAA;YACzB,MAAM,aAAa,GAAG,SAAS;gBAC7B,CAAC,CAAC,CAAC;gBACH,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,iBAAiB,CAAC,CAAA;YACzD,wDAAwD;YACxD,wDAAwD;YACxD,qDAAqD;YACrD,yDAAyD;YACzD,8CAA8C;YAC9C,MAAM,mBAAmB,GAAG,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,mBAAmB,CAAA;YACzE,OAAO,EAAE,GAAG,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,CAAA;QACzD,CAAC,CAAC,CAAC,IAAI,CACL,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,EACnC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EACtD,MAAM,CAAC,MAAM,CACd,CAAA;QAEH,MAAM,OAAO,GAAG,CAAC,SAAiB,EAAE,EAAE,CACpC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YAClB,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC1B,GAAG,CAAC;gBACJ,QAAQ,EAAE,SAAS;gBACnB,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;aAClC,CAAC,CAAC,CAAA;YACH,MAAM,MAAM,GAAsB,KAAK,CAAC,CAAC,WAAW,CAAC,SAAS,EAAE;gBAC9D,EAAE,EAAE,MAAM,CAAC,EAAE;gBACb,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,cAAc,EAAE,cAAc,CAAC,MAAM,CAAC;gBACtC,aAAa,EAAE,MAAM,CAAC,aAAa;gBACnC,OAAO;gBACP,GAAG,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,MAAM,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACjF,CAAC,CAAC,IAAI,CACL,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,EAAE,CACtB,MAAM,CAAC,QAAQ,CAAC,0BAA0B,SAAS,KAAK,GAAG,CAAC,MAAM,EAAE,EAAE,GAAG,CAAC,CAAC,IAAI,CAC7E,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAChB,CACF,CACF,CAAA;YACD,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC1B,GAAG,CAAC;gBACJ,QAAQ,EAAE,IAAI;gBACd,iBAAiB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,iBAAiB,GAAG,CAAC,CAAC;aACxD,CAAC,CAAC,CAAA;YACH,OAAO,MAAM,CAAA;QACf,CAAC,CAAC,CAAA;QAEJ,qEAAqE;QACrE,6CAA6C;QAC7C,EAAE;QACF,oEAAoE;QACpE,kEAAkE;QAClE,kEAAkE;QAClE,kEAAkE;QAClE,+DAA+D;QAC/D,8DAA8D;QAC9D,8DAA8D;QAC9D,yCAAyC;QACzC,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,IAAI,CACxC,MAAM,CAAC,SAAS,CACd,CAAC,SAAiB,EAAE,EAAE,CACpB,OAAO,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,EACtE,EAAE,WAAW,EAAE,CAAC,EAAE,CACnB,EACD,MAAM,CAAC,QAAQ,CAChB,CAAA;QACD,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAA;QAE/B,MAAM,OAAO,GAAG,CAAC,SAAiB,EAAE,EAAE,CACpC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YAClB,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;YACpC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;QAC9D,CAAC,CAAC,CAAA;QAEJ,MAAM,SAAS,GAAG,GAAG,EAAE,CACrB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YAClB,+DAA+D;YAC/D,2DAA2D;YAC3D,4DAA4D;YAC5D,yDAAyD;YACzD,8DAA8D;YAC9D,6DAA6D;YAC7D,4DAA4D;YAC5D,gDAAgD;YAChD,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAClC,MAAM,CAAC,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;gBACzB,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE;qBACnB,OAAO,CAAC,kDAAkD,CAAC;qBAC3D,GAAG,EAA2B,CAAA;gBACjC,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,yCAAyC,CAAC,CAAC,GAAG,EAAE,CAAA;gBAClE,OAAO,IAAI,CAAA;YACb,CAAC,CAAC,EAAE,CACL,CAAA;YACD,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC1B,GAAG,CAAC;gBACJ,iBAAiB,EAAE,GAAG,CAAC,MAAM;gBAC7B,qDAAqD;gBACrD,wDAAwD;gBACxD,sBAAsB;gBACtB,mBAAmB,EAAE,IAAI;aAC1B,CAAC,CAAC,CAAA;YACH,KAAK,MAAM,CAAC,IAAI,GAAG,EAAE,CAAC;gBACpB,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;YACtB,CAAC;YACD,OAAO,GAAG,CAAC,MAAM,CAAA;QACnB,CAAC,CAAC,CAAA;QAEJ,MAAM,QAAQ,GAAG,GAAG,EAAE,CACpB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YAClB,+DAA+D;YAC/D,8DAA8D;YAC9D,2DAA2D;YAC3D,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,CAAC,CAAA;YACtC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,cAAc,EAAE,OAAO,EAAE,CAAC,CAAC,CAAA;YAC/D,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CACpC,uBAAuB,CAAC,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,CAC5C,CAAA;YACD,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,iBAAiB,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;YACvE,KAAK,MAAM,EAAE,IAAI,KAAK,EAAE,CAAC;gBACvB,KAAK,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;YACpB,CAAC;YACD,OAAO,KAAK,CAAC,MAAM,CAAA;QACrB,CAAC,CAAC,CAAA;QAEJ,OAAO;YACL,OAAO;YACP,SAAS;YACT,QAAQ;YACR,OAAO,EAAE,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC;YAClC,aAAa,EAAE,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC;YAC9C,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC;SACT,CAAA;IACxB,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;2DAC2D;AAC3D,MAAM,UAAU,WAAW,CAAC,MAAkB;IAC5C,OAAO,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACzB,OAAO,IAAI,EAAE,CAAC;YACZ,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAAA;YACjC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,QAAQ,KAAK,IAAI,IAAI,CAAC,CAAC,iBAAiB,KAAK,CAAC;gBAAE,OAAM;YAC9E,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;QAClC,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,qEAAqE;AACrE,OAAO,EAAE,KAAK,EAAE,CAAA"}
@@ -2,4 +2,5 @@ import type { SessionSource } from '../types.js';
2
2
  export declare function getSessionRoots(source: SessionSource): string[];
3
3
  export declare function detectSessionSource(filePath: string, sourceRoots?: Record<SessionSource, string[]>): SessionSource | undefined;
4
4
  export declare function getSessionWatchPatterns(source: SessionSource, roots?: string[]): string[];
5
+ export declare function isSessionFileForSource(source: SessionSource, filePath: string, root: string): boolean;
5
6
  //# sourceMappingURL=source-paths.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"source-paths.d.ts","sourceRoot":"","sources":["../../src/sync/source-paths.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAuBhD,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,EAAE,CA4B/D;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,WAAW,GAAE,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,CAI1C,GACA,aAAa,GAAG,SAAS,CAO3B;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,aAAa,EACrB,KAAK,WAA0B,GAC9B,MAAM,EAAE,CAGV"}
1
+ {"version":3,"file":"source-paths.d.ts","sourceRoot":"","sources":["../../src/sync/source-paths.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAuBhD,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,EAAE,CA4B/D;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,WAAW,GAAE,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,CAI1C,GACA,aAAa,GAAG,SAAS,CAO3B;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,aAAa,EACrB,KAAK,WAA0B,GAC9B,MAAM,EAAE,CAGV;AA8CD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAiBrG"}
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readdirSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
- import { basename, delimiter, isAbsolute, join, relative, resolve } from 'node:path';
3
+ import { basename, delimiter, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
4
  const SOURCE_DIR_NAMES = {
5
5
  claude: 'projects',
6
6
  codex: 'sessions',
@@ -100,7 +100,7 @@ function getGeminiBaseDir() {
100
100
  ? join(resolve(expandHome(geminiCliHome)), '.gemini')
101
101
  : join(homedir(), '.gemini');
102
102
  }
103
- function isSessionFileForSource(source, filePath, root) {
103
+ export function isSessionFileForSource(source, filePath, root) {
104
104
  if (!isWithinRoot(filePath, root))
105
105
  return false;
106
106
  if (source === 'gemini') {
@@ -108,7 +108,17 @@ function isSessionFileForSource(source, filePath, root) {
108
108
  && basename(filePath).startsWith('session-')
109
109
  && /(?:^|\/)chats\//.test(filePath);
110
110
  }
111
- return filePath.endsWith('.jsonl');
111
+ if (!filePath.endsWith('.jsonl'))
112
+ return false;
113
+ if (source === 'claude') {
114
+ // Claude sessions live at <root>/<slug>/<uuid>.jsonl — exactly two segments
115
+ // relative to root. Nested files (e.g. <slug>/<uuid>/subagents/agent-*.jsonl)
116
+ // are subagent scratchpads that share the parent's sessionId; indexing them
117
+ // clobbers the parent row via UNIQUE(session_uuid) and zeros message_count.
118
+ const rel = relative(root, filePath);
119
+ return rel.length > 0 && rel.split(sep).length === 2;
120
+ }
121
+ return true;
112
122
  }
113
123
  function isWithinRoot(filePath, root) {
114
124
  const resolvedFile = resolve(filePath);
@@ -1 +1 @@
1
- {"version":3,"file":"source-paths.js","sourceRoot":"","sources":["../../src/sync/source-paths.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"source-paths.js","sourceRoot":"","sources":["../../src/sync/source-paths.ts"],"names":[],"mappings":"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"}
@@ -7,11 +7,18 @@ export interface SyncProgressEvent {
7
7
  total: number;
8
8
  }
9
9
  export type SyncEventCallback = (event: SyncProgressEvent) => void;
10
+ /** Fired after `syncFile` commits a session's messages. Consumers
11
+ * (the scan worker hookup in main process) invalidate the session's
12
+ * scan_profile and re-enqueue it — necessary because offsets in
13
+ * the findings table point into the now-mutated `messages.content_text`.
14
+ * Receives the sessions.id, NOT the session_uuid. */
15
+ export type SessionChangedCallback = (sessionId: number) => void;
10
16
  export declare class Syncer {
11
17
  private db;
12
18
  private onProgress;
19
+ private onSessionChanged;
13
20
  private codexTitleIndex;
14
- constructor(db: Database.Database, onProgress?: SyncEventCallback);
21
+ constructor(db: Database.Database, onProgress?: SyncEventCallback, onSessionChanged?: SessionChangedCallback);
15
22
  syncAll(): SyncResult;
16
23
  private applyCodexTitles;
17
24
  syncFile(filePath: string, source: SessionSource, knownMtimes?: Map<string, string>, precomputedMtime?: string): 'added' | 'updated' | 'skipped' | 'error';
@@ -1 +1 @@
1
- {"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../../src/sync/syncer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAI1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAYhD,OAAO,KAAK,EAAiB,UAAU,EAAE,MAAM,aAAa,CAAA;AAI5D,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,MAAM,CAAA;IACnD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;AAElE,qBAAa,MAAM;IACjB,OAAO,CAAC,EAAE,CAAmB;IAC7B,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,eAAe,CAAiC;gBAE5C,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE,iBAAiB;IAKjE,OAAO,IAAI,UAAU;IAsFrB,OAAO,CAAC,gBAAgB;IAaxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO;CA8F3J"}
1
+ {"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../../src/sync/syncer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAI1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAYhD,OAAO,KAAK,EAAiB,UAAU,EAAE,MAAM,aAAa,CAAA;AAI5D,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,MAAM,CAAA;IACnD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;AAElE;;;;sDAIsD;AACtD,MAAM,MAAM,sBAAsB,GAAG,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;AAEhE,qBAAa,MAAM;IACjB,OAAO,CAAC,EAAE,CAAmB;IAC7B,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,gBAAgB,CAAoC;IAC5D,OAAO,CAAC,eAAe,CAAiC;gBAGtD,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,UAAU,CAAC,EAAE,iBAAiB,EAC9B,gBAAgB,CAAC,EAAE,sBAAsB;IAO3C,OAAO,IAAI,UAAU;IAsFrB,OAAO,CAAC,gBAAgB;IAaxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO;CA4G3J"}
@@ -4,17 +4,19 @@ import { homedir } from 'node:os';
4
4
  import { loadClaudeSession, decodeProjectSlug } from '../parsers/claude.js';
5
5
  import { loadCodexSession, CODEX_INDEX_VERSION } from '../parsers/codex.js';
6
6
  import { loadGeminiSession } from '../parsers/gemini.js';
7
- import { getSessionRoots } from './source-paths.js';
7
+ import { getSessionRoots, isSessionFileForSource } from './source-paths.js';
8
8
  import { deleteSessionByFilePath, getSourceId, getOrCreateProject, getSessionMtime, getAllSessionMtimes, upsertSession, upsertSessionSearch, insertMessages, } from '../db/queries.js';
9
9
  import { computeIdentity } from '../projects/identity.js';
10
10
  import { realFs } from '../projects/fs.js';
11
11
  export class Syncer {
12
12
  db;
13
13
  onProgress;
14
+ onSessionChanged;
14
15
  codexTitleIndex = new Map();
15
- constructor(db, onProgress) {
16
+ constructor(db, onProgress, onSessionChanged) {
16
17
  this.db = db;
17
18
  this.onProgress = onProgress;
19
+ this.onSessionChanged = onSessionChanged;
18
20
  }
19
21
  syncAll() {
20
22
  const seenPaths = new Set();
@@ -151,6 +153,7 @@ export class Syncer {
151
153
  const projectId = getOrCreateProject(this.db, sourceId, slug, identity.displayPath ?? displayPath, identity.displayName || displayName, { identityKind: identity.kind, identityKey: identity.key });
152
154
  const isNew = existingMtime === null;
153
155
  const hasToolUse = parsed.messages.some(m => m.toolNames.length > 0);
156
+ let committedSessionId = null;
154
157
  this.db.transaction(() => {
155
158
  const sessionId = upsertSession(this.db, {
156
159
  projectId,
@@ -173,11 +176,23 @@ export class Syncer {
173
176
  userText: buildSessionSearchText(parsed.messages, 'user'),
174
177
  assistantText: buildSessionSearchText(parsed.messages, 'assistant'),
175
178
  });
179
+ // Security Scan cascade: messages.content_text changed, so any
180
+ // existing findings now have stale offsets. Drop scan_profile
181
+ // inside the same transaction so a crash between here and the
182
+ // worker re-enqueue still leaves the session in "needs scan".
183
+ this.db.prepare(`UPDATE sessions SET scan_profile = NULL, scan_completed_at = NULL WHERE id = ?`).run(sessionId);
176
184
  this.db.prepare(`
177
185
  INSERT INTO sync_log (source_id, file_path, status)
178
186
  VALUES (?, ?, 'ok')
179
187
  `).run(sourceId, filePath);
188
+ committedSessionId = sessionId;
180
189
  })();
190
+ if (committedSessionId !== null && this.onSessionChanged) {
191
+ try {
192
+ this.onSessionChanged(committedSessionId);
193
+ }
194
+ catch { /* swallow callback errors */ }
195
+ }
181
196
  return isNew ? 'added' : 'updated';
182
197
  }
183
198
  catch (err) {
@@ -220,10 +235,10 @@ function getIndexVersion(source) {
220
235
  }
221
236
  function collectSessionFiles(dir, source) {
222
237
  const results = [];
223
- walkDir(dir, results, source);
238
+ walkDir(dir, dir, results, source);
224
239
  return results;
225
240
  }
226
- function walkDir(dir, results, source) {
241
+ function walkDir(dir, root, results, source) {
227
242
  let entries;
228
243
  try {
229
244
  entries = readdirSync(dir, { withFileTypes: true, encoding: 'utf8' });
@@ -236,9 +251,14 @@ function walkDir(dir, results, source) {
236
251
  if (entry.isDirectory()) {
237
252
  if (source === 'gemini' && !shouldTraverseGeminiDir(dir, fullPath, entry.name))
238
253
  continue;
239
- walkDir(fullPath, results, source);
254
+ // For claude, session files only live at <root>/<slug>/<uuid>.jsonl. Anything
255
+ // deeper is subagent / future-nested scratch data that hijacks the parent
256
+ // sessionId — see isSessionFileForSource for the matching read-side check.
257
+ if (source === 'claude' && dirname(fullPath) !== root)
258
+ continue;
259
+ walkDir(fullPath, root, results, source);
240
260
  }
241
- else if (entry.isFile() && isSessionFilePath(fullPath, source)) {
261
+ else if (entry.isFile() && isSessionFileForSource(source, fullPath, root)) {
242
262
  results.push({ path: fullPath, source });
243
263
  }
244
264
  }
@@ -252,14 +272,6 @@ function shouldTraverseGeminiDir(parentDir, fullPath, entryName) {
252
272
  return true;
253
273
  return existsSync(join(fullPath, 'chats'));
254
274
  }
255
- function isSessionFilePath(filePath, source) {
256
- if (source === 'gemini') {
257
- return filePath.endsWith('.json')
258
- && basename(filePath).startsWith('session-')
259
- && /(?:^|\/)chats\//.test(filePath);
260
- }
261
- return filePath.endsWith('.jsonl');
262
- }
263
275
  function loadCodexSessionIndex() {
264
276
  const titles = new Map();
265
277
  try {