chat-recall 0.0.1 → 0.4.8
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/LICENSE +93 -0
- package/dist/auto-update.d.ts +52 -0
- package/dist/auto-update.d.ts.map +1 -0
- package/dist/auto-update.js +157 -0
- package/dist/auto-update.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +26102 -0
- package/dist/cli.js.map +1 -0
- package/dist/client-events.d.ts +11 -0
- package/dist/client-events.d.ts.map +1 -0
- package/dist/client-events.js +77 -0
- package/dist/client-events.js.map +1 -0
- package/dist/collector-migrate.d.ts +27 -0
- package/dist/collector-migrate.d.ts.map +1 -0
- package/dist/collector-migrate.js +60 -0
- package/dist/collector-migrate.js.map +1 -0
- package/dist/device-auth.d.ts +37 -0
- package/dist/device-auth.d.ts.map +1 -0
- package/dist/device-auth.js +161 -0
- package/dist/device-auth.js.map +1 -0
- package/dist/escalate.d.ts +99 -0
- package/dist/escalate.d.ts.map +1 -0
- package/dist/escalate.js +219 -0
- package/dist/escalate.js.map +1 -0
- package/dist/http.d.ts +14 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +49 -0
- package/dist/http.js.map +1 -0
- package/dist/install-skills.d.ts +51 -0
- package/dist/install-skills.d.ts.map +1 -0
- package/dist/install-skills.js +223 -0
- package/dist/install-skills.js.map +1 -0
- package/dist/intent-drain.d.ts +25 -0
- package/dist/intent-drain.d.ts.map +1 -0
- package/dist/intent-drain.js +189 -0
- package/dist/intent-drain.js.map +1 -0
- package/dist/keyring.d.ts +28 -0
- package/dist/keyring.d.ts.map +1 -0
- package/dist/keyring.js +163 -0
- package/dist/keyring.js.map +1 -0
- package/dist/mcp.d.ts +8 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +23665 -0
- package/dist/mcp.js.map +1 -0
- package/dist/project-tasks.d.ts +54 -0
- package/dist/project-tasks.d.ts.map +1 -0
- package/dist/project-tasks.js +204 -0
- package/dist/project-tasks.js.map +1 -0
- package/dist/repair.d.ts +73 -0
- package/dist/repair.d.ts.map +1 -0
- package/dist/repair.js +267 -0
- package/dist/repair.js.map +1 -0
- package/dist/self-restart.d.ts +91 -0
- package/dist/self-restart.d.ts.map +1 -0
- package/dist/self-restart.js +137 -0
- package/dist/self-restart.js.map +1 -0
- package/dist/service-installer.d.ts +32 -0
- package/dist/service-installer.d.ts.map +1 -0
- package/dist/service-installer.js +231 -0
- package/dist/service-installer.js.map +1 -0
- package/dist/sync-client.d.ts +234 -0
- package/dist/sync-client.d.ts.map +1 -0
- package/dist/sync-client.js +1985 -0
- package/dist/sync-client.js.map +1 -0
- package/dist/sync-ledger.d.ts +131 -0
- package/dist/sync-ledger.d.ts.map +1 -0
- package/dist/sync-ledger.js +366 -0
- package/dist/sync-ledger.js.map +1 -0
- package/dist/update-notice.d.ts +16 -0
- package/dist/update-notice.d.ts.map +1 -0
- package/dist/update-notice.js +107 -0
- package/dist/update-notice.js.map +1 -0
- package/dist/verify-deep.d.ts +79 -0
- package/dist/verify-deep.d.ts.map +1 -0
- package/dist/verify-deep.js +97 -0
- package/dist/verify-deep.js.map +1 -0
- package/dist/verify-repair.d.ts +21 -0
- package/dist/verify-repair.d.ts.map +1 -0
- package/dist/verify-repair.js +58 -0
- package/dist/verify-repair.js.map +1 -0
- package/dist/watch.js +21550 -0
- package/hooks/chat_recall_escalate_hook.sh +51 -0
- package/hooks/chat_recall_resume_hook.sh +69 -0
- package/hooks/chat_recall_save_hook.sh +254 -0
- package/hooks/chat_recall_wakeup_hook.sh +39 -0
- package/package.json +38 -4
- package/skills/chat-recall/SKILL.md +76 -0
- package/skills/chat-recall-changes/SKILL.md +50 -0
- package/skills/chat-recall-find/SKILL.md +64 -0
- package/skills/chat-recall-memory/SKILL.md +55 -0
- package/skills/chat-recall-project/SKILL.md +75 -0
- package/skills/chat-recall-resume/SKILL.md +62 -0
- package/README.md +0 -7
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-conversation sync ledger — client-side bookkeeping for "is THIS
|
|
3
|
+
* session synced at its current version?".
|
|
4
|
+
*
|
|
5
|
+
* Replaces the global lastSyncAt watermark as the source of truth for
|
|
6
|
+
* incremental sync: every tick walks all sessions, skips the ones whose
|
|
7
|
+
* ledger row already covers their current mtime, pushes the rest, and
|
|
8
|
+
* marks a session ONLY after the server acked the batch containing it.
|
|
9
|
+
* A session that fails to upload stays unmarked and is retried on the
|
|
10
|
+
* next tick — nothing can be silently skipped forever.
|
|
11
|
+
*
|
|
12
|
+
* Keyed by server URL so switching endpoints (self-host ↔ SaaS) restarts
|
|
13
|
+
* coverage for the new target without forgetting the old one.
|
|
14
|
+
*
|
|
15
|
+
* Storage is a plain JSON file under the data dir — NOT SQLite. The thin
|
|
16
|
+
* collector has no local database, and this is local client state (it's
|
|
17
|
+
* meaningless on a server and not part of the StorageDriver contract). Keeping
|
|
18
|
+
* it as JSON is what lets the collector ship with zero native modules.
|
|
19
|
+
*
|
|
20
|
+
* Freshness has TWO dimensions, not one:
|
|
21
|
+
* - mtime: did the source transcript change?
|
|
22
|
+
* - extractor version: did the code that derives synced rows change?
|
|
23
|
+
* A session is up to date only when BOTH the synced mtime covers the file
|
|
24
|
+
* AND the synced extractor version is current. This is what makes an
|
|
25
|
+
* extraction fix self-heal — see extractor-version.ts.
|
|
26
|
+
*
|
|
27
|
+
* Shape: { "<serverUrl>": { "<sessionId>": <entry>, ... }, ... }
|
|
28
|
+
* where <entry> is { m: <syncedMtime>, v: <extractorVersion> } — or, for
|
|
29
|
+
* ledgers written before versioning, a bare <syncedMtime> number (read as
|
|
30
|
+
* version 0 so every legacy row is treated as extractor-stale once).
|
|
31
|
+
*/
|
|
32
|
+
import { readFileSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
|
|
33
|
+
import { join, dirname } from 'node:path';
|
|
34
|
+
import { getDataDir } from '@chat-recall/engine/core/paths.js';
|
|
35
|
+
import { extractorVersionForId } from '@chat-recall/engine/core/extractor-version.js';
|
|
36
|
+
/** Normalize either on-disk shape to {m,v,f}; legacy numbers are version 0. */
|
|
37
|
+
function toRow(entry) {
|
|
38
|
+
if (entry === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
return typeof entry === 'number' ? { m: entry, v: 0 } : entry;
|
|
41
|
+
}
|
|
42
|
+
const ledgerPath = () => join(getDataDir(), 'sync-ledger.json');
|
|
43
|
+
/** Where the ledger lives — exposed so repair tooling can back it up before
|
|
44
|
+
* editing the file that decides what gets uploaded. */
|
|
45
|
+
export function ledgerFilePath() { return ledgerPath(); }
|
|
46
|
+
// In-memory cache so a sync walk doesn't re-read the file per batch. Loaded
|
|
47
|
+
// lazily; writes go through atomic tmp+rename so a crash mid-write can't
|
|
48
|
+
// corrupt the ledger (worst case: a session re-uploads, which is safe).
|
|
49
|
+
let cache = null;
|
|
50
|
+
function load() {
|
|
51
|
+
if (cache)
|
|
52
|
+
return cache;
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(readFileSync(ledgerPath(), 'utf-8'));
|
|
55
|
+
cache = parsed && typeof parsed === 'object' ? parsed : {};
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
cache = {};
|
|
59
|
+
}
|
|
60
|
+
return cache;
|
|
61
|
+
}
|
|
62
|
+
function persist(data) {
|
|
63
|
+
const path = ledgerPath();
|
|
64
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
65
|
+
const tmp = `${path}.tmp`;
|
|
66
|
+
writeFileSync(tmp, JSON.stringify(data));
|
|
67
|
+
renameSync(tmp, path);
|
|
68
|
+
}
|
|
69
|
+
const itemVersionsPath = () => join(getDataDir(), 'item-versions.json');
|
|
70
|
+
let itemVersionsCache = null;
|
|
71
|
+
function loadItemVersionsAll() {
|
|
72
|
+
if (itemVersionsCache)
|
|
73
|
+
return itemVersionsCache;
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(readFileSync(itemVersionsPath(), 'utf-8'));
|
|
76
|
+
itemVersionsCache = parsed && typeof parsed === 'object' ? parsed : {};
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
itemVersionsCache = {};
|
|
80
|
+
}
|
|
81
|
+
return itemVersionsCache;
|
|
82
|
+
}
|
|
83
|
+
/** tool → last item-extractor version fully walked for this server. */
|
|
84
|
+
export function loadItemVersions(server) {
|
|
85
|
+
return { ...(loadItemVersionsAll()[server] || {}) };
|
|
86
|
+
}
|
|
87
|
+
/** Record (merge) the versions this server's items were just walked under. */
|
|
88
|
+
export function saveItemVersions(server, versions) {
|
|
89
|
+
const all = loadItemVersionsAll();
|
|
90
|
+
all[server] = { ...(all[server] || {}), ...versions };
|
|
91
|
+
const path = itemVersionsPath();
|
|
92
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
93
|
+
const tmp = `${path}.tmp`;
|
|
94
|
+
writeFileSync(tmp, JSON.stringify(all));
|
|
95
|
+
renameSync(tmp, path);
|
|
96
|
+
}
|
|
97
|
+
/** session_id → mtime the server has acked, for one target server. */
|
|
98
|
+
export function getSyncedMtimes(server) {
|
|
99
|
+
const data = load();
|
|
100
|
+
const out = new Map();
|
|
101
|
+
for (const [id, entry] of Object.entries(data[server] || {}))
|
|
102
|
+
out.set(id, toRow(entry).m);
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* session_id → {mtime, extractorVersion} the server has acked. This is the
|
|
107
|
+
* accessor the freshness gate uses: a session is current only when the synced
|
|
108
|
+
* mtime covers the file AND the synced version is >= EXTRACTOR_VERSION.
|
|
109
|
+
*/
|
|
110
|
+
export function getSyncedRows(server) {
|
|
111
|
+
const data = load();
|
|
112
|
+
const out = new Map();
|
|
113
|
+
for (const [id, entry] of Object.entries(data[server] || {}))
|
|
114
|
+
out.set(id, toRow(entry));
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
/** Raw data for one server, useful when you need to mutate (e.g. tombstones). */
|
|
118
|
+
export function getLedgerData(server) {
|
|
119
|
+
const data = load();
|
|
120
|
+
return data[server] || {};
|
|
121
|
+
}
|
|
122
|
+
/** Persist mutated data for one server. */
|
|
123
|
+
export function persistLedgerData(server, serverData) {
|
|
124
|
+
const data = load();
|
|
125
|
+
data[server] = serverData;
|
|
126
|
+
persist(data);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Mark sessions as synced at the given mtimes, stamped with the CURRENT
|
|
130
|
+
* extractor version (after a server ack). The version stamp is what lets a
|
|
131
|
+
* later extraction-code change invalidate these rows for re-sync.
|
|
132
|
+
*
|
|
133
|
+
* `offset` and `size` are the tail-sync byte cursor (append-only transcripts).
|
|
134
|
+
* They default to 0 (FULL sync) so callers that don't care about tail sync
|
|
135
|
+
* leave them absent/0 → the next tick treats the session as FULL-eligible
|
|
136
|
+
* only when mtime advances, exactly as before this field existed.
|
|
137
|
+
*/
|
|
138
|
+
/**
|
|
139
|
+
* Record sync progress.
|
|
140
|
+
*
|
|
141
|
+
* ── The byte cursor is a DELIVERY RECEIPT, not a measurement ──────────────
|
|
142
|
+
* `o`/`s` mean "the server has these bytes". `syncMode` returns SKIP/APPEND off
|
|
143
|
+
* them, so writing a cursor the server never acknowledged makes a session look
|
|
144
|
+
* complete forever: skipped on every pass, never re-exported, unreachable.
|
|
145
|
+
* Self-sealing, and silent — nothing reports it.
|
|
146
|
+
*
|
|
147
|
+
* That is not hypothetical. A call site stamped the cursor at the LOCAL
|
|
148
|
+
* `fileSize()` after a tail came back empty, purely to stop the gate retrying.
|
|
149
|
+
* Once `fileSize()` began summing copies across profile homes, the cursor
|
|
150
|
+
* jumped to the union size while the server still held one home's half —
|
|
151
|
+
* measured on session 7adc748c: ledger `s: 7195821`, server 2004 of 2698
|
|
152
|
+
* records, and it could never re-ship because it read as done.
|
|
153
|
+
*
|
|
154
|
+
* So the cursor now moves ONLY when the caller passes `acked: true`, meaning a
|
|
155
|
+
* server write for those bytes succeeded. Anti-thrash callers that just want to
|
|
156
|
+
* stop retrying within an mtime pass a row WITHOUT `acked` — mtime alone
|
|
157
|
+
* already yields SKIP for an unchanged file, which was all they needed.
|
|
158
|
+
*/
|
|
159
|
+
/** How many times a caller tried to move the cursor without an ack. Surfaced so
|
|
160
|
+
* a regression shows up as a number instead of silent data loss. */
|
|
161
|
+
let unackedCursorAttempts = 0;
|
|
162
|
+
export function unackedCursorAttemptCount() { return unackedCursorAttempts; }
|
|
163
|
+
export function _resetUnackedCursorCount() { unackedCursorAttempts = 0; }
|
|
164
|
+
export function markSynced(server, rows) {
|
|
165
|
+
if (rows.length === 0)
|
|
166
|
+
return;
|
|
167
|
+
const data = load();
|
|
168
|
+
const forServer = data[server] || (data[server] = {});
|
|
169
|
+
for (const r of rows) {
|
|
170
|
+
// PRESERVE per-field coverage `f` — it's owned by field reconciliation and
|
|
171
|
+
// must survive a conversation re-sync. Overwriting the whole entry here was
|
|
172
|
+
// wiping it, making every reconcile re-scan from scratch.
|
|
173
|
+
const prev = toRow(forServer[r.id]);
|
|
174
|
+
const prevF = prev?.f;
|
|
175
|
+
const prevOS = prev && (prev.o !== undefined || prev.s !== undefined)
|
|
176
|
+
? { o: prev.o, s: prev.s }
|
|
177
|
+
: {};
|
|
178
|
+
// Unacknowledged rows keep whatever cursor the server last confirmed. A
|
|
179
|
+
// local measurement must never move it forward — see the header.
|
|
180
|
+
const cursorFromCaller = r.acked === true;
|
|
181
|
+
if (!cursorFromCaller && (r.offset !== undefined || r.size !== undefined)) {
|
|
182
|
+
unackedCursorAttempts++;
|
|
183
|
+
}
|
|
184
|
+
const o = (cursorFromCaller ? r.offset : undefined) ?? prevOS.o ?? 0;
|
|
185
|
+
const s = (cursorFromCaller ? r.size : undefined) ?? prevOS.s ?? 0;
|
|
186
|
+
// Record the version that applies to THIS session's tool (per-tool), so a
|
|
187
|
+
// tool-specific extractor bump only re-ships that tool's sessions.
|
|
188
|
+
const ev = extractorVersionForId(r.id);
|
|
189
|
+
const base = prevF
|
|
190
|
+
? { m: Math.floor(r.mtime), v: ev, f: prevF }
|
|
191
|
+
: { m: Math.floor(r.mtime), v: ev };
|
|
192
|
+
if (o > 0 || s > 0) {
|
|
193
|
+
base.o = o;
|
|
194
|
+
base.s = s;
|
|
195
|
+
}
|
|
196
|
+
// Content hash: set when a FULL sync (or an unchanged-content skip) supplies
|
|
197
|
+
// it. An APPEND passes no hash → the stale head hash is dropped, so a later
|
|
198
|
+
// FULL never falsely matches the grown content.
|
|
199
|
+
if (r.hash !== undefined)
|
|
200
|
+
base.h = r.hash;
|
|
201
|
+
forServer[r.id] = base;
|
|
202
|
+
}
|
|
203
|
+
persist(data);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Does `field` need (re)scanning for a session whose ledger row is `row` and
|
|
207
|
+
* whose current transcript mtime is `mtime`? True when never covered, covered
|
|
208
|
+
* at an older field version, or (for mtime-sensitive fields) the transcript
|
|
209
|
+
* moved past the mtime it was scanned at. `force` overrides everything. An
|
|
210
|
+
* absent-but-current field returns false — that's the no-retry guarantee.
|
|
211
|
+
*/
|
|
212
|
+
export function fieldNeedsScan(row, field, mtime, force = false) {
|
|
213
|
+
if (force)
|
|
214
|
+
return true;
|
|
215
|
+
const cov = row?.f?.[field.name];
|
|
216
|
+
if (!cov)
|
|
217
|
+
return true;
|
|
218
|
+
if (cov.v < field.version)
|
|
219
|
+
return true;
|
|
220
|
+
if (field.mtimeSensitive && (cov.m ?? 0) < Math.floor(mtime))
|
|
221
|
+
return true;
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Record per-field coverage after the server acked (or, for absent fields,
|
|
226
|
+
* locally). Each row stamps ONE field's {version, present, mtime} onto the
|
|
227
|
+
* session's `f` map WITHOUT touching its {m,v} conversation state or any other
|
|
228
|
+
* field. Preserves/creates the row as needed.
|
|
229
|
+
*/
|
|
230
|
+
export function markFieldCoverage(server, field, rows) {
|
|
231
|
+
if (rows.length === 0)
|
|
232
|
+
return;
|
|
233
|
+
const data = load();
|
|
234
|
+
const forServer = data[server] || (data[server] = {});
|
|
235
|
+
for (const r of rows) {
|
|
236
|
+
const row = toRow(forServer[r.id]) ?? { m: 0, v: 0 };
|
|
237
|
+
const f = row.f ? { ...row.f } : {};
|
|
238
|
+
f[field.name] = { v: field.version, p: r.present ? 1 : 0, m: Math.floor(r.mtime) };
|
|
239
|
+
forServer[r.id] = { ...row, f };
|
|
240
|
+
}
|
|
241
|
+
persist(data);
|
|
242
|
+
}
|
|
243
|
+
const reconcilePath = () => join(getDataDir(), 'sync-field-reconcile.json');
|
|
244
|
+
let reconcileCache = null;
|
|
245
|
+
function loadReconcile() {
|
|
246
|
+
if (reconcileCache)
|
|
247
|
+
return reconcileCache;
|
|
248
|
+
try {
|
|
249
|
+
const parsed = JSON.parse(readFileSync(reconcilePath(), 'utf-8'));
|
|
250
|
+
reconcileCache = parsed && typeof parsed === 'object' ? parsed : {};
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
reconcileCache = {};
|
|
254
|
+
}
|
|
255
|
+
return reconcileCache;
|
|
256
|
+
}
|
|
257
|
+
function persistReconcile(data) {
|
|
258
|
+
const path = reconcilePath();
|
|
259
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
260
|
+
const tmp = `${path}.tmp`;
|
|
261
|
+
writeFileSync(tmp, JSON.stringify(data));
|
|
262
|
+
renameSync(tmp, path);
|
|
263
|
+
}
|
|
264
|
+
function stateFor(server) {
|
|
265
|
+
const data = loadReconcile();
|
|
266
|
+
return data[server] || (data[server] = { reconciled: {}, force: {} });
|
|
267
|
+
}
|
|
268
|
+
/** True if a one-time FULL backfill pass is owed for `field` on `server` —
|
|
269
|
+
* either never reconciled at this version, or a re-scan was forced. */
|
|
270
|
+
export function fieldNeedsFullPass(server, field) {
|
|
271
|
+
const st = stateFor(server);
|
|
272
|
+
return st.force[field.name] === true || (st.reconciled[field.name] ?? 0) < field.version;
|
|
273
|
+
}
|
|
274
|
+
/** Mark a field's full backfill pass complete for `server` at `version`, and
|
|
275
|
+
* clear any force flag. Call only after the whole pass succeeded. */
|
|
276
|
+
export function markFieldFullPassDone(server, field) {
|
|
277
|
+
const data = loadReconcile();
|
|
278
|
+
const st = data[server] || (data[server] = { reconciled: {}, force: {} });
|
|
279
|
+
st.reconciled[field.name] = field.version;
|
|
280
|
+
delete st.force[field.name];
|
|
281
|
+
persistReconcile(data);
|
|
282
|
+
}
|
|
283
|
+
/** UI/CLI hook: demand a one-time re-scan of `field` (optionally on one server,
|
|
284
|
+
* else all known servers). Sets the force flag the next sync honors. */
|
|
285
|
+
export function forceFieldRescan(fieldName, server) {
|
|
286
|
+
const data = loadReconcile();
|
|
287
|
+
const servers = server ? [server] : Object.keys(data);
|
|
288
|
+
if (servers.length === 0 && server)
|
|
289
|
+
servers.push(server);
|
|
290
|
+
for (const s of servers) {
|
|
291
|
+
const st = data[s] || (data[s] = { reconciled: {}, force: {} });
|
|
292
|
+
st.force[fieldName] = true;
|
|
293
|
+
}
|
|
294
|
+
persistReconcile(data);
|
|
295
|
+
}
|
|
296
|
+
/** Test-only: drop the in-memory caches so a fresh file read happens next call. */
|
|
297
|
+
export function _resetLedgerCacheForTests() {
|
|
298
|
+
cache = null;
|
|
299
|
+
reconcileCache = null;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Invalidate a session's ledger row so the next sync tick classifies it as
|
|
303
|
+
* FULL (never-synced). Used when the server signals `full_resync_needed` for
|
|
304
|
+
* an append — the client must not retry append (the server lost the prior
|
|
305
|
+
* envelope); it must FULL re-sync. Deleting the row (rather than clearing just
|
|
306
|
+
* o/s) is correct: a FULL re-sync re-derives everything including field
|
|
307
|
+
* coverage `f`, so there's nothing worth preserving. Preserves other sessions.
|
|
308
|
+
*/
|
|
309
|
+
export function markFullResync(server, sessionId) {
|
|
310
|
+
const data = load();
|
|
311
|
+
const forServer = data[server];
|
|
312
|
+
if (!forServer)
|
|
313
|
+
return;
|
|
314
|
+
if (!(sessionId in forServer))
|
|
315
|
+
return;
|
|
316
|
+
delete forServer[sessionId];
|
|
317
|
+
persist(data);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Classify one session's sync mode for this tick.
|
|
321
|
+
*
|
|
322
|
+
* @param row ledger row for this session (undefined = never synced)
|
|
323
|
+
* @param mtime current transcript mtime (ms, floored)
|
|
324
|
+
* @param fileSize current transcript byte size (0 when unknown / not append-only)
|
|
325
|
+
* @param extractorVersion current EXTRACTOR_VERSION
|
|
326
|
+
* @param isAppendOnly whether the backend's transcript is an append-only file
|
|
327
|
+
* (Claude/Gemini/Codex JSONL = true; OpenCode SQLite = false)
|
|
328
|
+
*/
|
|
329
|
+
export function syncMode(row, mtime, fileSize, extractorVersion, isAppendOnly) {
|
|
330
|
+
// Never synced → FULL.
|
|
331
|
+
if (!row)
|
|
332
|
+
return 'full';
|
|
333
|
+
// Extractor version bump → FULL (re-derive everything, even if mtime unchanged).
|
|
334
|
+
if (row.v < extractorVersion)
|
|
335
|
+
return 'full';
|
|
336
|
+
const offset = row.o ?? 0;
|
|
337
|
+
// Append-only file shrank below the last cursor (rotation/truncation/compaction)
|
|
338
|
+
// → the cursor is invalid; FULL re-sync.
|
|
339
|
+
if (isAppendOnly && offset > 0 && fileSize > 0 && fileSize < offset)
|
|
340
|
+
return 'full';
|
|
341
|
+
// Append-only file that grew since the last ship (size increased), with a
|
|
342
|
+
// valid prior offset → APPEND. Checked BEFORE skip because mtime can have
|
|
343
|
+
// coarse (second-level) granularity on some filesystems while size is the
|
|
344
|
+
// real growth signal — a sub-second append may not move mtime but does grow
|
|
345
|
+
// the file. (Shrink already handled above; size>=offset guaranteed here.)
|
|
346
|
+
//
|
|
347
|
+
// Safe now: the server enforces an OFFSET-CONTINUITY GUARD — an append only
|
|
348
|
+
// merges when the tail's base_offset equals the server's synced-through `o`,
|
|
349
|
+
// else it returns full_resync_needed and the client falls back to FULL. That
|
|
350
|
+
// catches the truncated/stale base that the original append blindly extended
|
|
351
|
+
// (a 1079-msg session once stored as 65). Default ON; CHAT_RECALL_TAIL_APPEND=0
|
|
352
|
+
// is the emergency off-switch. See docs/SYNC-INCREMENTAL.md.
|
|
353
|
+
if (process.env.CHAT_RECALL_TAIL_APPEND !== '0' &&
|
|
354
|
+
isAppendOnly && fileSize > 0 && offset > 0 && fileSize > (row.s ?? 0)) {
|
|
355
|
+
return 'append';
|
|
356
|
+
}
|
|
357
|
+
// mtime unchanged → the last sync (FULL or APPEND) covered this version of
|
|
358
|
+
// the file. SKIP. The byte cursor is NOT a skip prerequisite — a FULL sync
|
|
359
|
+
// that didn't record an offset still shipped the whole file at this mtime.
|
|
360
|
+
if (row.m >= Math.floor(mtime))
|
|
361
|
+
return 'skip';
|
|
362
|
+
// Everything else (mtime moved on a non-append-only backend, first tail sync
|
|
363
|
+
// with no prior offset, …) → FULL.
|
|
364
|
+
return 'full';
|
|
365
|
+
}
|
|
366
|
+
//# sourceMappingURL=sync-ledger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-ledger.js","sourceRoot":"","sources":["../src/sync-ledger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface UpdateCheck {
|
|
2
|
+
/** Newest version advertised by any server we sync to. */
|
|
3
|
+
serverVersion: string;
|
|
4
|
+
/** When we last asked (epoch ms). */
|
|
5
|
+
checkedAt: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The pending-update line, or null when current/unknown. Pure read of the
|
|
9
|
+
* cached probe — no I/O beyond one small file, safe on every command.
|
|
10
|
+
*/
|
|
11
|
+
export declare function updateNotice(): string | null;
|
|
12
|
+
/** Ask each logged-in server what it serves, and cache the newest. Best-effort. */
|
|
13
|
+
export declare function refreshUpdateCheck(force?: boolean): Promise<UpdateCheck | null>;
|
|
14
|
+
export declare function printUpdateNotice(): void;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=update-notice.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-notice.d.ts","sourceRoot":"","sources":["../src/update-notice.ts"],"names":[],"mappings":"AAsBA,UAAU,WAAW;IACnB,0DAA0D;IAC1D,aAAa,EAAE,MAAM,CAAC;IACtB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;CACnB;AAeD;;;GAGG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAO5C;AAED,mFAAmF;AACnF,wBAAsB,kBAAkB,CAAC,KAAK,UAAQ,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAwBnF;AASD,wBAAgB,iBAAiB,IAAI,IAAI,CASxC"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "You are running an old CLI" — the last-resort channel.
|
|
3
|
+
*
|
|
4
|
+
* Auto-update (auto-update.ts) handles the normal case, but it can only help a
|
|
5
|
+
* client that already HAS it: anything older simply never asks, and a machine
|
|
6
|
+
* whose `npm i -g` fails retries silently forever. Both cases end the same way —
|
|
7
|
+
* a box quietly runs a months-old collector and nobody finds out until someone
|
|
8
|
+
* notices missing data by hand.
|
|
9
|
+
*
|
|
10
|
+
* So: every CLI invocation prints ONE stderr line when the server it syncs to
|
|
11
|
+
* advertises a newer release. Reading a cached probe keeps that free — the
|
|
12
|
+
* actual capabilities call runs at most every 6h, detached, and never blocks or
|
|
13
|
+
* fails a command.
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { getDataDir } from '@chat-recall/engine/core/paths.js';
|
|
18
|
+
import { compareVersions } from './auto-update.js';
|
|
19
|
+
import { fetchWithTimeout, cliVersion } from './http.js';
|
|
20
|
+
const CHECK_TTL_MS = 6 * 3600 * 1000;
|
|
21
|
+
const checkPath = () => join(getDataDir(), 'update-check.json');
|
|
22
|
+
function readCheck() {
|
|
23
|
+
try {
|
|
24
|
+
const c = JSON.parse(readFileSync(checkPath(), 'utf-8'));
|
|
25
|
+
return c && typeof c.serverVersion === 'string' ? c : null;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function writeCheck(c) {
|
|
32
|
+
try {
|
|
33
|
+
writeFileSync(checkPath(), JSON.stringify(c));
|
|
34
|
+
}
|
|
35
|
+
catch { /* cache is a nicety */ }
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The pending-update line, or null when current/unknown. Pure read of the
|
|
39
|
+
* cached probe — no I/O beyond one small file, safe on every command.
|
|
40
|
+
*/
|
|
41
|
+
export function updateNotice() {
|
|
42
|
+
const c = readCheck();
|
|
43
|
+
if (!c)
|
|
44
|
+
return null;
|
|
45
|
+
const own = cliVersion();
|
|
46
|
+
if (compareVersions(c.serverVersion, own) <= 0)
|
|
47
|
+
return null;
|
|
48
|
+
return `chat-recall ${c.serverVersion} is available (you have ${own}). `
|
|
49
|
+
+ `Auto-update should apply it; force it now with: chat-recall update`;
|
|
50
|
+
}
|
|
51
|
+
/** Ask each logged-in server what it serves, and cache the newest. Best-effort. */
|
|
52
|
+
export async function refreshUpdateCheck(force = false) {
|
|
53
|
+
const prev = readCheck();
|
|
54
|
+
if (!force && prev && Date.now() - prev.checkedAt < CHECK_TTL_MS)
|
|
55
|
+
return prev;
|
|
56
|
+
let newest = '0.0.0';
|
|
57
|
+
try {
|
|
58
|
+
const { loadAllCredentials } = await import('./sync-client.js');
|
|
59
|
+
for (const cred of loadAllCredentials()) {
|
|
60
|
+
if (!cred.serverUrl)
|
|
61
|
+
continue;
|
|
62
|
+
const base = cred.serverUrl.replace(/\/+$/, '');
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetchWithTimeout(`${base}/api/capabilities`, {
|
|
65
|
+
headers: cred.token ? { authorization: `Bearer ${cred.token}` } : {},
|
|
66
|
+
}, 5_000);
|
|
67
|
+
if (!res.ok)
|
|
68
|
+
continue;
|
|
69
|
+
const caps = (await res.json());
|
|
70
|
+
const v = caps?.cli?.version;
|
|
71
|
+
if (v && compareVersions(v, newest) > 0)
|
|
72
|
+
newest = v;
|
|
73
|
+
}
|
|
74
|
+
catch { /* one unreachable server must not poison the check */ }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return prev;
|
|
79
|
+
}
|
|
80
|
+
if (newest === '0.0.0')
|
|
81
|
+
return prev;
|
|
82
|
+
const next = { serverVersion: newest, checkedAt: Date.now() };
|
|
83
|
+
writeCheck(next);
|
|
84
|
+
return next;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Print the notice (once per process) and kick a detached refresh.
|
|
88
|
+
*
|
|
89
|
+
* stderr, never stdout: piping `chat-recall search … | jq` must keep working.
|
|
90
|
+
* The refresh is unref'd so it can never hold a short command open.
|
|
91
|
+
*/
|
|
92
|
+
let printed = false;
|
|
93
|
+
export function printUpdateNotice() {
|
|
94
|
+
if (printed)
|
|
95
|
+
return;
|
|
96
|
+
printed = true;
|
|
97
|
+
try {
|
|
98
|
+
const msg = updateNotice();
|
|
99
|
+
if (msg)
|
|
100
|
+
console.error(`\n ⚠ ${msg}\n`);
|
|
101
|
+
const t = setTimeout(() => { void refreshUpdateCheck().catch(() => { }); }, 0);
|
|
102
|
+
if (typeof t.unref === 'function')
|
|
103
|
+
t.unref();
|
|
104
|
+
}
|
|
105
|
+
catch { /* a nudge must never break a command */ }
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=update-notice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-notice.js","sourceRoot":"","sources":["../src/update-notice.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chat-recall verify --deep` — does the server actually hold what this machine
|
|
3
|
+
* has, per session?
|
|
4
|
+
*
|
|
5
|
+
* Every silent-loss incident in this codebase was found by a human asking about
|
|
6
|
+
* one specific session. Nothing reported them, because every symptom looks like
|
|
7
|
+
* healthy operation: `15604 skipped` is what a fully-synced corpus prints too.
|
|
8
|
+
* This is the check that turns "did session X make it?" from an investigation
|
|
9
|
+
* into a command.
|
|
10
|
+
*
|
|
11
|
+
* It compares in the unit the server's shrink guard uses — `gzipContainer().size`
|
|
12
|
+
* of the local union versus the archive's stored `size` — so a difference means
|
|
13
|
+
* the server is genuinely missing records, not that two measurements disagree.
|
|
14
|
+
* Sub-unit noise is ignored via a tolerance.
|
|
15
|
+
*
|
|
16
|
+
* Both sides must be the REDACTED container (see Deps.localContainerSize): the
|
|
17
|
+
* archive stores what was shipped, and what was shipped had its secrets
|
|
18
|
+
* replaced by sentinels.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately NOT an mtime comparison: the server stores the client-sent mtime,
|
|
21
|
+
* which drifts sub-second against the file for essentially every session. An
|
|
22
|
+
* mtime-based version of this check reported 9,975 of 10,032 sessions suspect
|
|
23
|
+
* when the true number was 13.
|
|
24
|
+
*
|
|
25
|
+
* Classification matters as much as detection:
|
|
26
|
+
* pending — the ledger knows there is more to send; it will go on its own
|
|
27
|
+
* STRANDED — the ledger claims complete while the server holds less, so
|
|
28
|
+
* nothing will ever re-ship it. This is the actionable class.
|
|
29
|
+
*/
|
|
30
|
+
export interface VerifyFinding {
|
|
31
|
+
sessionId: string;
|
|
32
|
+
projectPath: string;
|
|
33
|
+
localSize: number;
|
|
34
|
+
serverSize: number;
|
|
35
|
+
deficit: number;
|
|
36
|
+
/** Ledger claims complete ⇒ nothing will re-ship it without intervention. */
|
|
37
|
+
stranded: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface VerifyReport {
|
|
40
|
+
server: string;
|
|
41
|
+
checked: number;
|
|
42
|
+
complete: number;
|
|
43
|
+
missingArchive: number;
|
|
44
|
+
pending: VerifyFinding[];
|
|
45
|
+
stranded: VerifyFinding[];
|
|
46
|
+
}
|
|
47
|
+
interface Deps {
|
|
48
|
+
listSessions(sinceMs: number): Array<{
|
|
49
|
+
rawId: string;
|
|
50
|
+
prefixedId: string;
|
|
51
|
+
projectPath: string;
|
|
52
|
+
mtime: number;
|
|
53
|
+
}>;
|
|
54
|
+
/**
|
|
55
|
+
* Size of the container as it WOULD BE SHIPPED — i.e. REDACTED, then gzipped.
|
|
56
|
+
*
|
|
57
|
+
* This is a hard requirement, not a preference. The sync path redacts before
|
|
58
|
+
* archiving, so measuring the raw container compares two different artifacts:
|
|
59
|
+
* a long secret becomes a short sentinel and the local side reads bigger
|
|
60
|
+
* forever. Two sessions were reported stranded through a full repair+sync
|
|
61
|
+
* cycle for exactly this reason — the deficit was redaction, not loss, and no
|
|
62
|
+
* amount of re-shipping could ever close it.
|
|
63
|
+
*/
|
|
64
|
+
localContainerSize(rawId: string): number | null;
|
|
65
|
+
fileSize(rawId: string): number;
|
|
66
|
+
serverSizes(server: string, token: string): Promise<Map<string, number>>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Pure comparison, injectable so it can be tested without a server or a real
|
|
70
|
+
* transcript corpus.
|
|
71
|
+
*/
|
|
72
|
+
export declare function verifyAgainstServer(server: string, token: string, sinceMs: number, deps: Deps, now?: number): Promise<VerifyReport>;
|
|
73
|
+
/** Every configured target, so a multi-server setup is verified as a whole. */
|
|
74
|
+
export declare function verifyTargets(): Array<{
|
|
75
|
+
serverUrl: string;
|
|
76
|
+
token: string;
|
|
77
|
+
}>;
|
|
78
|
+
export {};
|
|
79
|
+
//# sourceMappingURL=verify-deep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify-deep.d.ts","sourceRoot":"","sources":["../src/verify-deep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAKH,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAmBD,UAAU,IAAI;IACZ,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChH;;;;;;;;;OASG;IACH,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAC1E;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,IAAI,EACV,GAAG,GAAE,MAAmB,GACvB,OAAO,CAAC,YAAY,CAAC,CAyCvB;AAED,+EAA+E;AAC/E,wBAAgB,aAAa,IAAI,KAAK,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAE3E"}
|