@evident-ai/runner-synchroniser 0.1.1-dev.0d89dd3 → 0.1.1-dev.1e72251
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 +124 -10
- package/dist/cli.js +411 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,11 +32,25 @@ npx @evident-ai/runner-synchroniser@dev env
|
|
|
32
32
|
`dist/cli.js` is a single self-contained ESM bundle with **no runtime dependencies** —
|
|
33
33
|
the AWS SDK is bundled at build time, so installing it pulls nothing else in.
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
### Versioning
|
|
36
|
+
|
|
37
|
+
The package's version **tracks the product release version**, exactly like
|
|
38
|
+
`@evident-ai/cli`. `latest` is published on a product release, at the release
|
|
39
|
+
version. `dev` is published on every merge to `main` that touches this package or
|
|
40
|
+
the root `package.json`, at `<root major>.<minor>.<patch+1>-dev.<sha>`, so `dev`
|
|
41
|
+
stays strictly ahead of `latest`.
|
|
42
|
+
|
|
43
|
+
The version committed in this package's `package.json` is **vestigial** — CI
|
|
44
|
+
overwrites it before every publish. This is why `latest` jumped straight from
|
|
45
|
+
`0.1.0` to `3.x.y`: the `0.1.x` line predates the policy, not a missing
|
|
46
|
+
major-version story, and because npm publishes are immutable that jump is
|
|
47
|
+
permanent.
|
|
48
|
+
|
|
49
|
+
The two consumers still differ in how they get the binary: the Fargate runner
|
|
50
|
+
image installs the published package
|
|
51
|
+
(`ARG RUNNER_SYNCHRONISER_VERSION=dev`, resolved to a concrete version by the
|
|
52
|
+
deploy workflow); the MicroVM still builds it from source and reads no npm
|
|
53
|
+
version at all.
|
|
40
54
|
|
|
41
55
|
## Commands
|
|
42
56
|
|
|
@@ -48,6 +62,7 @@ the AWS SDK is bundled at build time, so installing it pulls nothing else in.
|
|
|
48
62
|
| `sync-once <claude\|opencode>` | — | `0` = ran; non-zero = tool broken |
|
|
49
63
|
| `model-auth-ready` | — | `0` = ready, `10` = not ready; other = tool broken |
|
|
50
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 |
|
|
51
66
|
|
|
52
67
|
`self-stop` scales this agent's own ECS service to `desiredCount=0` on a clean idle exit
|
|
53
68
|
(see Configuration for `CLUSTER`/`SERVICE`/`EVIDENT_SELFSTOP_ROLE_ARN`). It exits `0`
|
|
@@ -60,6 +75,87 @@ broader: `src/logger.ts` marks it verbatim because operator greps and CloudWatch
|
|
|
60
75
|
match on that exact string, so widening what it labels is cheaper than breaking every
|
|
61
76
|
saved query.
|
|
62
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)` | `31` | attempt 2, `prune`; the newest L0 isn't provably corrupt, so it declined to delete it |
|
|
119
|
+
| `unusableReplica(noL0Present)` | `31` | attempt 2, `prune`; no level-0 object was present to prune — **normal**, see below |
|
|
120
|
+
| `unusableReplica(layoutMismatch)` | `31` | attempt 2, `prune`; objects exist under the prefix but **none** parse as an LTX key at all — **a bug**, see below |
|
|
121
|
+
| `unusableReplica(recoveryFailed)` | `31` | attempt 2, `prune`'s delete itself threw |
|
|
122
|
+
| `fatal(misconfig)` | `30` | no object store while persistence is enabled, or the probe failed on attempt 2 |
|
|
123
|
+
| `fatal(deliberate)` | `30` | attempt 2, `crash` |
|
|
124
|
+
|
|
125
|
+
`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.
|
|
126
|
+
|
|
127
|
+
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.
|
|
128
|
+
|
|
129
|
+
#### The four `--on-unusable-replica` strategies
|
|
130
|
+
|
|
131
|
+
- **`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`/`noL0Present`/`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.
|
|
132
|
+
- **`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.
|
|
133
|
+
- **`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.
|
|
134
|
+
- **`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.
|
|
135
|
+
|
|
136
|
+
#### The measured real-world key layout (litestream 0.5.13)
|
|
137
|
+
|
|
138
|
+
`parseLtxKey` (`src/replica-keys.ts`) expects
|
|
139
|
+
`<prefix>/opencode.db/<level:04d>/<minTxid>-<maxTxid>.ltx` — **no `ltx/` path segment**,
|
|
140
|
+
despite litestream.io's generic docs prose suggesting otherwise (that mismatch was
|
|
141
|
+
#1081: every real key failed to parse, silently disabling `prune` in production).
|
|
142
|
+
Verbatim key sampled live from the production replica:
|
|
143
|
+
`agents/e5d866ac-b699-4940-b369-633b25f8b601/opencode.db/0000/000000000001a978-000000000001a978.ltx`.
|
|
144
|
+
Levels observed on that replica: `0000`/`0001`/`0002`/`0003`/`0009`. The zero-padding is
|
|
145
|
+
an observation, not a contract — `parseLtxKey` accepts an unpadded level directory too.
|
|
146
|
+
|
|
147
|
+
**L0 objects are short-lived.** litestream's own startup log measured a level-0
|
|
148
|
+
retention of **5 minutes** (`starting L0 retention monitor interval=15s retention=5m0s`;
|
|
149
|
+
compaction runs L1=30s, L2=5m, L3=1h, L9=24h). litestream expires L0 objects itself, so
|
|
150
|
+
at boot the newest L0 is normally *absent* (`noL0Present`) or freshly written and sound
|
|
151
|
+
(`targetHealthy`) — `prune`'s real-world reach is narrower than the code alone suggests,
|
|
152
|
+
and it can never repair corruption at a higher compaction level.
|
|
153
|
+
|
|
154
|
+
`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.
|
|
155
|
+
|
|
156
|
+
See `specs/local-runner.feature`'s "Recovering session history at startup" scenarios for
|
|
157
|
+
the behavioural anchor (31 comes online, 30 does not).
|
|
158
|
+
|
|
63
159
|
### Why the contract is asymmetric
|
|
64
160
|
|
|
65
161
|
`restore` and `sync-once` **never** report a domain outcome through the exit code. A
|
|
@@ -83,6 +179,8 @@ that it has no auth. `self-stop` decides whether a task may go away, so a broken
|
|
|
83
179
|
must never kill one: `entrypoint.sh` reads any non-zero exactly like `20` and keeps the
|
|
84
180
|
task, and only a `0` — returned solely on a confirmed `desiredCount` of 0 — lets it stop.
|
|
85
181
|
|
|
182
|
+
`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.
|
|
183
|
+
|
|
86
184
|
`src/shell-contract.json` is the machine-checked source of truth for the command list, and
|
|
87
185
|
`shell-contract.test.ts` holds **every** shell that speaks it to it — Fargate's
|
|
88
186
|
`packages/runner-image/entrypoint.sh` and the MicroVM's
|
|
@@ -128,10 +226,23 @@ The paths and keys that follow from the bucket/prefix:
|
|
|
128
226
|
| OpenCode DB | `$HOME/.local/share/opencode/opencode.db` | `<prefix>/opencode.db` (litestream) |
|
|
129
227
|
|
|
130
228
|
The session database is replicated by litestream itself; this package only generates its
|
|
131
|
-
config. `env` emits `
|
|
229
|
+
config. `env` emits `PERSISTENCE_BUCKET`, `CLAUDE_CREDS`, `OPENCODE_DB_PATH` and
|
|
132
230
|
`CREDS_SYNC_INTERVAL`, each single-quoted so a value containing shell metacharacters is
|
|
133
231
|
inert when `eval`'d.
|
|
134
232
|
|
|
233
|
+
`entrypoint.sh` asserts, right after it `eval`s this output, that each key is *defined*
|
|
234
|
+
(not merely non-empty) and splits the four into two groups: a key is **required** — the
|
|
235
|
+
shell dies naming it — if it has no safe degradation; it is **tolerated** — the shell logs
|
|
236
|
+
a WARNING naming it and carries on — if it does. `OPENCODE_DB_PATH` and
|
|
237
|
+
`CREDS_SYNC_INTERVAL` are required: both are dereferenced for real work (deleting/creating
|
|
238
|
+
the session DB, sizing the sync-loop sleep), so a guessed value would be actively wrong,
|
|
239
|
+
not merely absent. `PERSISTENCE_BUCKET` and `CLAUDE_CREDS` are tolerated: an empty
|
|
240
|
+
`PERSISTENCE_BUCKET` is already the designed "persistence disabled" state, and
|
|
241
|
+
`CLAUDE_CREDS` is dereferenced only in operator-facing message text. If you change
|
|
242
|
+
`renderEnv` to make a required key conditional, the entrypoint will brick every container
|
|
243
|
+
running that version — the runner installs this package from a floating npm tag (`dev` /
|
|
244
|
+
`latest`), so the shell and the CLI can genuinely be different versions (#779, #821).
|
|
245
|
+
|
|
135
246
|
## Behaviour worth knowing
|
|
136
247
|
|
|
137
248
|
- **A non-empty local file that is not valid JSON is protected**, never overwritten and
|
|
@@ -156,11 +267,14 @@ inert when `eval`'d.
|
|
|
156
267
|
|
|
157
268
|
## The `ObjectStore` port
|
|
158
269
|
|
|
159
|
-
`restore` and `
|
|
160
|
-
the object is absent,
|
|
161
|
-
|
|
270
|
+
`restore`/`sync` and `session-db-classify` talk to a four-method interface: `get(key)`
|
|
271
|
+
returning `null` when the object is absent, `put(key, body)`, `list(prefix)` and
|
|
272
|
+
`delete(key)` for session-DB recovery. `list` throws on any error and never maps one to
|
|
273
|
+
`[]` (an empty array means "reached the store, nothing there"); the adapter owns
|
|
274
|
+
pagination, and `delete` is idempotent. S3 vocabulary (`@aws-sdk/client-s3`, the typed
|
|
275
|
+
`NoSuchKey`/`NotFound` errors) is confined to the single adapter in
|
|
162
276
|
`src/s3-object-store.ts`, so the storage backend can be swapped without touching the
|
|
163
|
-
restore/sync logic.
|
|
277
|
+
restore/sync/classify logic.
|
|
164
278
|
|
|
165
279
|
The S3 adapter is the only one that exists. It uses no static credentials — auth is
|
|
166
280
|
whatever the ambient AWS credential chain resolves (on the Evident runner, the ECS task
|
package/dist/cli.js
CHANGED
|
@@ -39606,6 +39606,89 @@ function describeRestoreOutcome(label, path2, outcome) {
|
|
|
39606
39606
|
return assertNever(outcome);
|
|
39607
39607
|
}
|
|
39608
39608
|
}
|
|
39609
|
+
function assertNeverSessionDb(value) {
|
|
39610
|
+
throw new Error(`Unhandled session-DB restore outcome: ${JSON.stringify(value)}`);
|
|
39611
|
+
}
|
|
39612
|
+
function assertNeverUnusableReplicaReason(value) {
|
|
39613
|
+
throw new Error(`Unhandled unusable-replica reason: ${JSON.stringify(value)}`);
|
|
39614
|
+
}
|
|
39615
|
+
function assertNeverFatalCause(value) {
|
|
39616
|
+
throw new Error(`Unhandled fatal cause: ${JSON.stringify(value)}`);
|
|
39617
|
+
}
|
|
39618
|
+
function sessionDbExitCode(outcome) {
|
|
39619
|
+
switch (outcome.kind) {
|
|
39620
|
+
case "disabled":
|
|
39621
|
+
case "restored":
|
|
39622
|
+
case "noReplica":
|
|
39623
|
+
return 0;
|
|
39624
|
+
case "retryTransient":
|
|
39625
|
+
case "recovered":
|
|
39626
|
+
return 32;
|
|
39627
|
+
// re-run `litestream restore` and ask again
|
|
39628
|
+
case "unusableReplica":
|
|
39629
|
+
return 31;
|
|
39630
|
+
// booted with a fresh DB; the shell must skip `litestream replicate`
|
|
39631
|
+
case "fatal":
|
|
39632
|
+
return 30;
|
|
39633
|
+
// shared by a genuine misconfig and a deliberate `crash` choice
|
|
39634
|
+
default:
|
|
39635
|
+
return assertNeverSessionDb(outcome);
|
|
39636
|
+
}
|
|
39637
|
+
}
|
|
39638
|
+
function describeUnusableReplica(reason) {
|
|
39639
|
+
const base = "SESSION-DB-REPLICA-UNUSABLE: booting with a FRESH opencode.db; prior session history is lost until the replica is fixed.";
|
|
39640
|
+
switch (reason) {
|
|
39641
|
+
case "strategy":
|
|
39642
|
+
return `${base} (--on-unusable-replica=leave was selected; nothing in S3 was touched.)`;
|
|
39643
|
+
case "attemptsExhausted":
|
|
39644
|
+
return `${base} (recovery did not make the replica usable after retrying; giving up.)`;
|
|
39645
|
+
case "layoutMismatch":
|
|
39646
|
+
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.)`;
|
|
39647
|
+
case "noL0Present":
|
|
39648
|
+
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.)`;
|
|
39649
|
+
case "recoveryFailed":
|
|
39650
|
+
return `${base} (the recovery delete itself failed; see the warning above for the S3 error.)`;
|
|
39651
|
+
case "targetHealthy":
|
|
39652
|
+
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.)`;
|
|
39653
|
+
default:
|
|
39654
|
+
return assertNeverUnusableReplicaReason(reason);
|
|
39655
|
+
}
|
|
39656
|
+
}
|
|
39657
|
+
function describeSessionDbFatal(outcome, config) {
|
|
39658
|
+
switch (outcome.cause) {
|
|
39659
|
+
// Names the bucket/region and the probe error — never the crash flag.
|
|
39660
|
+
case "misconfig":
|
|
39661
|
+
return `FATAL: could not verify the opencode.db replica in S3 is reachable/authorised (bucket=${config.bucket ?? "<unset>"}, region=${config.region ?? "<unset>"}): ${outcome.detail}`;
|
|
39662
|
+
// Names the flag and states nothing was touched — never bucket/region, so an
|
|
39663
|
+
// operator reading this is never sent chasing IAM over their own flag.
|
|
39664
|
+
case "deliberate":
|
|
39665
|
+
return `FATAL: ${outcome.detail}`;
|
|
39666
|
+
default:
|
|
39667
|
+
return assertNeverFatalCause(outcome.cause);
|
|
39668
|
+
}
|
|
39669
|
+
}
|
|
39670
|
+
function describeSessionDbClassification(outcome, config) {
|
|
39671
|
+
switch (outcome.kind) {
|
|
39672
|
+
case "disabled":
|
|
39673
|
+
return "No opencode.db restore attempted; credential persistence is disabled (LITESTREAM_BUCKET/LITESTREAM_PREFIX unset).";
|
|
39674
|
+
case "restored":
|
|
39675
|
+
return "opencode.db restored from the object store.";
|
|
39676
|
+
case "noReplica":
|
|
39677
|
+
return "No opencode.db replica found in the object store; opencode will create a fresh session DB.";
|
|
39678
|
+
case "retryTransient":
|
|
39679
|
+
return "opencode.db replica restore failed (transient); retrying.";
|
|
39680
|
+
case "recovered": {
|
|
39681
|
+
const objects = outcome.deleted.length === 0 ? "no objects (see the warnings above for what failed)" : `${outcome.deleted.length} object${outcome.deleted.length === 1 ? "" : "s"} (${outcome.deleted.join(", ")})`;
|
|
39682
|
+
return `Attempted opencode.db replica recovery via --on-unusable-replica=${outcome.strategy}, deleting ${objects}; retrying the restore.` + (outcome.strategy === "clear" ? " Prior session history is lost." : "");
|
|
39683
|
+
}
|
|
39684
|
+
case "unusableReplica":
|
|
39685
|
+
return describeUnusableReplica(outcome.reason);
|
|
39686
|
+
case "fatal":
|
|
39687
|
+
return describeSessionDbFatal(outcome, config);
|
|
39688
|
+
default:
|
|
39689
|
+
return assertNeverSessionDb(outcome);
|
|
39690
|
+
}
|
|
39691
|
+
}
|
|
39609
39692
|
|
|
39610
39693
|
// src/file-ops.ts
|
|
39611
39694
|
init_esm_shims();
|
|
@@ -39708,6 +39791,51 @@ async function modelAuthReady(config, fileOps) {
|
|
|
39708
39791
|
return config.hasModelApiKey;
|
|
39709
39792
|
}
|
|
39710
39793
|
|
|
39794
|
+
// src/replica-keys.ts
|
|
39795
|
+
init_esm_shims();
|
|
39796
|
+
var DEFAULT_UNUSABLE_REPLICA_STRATEGY = "prune";
|
|
39797
|
+
var STRATEGIES = ["prune", "leave", "clear", "crash"];
|
|
39798
|
+
function parseUnusableReplicaStrategy(value) {
|
|
39799
|
+
if (value === void 0) return DEFAULT_UNUSABLE_REPLICA_STRATEGY;
|
|
39800
|
+
return STRATEGIES.includes(value) ? value : null;
|
|
39801
|
+
}
|
|
39802
|
+
function replicaDbPrefix(prefix) {
|
|
39803
|
+
return `${prefix}/opencode.db/`;
|
|
39804
|
+
}
|
|
39805
|
+
var LTX_FILENAME = /^([0-9a-fA-F]+)-([0-9a-fA-F]+)\.ltx$/;
|
|
39806
|
+
function parseLtxKey(prefix, key) {
|
|
39807
|
+
if (key.split("/").includes("..")) return null;
|
|
39808
|
+
const root12 = replicaDbPrefix(prefix);
|
|
39809
|
+
if (!key.startsWith(root12)) return null;
|
|
39810
|
+
const [levelPart, filename, ...rest] = key.slice(root12.length).split("/");
|
|
39811
|
+
if (rest.length > 0 || filename === void 0 || !/^\d+$/.test(levelPart)) return null;
|
|
39812
|
+
const match = LTX_FILENAME.exec(filename);
|
|
39813
|
+
if (!match) return null;
|
|
39814
|
+
return {
|
|
39815
|
+
level: Number.parseInt(levelPart, 10),
|
|
39816
|
+
minTxid: BigInt(`0x${match[1]}`),
|
|
39817
|
+
maxTxid: BigInt(`0x${match[2]}`)
|
|
39818
|
+
};
|
|
39819
|
+
}
|
|
39820
|
+
function newestL0Key(prefix, keys) {
|
|
39821
|
+
let newest = null;
|
|
39822
|
+
for (const key of keys) {
|
|
39823
|
+
const parsed = parseLtxKey(prefix, key);
|
|
39824
|
+
if (parsed === null || parsed.level !== 0) continue;
|
|
39825
|
+
if (newest === null || parsed.maxTxid > newest.maxTxid) {
|
|
39826
|
+
newest = { key, maxTxid: parsed.maxTxid };
|
|
39827
|
+
}
|
|
39828
|
+
}
|
|
39829
|
+
return newest?.key ?? null;
|
|
39830
|
+
}
|
|
39831
|
+
function hasParsableLtxKey(prefix, keys) {
|
|
39832
|
+
return keys.some((key) => parseLtxKey(prefix, key) !== null);
|
|
39833
|
+
}
|
|
39834
|
+
function isDeletableReplicaKey(prefix, key) {
|
|
39835
|
+
if (!key.startsWith(replicaDbPrefix(prefix))) return false;
|
|
39836
|
+
return !key.split("/").includes("..");
|
|
39837
|
+
}
|
|
39838
|
+
|
|
39711
39839
|
// src/restore.ts
|
|
39712
39840
|
init_esm_shims();
|
|
39713
39841
|
import { dirname as dirname3 } from "path";
|
|
@@ -43273,6 +43401,10 @@ var _ep4 = {
|
|
|
43273
43401
|
DisableS3ExpressSessionAuth: { type: "staticContextParams", value: true },
|
|
43274
43402
|
Bucket: { type: "contextParams", name: "Bucket" }
|
|
43275
43403
|
};
|
|
43404
|
+
var _ep8 = {
|
|
43405
|
+
Bucket: { type: "contextParams", name: "Bucket" },
|
|
43406
|
+
Prefix: { type: "contextParams", name: "Prefix" }
|
|
43407
|
+
};
|
|
43276
43408
|
var _mw011 = (Command3, cs, config, o7) => [
|
|
43277
43409
|
getThrow200ExceptionsPlugin(config)
|
|
43278
43410
|
];
|
|
@@ -49549,11 +49681,21 @@ var S3Client = class extends Client {
|
|
|
49549
49681
|
}
|
|
49550
49682
|
};
|
|
49551
49683
|
|
|
49684
|
+
// ../../node_modules/.pnpm/@aws-sdk+client-s3@3.1095.0/node_modules/@aws-sdk/client-s3/dist-es/commands/DeleteObjectCommand.js
|
|
49685
|
+
init_esm_shims();
|
|
49686
|
+
var DeleteObjectCommand = class extends command11(_ep011, _mw011, "DeleteObject", DeleteObject$) {
|
|
49687
|
+
};
|
|
49688
|
+
|
|
49552
49689
|
// ../../node_modules/.pnpm/@aws-sdk+client-s3@3.1095.0/node_modules/@aws-sdk/client-s3/dist-es/commands/GetObjectCommand.js
|
|
49553
49690
|
init_esm_shims();
|
|
49554
49691
|
var GetObjectCommand = class extends command11(_ep011, _mw7, "GetObject", GetObject$) {
|
|
49555
49692
|
};
|
|
49556
49693
|
|
|
49694
|
+
// ../../node_modules/.pnpm/@aws-sdk+client-s3@3.1095.0/node_modules/@aws-sdk/client-s3/dist-es/commands/ListObjectsV2Command.js
|
|
49695
|
+
init_esm_shims();
|
|
49696
|
+
var ListObjectsV2Command = class extends command11(_ep8, _mw011, "ListObjectsV2", ListObjectsV2$) {
|
|
49697
|
+
};
|
|
49698
|
+
|
|
49557
49699
|
// ../../node_modules/.pnpm/@aws-sdk+client-s3@3.1095.0/node_modules/@aws-sdk/client-s3/dist-es/commands/PutObjectCommand.js
|
|
49558
49700
|
init_esm_shims();
|
|
49559
49701
|
var PutObjectCommand = class extends command11(_ep011, _mw11, "PutObject", PutObject$) {
|
|
@@ -49588,8 +49730,200 @@ var S3ObjectStore = class {
|
|
|
49588
49730
|
async put(key, body) {
|
|
49589
49731
|
await this.client.send(new PutObjectCommand({ Bucket: this.bucket, Key: key, Body: body }));
|
|
49590
49732
|
}
|
|
49733
|
+
async list(prefix) {
|
|
49734
|
+
const keys = [];
|
|
49735
|
+
let continuationToken;
|
|
49736
|
+
do {
|
|
49737
|
+
const response = await this.client.send(
|
|
49738
|
+
new ListObjectsV2Command({
|
|
49739
|
+
Bucket: this.bucket,
|
|
49740
|
+
Prefix: prefix,
|
|
49741
|
+
ContinuationToken: continuationToken
|
|
49742
|
+
})
|
|
49743
|
+
);
|
|
49744
|
+
for (const object of response.Contents ?? []) {
|
|
49745
|
+
if (object.Key) keys.push(object.Key);
|
|
49746
|
+
}
|
|
49747
|
+
continuationToken = response.NextContinuationToken;
|
|
49748
|
+
} while (continuationToken);
|
|
49749
|
+
return keys;
|
|
49750
|
+
}
|
|
49751
|
+
async delete(key) {
|
|
49752
|
+
try {
|
|
49753
|
+
await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
|
|
49754
|
+
} catch (error) {
|
|
49755
|
+
if (isNotFound(error)) return;
|
|
49756
|
+
throw error;
|
|
49757
|
+
}
|
|
49758
|
+
}
|
|
49591
49759
|
};
|
|
49592
49760
|
|
|
49761
|
+
// src/session-db.ts
|
|
49762
|
+
init_esm_shims();
|
|
49763
|
+
|
|
49764
|
+
// src/replica-recovery.ts
|
|
49765
|
+
init_esm_shims();
|
|
49766
|
+
|
|
49767
|
+
// src/ltx-format.ts
|
|
49768
|
+
init_esm_shims();
|
|
49769
|
+
var LTX_HEADER_SIZE = 100;
|
|
49770
|
+
var LTX_MAGIC = "LTX1";
|
|
49771
|
+
function isStructurallySoundLtxObject(bytes) {
|
|
49772
|
+
if (bytes.length < LTX_HEADER_SIZE) return false;
|
|
49773
|
+
return bytes.subarray(0, LTX_MAGIC.length).toString("ascii") === LTX_MAGIC;
|
|
49774
|
+
}
|
|
49775
|
+
|
|
49776
|
+
// src/replica-recovery.ts
|
|
49777
|
+
async function probeReplica(store, prefix, log) {
|
|
49778
|
+
const root12 = replicaDbPrefix(prefix);
|
|
49779
|
+
try {
|
|
49780
|
+
const keys = await store.list(root12);
|
|
49781
|
+
return { ok: true, keys };
|
|
49782
|
+
} catch (error) {
|
|
49783
|
+
const detail = describeError(error);
|
|
49784
|
+
log(`WARNING: could not list the replica prefix ${root12}: ${detail}`);
|
|
49785
|
+
return { ok: false, detail };
|
|
49786
|
+
}
|
|
49787
|
+
}
|
|
49788
|
+
function logLayoutMismatch(prefix, keys, log) {
|
|
49789
|
+
log(
|
|
49790
|
+
`WARNING: SESSION-DB-REPLICA-LAYOUT-MISMATCH: listed ${keys.length} object(s) under the replica prefix ${replicaDbPrefix(prefix)} but none of them match the expected LTX key layout (<prefix>/opencode.db/<level>/<minTxid>-<maxTxid>.ltx). Sample key: ${keys[0]}. --on-unusable-replica=prune cannot select anything to delete until this is fixed.`
|
|
49791
|
+
);
|
|
49792
|
+
}
|
|
49793
|
+
async function pruneNewestL0(store, prefix, keys, log) {
|
|
49794
|
+
const target = newestL0Key(prefix, keys);
|
|
49795
|
+
if (target === null) {
|
|
49796
|
+
if (keys.length > 0 && !hasParsableLtxKey(prefix, keys)) {
|
|
49797
|
+
logLayoutMismatch(prefix, keys, log);
|
|
49798
|
+
return { deleted: null, reason: "layoutMismatch" };
|
|
49799
|
+
}
|
|
49800
|
+
return { deleted: null, reason: "noL0Present" };
|
|
49801
|
+
}
|
|
49802
|
+
if (!isDeletableReplicaKey(prefix, target)) {
|
|
49803
|
+
log(`WARNING: refusing to prune ${target}: failed the replica-key guard.`);
|
|
49804
|
+
return { deleted: null, reason: "layoutMismatch" };
|
|
49805
|
+
}
|
|
49806
|
+
let bytes;
|
|
49807
|
+
try {
|
|
49808
|
+
bytes = await store.get(target);
|
|
49809
|
+
} catch (error) {
|
|
49810
|
+
log(
|
|
49811
|
+
`WARNING: could not re-read ${target} to check whether it is actually corrupt before pruning it: ${describeError(error)}. Declining to prune without positive evidence.`
|
|
49812
|
+
);
|
|
49813
|
+
return { deleted: null, reason: "targetHealthy" };
|
|
49814
|
+
}
|
|
49815
|
+
if (bytes === null || isStructurallySoundLtxObject(bytes)) {
|
|
49816
|
+
log(
|
|
49817
|
+
`INFO: the newest L0 replica object ${target} ` + (bytes === null ? "is already gone" : "looks structurally sound") + "; declining to prune it without positive evidence it is the corrupt one."
|
|
49818
|
+
);
|
|
49819
|
+
return { deleted: null, reason: "targetHealthy" };
|
|
49820
|
+
}
|
|
49821
|
+
log(`INFO: pruning the newest L0 replica object ${target} (fails the LTX structural check).`);
|
|
49822
|
+
try {
|
|
49823
|
+
await store.delete(target);
|
|
49824
|
+
} catch (error) {
|
|
49825
|
+
log(`WARNING: failed to prune ${target}: ${describeError(error)}`);
|
|
49826
|
+
return { deleted: null, reason: "deleteFailed" };
|
|
49827
|
+
}
|
|
49828
|
+
return { deleted: target };
|
|
49829
|
+
}
|
|
49830
|
+
async function clearReplica(store, prefix, keys, log) {
|
|
49831
|
+
const deleted = [];
|
|
49832
|
+
const failed = [];
|
|
49833
|
+
for (const key of keys) {
|
|
49834
|
+
if (!isDeletableReplicaKey(prefix, key)) {
|
|
49835
|
+
log(`WARNING: refusing to delete ${key}: outside the replica prefix.`);
|
|
49836
|
+
continue;
|
|
49837
|
+
}
|
|
49838
|
+
try {
|
|
49839
|
+
await store.delete(key);
|
|
49840
|
+
deleted.push(key);
|
|
49841
|
+
} catch (error) {
|
|
49842
|
+
log(`WARNING: failed to delete ${key}: ${describeError(error)}`);
|
|
49843
|
+
failed.push(key);
|
|
49844
|
+
}
|
|
49845
|
+
}
|
|
49846
|
+
return { deleted, failed };
|
|
49847
|
+
}
|
|
49848
|
+
|
|
49849
|
+
// src/session-db.ts
|
|
49850
|
+
function assertNeverStrategy(value) {
|
|
49851
|
+
throw new Error(`Unhandled unusable-replica strategy: ${JSON.stringify(value)}`);
|
|
49852
|
+
}
|
|
49853
|
+
async function classifySessionDbRestore(params) {
|
|
49854
|
+
const outcome = await decideSessionDbRestore(params);
|
|
49855
|
+
if (outcome.kind === "unusableReplica") {
|
|
49856
|
+
await discardLocalDebris(params.fileOps, params.config.opencodeDbPath, params.log);
|
|
49857
|
+
}
|
|
49858
|
+
return outcome;
|
|
49859
|
+
}
|
|
49860
|
+
async function decideSessionDbRestore(params) {
|
|
49861
|
+
const { exitCode, attempt, strategy, config, fileOps, store, log } = params;
|
|
49862
|
+
if (config.bucket === null || config.prefix === null) {
|
|
49863
|
+
return { kind: "disabled" };
|
|
49864
|
+
}
|
|
49865
|
+
const prefix = config.prefix;
|
|
49866
|
+
if (exitCode === 0) {
|
|
49867
|
+
const stat2 = await fileOps.stat(config.opencodeDbPath);
|
|
49868
|
+
return stat2 !== null && stat2.size > 0 ? { kind: "restored" } : { kind: "noReplica" };
|
|
49869
|
+
}
|
|
49870
|
+
if (store === null) {
|
|
49871
|
+
return {
|
|
49872
|
+
kind: "fatal",
|
|
49873
|
+
cause: "misconfig",
|
|
49874
|
+
detail: "persistence is enabled (LITESTREAM_BUCKET/LITESTREAM_PREFIX are set) but no object store is available to verify the replica is reachable"
|
|
49875
|
+
};
|
|
49876
|
+
}
|
|
49877
|
+
if (attempt === 1) {
|
|
49878
|
+
return { kind: "retryTransient" };
|
|
49879
|
+
}
|
|
49880
|
+
if (attempt >= 3) {
|
|
49881
|
+
return { kind: "unusableReplica", reason: "attemptsExhausted" };
|
|
49882
|
+
}
|
|
49883
|
+
const probe = await probeReplica(store, prefix, log);
|
|
49884
|
+
if (!probe.ok) {
|
|
49885
|
+
return {
|
|
49886
|
+
kind: "fatal",
|
|
49887
|
+
cause: "misconfig",
|
|
49888
|
+
detail: `could not verify the session-DB replica is reachable: ${probe.detail}`
|
|
49889
|
+
};
|
|
49890
|
+
}
|
|
49891
|
+
const keys = probe.keys;
|
|
49892
|
+
switch (strategy) {
|
|
49893
|
+
case "crash":
|
|
49894
|
+
return {
|
|
49895
|
+
kind: "fatal",
|
|
49896
|
+
cause: "deliberate",
|
|
49897
|
+
detail: "the replica is unusable and --on-unusable-replica=crash was selected; no S3 object was touched"
|
|
49898
|
+
};
|
|
49899
|
+
case "leave":
|
|
49900
|
+
return { kind: "unusableReplica", reason: "strategy" };
|
|
49901
|
+
case "prune": {
|
|
49902
|
+
const result = await pruneNewestL0(store, prefix, keys, log);
|
|
49903
|
+
if (result.deleted === null) {
|
|
49904
|
+
const reason = result.reason === "deleteFailed" ? "recoveryFailed" : result.reason;
|
|
49905
|
+
return { kind: "unusableReplica", reason };
|
|
49906
|
+
}
|
|
49907
|
+
return { kind: "recovered", strategy: "prune", deleted: [result.deleted] };
|
|
49908
|
+
}
|
|
49909
|
+
case "clear": {
|
|
49910
|
+
const result = await clearReplica(store, prefix, keys, log);
|
|
49911
|
+
return { kind: "recovered", strategy: "clear", deleted: result.deleted };
|
|
49912
|
+
}
|
|
49913
|
+
default:
|
|
49914
|
+
return assertNeverStrategy(strategy);
|
|
49915
|
+
}
|
|
49916
|
+
}
|
|
49917
|
+
async function discardLocalDebris(fileOps, dbPath, log) {
|
|
49918
|
+
for (const path2 of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
49919
|
+
try {
|
|
49920
|
+
await fileOps.remove(path2);
|
|
49921
|
+
} catch (error) {
|
|
49922
|
+
log(`WARNING: could not remove local session-DB debris at ${path2}: ${describeError(error)}`);
|
|
49923
|
+
}
|
|
49924
|
+
}
|
|
49925
|
+
}
|
|
49926
|
+
|
|
49593
49927
|
// src/self-stop.ts
|
|
49594
49928
|
init_esm_shims();
|
|
49595
49929
|
function isComplete(credentials) {
|
|
@@ -49666,7 +50000,15 @@ async function selfStop({
|
|
|
49666
50000
|
|
|
49667
50001
|
// src/shell-contract.json
|
|
49668
50002
|
var shell_contract_default = {
|
|
49669
|
-
commands: [
|
|
50003
|
+
commands: [
|
|
50004
|
+
"env",
|
|
50005
|
+
"litestream-config",
|
|
50006
|
+
"restore",
|
|
50007
|
+
"sync-once",
|
|
50008
|
+
"model-auth-ready",
|
|
50009
|
+
"self-stop",
|
|
50010
|
+
"session-db-classify"
|
|
50011
|
+
]
|
|
49670
50012
|
};
|
|
49671
50013
|
|
|
49672
50014
|
// src/sync.ts
|
|
@@ -49718,6 +50060,9 @@ var EXIT_NOT_READY = 10;
|
|
|
49718
50060
|
var EXIT_KEEP_TASK = 20;
|
|
49719
50061
|
var EXIT_USAGE = 2;
|
|
49720
50062
|
var EXIT_BROKEN = 1;
|
|
50063
|
+
var EXIT_SESSION_DB_FATAL = 30;
|
|
50064
|
+
var EXIT_SESSION_DB_UNUSABLE = 31;
|
|
50065
|
+
var EXIT_SESSION_DB_RETRY = 32;
|
|
49721
50066
|
var STATE_SUFFIX = ".synchash";
|
|
49722
50067
|
var COMMANDS = shell_contract_default.commands;
|
|
49723
50068
|
var USAGE = `Usage: runner-synchroniser <command> [args]
|
|
@@ -49729,6 +50074,12 @@ var USAGE = `Usage: runner-synchroniser <command> [args]
|
|
|
49729
50074
|
model-auth-ready exit 0 when some model auth is configured, ${EXIT_NOT_READY} when not
|
|
49730
50075
|
self-stop scale this agent's own ECS service to 0; exit 0 only
|
|
49731
50076
|
when desiredCount is confirmed 0, ${EXIT_KEEP_TASK} to keep the task
|
|
50077
|
+
session-db-classify <litestream-restore-exit-code> <attempt>
|
|
50078
|
+
[--on-unusable-replica=<prune|leave|clear|crash>]
|
|
50079
|
+
classify a just-run \`litestream restore\` of opencode.db;
|
|
50080
|
+
exit 0 restored/no-replica/disabled, ${EXIT_SESSION_DB_RETRY} re-run
|
|
50081
|
+
and ask again, ${EXIT_SESSION_DB_UNUSABLE} unusable (booted fresh,
|
|
50082
|
+
skip replicate), ${EXIT_SESSION_DB_FATAL} fatal
|
|
49732
50083
|
`;
|
|
49733
50084
|
function shellQuote(value) {
|
|
49734
50085
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
@@ -49742,11 +50093,39 @@ function awsSelfStopAccessFor(config) {
|
|
|
49742
50093
|
function parseStoreName(value) {
|
|
49743
50094
|
return value === "claude" || value === "opencode" ? value : null;
|
|
49744
50095
|
}
|
|
50096
|
+
var ON_UNUSABLE_REPLICA_FLAG = "--on-unusable-replica=";
|
|
50097
|
+
function parseNonNegativeInt(value) {
|
|
50098
|
+
return /^\d+$/.test(value) ? Number.parseInt(value, 10) : null;
|
|
50099
|
+
}
|
|
50100
|
+
function parseSessionDbClassifyArgs(args) {
|
|
50101
|
+
let strategyRaw;
|
|
50102
|
+
const positional = [];
|
|
50103
|
+
for (const arg of args) {
|
|
50104
|
+
if (arg.startsWith(ON_UNUSABLE_REPLICA_FLAG)) {
|
|
50105
|
+
if (strategyRaw !== void 0) return null;
|
|
50106
|
+
strategyRaw = arg.slice(ON_UNUSABLE_REPLICA_FLAG.length);
|
|
50107
|
+
} else if (arg.startsWith("--")) {
|
|
50108
|
+
return null;
|
|
50109
|
+
} else {
|
|
50110
|
+
positional.push(arg);
|
|
50111
|
+
}
|
|
50112
|
+
}
|
|
50113
|
+
if (positional.length !== 2) return null;
|
|
50114
|
+
const exitCode = parseNonNegativeInt(positional[0]);
|
|
50115
|
+
const attempt = parseNonNegativeInt(positional[1]);
|
|
50116
|
+
const strategy = parseUnusableReplicaStrategy(strategyRaw);
|
|
50117
|
+
if (exitCode === null || attempt === null || attempt < 1 || strategy === null) return null;
|
|
50118
|
+
return { exitCode, attempt, strategy };
|
|
50119
|
+
}
|
|
49745
50120
|
function renderEnv(config) {
|
|
49746
50121
|
return [
|
|
49747
|
-
//
|
|
49748
|
-
//
|
|
49749
|
-
|
|
50122
|
+
// The bucket NAME, kept (not reduced to a boolean) so an operator can
|
|
50123
|
+
// read it; empty exactly when persistence is disabled (both
|
|
50124
|
+
// LITESTREAM_BUCKET and LITESTREAM_PREFIX must be set — see config.ts's
|
|
50125
|
+
// persistenceEnabled). entrypoint.sh gates BOTH the credential-sync
|
|
50126
|
+
// loop and whether it starts litestream replicate on this being
|
|
50127
|
+
// non-empty.
|
|
50128
|
+
`PERSISTENCE_BUCKET=${shellQuote(config.bucket ?? "")}`,
|
|
49750
50129
|
`CLAUDE_CREDS=${shellQuote(config.claude.path)}`,
|
|
49751
50130
|
`OPENCODE_DB_PATH=${shellQuote(config.opencodeDbPath)}`,
|
|
49752
50131
|
`CREDS_SYNC_INTERVAL=${shellQuote(String(config.syncIntervalSeconds))}`
|
|
@@ -49812,6 +50191,20 @@ async function commandSyncOnce(name, config, deps) {
|
|
|
49812
50191
|
await writeLastHash(fileOps, statePath, result.hash, log);
|
|
49813
50192
|
}
|
|
49814
50193
|
}
|
|
50194
|
+
async function commandSessionDbClassify(args, config, deps) {
|
|
50195
|
+
const { fileOps, log } = deps;
|
|
50196
|
+
const outcome = await classifySessionDbRestore({
|
|
50197
|
+
exitCode: args.exitCode,
|
|
50198
|
+
attempt: args.attempt,
|
|
50199
|
+
strategy: args.strategy,
|
|
50200
|
+
config: { bucket: config.bucket, prefix: config.prefix, opencodeDbPath: config.opencodeDbPath },
|
|
50201
|
+
fileOps,
|
|
50202
|
+
store: (deps.objectStoreFor ?? s3ObjectStoreFor)(config),
|
|
50203
|
+
log
|
|
50204
|
+
});
|
|
50205
|
+
log(describeSessionDbClassification(outcome, { bucket: config.bucket, region: config.region }));
|
|
50206
|
+
return sessionDbExitCode(outcome);
|
|
50207
|
+
}
|
|
49815
50208
|
async function main(argv, deps) {
|
|
49816
50209
|
const { env: env3, fileOps, log, out } = deps;
|
|
49817
50210
|
const [command12, ...rest] = argv;
|
|
@@ -49847,6 +50240,17 @@ ${USAGE}`);
|
|
|
49847
50240
|
});
|
|
49848
50241
|
return stopped ? 0 : EXIT_KEEP_TASK;
|
|
49849
50242
|
}
|
|
50243
|
+
if (command12 === "session-db-classify") {
|
|
50244
|
+
const args = parseSessionDbClassifyArgs(rest);
|
|
50245
|
+
if (args === null) {
|
|
50246
|
+
log(
|
|
50247
|
+
`'session-db-classify' needs <litestream-restore-exit-code> <attempt> and an optional --on-unusable-replica=<prune|leave|clear|crash>.
|
|
50248
|
+
${USAGE}`
|
|
50249
|
+
);
|
|
50250
|
+
return EXIT_USAGE;
|
|
50251
|
+
}
|
|
50252
|
+
return commandSessionDbClassify(args, config, deps);
|
|
50253
|
+
}
|
|
49850
50254
|
const name = parseStoreName(rest[0]);
|
|
49851
50255
|
if (name === null) {
|
|
49852
50256
|
log(`'${command12}' needs a store name: claude or opencode.
|
|
@@ -49886,6 +50290,9 @@ export {
|
|
|
49886
50290
|
EXIT_BROKEN,
|
|
49887
50291
|
EXIT_KEEP_TASK,
|
|
49888
50292
|
EXIT_NOT_READY,
|
|
50293
|
+
EXIT_SESSION_DB_FATAL,
|
|
50294
|
+
EXIT_SESSION_DB_RETRY,
|
|
50295
|
+
EXIT_SESSION_DB_UNUSABLE,
|
|
49889
50296
|
EXIT_USAGE,
|
|
49890
50297
|
main
|
|
49891
50298
|
};
|
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.1e72251",
|
|
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",
|