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.
@@ -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 getFallbackProjectPath(homeDir);
7910
- return extractProjectPathFromTranscript(deriveFrom, homeDir) || getFallbackProjectPath(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 readdirSync8, readFileSync as readFileSync8, statSync as statSync10, openSync as openSync4, readSync as readSync4, closeSync as closeSync4 } from "fs";
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 readdirSync8(root, { withFileTypes: true })) {
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 = decodeProjectDirName(proj.name);
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 = readdirSync8(projPath);
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 readdirSync8(root, { withFileTypes: true })) {
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 = readdirSync8(projPath);
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 readdirSync8(subDir)) {
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 readdirSync9, readFileSync as readFileSync9, statSync as statSync11, openSync as openSync5, readSync as readSync5, closeSync as closeSync5 } from "fs";
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 readdirSync10, readFileSync as readFileSync10, statSync as statSync12, openSync as openSync6, readSync as readSync6, closeSync as closeSync6 } from "fs";
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 = readdirSync10(root, { withFileTypes: true });
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 = readdirSync10(dir);
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: decodeProjectDirName(entry.name)
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 = readdirSync10(subDir).filter((f) => f.endsWith(".jsonl")).sort().map((f) => join14(subDir, f));
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 = readdirSync10(tmpRoot, { withFileTypes: true });
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 = readdirSync10(chats);
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 readdirSync10(root, { withFileTypes: true })) {
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 readdirSync10(yearPath, { withFileTypes: true })) {
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 readdirSync10(monthPath, { withFileTypes: true })) {
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 = readdirSync10(dayPath);
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 readdirSync10(dir, { withFileTypes: true })) {
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)
@@ -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":"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"}
1
+ {"version":3,"file":"sync-client.d.ts","sourceRoot":"","sources":["../src/sync-client.ts"],"names":[],"mappings":"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"}
@@ -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
- if (m.includes('timed out') || m.includes('etimedout'))
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
- if (m.includes('plain http') || m.includes('https'))
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 (/5\d\d/.test(m))
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
- kind: next.open ? 'breaker_trip' : 'target_failure',
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
- return agg;
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 new Error(risk);
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 new Error(`server rejected this machine's device token (HTTP ${res.status} — revoked?). Reconnect with: chat-recall login ${base}`);
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
- throw new Error(`sync paused by the server: ${detail}`);
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
- const fatal = err instanceof Error && /HTTP 4\d\d|sync failed after/.test(err.message);
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 tool only. Knowing that customers hit this, and how big
2094
- // their sessions get, is how the ceiling gets set from evidence rather
2095
- // than from one developer's corpus.
2096
- record({ kind: 'oversized_session', mb: Math.round(containerBytes / 1048576), tool: ref.toolId });
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
  }