@evident-ai/runner-synchroniser 0.1.1-dev.9639f77 → 0.1.1-dev.96bc2ed
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 +84 -5
- package/dist/cli.js +7 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ version at all.
|
|
|
62
62
|
| `sync-once <claude\|opencode>` | — | `0` = ran; non-zero = tool broken |
|
|
63
63
|
| `model-auth-ready` | — | `0` = ready, `10` = not ready; other = tool broken |
|
|
64
64
|
| `self-stop` | — | `0` = stopped, `20` = keep the task; other = tool broken |
|
|
65
|
+
| `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, skip replicate), `30` = fatal; other = usage/tool broken |
|
|
65
66
|
|
|
66
67
|
`self-stop` scales this agent's own ECS service to `desiredCount=0` on a clean idle exit
|
|
67
68
|
(see Configuration for `CLUSTER`/`SERVICE`/`EVIDENT_SELFSTOP_ROLE_ARN`). It exits `0`
|
|
@@ -74,6 +75,66 @@ broader: `src/logger.ts` marks it verbatim because operator greps and CloudWatch
|
|
|
74
75
|
match on that exact string, so widening what it labels is cheaper than breaking every
|
|
75
76
|
saved query.
|
|
76
77
|
|
|
78
|
+
### `session-db-classify`
|
|
79
|
+
|
|
80
|
+
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.
|
|
81
|
+
|
|
82
|
+
#### Positionals
|
|
83
|
+
|
|
84
|
+
| Positional | Meaning |
|
|
85
|
+
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
86
|
+
| `<litestream-restore-exit-code>` | Status of the `litestream restore` the shell just ran. `0` → stat the local DB; any non-zero → the recovery ladder below. The value itself is never inspected (a Go panic `2` is treated exactly like a `1`). |
|
|
87
|
+
| `<attempt>` | 1-based (`/^\d+$/`, `>= 1`) — the sole mutation gate. |
|
|
88
|
+
|
|
89
|
+
#### The attempt ladder
|
|
90
|
+
|
|
91
|
+
| Attempt | Behaviour |
|
|
92
|
+
| ------- | ---------------------------------------------------------------- |
|
|
93
|
+
| 1 | Plain retry — **zero S3 calls, not even the probe**. |
|
|
94
|
+
| 2 | The **only** attempt that may probe or mutate S3. |
|
|
95
|
+
| ≥ 3 | Gives up **before** the probe. |
|
|
96
|
+
|
|
97
|
+
`entrypoint.sh` caps its retry loop at a hard-coded `3` as a real second bound: the classifier is stateless per invocation, so nothing except that loop stops attempt 2 from being asked twice.
|
|
98
|
+
|
|
99
|
+
#### `--on-unusable-replica` flag syntax
|
|
100
|
+
|
|
101
|
+
- Only the `=`-joined form (`--on-unusable-replica=leave`); the space-separated form is rejected.
|
|
102
|
+
- Position-free.
|
|
103
|
+
- Repeating the flag is rejected, even with the same value both times.
|
|
104
|
+
- Any other `--` token is rejected.
|
|
105
|
+
- Every rejection exits `2` (`EXIT_USAGE`) with the usage message — **never** a silent fall-back to the destructive default.
|
|
106
|
+
|
|
107
|
+
#### Outcome → exit code
|
|
108
|
+
|
|
109
|
+
| Outcome | Code | When |
|
|
110
|
+
| ----------------------------------------------- | ---- | -------------------------------------------------------------------------------- |
|
|
111
|
+
| `disabled` | `0` | bucket or prefix unset — short-circuits **before** the exit code is examined |
|
|
112
|
+
| `restored` | `0` | restore exit `0` and the local DB is non-empty |
|
|
113
|
+
| `noReplica` | `0` | restore exit `0` but the DB is absent or zero-byte |
|
|
114
|
+
| `retryTransient` | `32` | restore failed, attempt 1 — no probe, no S3 call at all |
|
|
115
|
+
| `recovered` | `32` | attempt 2, a successful `prune` (1 key) or any `clear` (including a partial one) |
|
|
116
|
+
| `unusableReplica(strategy)` | `31` | attempt 2, `leave` |
|
|
117
|
+
| `unusableReplica(attemptsExhausted)` | `31` | attempt ≥ 3, before the probe |
|
|
118
|
+
| `unusableReplica(targetHealthy\|layoutMismatch)` | `31` | attempt 2, `prune` declined to delete anything |
|
|
119
|
+
| `unusableReplica(recoveryFailed)` | `31` | attempt 2, `prune`'s delete itself threw |
|
|
120
|
+
| `fatal(misconfig)` | `30` | no object store while persistence is enabled, or the probe failed on attempt 2 |
|
|
121
|
+
| `fatal(deliberate)` | `30` | attempt 2, `crash` |
|
|
122
|
+
|
|
123
|
+
`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.
|
|
124
|
+
|
|
125
|
+
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.
|
|
126
|
+
|
|
127
|
+
#### The four `--on-unusable-replica` strategies
|
|
128
|
+
|
|
129
|
+
- **`prune` (default)** — ≤1 guarded delete iff the newest L0's own bytes fail the LTX header check (`length >= 100` and magic `"LTX1"`) → `recovered`/`32`; declined (`targetHealthy`/`layoutMismatch`) or a failed delete (`recoveryFailed`) → `31`. **Header-only** check: a valid header with deeper corruption is judged sound and left alone — the real trade-off, and **not** "can destroy a healthy L0 object when an older one is corrupt" (false of merged `main`: the delete is unreachable unless the object's own bytes fail the check). `targetHealthy` also covers an unreadable object (absent evidence must not unlock a delete). Never escalates to `clear`, never prunes twice.
|
|
130
|
+
- **`leave`** — zero S3 mutation → `31`. Cost is not just "prior history lost": `entrypoint.sh` also skips `litestream replicate` for the **whole boot**, so this boot's history is ephemeral too. Upside: replica left byte-intact for forensics, no crash loop.
|
|
131
|
+
- **`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.
|
|
132
|
+
- **`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.
|
|
133
|
+
|
|
134
|
+
`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` 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.
|
|
135
|
+
|
|
136
|
+
See `specs/local-runner.feature:49-70` for the behavioural anchor (31 comes online, 30 does not).
|
|
137
|
+
|
|
77
138
|
### Why the contract is asymmetric
|
|
78
139
|
|
|
79
140
|
`restore` and `sync-once` **never** report a domain outcome through the exit code. A
|
|
@@ -97,6 +158,8 @@ that it has no auth. `self-stop` decides whether a task may go away, so a broken
|
|
|
97
158
|
must never kill one: `entrypoint.sh` reads any non-zero exactly like `20` and keeps the
|
|
98
159
|
task, and only a `0` — returned solely on a confirmed `desiredCount` of 0 — lets it stop.
|
|
99
160
|
|
|
161
|
+
`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.
|
|
162
|
+
|
|
100
163
|
`src/shell-contract.json` is the machine-checked source of truth for the command list, and
|
|
101
164
|
`shell-contract.test.ts` holds **every** shell that speaks it to it — Fargate's
|
|
102
165
|
`packages/runner-image/entrypoint.sh` and the MicroVM's
|
|
@@ -142,10 +205,23 @@ The paths and keys that follow from the bucket/prefix:
|
|
|
142
205
|
| OpenCode DB | `$HOME/.local/share/opencode/opencode.db` | `<prefix>/opencode.db` (litestream) |
|
|
143
206
|
|
|
144
207
|
The session database is replicated by litestream itself; this package only generates its
|
|
145
|
-
config. `env` emits `
|
|
208
|
+
config. `env` emits `PERSISTENCE_BUCKET`, `CLAUDE_CREDS`, `OPENCODE_DB_PATH` and
|
|
146
209
|
`CREDS_SYNC_INTERVAL`, each single-quoted so a value containing shell metacharacters is
|
|
147
210
|
inert when `eval`'d.
|
|
148
211
|
|
|
212
|
+
`entrypoint.sh` asserts, right after it `eval`s this output, that each key is *defined*
|
|
213
|
+
(not merely non-empty) and splits the four into two groups: a key is **required** — the
|
|
214
|
+
shell dies naming it — if it has no safe degradation; it is **tolerated** — the shell logs
|
|
215
|
+
a WARNING naming it and carries on — if it does. `OPENCODE_DB_PATH` and
|
|
216
|
+
`CREDS_SYNC_INTERVAL` are required: both are dereferenced for real work (deleting/creating
|
|
217
|
+
the session DB, sizing the sync-loop sleep), so a guessed value would be actively wrong,
|
|
218
|
+
not merely absent. `PERSISTENCE_BUCKET` and `CLAUDE_CREDS` are tolerated: an empty
|
|
219
|
+
`PERSISTENCE_BUCKET` is already the designed "persistence disabled" state, and
|
|
220
|
+
`CLAUDE_CREDS` is dereferenced only in operator-facing message text. If you change
|
|
221
|
+
`renderEnv` to make a required key conditional, the entrypoint will brick every container
|
|
222
|
+
running that version — the runner installs this package from a floating npm tag (`dev` /
|
|
223
|
+
`latest`), so the shell and the CLI can genuinely be different versions (#779, #821).
|
|
224
|
+
|
|
149
225
|
## Behaviour worth knowing
|
|
150
226
|
|
|
151
227
|
- **A non-empty local file that is not valid JSON is protected**, never overwritten and
|
|
@@ -170,11 +246,14 @@ inert when `eval`'d.
|
|
|
170
246
|
|
|
171
247
|
## The `ObjectStore` port
|
|
172
248
|
|
|
173
|
-
`restore` and `
|
|
174
|
-
the object is absent,
|
|
175
|
-
|
|
249
|
+
`restore`/`sync` and `session-db-classify` talk to a four-method interface: `get(key)`
|
|
250
|
+
returning `null` when the object is absent, `put(key, body)`, `list(prefix)` and
|
|
251
|
+
`delete(key)` for session-DB recovery. `list` throws on any error and never maps one to
|
|
252
|
+
`[]` (an empty array means "reached the store, nothing there"); the adapter owns
|
|
253
|
+
pagination, and `delete` is idempotent. S3 vocabulary (`@aws-sdk/client-s3`, the typed
|
|
254
|
+
`NoSuchKey`/`NotFound` errors) is confined to the single adapter in
|
|
176
255
|
`src/s3-object-store.ts`, so the storage backend can be swapped without touching the
|
|
177
|
-
restore/sync logic.
|
|
256
|
+
restore/sync/classify logic.
|
|
178
257
|
|
|
179
258
|
The S3 adapter is the only one that exists. It uses no static credentials — auth is
|
|
180
259
|
whatever the ambient AWS credential chain resolves (on the Evident runner, the ECS task
|
package/dist/cli.js
CHANGED
|
@@ -50104,9 +50104,13 @@ function parseSessionDbClassifyArgs(args) {
|
|
|
50104
50104
|
}
|
|
50105
50105
|
function renderEnv(config) {
|
|
50106
50106
|
return [
|
|
50107
|
-
//
|
|
50108
|
-
//
|
|
50109
|
-
|
|
50107
|
+
// The bucket NAME, kept (not reduced to a boolean) so an operator can
|
|
50108
|
+
// read it; empty exactly when persistence is disabled (both
|
|
50109
|
+
// LITESTREAM_BUCKET and LITESTREAM_PREFIX must be set — see config.ts's
|
|
50110
|
+
// persistenceEnabled). entrypoint.sh gates BOTH the credential-sync
|
|
50111
|
+
// loop and whether it starts litestream replicate on this being
|
|
50112
|
+
// non-empty.
|
|
50113
|
+
`PERSISTENCE_BUCKET=${shellQuote(config.bucket ?? "")}`,
|
|
50110
50114
|
`CLAUDE_CREDS=${shellQuote(config.claude.path)}`,
|
|
50111
50115
|
`OPENCODE_DB_PATH=${shellQuote(config.opencodeDbPath)}`,
|
|
50112
50116
|
`CREDS_SYNC_INTERVAL=${shellQuote(String(config.syncIntervalSeconds))}`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evident-ai/runner-synchroniser",
|
|
3
|
-
"version": "0.1.1-dev.
|
|
3
|
+
"version": "0.1.1-dev.96bc2ed",
|
|
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",
|