chat-recall 0.5.30 → 0.5.32
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/auto-update.d.ts +0 -4
- package/dist/auto-update.d.ts.map +1 -1
- package/dist/auto-update.js +20 -1
- package/dist/auto-update.js.map +1 -1
- package/dist/bulk-delete.d.ts +131 -0
- package/dist/bulk-delete.d.ts.map +1 -0
- package/dist/bulk-delete.js +211 -0
- package/dist/bulk-delete.js.map +1 -0
- package/dist/cli.js +765 -467
- package/dist/cli.js.map +1 -1
- package/dist/mcp.js +273 -221
- package/dist/scan-worker.js +61 -35
- package/dist/sync-client.d.ts +37 -0
- package/dist/sync-client.d.ts.map +1 -1
- package/dist/sync-client.js +109 -16
- package/dist/sync-client.js.map +1 -1
- package/dist/target-breaker.d.ts +13 -0
- package/dist/target-breaker.d.ts.map +1 -1
- package/dist/target-breaker.js +6 -4
- package/dist/target-breaker.js.map +1 -1
- package/dist/telemetry.d.ts +14 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +23 -0
- package/dist/telemetry.js.map +1 -1
- package/dist/watch.js +276 -225
- package/package.json +1 -1
package/dist/scan-worker.js
CHANGED
|
@@ -7881,19 +7881,6 @@ var init_codex = __esm({
|
|
|
7881
7881
|
// ../engine/dist/core/backends/agy.js
|
|
7882
7882
|
import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync4, statSync as statSync6 } from "fs";
|
|
7883
7883
|
import { basename as basename6, dirname as dirname8, join as join8 } from "path";
|
|
7884
|
-
function getFallbackProjectPath(homeDir) {
|
|
7885
|
-
try {
|
|
7886
|
-
const settingsPath2 = join8(homeDir, "settings.json");
|
|
7887
|
-
if (existsSync7(settingsPath2)) {
|
|
7888
|
-
const json = JSON.parse(readFileSync4(settingsPath2, "utf-8"));
|
|
7889
|
-
if (Array.isArray(json?.trustedWorkspaces) && json.trustedWorkspaces[0]) {
|
|
7890
|
-
return json.trustedWorkspaces[0];
|
|
7891
|
-
}
|
|
7892
|
-
}
|
|
7893
|
-
} catch {
|
|
7894
|
-
}
|
|
7895
|
-
return "";
|
|
7896
|
-
}
|
|
7897
7884
|
function agyTranscriptPath(logsDir) {
|
|
7898
7885
|
const full = join8(logsDir, "transcript_full.jsonl");
|
|
7899
7886
|
if (existsSync7(full))
|
|
@@ -7906,8 +7893,8 @@ function agyTranscriptPath(logsDir) {
|
|
|
7906
7893
|
function resolveAgyProject(logsDir, homeDir) {
|
|
7907
7894
|
const deriveFrom = agyTranscriptPath(logsDir);
|
|
7908
7895
|
if (!deriveFrom)
|
|
7909
|
-
return
|
|
7910
|
-
return extractProjectPathFromTranscript(deriveFrom, homeDir)
|
|
7896
|
+
return "";
|
|
7897
|
+
return extractProjectPathFromTranscript(deriveFrom, homeDir);
|
|
7911
7898
|
}
|
|
7912
7899
|
function extractProjectPathFromTranscript(path, homeDir) {
|
|
7913
7900
|
let content;
|
|
@@ -9874,6 +9861,7 @@ var init_first_prompt = __esm({
|
|
|
9874
9861
|
});
|
|
9875
9862
|
|
|
9876
9863
|
// ../engine/dist/core/project-dir-name.js
|
|
9864
|
+
import { readdirSync as readdirSync8 } from "node:fs";
|
|
9877
9865
|
function looksWindowsEncoded(dirName) {
|
|
9878
9866
|
return /^[A-Za-z]--/.test(dirName);
|
|
9879
9867
|
}
|
|
@@ -9887,6 +9875,43 @@ function decodeProjectDirName(dirName) {
|
|
|
9887
9875
|
}
|
|
9888
9876
|
return "/" + dirName.replace(/^-/, "").replace(/-/g, "/");
|
|
9889
9877
|
}
|
|
9878
|
+
function resolveProjectDirName(dirName, deps = {}) {
|
|
9879
|
+
const structural = decodeProjectDirName(dirName);
|
|
9880
|
+
if (!dirName)
|
|
9881
|
+
return structural;
|
|
9882
|
+
if (looksWindowsEncoded(dirName))
|
|
9883
|
+
return structural;
|
|
9884
|
+
const readdir = deps.readdir ?? ((p) => readdirSync8(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name));
|
|
9885
|
+
const encodeName = (name) => name.replace(/[-_.]/g, "-");
|
|
9886
|
+
let remaining = dirName.replace(/^-/, "");
|
|
9887
|
+
let current = "/";
|
|
9888
|
+
while (remaining) {
|
|
9889
|
+
let children;
|
|
9890
|
+
try {
|
|
9891
|
+
children = readdir(current);
|
|
9892
|
+
} catch {
|
|
9893
|
+
break;
|
|
9894
|
+
}
|
|
9895
|
+
let best = "";
|
|
9896
|
+
for (const child of children) {
|
|
9897
|
+
const encoded = encodeName(child);
|
|
9898
|
+
if (!encoded || encoded.length < best.length)
|
|
9899
|
+
continue;
|
|
9900
|
+
if (!remaining.startsWith(encoded))
|
|
9901
|
+
continue;
|
|
9902
|
+
const next = remaining[encoded.length];
|
|
9903
|
+
if (next !== void 0 && next !== "-")
|
|
9904
|
+
continue;
|
|
9905
|
+
if (encoded.length > best.length)
|
|
9906
|
+
best = child;
|
|
9907
|
+
}
|
|
9908
|
+
if (!best)
|
|
9909
|
+
break;
|
|
9910
|
+
current = current === "/" ? `/${best}` : `${current}/${best}`;
|
|
9911
|
+
remaining = remaining.slice(encodeName(best).length).replace(/^-/, "");
|
|
9912
|
+
}
|
|
9913
|
+
return remaining ? structural : current;
|
|
9914
|
+
}
|
|
9890
9915
|
var init_project_dir_name = __esm({
|
|
9891
9916
|
"../engine/dist/core/project-dir-name.js"() {
|
|
9892
9917
|
"use strict";
|
|
@@ -9894,7 +9919,7 @@ var init_project_dir_name = __esm({
|
|
|
9894
9919
|
});
|
|
9895
9920
|
|
|
9896
9921
|
// ../engine/dist/core/backends/claude.js
|
|
9897
|
-
import { existsSync as existsSync12, readdirSync as
|
|
9922
|
+
import { existsSync as existsSync12, readdirSync as readdirSync9, readFileSync as readFileSync8, statSync as statSync10, openSync as openSync4, readSync as readSync4, closeSync as closeSync4 } from "fs";
|
|
9898
9923
|
import { join as join12, basename as basename7 } from "path";
|
|
9899
9924
|
function stripReminders(text) {
|
|
9900
9925
|
return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
|
|
@@ -10049,13 +10074,13 @@ var init_claude = __esm({
|
|
|
10049
10074
|
collectSessionsFromRoot(root, cutoff, filter, seen, out) {
|
|
10050
10075
|
if (!existsSync12(root))
|
|
10051
10076
|
return;
|
|
10052
|
-
for (const proj of
|
|
10077
|
+
for (const proj of readdirSync9(root, { withFileTypes: true })) {
|
|
10053
10078
|
if (!proj.isDirectory())
|
|
10054
10079
|
continue;
|
|
10055
10080
|
if (filter && !proj.name.toLowerCase().includes(filter))
|
|
10056
10081
|
continue;
|
|
10057
10082
|
const projPath = join12(root, proj.name);
|
|
10058
|
-
const projectPath =
|
|
10083
|
+
const projectPath = resolveProjectDirName(proj.name);
|
|
10059
10084
|
const indexPath = join12(projPath, "sessions-index.json");
|
|
10060
10085
|
if (existsSync12(indexPath)) {
|
|
10061
10086
|
try {
|
|
@@ -10086,7 +10111,7 @@ var init_claude = __esm({
|
|
|
10086
10111
|
}
|
|
10087
10112
|
let files;
|
|
10088
10113
|
try {
|
|
10089
|
-
files =
|
|
10114
|
+
files = readdirSync9(projPath);
|
|
10090
10115
|
} catch {
|
|
10091
10116
|
continue;
|
|
10092
10117
|
}
|
|
@@ -10355,7 +10380,7 @@ var init_claude = __esm({
|
|
|
10355
10380
|
for (const root of claudeProjectDirs()) {
|
|
10356
10381
|
if (!existsSync12(root))
|
|
10357
10382
|
continue;
|
|
10358
|
-
for (const projEntry of
|
|
10383
|
+
for (const projEntry of readdirSync9(root, { withFileTypes: true })) {
|
|
10359
10384
|
if (!projEntry.isDirectory())
|
|
10360
10385
|
continue;
|
|
10361
10386
|
if (opts.projectFilter && !projEntry.name.toLowerCase().includes(opts.projectFilter.toLowerCase()))
|
|
@@ -10363,7 +10388,7 @@ var init_claude = __esm({
|
|
|
10363
10388
|
const projPath = join12(root, projEntry.name);
|
|
10364
10389
|
let files;
|
|
10365
10390
|
try {
|
|
10366
|
-
files =
|
|
10391
|
+
files = readdirSync9(projPath);
|
|
10367
10392
|
} catch {
|
|
10368
10393
|
continue;
|
|
10369
10394
|
}
|
|
@@ -10376,7 +10401,7 @@ var init_claude = __esm({
|
|
|
10376
10401
|
let mtime = st.mtimeMs;
|
|
10377
10402
|
const subDir = join12(projPath, f.slice(0, -6), "subagents");
|
|
10378
10403
|
if (existsSync12(subDir)) {
|
|
10379
|
-
for (const sub of
|
|
10404
|
+
for (const sub of readdirSync9(subDir)) {
|
|
10380
10405
|
try {
|
|
10381
10406
|
const subSt = statSync10(join12(subDir, sub));
|
|
10382
10407
|
if (subSt.mtimeMs > mtime)
|
|
@@ -10487,7 +10512,7 @@ var init_model_pricing = __esm({
|
|
|
10487
10512
|
});
|
|
10488
10513
|
|
|
10489
10514
|
// ../engine/dist/parsers/session.js
|
|
10490
|
-
import { createReadStream as createReadStream2, existsSync as existsSync13, readdirSync as
|
|
10515
|
+
import { createReadStream as createReadStream2, existsSync as existsSync13, readdirSync as readdirSync10, readFileSync as readFileSync9, statSync as statSync11, openSync as openSync5, readSync as readSync5, closeSync as closeSync5 } from "fs";
|
|
10491
10516
|
import { join as join13, basename as basename8 } from "path";
|
|
10492
10517
|
function hasSubagentsDir(sessionPath) {
|
|
10493
10518
|
const dir = sessionPath.slice(0, -6);
|
|
@@ -10499,6 +10524,7 @@ var init_session = __esm({
|
|
|
10499
10524
|
init_chunker();
|
|
10500
10525
|
init_claude();
|
|
10501
10526
|
init_tool_paths();
|
|
10527
|
+
init_project_dir_name();
|
|
10502
10528
|
init_model_pricing();
|
|
10503
10529
|
}
|
|
10504
10530
|
});
|
|
@@ -10612,7 +10638,7 @@ var init_shadow = __esm({
|
|
|
10612
10638
|
});
|
|
10613
10639
|
|
|
10614
10640
|
// ../engine/dist/core/live-session-scan.js
|
|
10615
|
-
import { existsSync as existsSync14, readdirSync as
|
|
10641
|
+
import { existsSync as existsSync14, readdirSync as readdirSync11, readFileSync as readFileSync10, statSync as statSync12, openSync as openSync6, readSync as readSync6, closeSync as closeSync6 } from "fs";
|
|
10616
10642
|
import { join as join14, basename as basename9 } from "path";
|
|
10617
10643
|
import { createHash as createHash3 } from "crypto";
|
|
10618
10644
|
function lazyGeminiProjectsJson() {
|
|
@@ -10632,7 +10658,7 @@ function sessionFileIndex() {
|
|
|
10632
10658
|
seenRoots.add(root);
|
|
10633
10659
|
let entries;
|
|
10634
10660
|
try {
|
|
10635
|
-
entries =
|
|
10661
|
+
entries = readdirSync11(root, { withFileTypes: true });
|
|
10636
10662
|
} catch {
|
|
10637
10663
|
continue;
|
|
10638
10664
|
}
|
|
@@ -10642,7 +10668,7 @@ function sessionFileIndex() {
|
|
|
10642
10668
|
const dir = join14(root, entry.name);
|
|
10643
10669
|
let names;
|
|
10644
10670
|
try {
|
|
10645
|
-
names =
|
|
10671
|
+
names = readdirSync11(dir);
|
|
10646
10672
|
} catch {
|
|
10647
10673
|
continue;
|
|
10648
10674
|
}
|
|
@@ -10653,7 +10679,7 @@ function sessionFileIndex() {
|
|
|
10653
10679
|
const located = {
|
|
10654
10680
|
path: join14(dir, name),
|
|
10655
10681
|
projectDir: entry.name,
|
|
10656
|
-
projectPath:
|
|
10682
|
+
projectPath: resolveProjectDirName(entry.name)
|
|
10657
10683
|
};
|
|
10658
10684
|
const prior = byId.get(id);
|
|
10659
10685
|
if (prior)
|
|
@@ -10740,7 +10766,7 @@ function resolveSessionContentPaths(sessionFile) {
|
|
|
10740
10766
|
const subDir = join14(sessionFile.slice(0, -6), "subagents");
|
|
10741
10767
|
if (!existsSync14(subDir))
|
|
10742
10768
|
return [sessionFile];
|
|
10743
|
-
const subPaths =
|
|
10769
|
+
const subPaths = readdirSync11(subDir).filter((f) => f.endsWith(".jsonl")).sort().map((f) => join14(subDir, f));
|
|
10744
10770
|
return [sessionFile, ...subPaths];
|
|
10745
10771
|
}
|
|
10746
10772
|
function loadGeminiProjectMap() {
|
|
@@ -10785,7 +10811,7 @@ function geminiIndex() {
|
|
|
10785
10811
|
continue;
|
|
10786
10812
|
let projs;
|
|
10787
10813
|
try {
|
|
10788
|
-
projs =
|
|
10814
|
+
projs = readdirSync11(tmpRoot, { withFileTypes: true });
|
|
10789
10815
|
} catch {
|
|
10790
10816
|
continue;
|
|
10791
10817
|
}
|
|
@@ -10797,7 +10823,7 @@ function geminiIndex() {
|
|
|
10797
10823
|
continue;
|
|
10798
10824
|
let files;
|
|
10799
10825
|
try {
|
|
10800
|
-
files =
|
|
10826
|
+
files = readdirSync11(chats);
|
|
10801
10827
|
} catch {
|
|
10802
10828
|
continue;
|
|
10803
10829
|
}
|
|
@@ -10916,21 +10942,21 @@ function findCodexSessionFile(sessionId) {
|
|
|
10916
10942
|
function findCodexSessionFileIn(root, sessionId) {
|
|
10917
10943
|
if (!existsSync14(root))
|
|
10918
10944
|
return null;
|
|
10919
|
-
for (const year of
|
|
10945
|
+
for (const year of readdirSync11(root, { withFileTypes: true })) {
|
|
10920
10946
|
if (!year.isDirectory())
|
|
10921
10947
|
continue;
|
|
10922
10948
|
const yearPath = join14(root, year.name);
|
|
10923
|
-
for (const month of
|
|
10949
|
+
for (const month of readdirSync11(yearPath, { withFileTypes: true })) {
|
|
10924
10950
|
if (!month.isDirectory())
|
|
10925
10951
|
continue;
|
|
10926
10952
|
const monthPath = join14(yearPath, month.name);
|
|
10927
|
-
for (const day of
|
|
10953
|
+
for (const day of readdirSync11(monthPath, { withFileTypes: true })) {
|
|
10928
10954
|
if (!day.isDirectory())
|
|
10929
10955
|
continue;
|
|
10930
10956
|
const dayPath = join14(monthPath, day.name);
|
|
10931
10957
|
let files;
|
|
10932
10958
|
try {
|
|
10933
|
-
files =
|
|
10959
|
+
files = readdirSync11(dayPath);
|
|
10934
10960
|
} catch {
|
|
10935
10961
|
continue;
|
|
10936
10962
|
}
|
|
@@ -10945,7 +10971,7 @@ function findCodexSessionFileIn(root, sessionId) {
|
|
|
10945
10971
|
}
|
|
10946
10972
|
}
|
|
10947
10973
|
function scanDir(dir) {
|
|
10948
|
-
for (const entry of
|
|
10974
|
+
for (const entry of readdirSync11(dir, { withFileTypes: true })) {
|
|
10949
10975
|
if (entry.isDirectory()) {
|
|
10950
10976
|
const found = scanDir(join14(dir, entry.name));
|
|
10951
10977
|
if (found)
|
package/dist/sync-client.d.ts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { type ScanFinding, type DirScanFinding } from '@chat-recall/engine/core/secret-scanner.js';
|
|
2
2
|
import { type SessionRef } from '@chat-recall/engine/core/tool-backend.js';
|
|
3
|
+
/** An Error that already knows its own failure class. */
|
|
4
|
+
export declare function classedError(errorClass: FailureClass, message: string): Error;
|
|
5
|
+
/** True when this error must not be retried. Exported for the tests. */
|
|
6
|
+
export declare function isStandingFailure(err: unknown): boolean;
|
|
7
|
+
export type FailureClass = 'rate_limited' | 'payment_required' | 'auth' | 'timeout' | 'refused' | 'dns' | 'network' | 'insecure_transport' | 'server_error' | 'other';
|
|
8
|
+
/**
|
|
9
|
+
* Reduce an error message to a bounded CLASS.
|
|
10
|
+
*
|
|
11
|
+
* Telemetry must never carry a raw error string: server errors quote paths,
|
|
12
|
+
* payload fragments and occasionally tokens. An enum is enough to answer "what
|
|
13
|
+
* is breaking for customers", and cannot leak.
|
|
14
|
+
*
|
|
15
|
+
* A tag from the throw site always wins. The message tests remain for errors
|
|
16
|
+
* thrown by fetch itself, and by code that predates the tag.
|
|
17
|
+
*/
|
|
18
|
+
export declare function classifyError(msg: string, err?: unknown): FailureClass;
|
|
3
19
|
import '@chat-recall/engine/core/backends/index.js';
|
|
4
20
|
export interface Credentials {
|
|
5
21
|
serverUrl: string;
|
|
@@ -44,6 +60,27 @@ export interface SyncResult {
|
|
|
44
60
|
scanned: number;
|
|
45
61
|
/** Total wall-clock ms spent scanning this run. */
|
|
46
62
|
scanMs: number;
|
|
63
|
+
/**
|
|
64
|
+
* THIS WALK'S OUTCOME PER TARGET, keyed by server URL.
|
|
65
|
+
*
|
|
66
|
+
* The aggregate above cannot answer "did target X accept anything", and the
|
|
67
|
+
* daemon needs exactly that: it writes the per-target health file that
|
|
68
|
+
* `chat-recall doctor` reads. Without this it marked EVERY target with the
|
|
69
|
+
* walk's aggregate verdict, so a walk where the SaaS succeeded and a LAN box
|
|
70
|
+
* refused the connection wrote `lastOkAt = now, failures = 0` for both — and
|
|
71
|
+
* `doctor` cheerfully reported a target that had not accepted a byte in days.
|
|
72
|
+
*
|
|
73
|
+
* Set by `syncSessions` (the multi-target walk) and absent from a
|
|
74
|
+
* single-target result, so a consumer must treat "absent" as "unknown" and
|
|
75
|
+
* mark nothing, never as "all fine".
|
|
76
|
+
*/
|
|
77
|
+
perTarget?: Record<string, TargetOutcome>;
|
|
78
|
+
}
|
|
79
|
+
/** One target's outcome for one walk. */
|
|
80
|
+
export interface TargetOutcome {
|
|
81
|
+
ok: boolean;
|
|
82
|
+
/** Why it failed, or why it was not attempted. Absent when ok. */
|
|
83
|
+
error?: string;
|
|
47
84
|
}
|
|
48
85
|
export declare function syncSessions(opts?: {
|
|
49
86
|
sinceMs?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-client.d.ts","sourceRoot":"","sources":["../src/sync-client.ts"],"names":[],"mappings":"AAyCA,OAAO,EAAoF,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,4CAA4C,CAAC;AAWrL,OAAO,EAA+B,KAAK,UAAU,EAAE,MAAM,0CAA0C,CAAC;
|
|
1
|
+
{"version":3,"file":"sync-client.d.ts","sourceRoot":"","sources":["../src/sync-client.ts"],"names":[],"mappings":"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"}
|
package/dist/sync-client.js
CHANGED
|
@@ -81,7 +81,7 @@ import { scanPool } from './scan-pool.js';
|
|
|
81
81
|
import { isPrivateHost, transportRisk, assertTransportSafe } from './transport-safety.js';
|
|
82
82
|
import { breakerState, breakerNotice, noteTargetSuccess, noteTargetFailure } from './target-breaker.js';
|
|
83
83
|
import { setTelemetryEligible } from './telemetry-consent.js';
|
|
84
|
-
import { record, flush } from './telemetry.js';
|
|
84
|
+
import { record, flush, stableMark } from './telemetry.js';
|
|
85
85
|
/**
|
|
86
86
|
* The server rule pack as it arrived, kept for the scan workers.
|
|
87
87
|
*
|
|
@@ -96,14 +96,67 @@ import { record, flush } from './telemetry.js';
|
|
|
96
96
|
* poster) are far apart, and threading it through every batcher signature to
|
|
97
97
|
* deliver a cosmetic field would be worse than a single slot.
|
|
98
98
|
*/
|
|
99
|
+
/**
|
|
100
|
+
* The failure class, carried ON the error, decided where the status is known.
|
|
101
|
+
*
|
|
102
|
+
* ── Why a tag and not a message test ─────────────────────────────────────
|
|
103
|
+
* `classifyError` below used to be the only classifier, and it read the message.
|
|
104
|
+
* That is a guess, and it guessed wrong for the most common failure this product
|
|
105
|
+
* has. The 402 handler deliberately prints the server's own sentence instead of
|
|
106
|
+
* the HTTP line, so `402` is NOT in the message, while the `upgradeUrl` it
|
|
107
|
+
* appends IS — and a substring test for `https` therefore filed a monthly quota
|
|
108
|
+
* pause as `insecure_transport`.
|
|
109
|
+
*
|
|
110
|
+
* One device reported 651 of them in a week. The fleet panel read that back as
|
|
111
|
+
* "refusing to sync to a server over plain HTTP — nothing from this device is
|
|
112
|
+
* reaching it", which was false twice over: the target was a LAN box on http://
|
|
113
|
+
* that the transport gate had never objected to, and the real condition — sync
|
|
114
|
+
* paused for billing — had no warning of its own at all.
|
|
115
|
+
*
|
|
116
|
+
* The same rewritten message also defeated the retry gate below, which decides
|
|
117
|
+
* "is this fatal" with /HTTP 4\d\d/. A quota pause was retried four times with
|
|
118
|
+
* backoff before failing, every batch, for days.
|
|
119
|
+
*
|
|
120
|
+
* So the class travels with the error from the place that read the status.
|
|
121
|
+
*/
|
|
122
|
+
const ERROR_CLASS = '__chatRecallErrorClass';
|
|
123
|
+
/** An Error that already knows its own failure class. */
|
|
124
|
+
export function classedError(errorClass, message) {
|
|
125
|
+
const e = new Error(message);
|
|
126
|
+
Object.defineProperty(e, ERROR_CLASS, { value: errorClass, enumerable: false });
|
|
127
|
+
return e;
|
|
128
|
+
}
|
|
129
|
+
/** The class a throw site attached, or null when it did not. */
|
|
130
|
+
function taggedClass(err) {
|
|
131
|
+
const v = err?.[ERROR_CLASS];
|
|
132
|
+
return typeof v === 'string' && v ? v : null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Classes that describe a STANDING condition of the plan or the target, not a
|
|
136
|
+
* glitch. Retrying one cannot change the answer, so the retry loop stops.
|
|
137
|
+
*/
|
|
138
|
+
const STANDING_FAILURE = new Set([
|
|
139
|
+
'auth', 'payment_required', 'insecure_transport',
|
|
140
|
+
]);
|
|
141
|
+
/** True when this error must not be retried. Exported for the tests. */
|
|
142
|
+
export function isStandingFailure(err) {
|
|
143
|
+
const c = taggedClass(err);
|
|
144
|
+
return c !== null && STANDING_FAILURE.has(c);
|
|
145
|
+
}
|
|
99
146
|
/**
|
|
100
147
|
* Reduce an error message to a bounded CLASS.
|
|
101
148
|
*
|
|
102
149
|
* Telemetry must never carry a raw error string: server errors quote paths,
|
|
103
150
|
* payload fragments and occasionally tokens. An enum is enough to answer "what
|
|
104
151
|
* is breaking for customers", and cannot leak.
|
|
152
|
+
*
|
|
153
|
+
* A tag from the throw site always wins. The message tests remain for errors
|
|
154
|
+
* thrown by fetch itself, and by code that predates the tag.
|
|
105
155
|
*/
|
|
106
|
-
function classifyError(msg) {
|
|
156
|
+
export function classifyError(msg, err) {
|
|
157
|
+
const tagged = taggedClass(err);
|
|
158
|
+
if (tagged)
|
|
159
|
+
return tagged;
|
|
107
160
|
const m = msg.toLowerCase();
|
|
108
161
|
if (m.includes('429'))
|
|
109
162
|
return 'rate_limited';
|
|
@@ -111,7 +164,11 @@ function classifyError(msg) {
|
|
|
111
164
|
return 'payment_required';
|
|
112
165
|
if (m.includes('401') || m.includes('403') || m.includes('token'))
|
|
113
166
|
return 'auth';
|
|
114
|
-
|
|
167
|
+
// 'aborted due to timeout' is what AbortSignal.timeout actually produces, and
|
|
168
|
+
// it matches neither of the other two spellings — so the most common timeout
|
|
169
|
+
// this collector suffers was being filed as 'other'. One machine logged 441 of
|
|
170
|
+
// them against /api/capabilities alone.
|
|
171
|
+
if (m.includes('timed out') || m.includes('etimedout') || m.includes('aborted due to timeout'))
|
|
115
172
|
return 'timeout';
|
|
116
173
|
if (m.includes('econnrefused'))
|
|
117
174
|
return 'refused';
|
|
@@ -119,9 +176,12 @@ function classifyError(msg) {
|
|
|
119
176
|
return 'dns';
|
|
120
177
|
if (m.includes('fetch failed') || m.includes('econnreset'))
|
|
121
178
|
return 'network';
|
|
122
|
-
|
|
179
|
+
// The transport gate's OWN wording, and nothing else. A bare `https` test
|
|
180
|
+
// matched every message that merely quoted a URL — which is most of them,
|
|
181
|
+
// including every 5xx that used to fall through to the line below.
|
|
182
|
+
if (m.includes('over plain http') || m.includes('unsupported scheme'))
|
|
123
183
|
return 'insecure_transport';
|
|
124
|
-
if (
|
|
184
|
+
if (/\b5\d\d\b/.test(m))
|
|
125
185
|
return 'server_error';
|
|
126
186
|
return 'other';
|
|
127
187
|
}
|
|
@@ -474,6 +534,8 @@ export async function syncSessions(opts = {}) {
|
|
|
474
534
|
let agg = null;
|
|
475
535
|
const errors = [];
|
|
476
536
|
const skippedTargets = [];
|
|
537
|
+
// Per target, for the health file the daemon writes. See SyncResult.perTarget.
|
|
538
|
+
const perTarget = {};
|
|
477
539
|
for (const cred of targets) {
|
|
478
540
|
// CIRCUIT BREAKER. Targets are walked in SEQUENCE, so a dead one does not
|
|
479
541
|
// just fail — it delays every ship to the healthy targets behind it, and
|
|
@@ -486,11 +548,18 @@ export async function syncSessions(opts = {}) {
|
|
|
486
548
|
if (notice)
|
|
487
549
|
console.error(`[sync] ${notice}`);
|
|
488
550
|
skippedTargets.push(cred.serverUrl);
|
|
551
|
+
// A skipped target did NOT sync. Saying so is the whole point: it is the
|
|
552
|
+
// case that used to be recorded as a success.
|
|
553
|
+
perTarget[cred.serverUrl] = {
|
|
554
|
+
ok: false,
|
|
555
|
+
error: notice ?? `in failure cooldown for ~${Math.max(1, Math.round(verdict.retryInMs / 60_000))}m`,
|
|
556
|
+
};
|
|
489
557
|
continue;
|
|
490
558
|
}
|
|
491
559
|
try {
|
|
492
560
|
const r = await syncToTarget(cred, opts);
|
|
493
561
|
noteTargetSuccess(cred.serverUrl);
|
|
562
|
+
perTarget[cred.serverUrl] = { ok: true };
|
|
494
563
|
agg = agg ? {
|
|
495
564
|
uploaded: agg.uploaded + r.uploaded, skipped: agg.skipped + r.skipped, redactions: agg.redactions + r.redactions,
|
|
496
565
|
items: agg.items + r.items, links: agg.links + r.links, findings: agg.findings + r.findings,
|
|
@@ -501,6 +570,7 @@ export async function syncSessions(opts = {}) {
|
|
|
501
570
|
catch (e) {
|
|
502
571
|
const msg = e instanceof Error ? e.message : String(e);
|
|
503
572
|
errors.push(`${cred.serverUrl}: ${msg}`);
|
|
573
|
+
perTarget[cred.serverUrl] = { ok: false, error: msg };
|
|
504
574
|
// Report the NEXT attempt in the same breath as the failure, so the log
|
|
505
575
|
// says what will happen rather than leaving the user to infer it.
|
|
506
576
|
const next = noteTargetFailure(cred.serverUrl, msg);
|
|
@@ -510,10 +580,15 @@ export async function syncSessions(opts = {}) {
|
|
|
510
580
|
// The error CLASS, never the message: a server error string can quote a
|
|
511
581
|
// path or a payload, and this channel must not carry either.
|
|
512
582
|
record({
|
|
513
|
-
|
|
583
|
+
// `tripped`, not `open`: an open breaker re-opens on every attempt until
|
|
584
|
+
// a success clears it, so `open` counted elapsed time. Every failure is
|
|
585
|
+
// still reported — as `target_failure`, carrying the streak in
|
|
586
|
+
// `failures` — so nothing becomes invisible, it just stops being counted
|
|
587
|
+
// as a fresh incident.
|
|
588
|
+
kind: next.tripped ? 'breaker_trip' : 'target_failure',
|
|
514
589
|
failures: next.failures,
|
|
515
590
|
retryInMs: next.retryInMs,
|
|
516
|
-
errorClass: classifyError(msg),
|
|
591
|
+
errorClass: classifyError(msg, e),
|
|
517
592
|
local: isLocalHost(cred.serverUrl) ? 1 : 0,
|
|
518
593
|
});
|
|
519
594
|
}
|
|
@@ -556,7 +631,9 @@ export async function syncSessions(opts = {}) {
|
|
|
556
631
|
throw new Error(`sync failed for all targets:\n ${errors.join('\n ')}`);
|
|
557
632
|
if (errors.length > 0)
|
|
558
633
|
console.error(`[sync] ${errors.length} target(s) failed (others succeeded):\n ${errors.join('\n ')}`);
|
|
559
|
-
|
|
634
|
+
// The per-target verdicts ride out with the aggregate, so the caller never has
|
|
635
|
+
// to infer one target's fate from the sum of all of them.
|
|
636
|
+
return { ...agg, perTarget };
|
|
560
637
|
}
|
|
561
638
|
/** POST one derived-field batch, retrying the ingest gate's 429 (honoring
|
|
562
639
|
* Retry-After) and transient 5xx. */
|
|
@@ -894,7 +971,7 @@ async function syncToTargetInScope(cred, opts = {}) {
|
|
|
894
971
|
// the check entirely. The upload path is the last place to say no.
|
|
895
972
|
const risk = transportRisk(cred.serverUrl);
|
|
896
973
|
if (risk)
|
|
897
|
-
throw
|
|
974
|
+
throw classedError('insecure_transport', risk);
|
|
898
975
|
const sync = loadSettings().sync;
|
|
899
976
|
// Exclusions = local settings ∪ server-side tenant config (dashboard-edited).
|
|
900
977
|
const serverCfg = await fetchTenantSyncConfig(cred);
|
|
@@ -1113,7 +1190,7 @@ async function syncToTargetInScope(cred, opts = {}) {
|
|
|
1113
1190
|
// token was revoked or the tenant is gone. Every future sync will fail
|
|
1114
1191
|
// the same way until the human reconnects — say exactly that.
|
|
1115
1192
|
if (res.status === 401 || res.status === 403) {
|
|
1116
|
-
throw
|
|
1193
|
+
throw classedError('auth', `server rejected this machine's device token (HTTP ${res.status} — revoked?). Reconnect with: chat-recall login ${base}`);
|
|
1117
1194
|
}
|
|
1118
1195
|
// 402 is a standing plan condition, not a glitch: a meter (monthly
|
|
1119
1196
|
// quota / storage cap) or a missing email confirmation. The server's
|
|
@@ -1135,7 +1212,9 @@ async function syncToTargetInScope(cred, opts = {}) {
|
|
|
1135
1212
|
detail += `\n ${body.upgradeUrl}`;
|
|
1136
1213
|
}
|
|
1137
1214
|
catch { /* non-JSON 402 — print as-is */ }
|
|
1138
|
-
|
|
1215
|
+
// CLASSED, because this message deliberately drops the status: the
|
|
1216
|
+
// reader gets the server's sentence, and the machine gets the class.
|
|
1217
|
+
throw classedError('payment_required', `sync paused by the server: ${detail}`);
|
|
1139
1218
|
}
|
|
1140
1219
|
if (!retryable)
|
|
1141
1220
|
throw new Error(`sync failed: HTTP ${res.status} ${text}`);
|
|
@@ -1151,7 +1230,11 @@ async function syncToTargetInScope(cred, opts = {}) {
|
|
|
1151
1230
|
continue;
|
|
1152
1231
|
}
|
|
1153
1232
|
catch (err) {
|
|
1154
|
-
|
|
1233
|
+
// A STANDING condition first, by tag — a 402 whose message was rewritten
|
|
1234
|
+
// for the reader has no `HTTP 402` left to match, so the regex alone
|
|
1235
|
+
// retried every quota-paused batch four times with backoff.
|
|
1236
|
+
const fatal = isStandingFailure(err)
|
|
1237
|
+
|| (err instanceof Error && /HTTP 4\d\d|sync failed after/.test(err.message));
|
|
1155
1238
|
if (fatal || attempt >= RETRY_DELAYS_MS.length)
|
|
1156
1239
|
throw err;
|
|
1157
1240
|
console.error(`[sync] upload attempt ${attempt + 1} failed (${err instanceof Error ? err.message : err}) — retrying in ${RETRY_DELAYS_MS[attempt] / 1000}s`);
|
|
@@ -2090,10 +2173,20 @@ export async function buildConversationSync(ref, mtime, opts = {}) {
|
|
|
2090
2173
|
console.error(`[sync] ${ref.prefixedId} is ${(containerBytes / 1048576).toFixed(0)}MB `
|
|
2091
2174
|
+ `(over the ${(FULL_BUILD_MAX_BYTES / 1048576).toFixed(0)}MB ceiling) — shipping without `
|
|
2092
2175
|
+ 'the raw archive and derived data to keep the walk in memory');
|
|
2093
|
-
// Size and
|
|
2094
|
-
// their sessions get, is how the ceiling gets set
|
|
2095
|
-
// than from one developer's corpus.
|
|
2096
|
-
|
|
2176
|
+
// Size, tool, and a de-duplication key. Knowing that customers hit
|
|
2177
|
+
// this, and how big their sessions get, is how the ceiling gets set
|
|
2178
|
+
// from evidence rather than from one developer's corpus.
|
|
2179
|
+
//
|
|
2180
|
+
// `dedupeKey` is what makes the count mean SESSIONS. This event repeats
|
|
2181
|
+
// on every walk while the transcript stays oversized, so the raw event
|
|
2182
|
+
// count read as 38 oversized sessions on a device that had one — the
|
|
2183
|
+
// same 117MB transcript, reported 38 times in a week.
|
|
2184
|
+
record({
|
|
2185
|
+
kind: 'oversized_session',
|
|
2186
|
+
mb: Math.round(containerBytes / 1048576),
|
|
2187
|
+
tool: ref.toolId,
|
|
2188
|
+
dedupeKey: stableMark(ref.prefixedId),
|
|
2189
|
+
});
|
|
2097
2190
|
includeRaw = false;
|
|
2098
2191
|
includeMeta = false;
|
|
2099
2192
|
}
|