@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.
- package/dist/db/db.d.ts +10 -2
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +111 -3
- package/dist/db/db.js.map +1 -1
- package/dist/db/queries.d.ts +1 -2
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +23 -9
- package/dist/db/queries.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/projects/sessions.d.ts +29 -1
- package/dist/projects/sessions.d.ts.map +1 -1
- package/dist/projects/sessions.js +115 -11
- package/dist/projects/sessions.js.map +1 -1
- package/dist/security/index.d.ts +14 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +7 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/maintenance.d.ts +15 -0
- package/dist/security/maintenance.d.ts.map +1 -0
- package/dist/security/maintenance.js +68 -0
- package/dist/security/maintenance.js.map +1 -0
- package/dist/security/profile.d.ts +47 -0
- package/dist/security/profile.d.ts.map +1 -0
- package/dist/security/profile.js +122 -0
- package/dist/security/profile.js.map +1 -0
- package/dist/security/purge.d.ts +49 -0
- package/dist/security/purge.d.ts.map +1 -0
- package/dist/security/purge.js +159 -0
- package/dist/security/purge.js.map +1 -0
- package/dist/security/repo.d.ts +141 -0
- package/dist/security/repo.d.ts.map +1 -0
- package/dist/security/repo.js +469 -0
- package/dist/security/repo.js.map +1 -0
- package/dist/security/scan.d.ts +50 -0
- package/dist/security/scan.d.ts.map +1 -0
- package/dist/security/scan.js +120 -0
- package/dist/security/scan.js.map +1 -0
- package/dist/security/types.d.ts +128 -0
- package/dist/security/types.d.ts.map +1 -0
- package/dist/security/types.js +10 -0
- package/dist/security/types.js.map +1 -0
- package/dist/security/worker.d.ts +53 -0
- package/dist/security/worker.d.ts.map +1 -0
- package/dist/security/worker.js +180 -0
- package/dist/security/worker.js.map +1 -0
- package/dist/sync/source-paths.d.ts +1 -0
- package/dist/sync/source-paths.d.ts.map +1 -1
- package/dist/sync/source-paths.js +13 -3
- package/dist/sync/source-paths.js.map +1 -1
- package/dist/sync/syncer.d.ts +8 -1
- package/dist/sync/syncer.d.ts.map +1 -1
- package/dist/sync/syncer.js +26 -14
- package/dist/sync/syncer.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/util/resolve-bin.d.ts +34 -0
- package/dist/util/resolve-bin.d.ts.map +1 -1
- package/dist/util/resolve-bin.js +175 -20
- package/dist/util/resolve-bin.js.map +1 -1
- 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":"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"}
|
|
@@ -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
|
-
|
|
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":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"source-paths.js","sourceRoot":"","sources":["../../src/sync/source-paths.ts"],"names":[],"mappings":"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"}
|
package/dist/sync/syncer.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/sync/syncer.js
CHANGED
|
@@ -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
|
-
|
|
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() &&
|
|
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 {
|