chat-recall 0.5.30 → 0.5.31

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/mcp.js CHANGED
@@ -11716,19 +11716,6 @@ var init_codex2 = __esm({
11716
11716
  // ../engine/dist/core/backends/agy.js
11717
11717
  import { existsSync as existsSync14, readdirSync as readdirSync10, readFileSync as readFileSync9, statSync as statSync12 } from "fs";
11718
11718
  import { basename as basename12, dirname as dirname12, join as join15 } from "path";
11719
- function getFallbackProjectPath(homeDir) {
11720
- try {
11721
- const settingsPath2 = join15(homeDir, "settings.json");
11722
- if (existsSync14(settingsPath2)) {
11723
- const json = JSON.parse(readFileSync9(settingsPath2, "utf-8"));
11724
- if (Array.isArray(json?.trustedWorkspaces) && json.trustedWorkspaces[0]) {
11725
- return json.trustedWorkspaces[0];
11726
- }
11727
- }
11728
- } catch {
11729
- }
11730
- return "";
11731
- }
11732
11719
  function agyTranscriptPath(logsDir) {
11733
11720
  const full = join15(logsDir, "transcript_full.jsonl");
11734
11721
  if (existsSync14(full))
@@ -11741,8 +11728,8 @@ function agyTranscriptPath(logsDir) {
11741
11728
  function resolveAgyProject(logsDir, homeDir) {
11742
11729
  const deriveFrom = agyTranscriptPath(logsDir);
11743
11730
  if (!deriveFrom)
11744
- return getFallbackProjectPath(homeDir);
11745
- return extractProjectPathFromTranscript(deriveFrom, homeDir) || getFallbackProjectPath(homeDir);
11731
+ return "";
11732
+ return extractProjectPathFromTranscript(deriveFrom, homeDir);
11746
11733
  }
11747
11734
  function extractProjectPathFromTranscript(path, homeDir) {
11748
11735
  let content;
@@ -20274,7 +20261,7 @@ var init_extractor_version = __esm({
20274
20261
  command: 1
20275
20262
  };
20276
20263
  TOOL_EXTRACTOR_BUMP = {
20277
- agy: 2,
20264
+ agy: 3,
20278
20265
  claude: 1,
20279
20266
  codex: 1
20280
20267
  };
@@ -20653,7 +20640,7 @@ var init_http = __esm({
20653
20640
  CLI_VERSION = JSON.parse(readFileSync37(new URL("../package.json", import.meta.url), "utf-8")).version || CLI_VERSION;
20654
20641
  } catch {
20655
20642
  try {
20656
- if (true) CLI_VERSION = "0.5.30";
20643
+ if (true) CLI_VERSION = "0.5.31";
20657
20644
  } catch {
20658
20645
  }
20659
20646
  }
@@ -21003,9 +20990,9 @@ function cooldownFor(failures) {
21003
20990
  function breakerState(serverUrl, now = Date.now()) {
21004
20991
  const s = state.get(key(serverUrl));
21005
20992
  if (!s || s.openUntil <= now) {
21006
- return { open: false, retryInMs: 0, failures: s?.failures ?? 0, lastError: s?.lastError ?? "" };
20993
+ return { open: false, retryInMs: 0, failures: s?.failures ?? 0, lastError: s?.lastError ?? "", tripped: false };
21007
20994
  }
21008
- return { open: true, retryInMs: s.openUntil - now, failures: s.failures, lastError: s.lastError };
20995
+ return { open: true, retryInMs: s.openUntil - now, failures: s.failures, lastError: s.lastError, tripped: false };
21009
20996
  }
21010
20997
  function noteTargetSuccess(serverUrl) {
21011
20998
  state.delete(key(serverUrl));
@@ -21017,11 +21004,11 @@ function noteTargetFailure(serverUrl, error, now = Date.now()) {
21017
21004
  const lastError = error.slice(0, 200);
21018
21005
  if (failures < TRIP_AFTER_FAILURES) {
21019
21006
  state.set(k, { failures, openUntil: 0, lastError });
21020
- return { open: false, retryInMs: 0, failures, lastError };
21007
+ return { open: false, retryInMs: 0, failures, lastError, tripped: false };
21021
21008
  }
21022
21009
  const cooldown = cooldownFor(failures);
21023
21010
  state.set(k, { failures, openUntil: now + cooldown, lastError });
21024
- return { open: true, retryInMs: cooldown, failures, lastError };
21011
+ return { open: true, retryInMs: cooldown, failures, lastError, tripped: failures === TRIP_AFTER_FAILURES };
21025
21012
  }
21026
21013
  function breakerNotice(serverUrl, v) {
21027
21014
  if (!v.open) return null;
@@ -21148,6 +21135,14 @@ async function flush() {
21148
21135
  }
21149
21136
  }));
21150
21137
  }
21138
+ function stableMark(value) {
21139
+ try {
21140
+ const { createHash: createHash16 } = __require("node:crypto");
21141
+ return createHash16("sha256").update(value).digest("hex").slice(0, 12);
21142
+ } catch {
21143
+ return "";
21144
+ }
21145
+ }
21151
21146
  function hashHost() {
21152
21147
  try {
21153
21148
  const { createHash: createHash16 } = __require("node:crypto");
@@ -21165,7 +21160,7 @@ var init_telemetry = __esm({
21165
21160
  init_sync_client();
21166
21161
  cliVersion2 = () => {
21167
21162
  try {
21168
- return true ? "0.5.30" : "dev";
21163
+ return true ? "0.5.31" : "dev";
21169
21164
  } catch {
21170
21165
  return "dev";
21171
21166
  }
@@ -21473,7 +21468,17 @@ async function runAutoUpdate(base, authHeaders, ownVersion2, deps) {
21473
21468
  }
21474
21469
  const effectiveOwn = newerOf(ownVersion2, installedVersion());
21475
21470
  const plan = planAutoUpdate(base, caps, effectiveOwn, process.env.CHAT_RECALL_AUTO_UPDATE);
21476
- if (!plan.update) return { updated: false, reason: plan.reason };
21471
+ if (!plan.update) {
21472
+ if (/disabled|no CLI release/.test(plan.reason) && !reportedStandingSkip.has(base)) {
21473
+ reportedStandingSkip.add(base);
21474
+ try {
21475
+ const { reportClientEvent: reportClientEvent3 } = await Promise.resolve().then(() => (init_client_events(), client_events_exports));
21476
+ reportClientEvent3("auto_update_skipped", { message: `${plan.reason} (server offers ${caps.cli?.version ?? "?"}, this device runs ${effectiveOwn})` });
21477
+ } catch {
21478
+ }
21479
+ }
21480
+ return { updated: false, reason: plan.reason };
21481
+ }
21477
21482
  const result = await executeAutoUpdate(plan, {
21478
21483
  download: deps?.download ?? realDownload,
21479
21484
  install: deps?.install ?? realInstall,
@@ -21489,12 +21494,13 @@ async function runAutoUpdate(base, authHeaders, ownVersion2, deps) {
21489
21494
  }
21490
21495
  return result;
21491
21496
  }
21492
- var tarballUrl;
21497
+ var tarballUrl, reportedStandingSkip;
21493
21498
  var init_auto_update = __esm({
21494
21499
  "src/auto-update.ts"() {
21495
21500
  "use strict";
21496
21501
  init_http();
21497
21502
  tarballUrl = (base) => `${base.replace(/\/+$/, "")}/install/chat-recall.tgz`;
21503
+ reportedStandingSkip = /* @__PURE__ */ new Set();
21498
21504
  }
21499
21505
  });
21500
21506
 
@@ -21510,6 +21516,9 @@ __export(sync_client_exports, {
21510
21516
  batchScanExternal: () => batchScanExternal,
21511
21517
  buildConversationSync: () => buildConversationSync,
21512
21518
  buildConversationTail: () => buildConversationTail,
21519
+ classedError: () => classedError,
21520
+ classifyError: () => classifyError,
21521
+ isStandingFailure: () => isStandingFailure,
21513
21522
  isSyncSkip: () => isSyncSkip,
21514
21523
  loadAllCredentials: () => loadAllCredentials,
21515
21524
  loadCredentials: () => loadCredentials,
@@ -21530,17 +21539,32 @@ import { tmpdir as tmpdir3, hostname as hostname2 } from "node:os";
21530
21539
  import { createHash as createHash14 } from "node:crypto";
21531
21540
  import { fileURLToPath as fileURLToPath3 } from "node:url";
21532
21541
  import { gzipSync as gzipSync3 } from "zlib";
21533
- function classifyError(msg) {
21542
+ function classedError(errorClass, message) {
21543
+ const e = new Error(message);
21544
+ Object.defineProperty(e, ERROR_CLASS, { value: errorClass, enumerable: false });
21545
+ return e;
21546
+ }
21547
+ function taggedClass(err) {
21548
+ const v = err?.[ERROR_CLASS];
21549
+ return typeof v === "string" && v ? v : null;
21550
+ }
21551
+ function isStandingFailure(err) {
21552
+ const c = taggedClass(err);
21553
+ return c !== null && STANDING_FAILURE.has(c);
21554
+ }
21555
+ function classifyError(msg, err) {
21556
+ const tagged = taggedClass(err);
21557
+ if (tagged) return tagged;
21534
21558
  const m = msg.toLowerCase();
21535
21559
  if (m.includes("429")) return "rate_limited";
21536
21560
  if (m.includes("402")) return "payment_required";
21537
21561
  if (m.includes("401") || m.includes("403") || m.includes("token")) return "auth";
21538
- if (m.includes("timed out") || m.includes("etimedout")) return "timeout";
21562
+ if (m.includes("timed out") || m.includes("etimedout") || m.includes("aborted due to timeout")) return "timeout";
21539
21563
  if (m.includes("econnrefused")) return "refused";
21540
21564
  if (m.includes("enotfound") || m.includes("dns")) return "dns";
21541
21565
  if (m.includes("fetch failed") || m.includes("econnreset")) return "network";
21542
- if (m.includes("plain http") || m.includes("https")) return "insecure_transport";
21543
- if (/5\d\d/.test(m)) return "server_error";
21566
+ if (m.includes("over plain http") || m.includes("unsupported scheme")) return "insecure_transport";
21567
+ if (/\b5\d\d\b/.test(m)) return "server_error";
21544
21568
  return "other";
21545
21569
  }
21546
21570
  function saveCredentials(c) {
@@ -21758,17 +21782,23 @@ async function syncSessions(opts = {}) {
21758
21782
  let agg = null;
21759
21783
  const errors = [];
21760
21784
  const skippedTargets = [];
21785
+ const perTarget = {};
21761
21786
  for (const cred of targets) {
21762
21787
  const verdict = breakerState(cred.serverUrl);
21763
21788
  if (verdict.open) {
21764
21789
  const notice = breakerNotice(cred.serverUrl, verdict);
21765
21790
  if (notice) console.error(`[sync] ${notice}`);
21766
21791
  skippedTargets.push(cred.serverUrl);
21792
+ perTarget[cred.serverUrl] = {
21793
+ ok: false,
21794
+ error: notice ?? `in failure cooldown for ~${Math.max(1, Math.round(verdict.retryInMs / 6e4))}m`
21795
+ };
21767
21796
  continue;
21768
21797
  }
21769
21798
  try {
21770
21799
  const r = await syncToTarget(cred, opts);
21771
21800
  noteTargetSuccess(cred.serverUrl);
21801
+ perTarget[cred.serverUrl] = { ok: true };
21772
21802
  agg = agg ? {
21773
21803
  uploaded: agg.uploaded + r.uploaded,
21774
21804
  skipped: agg.skipped + r.skipped,
@@ -21785,14 +21815,20 @@ async function syncSessions(opts = {}) {
21785
21815
  } catch (e) {
21786
21816
  const msg = e instanceof Error ? e.message : String(e);
21787
21817
  errors.push(`${cred.serverUrl}: ${msg}`);
21818
+ perTarget[cred.serverUrl] = { ok: false, error: msg };
21788
21819
  const next = noteTargetFailure(cred.serverUrl, msg);
21789
21820
  const notice = breakerNotice(cred.serverUrl, next);
21790
21821
  if (notice) console.error(`[sync] ${notice}`);
21791
21822
  record({
21792
- kind: next.open ? "breaker_trip" : "target_failure",
21823
+ // `tripped`, not `open`: an open breaker re-opens on every attempt until
21824
+ // a success clears it, so `open` counted elapsed time. Every failure is
21825
+ // still reported — as `target_failure`, carrying the streak in
21826
+ // `failures` — so nothing becomes invisible, it just stops being counted
21827
+ // as a fresh incident.
21828
+ kind: next.tripped ? "breaker_trip" : "target_failure",
21793
21829
  failures: next.failures,
21794
21830
  retryInMs: next.retryInMs,
21795
- errorClass: classifyError(msg),
21831
+ errorClass: classifyError(msg, e),
21796
21832
  local: isLocalHost(cred.serverUrl) ? 1 : 0
21797
21833
  });
21798
21834
  }
@@ -21823,7 +21859,7 @@ async function syncSessions(opts = {}) {
21823
21859
  ${errors.join("\n ")}`);
21824
21860
  if (errors.length > 0) console.error(`[sync] ${errors.length} target(s) failed (others succeeded):
21825
21861
  ${errors.join("\n ")}`);
21826
- return agg;
21862
+ return { ...agg, perTarget };
21827
21863
  }
21828
21864
  async function postFieldBatch(base, token, rows) {
21829
21865
  const headers = { "content-type": "application/json" };
@@ -22010,7 +22046,7 @@ async function syncToTarget(cred, opts = {}) {
22010
22046
  }
22011
22047
  async function syncToTargetInScope(cred, opts = {}) {
22012
22048
  const risk = transportRisk(cred.serverUrl);
22013
- if (risk) throw new Error(risk);
22049
+ if (risk) throw classedError("insecure_transport", risk);
22014
22050
  const sync = loadSettings().sync;
22015
22051
  const serverCfg = await fetchTenantSyncConfig(cred);
22016
22052
  const excludeTools = /* @__PURE__ */ new Set([...sync?.excludeTools ?? [], ...serverCfg.excludeTools]);
@@ -22110,7 +22146,7 @@ async function syncToTargetInScope(cred, opts = {}) {
22110
22146
  const retryable = res.status === 429 || res.status >= 500;
22111
22147
  const text = await res.text().catch(() => "");
22112
22148
  if (res.status === 401 || res.status === 403) {
22113
- throw new Error(`server rejected this machine's device token (HTTP ${res.status} \u2014 revoked?). Reconnect with: chat-recall login ${base}`);
22149
+ throw classedError("auth", `server rejected this machine's device token (HTTP ${res.status} \u2014 revoked?). Reconnect with: chat-recall login ${base}`);
22114
22150
  }
22115
22151
  if (res.status === 402) {
22116
22152
  let detail = text;
@@ -22124,7 +22160,7 @@ async function syncToTargetInScope(cred, opts = {}) {
22124
22160
  ${body2.upgradeUrl}`;
22125
22161
  } catch {
22126
22162
  }
22127
- throw new Error(`sync paused by the server: ${detail}`);
22163
+ throw classedError("payment_required", `sync paused by the server: ${detail}`);
22128
22164
  }
22129
22165
  if (!retryable) throw new Error(`sync failed: HTTP ${res.status} ${text}`);
22130
22166
  if (attempt >= RETRY_DELAYS_MS.length) throw new Error(`sync failed after ${attempt + 1} attempts: HTTP ${res.status} ${text}`);
@@ -22134,7 +22170,7 @@ async function syncToTargetInScope(cred, opts = {}) {
22134
22170
  await sleep(waitMs);
22135
22171
  continue;
22136
22172
  } catch (err) {
22137
- const fatal = err instanceof Error && /HTTP 4\d\d|sync failed after/.test(err.message);
22173
+ const fatal = isStandingFailure(err) || err instanceof Error && /HTTP 4\d\d|sync failed after/.test(err.message);
22138
22174
  if (fatal || attempt >= RETRY_DELAYS_MS.length) throw err;
22139
22175
  console.error(`[sync] upload attempt ${attempt + 1} failed (${err instanceof Error ? err.message : err}) \u2014 retrying in ${RETRY_DELAYS_MS[attempt] / 1e3}s`);
22140
22176
  await sleep(RETRY_DELAYS_MS[attempt]);
@@ -22673,7 +22709,12 @@ async function buildConversationSync(ref, mtime, opts = {}) {
22673
22709
  const containerBytes = built ? built.files.reduce((n, f) => n + Buffer.byteLength(f.text), 0) : 0;
22674
22710
  if (containerBytes > FULL_BUILD_MAX_BYTES) {
22675
22711
  console.error(`[sync] ${ref.prefixedId} is ${(containerBytes / 1048576).toFixed(0)}MB (over the ${(FULL_BUILD_MAX_BYTES / 1048576).toFixed(0)}MB ceiling) \u2014 shipping without the raw archive and derived data to keep the walk in memory`);
22676
- record({ kind: "oversized_session", mb: Math.round(containerBytes / 1048576), tool: ref.toolId });
22712
+ record({
22713
+ kind: "oversized_session",
22714
+ mb: Math.round(containerBytes / 1048576),
22715
+ tool: ref.toolId,
22716
+ dedupeKey: stableMark(ref.prefixedId)
22717
+ });
22677
22718
  includeRaw = false;
22678
22719
  includeMeta = false;
22679
22720
  }
@@ -22984,7 +23025,7 @@ async function syncIncremental(opts = {}) {
22984
23025
  lock.release();
22985
23026
  }
22986
23027
  }
22987
- var ownVersion, walkProgressForUpload, lastServerPackSpec, credPath, _securityConfigCache, SECURITY_CONFIG_TTL_MS, _syncConfigCache, EMPTY_SYNC_CONFIG, _fetchTenantSyncConfigForTests, SYNC_RULES_REFUSAL, hashPath, MAX_DERIVED_ROW_BYTES, isLocalHost, CAPS_TTL_MS, capsOk, advertisedIngestConc, advertisedGzipBody, FULL_BUILD_MAX_BYTES, FULL_BUILD_TAIL_BYTES, BATCH_SCAN_MAX_BYTES;
23028
+ var ownVersion, ERROR_CLASS, STANDING_FAILURE, walkProgressForUpload, lastServerPackSpec, credPath, _securityConfigCache, SECURITY_CONFIG_TTL_MS, _syncConfigCache, EMPTY_SYNC_CONFIG, _fetchTenantSyncConfigForTests, SYNC_RULES_REFUSAL, hashPath, MAX_DERIVED_ROW_BYTES, isLocalHost, CAPS_TTL_MS, capsOk, advertisedIngestConc, advertisedGzipBody, FULL_BUILD_MAX_BYTES, FULL_BUILD_TAIL_BYTES, BATCH_SCAN_MAX_BYTES;
22988
23029
  var init_sync_client = __esm({
22989
23030
  "src/sync-client.ts"() {
22990
23031
  "use strict";
@@ -23030,10 +23071,16 @@ var init_sync_client = __esm({
23030
23071
  ownVersion = JSON.parse(readFileSync39(join49(dir, "../package.json"), "utf-8")).version || ownVersion;
23031
23072
  } catch {
23032
23073
  try {
23033
- if (true) ownVersion = "0.5.30";
23074
+ if (true) ownVersion = "0.5.31";
23034
23075
  } catch {
23035
23076
  }
23036
23077
  }
23078
+ ERROR_CLASS = "__chatRecallErrorClass";
23079
+ STANDING_FAILURE = /* @__PURE__ */ new Set([
23080
+ "auth",
23081
+ "payment_required",
23082
+ "insecure_transport"
23083
+ ]);
23037
23084
  walkProgressForUpload = null;
23038
23085
  lastServerPackSpec = null;
23039
23086
  credPath = () => join49(getDataDir(), "credentials.json");
@@ -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;
@@ -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
  }