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/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/cli.js +86 -39
- package/dist/mcp.js +85 -38
- package/dist/scan-worker.js +2 -15
- 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 +87 -41
- package/package.json +1 -1
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
|
|
11745
|
-
return extractProjectPathFromTranscript(deriveFrom, 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:
|
|
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.
|
|
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.
|
|
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)
|
|
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
|
|
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("
|
|
21543
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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({
|
|
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.
|
|
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");
|
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;
|
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
|
}
|