@botbuddy/cli 1.60.8 → 1.60.9
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.
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.60.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.60.9",
|
|
3
|
+
"source_commit": "460663423af03d399c5b15a72a0a493bdb888813",
|
|
4
|
+
"source_sha256": "bb012b774ec1728801e48d3239f0a0aecd4e948d21fae5ab866d91935a4d566f",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "64623b249bafec25024c52e1953382ee6d15e09652fd8450593275a0504059c0"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.60.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.60.9",
|
|
3
|
+
"source_commit": "460663423af03d399c5b15a72a0a493bdb888813",
|
|
4
|
+
"source_sha256": "bb012b774ec1728801e48d3239f0a0aecd4e948d21fae5ab866d91935a4d566f",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "2b75efb4413d95344478fdf9506679e435f48ef2f048b44236f00e941bcad445"
|
|
7
7
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"release_version": "1.60.
|
|
4
|
-
"source_commit": "
|
|
5
|
-
"source_sha256": "
|
|
3
|
+
"release_version": "1.60.9",
|
|
4
|
+
"source_commit": "460663423af03d399c5b15a72a0a493bdb888813",
|
|
5
|
+
"source_sha256": "bb012b774ec1728801e48d3239f0a0aecd4e948d21fae5ab866d91935a4d566f",
|
|
6
6
|
"targets": {
|
|
7
7
|
"darwin-arm64": {
|
|
8
8
|
"goos": "darwin",
|
|
9
9
|
"goarch": "arm64",
|
|
10
10
|
"binary": "daemon/bin/darwin-arm64/botbuddy-daemon",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "64623b249bafec25024c52e1953382ee6d15e09652fd8450593275a0504059c0",
|
|
12
12
|
"mode": "0755"
|
|
13
13
|
},
|
|
14
14
|
"linux-x64": {
|
|
15
15
|
"goos": "linux",
|
|
16
16
|
"goarch": "amd64",
|
|
17
17
|
"binary": "daemon/bin/linux-x64/botbuddy-daemon",
|
|
18
|
-
"sha256": "
|
|
18
|
+
"sha256": "2b75efb4413d95344478fdf9506679e435f48ef2f048b44236f00e941bcad445",
|
|
19
19
|
"mode": "0755"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/package.json
CHANGED
package/src/usage/sync.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// success). It carries row COUNTS only — never paths, never prompt text, never
|
|
11
11
|
// token totals. Fail-open: any failure exits 0 unless --strict.
|
|
12
12
|
import { execFileSync } from "node:child_process";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
13
14
|
import { existsSync, readFileSync } from "node:fs";
|
|
14
15
|
import { readdir, stat } from "node:fs/promises";
|
|
15
16
|
import { homedir, hostname as systemHostname } from "node:os";
|
|
@@ -90,16 +91,20 @@ export function transcriptRoots(env = process.env, home = homedir()) {
|
|
|
90
91
|
* subdirectories) modified since `cutoffMs`. `shouldStop` is checked before
|
|
91
92
|
* every directory read and stat, so a huge history or a slow filesystem cannot
|
|
92
93
|
* outlive the run's time budget (the pre-push lane runs with 5 s).
|
|
93
|
-
*
|
|
94
|
+
* `errors` counts directories or files that exist but could not be read
|
|
95
|
+
* (EACCES, EPERM, …); a missing root or a file that vanished is not an error.
|
|
96
|
+
* @returns {Promise<{ files: string[], stopped: boolean, errors: number }>}
|
|
94
97
|
*/
|
|
95
98
|
export async function listTranscripts(root, cutoffMs, { shouldStop = () => false, keep = () => false } = {}) {
|
|
96
99
|
const out = [];
|
|
97
100
|
let stopped = false;
|
|
101
|
+
let errors = 0;
|
|
102
|
+
const benign = (error) => error?.code === "ENOENT" || error?.code === "ENOTDIR";
|
|
98
103
|
const halt = () => (stopped = stopped || shouldStop());
|
|
99
104
|
async function walk(dir) {
|
|
100
105
|
if (halt()) return;
|
|
101
106
|
let entries;
|
|
102
|
-
try { entries = await readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
107
|
+
try { entries = await readdir(dir, { withFileTypes: true }); } catch (error) { if (!benign(error)) errors += 1; return; }
|
|
103
108
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
104
109
|
for (const entry of entries) {
|
|
105
110
|
if (halt()) return;
|
|
@@ -109,12 +114,12 @@ export async function listTranscripts(root, cutoffMs, { shouldStop = () => false
|
|
|
109
114
|
try {
|
|
110
115
|
const info = await stat(path);
|
|
111
116
|
if (info.mtimeMs >= cutoffMs || keep(path, info)) out.push(path);
|
|
112
|
-
} catch { /* vanished between readdir and stat */ }
|
|
117
|
+
} catch (error) { if (!benign(error)) errors += 1; /* ENOENT: vanished between readdir and stat */ }
|
|
113
118
|
}
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
121
|
await walk(root);
|
|
117
|
-
return { files: out, stopped };
|
|
122
|
+
return { files: out, stopped, errors };
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
/** Which adapter owns a single `--file`: under a Codex root, or a rollout whose first record is session_meta. */
|
|
@@ -373,19 +378,24 @@ export async function syncUsage(opts, deps = {}) {
|
|
|
373
378
|
roots = transcriptRoots(env, home), repoFor = createRepoResolver(), branchFor = createBranchResolver({ env }), cliVersion = "0.0.0",
|
|
374
379
|
tenantFor = createTenantResolver({ env }), resolveTarget = defaultResolveTarget, readLines = readCompleteLines,
|
|
375
380
|
serverUrl = env.BOTBUDDY_SERVER_URL || DEFAULT_SERVER_URL, statePath = cursorPath(home),
|
|
381
|
+
newSyncId = randomUUID,
|
|
376
382
|
} = deps;
|
|
377
383
|
const startedMs = now();
|
|
378
384
|
const deadlineMs = startedMs + opts.maxSeconds * 1000;
|
|
379
385
|
const parseDeadlineMs = startedMs + opts.maxSeconds * 1000 * PARSE_BUDGET_FRACTION;
|
|
380
386
|
const startedAt = new Date(startedMs).toISOString();
|
|
387
|
+
// BOT-2649: one id for this whole invocation, sent with every batch (both
|
|
388
|
+
// providers), so v_usage_sync_hosts judges the attempt as a whole: one
|
|
389
|
+
// refused batch fails the sync even when a later batch succeeded.
|
|
390
|
+
const syncId = newSyncId();
|
|
381
391
|
const receipt = {
|
|
382
392
|
schema_version: 1, command: "usage sync", outcome: "ok", trigger: opts.trigger, dry_run: opts.dryRun,
|
|
383
393
|
providers: [], files_scanned: 0, files_read: 0, envelopes: 0, batches: 0,
|
|
384
394
|
tenant: null, skipped_other_tenant: 0, skipped_unbound: 0,
|
|
385
395
|
seen: 0, inserted: 0, superseded: 0, deduped: 0, refused: 0, refusal_reasons: {}, batches_all_refused: 0,
|
|
386
|
-
refused_local: 0, parse_errors: 0,
|
|
396
|
+
refused_local: 0, parse_errors: 0, read_errors: 0,
|
|
387
397
|
held_open_messages: 0, cost_state_withheld: 0,
|
|
388
|
-
privacy: "pass", budget_exhausted: false, run_ids: [], started_at: startedAt, duration_ms: 0,
|
|
398
|
+
privacy: "pass", budget_exhausted: false, run_ids: [], started_at: startedAt, sync_id: syncId, duration_ms: 0,
|
|
389
399
|
};
|
|
390
400
|
const finish = (outcome, error) => {
|
|
391
401
|
receipt.outcome = outcome;
|
|
@@ -441,6 +451,8 @@ export async function syncUsage(opts, deps = {}) {
|
|
|
441
451
|
},
|
|
442
452
|
});
|
|
443
453
|
if (listed.stopped) receipt.budget_exhausted = true;
|
|
454
|
+
// BOT-2649: an unreadable transcript directory is a read failure, not an idle host.
|
|
455
|
+
receipt.read_errors += listed.errors ?? 0;
|
|
444
456
|
plan.push({ provider, files: listed.stopped ? [] : listed.files });
|
|
445
457
|
}
|
|
446
458
|
}
|
|
@@ -466,7 +478,7 @@ export async function syncUsage(opts, deps = {}) {
|
|
|
466
478
|
: createClaudeAdapter({ hmac, environment, repoFor, tenantFor });
|
|
467
479
|
const run = {
|
|
468
480
|
host: hostname(), trigger: opts.trigger, provider, cli_version: cliVersion,
|
|
469
|
-
parser_version: adapter.parserVersion, started_at: startedAt,
|
|
481
|
+
parser_version: adapter.parserVersion, started_at: startedAt, sync_id: syncId,
|
|
470
482
|
};
|
|
471
483
|
|
|
472
484
|
// Envelopes are queued with a global sequence; a file's cursor commits only
|
|
@@ -535,7 +547,9 @@ export async function syncUsage(opts, deps = {}) {
|
|
|
535
547
|
for (const path of files) {
|
|
536
548
|
if (now() >= parseDeadlineMs) { receipt.budget_exhausted = true; break; }
|
|
537
549
|
let info;
|
|
538
|
-
|
|
550
|
+
// A file that vanished since listing is benign; any other stat failure
|
|
551
|
+
// (e.g. EACCES) means this run could not read pending usage (BOT-2649).
|
|
552
|
+
try { info = await stat(path); } catch (error) { if (error?.code !== "ENOENT") receipt.read_errors += 1; continue; }
|
|
539
553
|
const sourceHmac = hmac(resolve(path));
|
|
540
554
|
// Cursors are per (tenant, file): a run skips other tenants' records, so a
|
|
541
555
|
// shared per-file cursor would consume them for every later tenant's run.
|
|
@@ -560,6 +574,7 @@ export async function syncUsage(opts, deps = {}) {
|
|
|
560
574
|
parser.parseRecord(record, offset);
|
|
561
575
|
}, { shouldStop: () => now() >= parseDeadlineMs });
|
|
562
576
|
} catch {
|
|
577
|
+
receipt.read_errors += 1; // counted: a run that could not read is not a clean idle run
|
|
563
578
|
continue; // unreadable file: skip, cursor unchanged
|
|
564
579
|
}
|
|
565
580
|
if (result.stopped) receipt.budget_exhausted = true;
|
|
@@ -606,8 +621,43 @@ export async function syncUsage(opts, deps = {}) {
|
|
|
606
621
|
}
|
|
607
622
|
commitDelivered();
|
|
608
623
|
}
|
|
624
|
+
// BOT-2649: a run that uploaded nothing still records one empty ok run, so an
|
|
625
|
+
// idle but healthy host is not aged into "stale" by v_usage_sync_hosts. Only a
|
|
626
|
+
// CLEAN idle run does: one that refused records locally, hit unparseable lines,
|
|
627
|
+
// could not read a transcript or ran out of budget did not prove usage lands, so it records nothing and the
|
|
628
|
+
// host ages as before rather than reading healthy (independent review on #1324).
|
|
629
|
+
if (!opts.dryRun && receipt.batches === 0) {
|
|
630
|
+
const withheld = receipt.budget_exhausted ? "budget_exhausted"
|
|
631
|
+
: receipt.refused_local > 0 ? "refused_local"
|
|
632
|
+
: receipt.parse_errors > 0 ? "parse_errors"
|
|
633
|
+
: receipt.read_errors > 0 ? "read_errors" : null;
|
|
634
|
+
if (withheld) {
|
|
635
|
+
receipt.empty_run_withheld = withheld;
|
|
636
|
+
} else {
|
|
637
|
+
const error = await postEmptyRun();
|
|
638
|
+
if (error === OUT_OF_TIME) return finalize(null);
|
|
639
|
+
if (error) return finalize(error);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
609
642
|
return finalize(null);
|
|
610
643
|
|
|
644
|
+
async function postEmptyRun() {
|
|
645
|
+
// Same deadline rules as a batch: never START a request past the budget.
|
|
646
|
+
if (now() >= deadlineMs) { receipt.budget_exhausted = true; return OUT_OF_TIME; }
|
|
647
|
+
const resolved = await ensureAuth();
|
|
648
|
+
if (resolved.error) return resolved.error;
|
|
649
|
+
if (now() >= deadlineMs) { receipt.budget_exhausted = true; return OUT_OF_TIME; }
|
|
650
|
+
const run = { host: hostname(), trigger: opts.trigger, cli_version: cliVersion, started_at: startedAt, sync_id: syncId };
|
|
651
|
+
const result = await postBatch({
|
|
652
|
+
endpoint: usageIngestEndpoint(serverUrl), tenant: resolved.tenant,
|
|
653
|
+
headers: resolved.headers, run, envelopes: [], fetchImpl, timeoutMs: deadlineMs - now(),
|
|
654
|
+
});
|
|
655
|
+
if (!result.ok) return result.error;
|
|
656
|
+
receipt.empty_run = true;
|
|
657
|
+
if (result.run_id) receipt.run_ids.push(result.run_id);
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
|
|
611
661
|
async function finalize(error) {
|
|
612
662
|
const outcome = error ? "error" : opts.dryRun ? "dry_run" : receipt.budget_exhausted ? "partial" : "ok";
|
|
613
663
|
finish(outcome, error);
|