@evident-ai/runner-synchroniser 3.4.1-dev.74a16b2 → 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.
- package/README.md +23 -1
- package/dist/cli.js +195 -27
- 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
|
-
|
|
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
|
|
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 `${
|
|
52015
|
+
return `${base2} (--on-unusable-replica=leave was selected; nothing in S3 was touched.)`;
|
|
52014
52016
|
case "attemptsExhausted":
|
|
52015
|
-
return `${
|
|
52017
|
+
return `${base2} (recovery did not make the replica usable after retrying; giving up.)`;
|
|
52016
52018
|
case "layoutMismatch":
|
|
52017
|
-
return `${
|
|
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 `${
|
|
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 `${
|
|
52023
|
+
return `${base2} (the recovery delete itself failed; see the warning above for the S3 error.)`;
|
|
52022
52024
|
case "targetHealthy":
|
|
52023
|
-
return `${
|
|
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 {
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
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.1-dev.
|
|
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",
|