@evident-ai/runner-synchroniser 3.4.0 → 3.4.1-dev.7802262

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.
Files changed (3) hide show
  1. package/README.md +23 -1
  2. package/dist/cli.js +195 -27
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -59,7 +59,7 @@ build time.
59
59
  | `sync-once <claude\|opencode>` | — | `0` = ran; non-zero = tool broken |
60
60
  | `model-auth-ready` | — | `0` = ready, `10` = not ready; other = tool broken |
61
61
  | `self-stop` | — | `0` = stopped, `20` = keep the task; other = tool broken |
62
- | `session-db-classify <litestream-restore-exit-code> <attempt> [--on-unusable-replica=<prune\|leave\|clear\|crash>]` | — | `0` = restored/no-replica/disabled, `32` = re-run and ask again, `31` = unusable (booted fresh, starts a fresh backup chain), `30` = fatal; other = usage/tool broken |
62
+ | `session-db-classify <litestream-restore-exit-code> <attempt> [--on-unusable-replica=<prune\|leave\|clear\|crash>] [--recovery-occurred] [--fresh-db-fallback]` | — | `0` = restored/no-replica/disabled, `32` = re-run and ask again, `31` = unusable (booted fresh, starts a fresh backup chain), `30` = fatal; other = usage/tool broken |
63
63
  | `session-db-verify <litestream-config-path>` | — | `0` = healthy/skipped/walked-back, `33` = every retained restore point was exhausted (booted fresh, starts a fresh backup chain); other = usage/tool broken |
64
64
 
65
65
  `self-stop` scales this agent's own ECS service to `desiredCount=0` on a clean idle exit
@@ -77,6 +77,15 @@ saved query.
77
77
 
78
78
  Implementation-facing reference for the 7th command: it decides what a just-run `litestream restore` of `opencode.db` means and, on attempt 2 only, may run a recovery strategy against S3. `packages/runner-image/README.md`'s [Strategy/What/Cost table](../runner-image/README.md#when-the-replica-is-unusable-evident_on_unusable_replica) is the operator-facing view of the same command — this section doesn't restate it.
79
79
 
80
+ #### Activity recovery report
81
+
82
+ Reportable recovery outcomes append one versioned JSON line to the session-DB recovery
83
+ report. `evident run` drains that file once after authentication and records the outcome in
84
+ the runner activity log. Healthy boots do not write a report. An initial restore failure writes
85
+ a warning `restore_retried` record before the retry's result is known. Writing is best-effort
86
+ and never changes this command's exit code. The CLI reader owns this record contract: add a new
87
+ `v` rather than repurposing a field.
88
+
80
89
  #### Positionals
81
90
 
82
91
  | Positional | Meaning |
@@ -102,6 +111,14 @@ Implementation-facing reference for the 7th command: it decides what a just-run
102
111
  - Any other `--` token is rejected.
103
112
  - Every rejection exits `2` (`EXIT_USAGE`) with the usage message — **never** a silent fall-back to the destructive default.
104
113
 
114
+ #### Recovery-report flags
115
+
116
+ `--recovery-occurred` marks a later successful restore after this boot already recovered a
117
+ replica, so an empty result is reported as a fresh session database. `--fresh-db-fallback`
118
+ marks a runtime that intentionally has no retry budget and will therefore boot fresh after this
119
+ otherwise-transient failure. Each flag may appear once, does not change the command's exit code,
120
+ and is rejected when repeated.
121
+
105
122
  #### Outcome → exit code
106
123
 
107
124
  | Outcome | Code | When |
@@ -316,6 +333,10 @@ task, and only a `0` — returned solely on a confirmed `desiredCount` of 0 —
316
333
 
317
334
  `session-db-classify` is neither a domain outcome nor a predicate — it's a **third category**: a typed classification with four actionable answers (`0` ran, `32` retry, `31` unusable, `30` fatal), numbered above the two predicates' codes so they can't collide with a future one (`cli.ts:42-47`; see `diagnostics.ts`'s `sessionDbExitCode` for the single outcome → code mapping). The fail-safe direction inverts here: the two predicates above treat an unexpected code as the *safe* answer, but `entrypoint.sh` treats an unexpected code from `session-db-classify` as **fatal** (it `die`s) — correctly, because a boot that can't classify its own replica must not guess about deleting S3 objects.
