@bli-cockpit/cli 0.2.276 → 0.2.278

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.
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Which images Tower refused to store, and until when this machine leaves them alone (BLI-23329).
3
+ *
4
+ * Kept in its OWN file, apart from the described/failed record of the describe door
5
+ * (`image-descriptions.json`, shared with the BLI Memory daemon). Two reasons, both found in plan review:
6
+ * - the describe record's merge deletes a failure for every image that is described by now, whatever the failure
7
+ * was about (`mergeImageDescriptionStates`), so a refused store of an image the describer knows would be
8
+ * forgotten at the next write and retried on every tick;
9
+ * - the daemon rewrites that file from its own copy; a field it does not know can be dropped.
10
+ * This file is written by the sync tick alone (one writer under the sync lock), so neither can touch it.
11
+ *
12
+ * Holds keys, reason labels, attempt counts and times only. Never a path, a session id or a byte.
13
+ */
14
+ import { createHash } from "node:crypto";
15
+ import path from "node:path";
16
+ import { imageRetryDelayMs, imageStateKey } from "./image-description-state.js";
17
+ import { isMissingFileError, readJsonFile, writeJsonFileAtomic } from "./local-state-files.js";
18
+ export const IMAGE_STORE_FAILURES_FILENAME = "image-store-failures.json";
19
+ export const IMAGE_STORE_FAILURES_SCHEMA = "cockpit-image-store-failures.v1";
20
+ /** Oldest records leave first past this many. */
21
+ export const IMAGE_STORE_FAILURES_MAX = 5_000;
22
+ const KEY_CHARS = 32;
23
+ const TAG = "[image-store]";
24
+ export function imageStoreFailuresPath(stateDir) {
25
+ return path.join(stateDir, IMAGE_STORE_FAILURES_FILENAME);
26
+ }
27
+ /**
28
+ * The key a refusal is remembered under. Most refusals are about the IMAGE (its bytes, its type): the image's key.
29
+ * A refusal that is about the SESSION (a founder forgot it) must not stop another session storing the same picture,
30
+ * so it is remembered under a key that mixes both in.
31
+ */
32
+ export function storeFailureKey(sha256, sessionId, aboutSession) {
33
+ if (!aboutSession)
34
+ return imageStateKey(sha256);
35
+ return createHash("sha256").update(`${sessionId}|${sha256}`).digest("hex").slice(0, KEY_CHARS);
36
+ }
37
+ /** The failures on record. A missing file is empty; an unreadable or damaged one is empty and said out loud. */
38
+ export async function readStoreFailures(stateDir) {
39
+ let raw;
40
+ try {
41
+ raw = await readJsonFile(imageStoreFailuresPath(stateDir));
42
+ }
43
+ catch (error) {
44
+ if (!isMissingFileError(error)) {
45
+ console.error(TAG + " store failure record unreadable, starting from an empty one", JSON.stringify({ reason: "image_store_failures_unreadable" }));
46
+ }
47
+ return {};
48
+ }
49
+ const record = (raw && typeof raw === "object" ? raw : {});
50
+ const failed = {};
51
+ if (record.failed && typeof record.failed === "object" && !Array.isArray(record.failed)) {
52
+ for (const [key, value] of Object.entries(record.failed)) {
53
+ const entry = value;
54
+ if (key.length !== KEY_CHARS || !entry)
55
+ continue;
56
+ if (typeof entry.reason !== "string" || typeof entry.attempts !== "number" || typeof entry.retry_after !== "number")
57
+ continue;
58
+ failed[key] = { reason: entry.reason, attempts: entry.attempts, retry_after: entry.retry_after };
59
+ }
60
+ }
61
+ return failed;
62
+ }
63
+ export async function writeStoreFailures(stateDir, failures) {
64
+ const entries = Object.entries(failures).sort((a, b) => a[1].retry_after - b[1].retry_after);
65
+ const kept = Object.fromEntries(entries.slice(Math.max(0, entries.length - IMAGE_STORE_FAILURES_MAX)));
66
+ await writeJsonFileAtomic(imageStoreFailuresPath(stateDir), { schema_version: IMAGE_STORE_FAILURES_SCHEMA, failed: kept });
67
+ }
68
+ /** Remember one more refusal of this image and push its next try out along the same ladder the describe door uses. */
69
+ export function rememberStoreFailure(failures, key, reason, permanent, now) {
70
+ const attempts = (failures[key]?.attempts ?? 0) + 1;
71
+ const record = { reason, attempts, retry_after: now + imageRetryDelayMs(attempts, permanent) };
72
+ failures[key] = record;
73
+ return record;
74
+ }
75
+ export function isBackingOff(failures, key, now) {
76
+ const record = failures[key];
77
+ return Boolean(record && record.retry_after > now);
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.276",
3
+ "version": "0.2.278",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,9 +27,9 @@
27
27
  "test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-surface-doors.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-exit-contract.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-cli-doctor-json.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
28
28
  },
29
29
  "dependencies": {
30
- "@bli-cockpit/memory-mcp": "0.1.84",
31
- "@bli-cockpit/mcp": "0.1.168",
32
- "@bli-cockpit/telemetry-core": "0.1.91",
30
+ "@bli-cockpit/memory-mcp": "0.1.85",
31
+ "@bli-cockpit/mcp": "0.1.169",
32
+ "@bli-cockpit/telemetry-core": "0.1.92",
33
33
  "unpdf": "1.8.1"
34
34
  },
35
35
  "repository": {