@evident-ai/runner-synchroniser 3.4.1-dev.aa71e9a → 3.4.1-dev.b2ce99d

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 +48 -30
  2. package/dist/cli.js +33862 -34653
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -56,11 +56,11 @@ build time.
56
56
  | `env` | resolved config as shell-eval'able vars | `0` = ran; non-zero = tool broken |
57
57
  | `litestream-config` | the generated `litestream.yml` | `0` = ran; non-zero = tool broken |
58
58
  | `restore <claude\|opencode>` | — | `0` = ran; non-zero = tool broken |
59
- | `sync-once <claude\|opencode>` | — | `0` = ran; non-zero = tool broken |
59
+ | `sync-once <claude\|opencode>` | — | `0` = uploaded/unchanged/absent/disabled; `40` = not persisted; other = 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>] [--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
- | `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 |
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, replicates into the existing prefix), `30` = fatal; other = usage/tool broken |
63
+ | `session-db-verify <litestream-config-path>` | — | `0` = healthy/skipped/walked-back, `33` = exhausted after verified separation and local disposal, `34` = separation or disposal could not be proven; 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
66
66
  (see Configuration for `CLUSTER`/`SERVICE`/`EVIDENT_SELFSTOP_ROLE_ARN`). It exits `0`
@@ -75,7 +75,7 @@ saved query.
75
75
 
76
76
  ### `session-db-classify`
77
77
 
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.
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. `runner/docker-images/fargate/README.md`'s [Strategy/What/Cost table](../docker-images/fargate/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
80
  #### Activity recovery report
81
81
 
@@ -86,6 +86,13 @@ a warning `restore_retried` record before the retry's result is known. Writing i
86
86
  and never changes this command's exit code. The CLI reader owns this record contract: add a new
87
87
  `v` rather than repurposing a field.
88
88
 
89
+ MicroVM boot-shell give-ups use that same JSONL contract and path. The hook truncates the
90
+ report at the start of each `/run`, so a later startup decision supersedes an earlier one;
91
+ `/resume` preserves the current report. Their `replication_suspended` field is `true`, meaning
92
+ that start is not backing up its new session history. The normal mapper defaults the field to
93
+ `false`; the MicroVM `--fresh-db-fallback` outcome is also `true` because it cannot safely
94
+ replicate that boot.
95
+
89
96
  #### Positionals
90
97
 
91
98
  | Positional | Meaning |
@@ -119,6 +126,9 @@ marks a runtime that intentionally has no retry budget and will therefore boot f
119
126
  otherwise-transient failure. Each flag may appear once, does not change the command's exit code,
120
127
  and is rejected when repeated.
121
128
 
129
+ On the MicroVM's one-attempt path, `--fresh-db-fallback` also means the fresh database does not
130
+ replicate during that boot.
131
+
122
132
  #### Outcome → exit code
123
133
 
124
134
  | Outcome | Code | When |
@@ -138,6 +148,9 @@ and is rejected when repeated.
138
148
  | `fatal(misconfig)` | `30` | no object store while persistence is enabled, or the probe failed on attempt 2 |
139
149
  | `fatal(deliberate)` | `30` | attempt 2, `crash` |
140
150
 
151
+ Fatal exit codes 30 and 34 are reported directly to `POST /v1/runners/self/startup-failure`
152
+ before the CLI starts. Reporting is best-effort and never changes the exit code.
153
+
141
154
  `31` also guarantees local debris (`opencode.db`, `-wal`, `-shm`) is discarded, one `try` per path, at the classifier's single return point. Note `32` isn't purely "transient" — `recovered` shares it with `retryTransient` even though a `recovered` outcome already mutated S3.
142
155
 
143
156
  The probe is a `list`, not a `get`: S3 answers a wrong bucket name with `NoSuchBucket`, also a 404, so a `get`-based probe would read a misconfigured bucket as healthy and unlock recovery against it.
@@ -213,11 +226,11 @@ what it is.
213
226
  #### The four `--on-unusable-replica` strategies
214
227
 
215
228
  - **`prune` (default)** — ≤1 guarded delete iff the newest L0's own bytes fail the LTX header check (`length >= 100` and magic `"LTX1"`) → `recovered`/`32`; a failed delete (`recoveryFailed`) or a declined escalation → `31`. **Header-only** check: a valid header with deeper corruption is judged sound and left alone by the *delete* — and since #1106 that is no longer the end of the road. When prune finds nothing to delete (`targetHealthy`/`noL0Present`) **and** at least one key parses as an LTX object, it **escalates to quarantine**: every object under `<prefix>/opencode.db/` is moved to `<prefix>/quarantine/opencode.db/<timestamp>/` → `recovered(quarantine)`/`32`. It does **not** escalate on `layoutMismatch` (we don't understand the layout), on an unreadable object (absent evidence), or on an empty prefix. Never prunes twice.
216
- - **`leave`** — zero S3 mutation → `31`. Cost is "prior history lost this boot". Since #1106 it is **not** also ephemeral: the boot still starts a fresh backup chain, because the classifier guarantees the local DB is fresh rather than half-restored. Upside: replica left byte-intact for forensics, no crash loop.
229
+ - **`leave`** — zero S3 mutation → `31`. Cost is "prior history lost this boot". The fresh local DB replicates into the existing prefix; no new chain is started. Upside: the runner still comes online without a crash loop.
217
230
  - **`clear`** — deletes every key under `<prefix>/opencode.db/` passing `isDeletableReplicaKey`; that guard, not the `list()` prefix, is the boundary (IAM grants `s3:DeleteObject*` bucket-wide). One `try` per key. A **partial** clear still reports `recovered`/`32`. Cost: all saved history, unconditionally.
218
231
  - **`crash`** — first in the switch, no S3 mutation even considered → `fatal(deliberate)`/`30` → `entrypoint.sh` `die`s → task replaced → **crash loop** until an operator intervenes.
219
232
 
220
- #### The measured real-world key layout (litestream 0.5.13)
233
+ #### The measured real-world key layout (litestream 0.5.13 historical sample)
221
234
 
222
235
  `parseLtxKey` (`src/replica-keys.ts`) expects
223
236
  `<prefix>/opencode.db/<level:04d>/<minTxid>-<maxTxid>.ltx` — **no `ltx/` path segment**,
@@ -235,10 +248,9 @@ at boot the newest L0 is normally *absent* (`noL0Present`) or freshly written an
235
248
  (`targetHealthy`) — `prune`'s real-world reach is narrower than the code alone suggests,
236
249
  and it can never repair corruption at a higher compaction level.
237
250
 
238
- `EVIDENT_ON_UNUSABLE_REPLICA` is a `packages/runner-image` (entrypoint) variable translated into the flag above at boot — this CLI never reads it, so it has no row in this README's Configuration table below; see [runner-image's README](../runner-image/README.md#when-the-replica-is-unusable-evident_on_unusable_replica). An unrecognised value (including wrong casing, e.g. `Prune`) exits `2`, outside `entrypoint.sh`'s `0|10|20|30|31|32|33` allow-list — a typo **crash-loops the task on attempt 1** rather than falling back to `prune`, and also logs the misleading "credential persistence is DEGRADED" ERROR.
251
+ `EVIDENT_ON_UNUSABLE_REPLICA` is a `runner/docker-images/fargate` (entrypoint) variable translated into the flag above at boot — this CLI never reads it, so it has no row in this README's Configuration table below; see [the Fargate image README](../docker-images/fargate/README.md#when-the-replica-is-unusable-evident_on_unusable_replica). An unrecognised value (including wrong casing, e.g. `Prune`) exits `2`, outside `entrypoint.sh`'s `0|10|20|30|31|32|33` allow-list — a typo **crash-loops the task on attempt 1** rather than falling back to `prune`, and also logs the misleading "credential persistence is DEGRADED" ERROR.
239
252
 
240
- See `specs/local-runner.feature`'s "Recovering session history at startup" scenarios for
241
- the behavioural anchor (31 comes online, 30 does not).
253
+ The behavioural anchor is explicit: **31 comes online, 30 does not**.
242
254
 
243
255
  ### `session-db-verify`
244
256
 
@@ -262,9 +274,12 @@ nothing had ever run a real `PRAGMA integrity_check`).
262
274
  each into a scratch path and integrity-checking it there. The first one that passes
263
275
  is adopted — renamed over `opencode.db`, replacing it and its stale sidecars.
264
276
  3. If nothing retained passes (or there's nothing to try, or the search runs out of
265
- time/points), the corrupt local DB is discarded and the boot proceeds with a
266
- genuinely fresh one exactly `session-db-classify`'s `31` guarantee, so
267
- `litestream replicate` starting against it afterwards is just as safe.
277
+ time/points), the active replica prefix is separated first: its history is moved aside,
278
+ then the prefix is re-listed as empty before the corrupt local DB and sidecars are
279
+ discarded. Only both proofs permit a fresh boot. Otherwise exit `34` stops the boot before
280
+ OpenCode or Litestream starts; the backup history remains readable at its quarantine
281
+ destination or original key, and surviving local files remain in place without a process
282
+ opening or writing them.
268
283
 
269
284
  A walked-back boot keeps replicating into the **same** replica — no S3 mutation, no new
270
285
  prefix. litestream re-bases to the replica's high-water mark and continues the txid
@@ -279,10 +294,8 @@ against S3 at the ~1 GB scale a production replica can reach.)
279
294
  | `skipped` | `0` | local DB absent or zero bytes — nothing to verify |
280
295
  | `healthy` | `0` | the local DB passed its integrity check as-is |
281
296
  | `walkedBack` | `0` | the local DB failed, but an older retained restore point passed and was adopted |
282
- | `exhausted` (`noCandidates`) | `33` | the local DB failed and litestream retains no older restore point |
283
- | `exhausted` (`allFailed`) | `33` | every retained restore point was tried and every one failed |
284
- | `exhausted` (`budgetExhausted`) | `33` | the search stopped on `EVIDENT_SESSION_DB_WALKBACK_MAX_POINTS`/`_BUDGET_SECONDS` before trying every retained point |
285
- | `exhausted` (`enumerationFailed`) | `33` | `litestream ltx` could not be read (bad exit, non-JSON, malformed listing) |
297
+ | `exhausted` (`noCandidates`, `allFailed`, `budgetExhausted`, or `enumerationFailed`) | `33` | the active prefix was proven separated (or persistence is disabled) **and** the corrupt local DB and sidecars were proven discarded |
298
+ | `exhausted` | `34` | either the replica separation or local discard could not be proven; the boot stops before OpenCode and Litestream start |
286
299
 
287
300
  `healthy`/`walkedBack`/`skipped` all share exit `0` deliberately: the shell's behaviour
288
301
  afterwards — start `litestream replicate` — is identical either way, so a further split
@@ -301,7 +314,10 @@ before looking at everything" would report the wrong conclusion to the operator.
301
314
  | `INFO: SESSION-DB-WALKBACK-ADOPTED` | `adopt` (`session-db-verify.ts`) | A candidate passes and is adopted over `opencode.db` | `txid`, `candidates`, `bytes` |
302
315
  | `SESSION-DB-INTEGRITY` | `describeSessionDbVerification` (`diagnostics.ts`), logged once per invocation as the final outcome | The local DB passed its check as-is | `bytes` |
303
316
  | `SESSION-DB-INTEGRITY-WALKBACK` | `describeSessionDbVerification` (`diagnostics.ts`), logged once per invocation as the final outcome | Same event as `SESSION-DB-WALKBACK-ADOPTED` above, restated as the command's outcome | `txid`, `candidates`, `bytes` |
304
- | `SESSION-DB-INTEGRITY-EXHAUSTED` | `describeSessionDbVerification` (`diagnostics.ts`), logged once per invocation as the final outcome | Nothing retained passed (any `exhausted` reason); the boot proceeds with a fresh DB | `candidatesTried` (in prose, not `k=v`) |
317
+ | `SESSION-DB-INTEGRITY-EXHAUSTED` | `describeSessionDbVerification` (`diagnostics.ts`), logged once per invocation as the final outcome | Nothing retained passed; a fresh boot follows only when both separation and local disposal were proven | `candidatesTried` (in prose, not `k=v`) |
318
+ | `SESSION-DB-REPLICA-SEPARATED` | `separateCorruptReplica` (`replica-recovery.ts`) | The active prefix was re-listed empty after separation | replica root and quarantine destination |
319
+ | `SESSION-DB-REPLICA-SEPARATION-UNVERIFIED` | `separateCorruptReplica` (`replica-recovery.ts`) | A separation could not be proven | remaining objects or list error |
320
+ | `SESSION-DB-LOCAL-DISCARD-FAILED` | `verifySessionDb` (`session-db-verify.ts`) | The local DB or a sidecar could not be proven gone | removed and surviving paths |
305
321
 
306
322
  The first four fire only while the walkback loop runs; the last three are always the
307
323
  one message this command logs as its own final word on the outcome — `entrypoint.sh`
@@ -310,15 +326,16 @@ same way it already reuses `SESSION-DB-REPLICA-UNUSABLE` for `session-db-classif
310
326
 
311
327
  ### Why the contract is asymmetric
312
328
 
313
- `restore` and `sync-once` **never** report a domain outcome through the exit code. A
314
- missing remote object, a corrupt local file, a failed upload, an IAM denial — all of
315
- those are logged and still exit `0`. None of them should abort a boot: a runner with no
316
- credentials yet is a runner a human can still log into.
329
+ `restore` logs domain outcomes and still exits `0`: a missing remote object or a
330
+ restore failure should not abort a boot, because a runner with no credentials yet
331
+ is a runner a human can still log into. `sync-once` returns `40` when the current
332
+ credential file is not persisted (`failed`, `hashFailed`, or `localInvalid`), while
333
+ `absent` and `disabled` remain legitimate `0` outcomes.
317
334
 
318
- The consequence is the point: **any non-zero status from `restore`/`sync-once` means the
319
- tool itself broke** — bad arguments (`2`), an uncaught throw (`1`), or a bundle that
320
- would not run at all. The shell needs no case analysis to know something is wrong, so
321
- its "credential persistence is DEGRADED" error lives in exactly one helper.
335
+ The consequence is the point: **any non-zero status from `restore` means the tool itself
336
+ broke** — and `sync-once` uses `40` for its typed persistence answer; other non-zero
337
+ statuses still mean bad arguments (`2`), an uncaught throw (`1`), or a bundle that would
338
+ not run at all. The shell keeps that distinction in one helper.
322
339
 
323
340
  The **two predicates** — `model-auth-ready` and `self-stop` — each need to distinguish
324
341
  "the answer is no" from "the tool is broken", so each answers no with its own dedicated
@@ -334,13 +351,14 @@ task, and only a `0` — returned solely on a confirmed `desiredCount` of 0 —
334
351
  `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.
335
352
 
336
353
  `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.
354
+ history. A refused boot records `session_db_boot_refused`, but `evident run` has not started to
355
+ drain it, so operators see that refusal in boot logs and the restart loop. The report does not
356
+ alter the command's exit code.
339
357
 
340
358
  `src/shell-contract.json` is the machine-checked source of truth for the command list, and
341
359
  `shell-contract.test.ts` holds **every** shell that speaks it to it — Fargate's
342
- `packages/runner-image/entrypoint.sh` and the MicroVM's
343
- `packages/runner-cdk/microvm-image/hooks` (#608), discovered by grep so a third one
360
+ `runner/docker-images/fargate/entrypoint.sh` and the MicroVM's
361
+ `runner/docker-images/microvm/hooks` (#608), discovered by grep so a third one
344
362
  cannot go unchecked. Each shell must: call only subcommands the CLI implements (and, for
345
363
  `entrypoint.sh`, call all of them); route every call through one `run_synchroniser`; report
346
364
  a broken tool; and take each answer code the commands it calls can return **silently and by
@@ -370,7 +388,7 @@ key, so they never appear in `env`'s output or `litestream.yml`.
370
388
  | `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. |
371
389
  | `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`. |
372
390
 
373
- `packages/runner-image/README.md` documents these same three from the deployment side —
391
+ `runner/docker-images/fargate/README.md` documents these same three from the deployment side —
374
392
  keep them in sync.
375
393
 
376
394
  Requiring **both** `LITESTREAM_BUCKET` and `LITESTREAM_PREFIX` (never just one) means