318
335
 
336
+ `session-db-verify` appends the same best-effort versioned report for a walkback or exhausted
337
+ history. It is drained once by `evident run` into runner activity and does not alter its exit
338
+ code.
339
+
319
340
  `src/shell-contract.json` is the machine-checked source of truth for the command list, and
320
341
  `shell-contract.test.ts` holds **every** shell that speaks it to it — Fargate's
321
342
  `packages/runner-image/entrypoint.sh` and the MicroVM's
@@ -344,6 +365,7 @@ key, so they never appear in `env`'s output or `litestream.yml`.
344
365
  | `CREDS_SYNC_INTERVAL` | Seconds between sync ticks, reported by `env` for the caller's loop. | Falls back to `60`; also on unparseable or non-positive values. |
345
366
  | `EVIDENT_SESSION_DB_WALKBACK_MAX_POINTS` | Distinct restore points `session-db-verify`'s walkback will try before giving up. | Falls back to `10`; also on unparseable or non-positive values. |
346
367
  | `EVIDENT_SESSION_DB_WALKBACK_BUDGET_SECONDS` | Wall-clock budget, in seconds, for the whole walkback loop. | Falls back to `180`; also on unparseable or non-positive values. |
368
+ | `EVIDENT_SESSION_DB_RECOVERY_REPORT` | JSONL report drained once into runner activity after authentication. | `$HOME/.local/state/evident/session-db-recovery.jsonl`; blank values use the default. |
347
369
  | `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` | Presence alone counts as configured model auth (see `model-auth-ready`). | No API-key fallback; `model-auth-ready` then depends solely on the credential files. |
348
370
  | `CLUSTER` † / `SERVICE` † | ECS cluster/service `self-stop` scales to `desiredCount=0`. | Warns "cannot self-stop" and exits `20` (keep the task). Ignored by every other command. |
349
371
  | `EVIDENT_SELFSTOP_ROLE_ARN` † | Role `self-stop` assumes for its ECS calls. | Optional: falls back to the task role's own credentials. A failed/incomplete assume-role → `20`. |
package/dist/cli.js CHANGED
@@ -51907,7 +51907,8 @@ function positiveIntOr(value, defaultValue) {
51907
51907
  return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultValue;
51908
51908
  }
51909
51909
  function nonEmpty(value) {
51910
- return value !== void 0 && value !== "" ? value : null;
51910
+ const trimmed = value?.trim();
51911
+ return trimmed ? trimmed : null;
51911
51912
  }
51912
51913
  function resolveConfig(env4) {
51913
51914
  const homeDir = nonEmpty(env4.HOME);
@@ -51930,6 +51931,7 @@ function resolveConfig(env4) {
51930
51931
  key: persistenceEnabled ? `${prefix}/opencode/auth.json` : null
51931
51932
  },
51932
51933
  opencodeDbPath: `${homeDir}/.local/share/opencode/opencode.db`,
51934
+ sessionDbRecoveryReportPath: nonEmpty(env4.EVIDENT_SESSION_DB_RECOVERY_REPORT) ?? `${homeDir}/.local/state/evident/session-db-recovery.jsonl`,
51933
51935
  bucket: persistenceEnabled ? bucket : null,
51934
51936
  prefix: persistenceEnabled ? prefix : null,
51935
51937
  region: nonEmpty(env4.AWS_REGION),
@@ -52007,20 +52009,20 @@ function sessionDbExitCode(outcome) {
52007
52009
  }
52008
52010
  }
52009
52011
  function describeUnusableReplica(reason) {
52010
- const base = "SESSION-DB-REPLICA-UNUSABLE: booting with a FRESH opencode.db; prior session history is lost until the replica is fixed.";
52012
+ const base2 = "SESSION-DB-REPLICA-UNUSABLE: booting with a FRESH opencode.db; prior session history is lost until the replica is fixed.";
52011
52013
  switch (reason) {
52012
52014
  case "strategy":
52013
- return `${base} (--on-unusable-replica=leave was selected; nothing in S3 was touched.)`;
52015
+ return `${base2} (--on-unusable-replica=leave was selected; nothing in S3 was touched.)`;
52014
52016
  case "attemptsExhausted":
52015
- return `${base} (recovery did not make the replica usable after retrying; giving up.)`;
52017
+ return `${base2} (recovery did not make the replica usable after retrying; giving up.)`;
52016
52018
  case "layoutMismatch":
52017
- return `${base} (objects exist under the replica prefix but NONE of them match the LTX key layout --on-unusable-replica=prune expects \u2014 see the SESSION-DB-REPLICA-LAYOUT-MISMATCH warning above for a sample key. This means our key parsing is wrong, or litestream's on-disk layout changed.)`;
52019
+ return `${base2} (objects exist under the replica prefix but NONE of them match the LTX key layout --on-unusable-replica=prune expects \u2014 see the SESSION-DB-REPLICA-LAYOUT-MISMATCH warning above for a sample key. This means our key parsing is wrong, or litestream's on-disk layout changed.)`;
52018
52020
  case "noL0Present":
52019
- return `${base} (--on-unusable-replica=prune found no level-0 object to prune. This is expected: litestream expires level-0 objects itself after a few minutes, so at boot the newest one is normally absent. prune only ever targets level 0, so it cannot repair corruption at a higher compaction level.)`;
52021
+ return `${base2} (--on-unusable-replica=prune found no level-0 object to prune. This is expected: litestream expires level-0 objects itself after a few minutes, so at boot the newest one is normally absent. prune only ever targets level 0, so it cannot repair corruption at a higher compaction level.)`;
52020
52022
  case "recoveryFailed":
52021
- return `${base} (the recovery delete itself failed; see the warning above for the S3 error.)`;
52023
+ return `${base2} (the recovery delete itself failed; see the warning above for the S3 error.)`;
52022
52024
  case "targetHealthy":
52023
- return `${base} (--on-unusable-replica=prune found a newest L0 object but could not confirm it is corrupt \u2014 it either passed the LTX structural check or could not be re-read \u2014 so it declined to delete it; nothing in S3 was touched.)`;
52025
+ return `${base2} (--on-unusable-replica=prune found a newest L0 object but could not confirm it is corrupt \u2014 it either passed the LTX structural check or could not be re-read \u2014 so it declined to delete it; nothing in S3 was touched.)`;
52024
52026
  default:
52025
52027
  return assertNeverUnusableReplicaReason(reason);
52026
52028
  }
@@ -52138,7 +52140,17 @@ function describeSessionDbVerification(outcome) {
52138
52140
  // src/file-ops.ts
52139
52141
  init_esm_shims();
52140
52142
  import { createHash as createHash8 } from "node:crypto";
52141
- import { chmod, mkdir, open, readFile as readFile5, rename, rm, stat, writeFile as writeFile4 } from "node:fs/promises";
52143
+ import {
52144
+ appendFile,
52145
+ chmod,
52146
+ mkdir,
52147
+ open,
52148
+ readFile as readFile5,
52149
+ rename,
52150
+ rm,
52151
+ stat,
52152
+ writeFile as writeFile4
52153
+ } from "node:fs/promises";
52142
52154
  var nodeFileOps = {
52143
52155
  async mkdirp(dir) {
52144
52156
  await mkdir(dir, { recursive: true });
@@ -52158,6 +52170,9 @@ var nodeFileOps = {
52158
52170
  async writeFile(path, data) {
52159
52171
  await writeFile4(path, data);
52160
52172
  },
52173
+ async appendFile(path, data) {
52174
+ await appendFile(path, data);
52175
+ },
52161
52176
  async createExclusive(path) {
52162
52177
  const handle = await open(path, "wx", 384);
52163
52178
  await handle.close();
@@ -62324,7 +62339,7 @@ async function probeReplica(store, prefix, log) {
62324
62339
  const objectCount = objects.length;
62325
62340
  const totalBytes = objects.reduce((sum, object) => sum + object.size, 0);
62326
62341
  logReplicaSize(root12, objectCount, totalBytes, log);
62327
- return { ok: true, keys: objects.map((object) => object.key) };
62342
+ return { ok: true, objects };
62328
62343
  } catch (error) {
62329
62344
  const detail = describeError(error);
62330
62345
  log(`WARNING: could not list the replica prefix ${root12}: ${detail}`);
@@ -62381,12 +62396,14 @@ async function pruneNewestL0(store, prefix, keys, log) {
62381
62396
  function quarantineRoot(prefix, stamp) {
62382
62397
  return `${prefix}/quarantine/opencode.db/${stamp}/`;
62383
62398
  }
62384
- async function quarantineReplica(store, prefix, keys, log, stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")) {
62399
+ async function quarantineReplica(store, prefix, objects, log, stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")) {
62385
62400
  const destination = quarantineRoot(prefix, stamp);
62386
62401
  const root12 = replicaDbPrefix(prefix);
62387
62402
  const moved = [];
62388
62403
  const failed = [];
62389
- for (const key of keys) {
62404
+ let movedBytes = 0;
62405
+ for (const object of objects) {
62406
+ const { key } = object;
62390
62407
  if (!isDeletableReplicaKey(prefix, key)) {
62391
62408
  log(`WARNING: refusing to quarantine ${key}: outside the replica prefix.`);
62392
62409
  continue;
@@ -62411,9 +62428,10 @@ async function quarantineReplica(store, prefix, keys, log, stamp = (/* @__PURE__
62411
62428
  continue;
62412
62429
  }
62413
62430
  moved.push(key);
62431
+ movedBytes += object.size;
62414
62432
  }
62415
62433
  logQuarantined(destination, moved, failed, log);
62416
- return { destination, moved, failed };
62434
+ return { destination, moved, failed, movedBytes };
62417
62435
  }
62418
62436
  function logQuarantined(destination, moved, failed, log) {
62419
62437
  log(
@@ -62471,7 +62489,7 @@ async function decideSessionDbRestore(params) {
62471
62489
  return { kind: "retryTransient" };
62472
62490
  }
62473
62491
  if (attempt >= 3) {
62474
- return { kind: "unusableReplica", reason: "attemptsExhausted" };
62492
+ return { kind: "unusableReplica", reason: "attemptsExhausted", recovery: null };
62475
62493
  }
62476
62494
  const probe = await probeReplica(store, prefix, log);
62477
62495
  if (!probe.ok) {
@@ -62481,7 +62499,12 @@ async function decideSessionDbRestore(params) {
62481
62499
  detail: `could not verify the session-DB replica is reachable: ${probe.detail}`
62482
62500
  };
62483
62501
  }
62484
- const keys = probe.keys;
62502
+ const keys = probe.objects.map((object) => object.key);
62503
+ const recovery = {
62504
+ replicaObjects: probe.objects.length,
62505
+ replicaBytes: probe.objects.reduce((total, object) => total + object.size, 0),
62506
+ quarantine: null
62507
+ };
62485
62508
  switch (strategy) {
62486
62509
  case "crash":
62487
62510
  return {
@@ -62490,30 +62513,43 @@ async function decideSessionDbRestore(params) {
62490
62513
  detail: "the replica is unusable and --on-unusable-replica=crash was selected; no S3 object was touched"
62491
62514
  };
62492
62515
  case "leave":
62493
- return { kind: "unusableReplica", reason: "strategy" };
62516
+ return { kind: "unusableReplica", reason: "strategy", recovery };
62494
62517
  case "prune": {
62495
62518
  const result = await pruneNewestL0(store, prefix, keys, log);
62496
62519
  if (result.deleted === null) {
62497
62520
  if (shouldEscalateToQuarantine(result.reason, prefix, keys)) {
62498
- const quarantined = await quarantineReplica(store, prefix, keys, log);
62521
+ const quarantined = await quarantineReplica(store, prefix, probe.objects, log);
62499
62522
  if (quarantined.moved.length === 0) {
62500
- return { kind: "unusableReplica", reason: "recoveryFailed" };
62523
+ return { kind: "unusableReplica", reason: "recoveryFailed", recovery };
62501
62524
  }
62502
- return { kind: "recovered", strategy: "quarantine", deleted: quarantined.moved };
62525
+ return {
62526
+ kind: "recovered",
62527
+ strategy: "quarantine",
62528
+ deleted: quarantined.moved,
62529
+ recovery: {
62530
+ ...recovery,
62531
+ quarantine: {
62532
+ destination: quarantined.destination,
62533
+ movedObjects: quarantined.moved.length,
62534
+ failedObjects: quarantined.failed.length,
62535
+ movedBytes: quarantined.movedBytes
62536
+ }
62537
+ }
62538
+ };
62503
62539
  }
62504
62540
  if (result.reason === "deleteFailed") {
62505
- return { kind: "unusableReplica", reason: "recoveryFailed" };
62541
+ return { kind: "unusableReplica", reason: "recoveryFailed", recovery };
62506
62542
  }
62507
62543
  if (result.reason === "unreadable") {
62508
- return { kind: "unusableReplica", reason: "targetHealthy" };
62544
+ return { kind: "unusableReplica", reason: "targetHealthy", recovery };
62509
62545
  }
62510
- return { kind: "unusableReplica", reason: result.reason };
62546
+ return { kind: "unusableReplica", reason: result.reason, recovery };
62511
62547
  }
62512
- return { kind: "recovered", strategy: "prune", deleted: [result.deleted] };
62548
+ return { kind: "recovered", strategy: "prune", deleted: [result.deleted], recovery };
62513
62549
  }
62514
62550
  case "clear": {
62515
62551
  const result = await clearReplica(store, prefix, keys, log);
62516
- return { kind: "recovered", strategy: "clear", deleted: result.deleted };
62552
+ return { kind: "recovered", strategy: "clear", deleted: result.deleted, recovery };
62517
62553
  }
62518
62554
  default:
62519
62555
  return assertNeverStrategy(strategy);
@@ -62533,6 +62569,114 @@ async function discardLocalDebris(fileOps, dbPath, log) {
62533
62569
  }
62534
62570
  }
62535
62571
 
62572
+ // src/session-db-recovery-report.ts
62573
+ init_esm_shims();
62574
+ import { dirname as dirname4 } from "node:path";
62575
+ function base(at2, stage) {
62576
+ return {
62577
+ v: 1,
62578
+ event: "session_db_recovery",
62579
+ at: at2,
62580
+ stage,
62581
+ litestream_exit_code: null,
62582
+ attempt: null,
62583
+ replica_objects: null,
62584
+ replica_bytes: null,
62585
+ quarantine_destination: null,
62586
+ quarantined_objects: null,
62587
+ quarantine_failed_objects: null,
62588
+ quarantined_bytes: null,
62589
+ verified_restore_point: null,
62590
+ restore_points_tried: null
62591
+ };
62592
+ }
62593
+ function recordRestoreOutcome(outcome, at2, exitCode, attempt, recoveryOccurred, freshDbFallback = false) {
62594
+ const record = { ...base(at2, "restore"), litestream_exit_code: exitCode, attempt };
62595
+ if (outcome.kind === "disabled" || outcome.kind === "restored") return null;
62596
+ if (outcome.kind === "retryTransient")
62597
+ return freshDbFallback ? {
62598
+ ...record,
62599
+ outcome: "fresh_session_db",
62600
+ severity: "error",
62601
+ reason: "retry_budget_exhausted"
62602
+ } : {
62603
+ ...record,
62604
+ outcome: "restore_retried",
62605
+ severity: "warning",
62606
+ reason: "initial_restore_failed"
62607
+ };
62608
+ if (outcome.kind === "noReplica") {
62609
+ return recoveryOccurred ? {
62610
+ ...record,
62611
+ outcome: "fresh_session_db",
62612
+ severity: "error",
62613
+ reason: "no_replica_after_recovery"
62614
+ } : null;
62615
+ }
62616
+ if (outcome.kind === "fatal") return null;
62617
+ if (outcome.kind === "unusableReplica")
62618
+ return {
62619
+ ...record,
62620
+ outcome: "fresh_session_db",
62621
+ severity: "error",
62622
+ reason: outcome.reason,
62623
+ replica_objects: outcome.recovery?.replicaObjects ?? null,
62624
+ replica_bytes: outcome.recovery?.replicaBytes ?? null
62625
+ };
62626
+ return {
62627
+ ...record,
62628
+ outcome: "replica_recovered",
62629
+ severity: outcome.strategy === "clear" ? "error" : "warning",
62630
+ reason: outcome.strategy,
62631
+ replica_objects: outcome.recovery.replicaObjects,
62632
+ replica_bytes: outcome.recovery.replicaBytes,
62633
+ quarantine_destination: outcome.recovery.quarantine?.destination ?? null,
62634
+ quarantined_objects: outcome.recovery.quarantine?.movedObjects ?? null,
62635
+ quarantine_failed_objects: outcome.recovery.quarantine?.failedObjects ?? null,
62636
+ quarantined_bytes: outcome.recovery.quarantine?.movedBytes ?? null
62637
+ };
62638
+ }
62639
+ function recordVerifyOutcome(outcome, at2) {
62640
+ const record = base(at2, "verify");
62641
+ if (outcome.kind === "skipped" || outcome.kind === "healthy") return null;
62642
+ if (outcome.kind === "walkedBack")
62643
+ return {
62644
+ ...record,
62645
+ outcome: "history_rolled_back",
62646
+ severity: "warning",
62647
+ reason: "walkback",
62648
+ verified_restore_point: outcome.txid,
62649
+ restore_points_tried: outcome.candidatesTried
62650
+ };
62651
+ return {
62652
+ ...record,
62653
+ outcome: "fresh_session_db",
62654
+ severity: "error",
62655
+ reason: outcome.reason,
62656
+ restore_points_tried: outcome.candidatesTried
62657
+ };
62658
+ }
62659
+ var MAX_REPORT_BYTES = 64 * 1024;
62660
+ async function appendSessionDbRecoveryRecord(record, path, fileOps, log) {
62661
+ try {
62662
+ const line = Buffer.from(`${JSON.stringify(record)}
62663
+ `);
62664
+ const existing = await fileOps.stat(path);
62665
+ if ((existing?.size ?? 0) + line.length > MAX_REPORT_BYTES) {
62666
+ log(
62667
+ `WARNING: session-DB recovery report at ${path} reached its ${MAX_REPORT_BYTES}-byte limit`
62668
+ );
62669
+ return;
62670
+ }
62671
+ await fileOps.mkdirp(dirname4(path));
62672
+ await fileOps.appendFile(path, line);
62673
+ } catch (error) {
62674
+ log(
62675
+ `WARNING: could not record the session-DB recovery outcome for the runner's activity log at ${path}: ${describeError(error)}`
62676
+ );
62677
+ }
62678
+ }
62679
+
62536
62680
  // src/session-db-verify.ts
62537
62681
  init_esm_shims();
62538
62682
 
@@ -62857,7 +63001,8 @@ var USAGE = `Usage: runner-synchroniser <command> [args]
62857
63001
  self-stop scale this agent's own ECS service to 0; exit 0 only
62858
63002
  when desiredCount is confirmed 0, ${EXIT_KEEP_TASK} to keep the task
62859
63003
  session-db-classify <litestream-restore-exit-code> <attempt>
62860
- [--on-unusable-replica=<prune|leave|clear|crash>]
63004
+ [--on-unusable-replica=<prune|leave|clear|crash>] [--recovery-occurred]
63005
+ [--fresh-db-fallback]
62861
63006
  classify a just-run \`litestream restore\` of opencode.db;
62862
63007
  exit 0 restored/no-replica/disabled, ${EXIT_SESSION_DB_RETRY} re-run
62863
63008
  and ask again, ${EXIT_SESSION_DB_UNUSABLE} unusable (booted fresh,
@@ -62887,16 +63032,26 @@ function parseStoreName(value) {
62887
63032
  return value === "claude" || value === "opencode" ? value : null;
62888
63033
  }
62889
63034
  var ON_UNUSABLE_REPLICA_FLAG = "--on-unusable-replica=";
63035
+ var RECOVERY_OCCURRED_FLAG = "--recovery-occurred";
63036
+ var FRESH_DB_FALLBACK_FLAG = "--fresh-db-fallback";
62890
63037
  function parseNonNegativeInt(value) {
62891
63038
  return /^\d+$/.test(value) ? Number.parseInt(value, 10) : null;
62892
63039
  }
62893
63040
  function parseSessionDbClassifyArgs(args) {
62894
63041
  let strategyRaw;
63042
+ let recoveryOccurred = false;
63043
+ let freshDbFallback = false;
62895
63044
  const positional = [];
62896
63045
  for (const arg of args) {
62897
63046
  if (arg.startsWith(ON_UNUSABLE_REPLICA_FLAG)) {
62898
63047
  if (strategyRaw !== void 0) return null;
62899
63048
  strategyRaw = arg.slice(ON_UNUSABLE_REPLICA_FLAG.length);
63049
+ } else if (arg === RECOVERY_OCCURRED_FLAG) {
63050
+ if (recoveryOccurred) return null;
63051
+ recoveryOccurred = true;
63052
+ } else if (arg === FRESH_DB_FALLBACK_FLAG) {
63053
+ if (freshDbFallback) return null;
63054
+ freshDbFallback = true;
62900
63055
  } else if (arg.startsWith("--")) {
62901
63056
  return null;
62902
63057
  } else {
@@ -62908,7 +63063,7 @@ function parseSessionDbClassifyArgs(args) {
62908
63063
  const attempt = parseNonNegativeInt(positional[1]);
62909
63064
  const strategy = parseUnusableReplicaStrategy(strategyRaw);
62910
63065
  if (exitCode === null || attempt === null || attempt < 1 || strategy === null) return null;
62911
- return { exitCode, attempt, strategy };
63066
+ return { exitCode, attempt, strategy, recoveryOccurred, freshDbFallback };
62912
63067
  }
62913
63068
  function renderEnv(config) {
62914
63069
  return [
@@ -62996,6 +63151,16 @@ async function commandSessionDbClassify(args, config, deps) {
62996
63151
  log
62997
63152
  });
62998
63153
  log(describeSessionDbClassification(outcome, { bucket: config.bucket, region: config.region }));
63154
+ const record = recordRestoreOutcome(
63155
+ outcome,
63156
+ (deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
63157
+ args.exitCode,
63158
+ args.attempt,
63159
+ args.recoveryOccurred,
63160
+ args.freshDbFallback
63161
+ );
63162
+ if (record !== null)
63163
+ await appendSessionDbRecoveryRecord(record, config.sessionDbRecoveryReportPath, fileOps, log);
62999
63164
  return sessionDbExitCode(outcome);
63000
63165
  }
63001
63166
  async function commandSessionDbVerify(litestreamConfigPath, config, deps) {
@@ -63014,6 +63179,9 @@ async function commandSessionDbVerify(litestreamConfigPath, config, deps) {
63014
63179
  budgetSeconds: config.walkbackBudgetSeconds
63015
63180
  });
63016
63181
  log(describeSessionDbVerification(outcome));
63182
+ const record = recordVerifyOutcome(outcome, (deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString());
63183
+ if (record !== null)
63184
+ await appendSessionDbRecoveryRecord(record, config.sessionDbRecoveryReportPath, fileOps, log);
63017
63185
  return sessionDbVerifyExitCode(outcome);
63018
63186
  }
63019
63187
  async function main(argv, deps) {
@@ -63055,7 +63223,7 @@ ${USAGE}`);
63055
63223
  const args = parseSessionDbClassifyArgs(rest);
63056
63224
  if (args === null) {
63057
63225
  log(
63058
- `'session-db-classify' needs <litestream-restore-exit-code> <attempt> and an optional --on-unusable-replica=<prune|leave|clear|crash>.
63226
+ `'session-db-classify' needs <litestream-restore-exit-code> <attempt> and an optional --on-unusable-replica=<prune|leave|clear|crash>, --recovery-occurred, and --fresh-db-fallback.
63059
63227
  ${USAGE}`
63060
63228
  );
63061
63229
  return EXIT_USAGE;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evident-ai/runner-synchroniser",
3
- "version": "3.4.0",
3
+ "version": "3.4.1-dev.7802262",
4
4
  "description": "Restores and syncs the Evident runner's OpenCode credential stores (and litestream config) to an object store, so a runner survives task replacement with almost no state loss.",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",