@awebai/oats 0.31.0 → 0.33.0
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 +1 -1
- package/bin/oats.mjs +162 -42
- package/docs/capabilities.md +5 -0
- package/docs/configuration.md +65 -8
- package/docs/design/2026-09-23-workspace-module-contracts.md +3 -2
- package/docs/desktop-cli-api.md +167 -18
- package/docs/desktop.md +16 -0
- package/docs/first-team.md +20 -2
- package/docs/implementation.md +121 -1
- package/docs/integrations.md +1 -1
- package/docs/oats-local.schema.json +2 -1
- package/docs/official-catalog.md +2 -2
- package/docs/packages.md +2 -2
- package/docs/plans/0.30-close-out.md +7 -4
- package/docs/release-notes/v0.32.0.md +137 -0
- package/docs/release-notes/v0.33.0.md +174 -0
- package/docs/souls-and-instances.md +64 -0
- package/docs/workspaces.md +1 -1
- package/lib/automations.mjs +5 -1
- package/lib/core.mjs +164 -46
- package/lib/harness-trust.mjs +139 -0
- package/lib/instance-inspect.mjs +18 -3
- package/lib/instance-resolution.mjs +3 -3
- package/lib/local-inputs.mjs +46 -0
- package/lib/materialize.mjs +3 -2
- package/lib/packages.mjs +26 -6
- package/lib/process-group.mjs +54 -0
- package/lib/remote.mjs +1363 -115
- package/lib/resolve.mjs +23 -7
- package/lib/servers.mjs +7 -0
- package/lib/workspace.mjs +163 -56
- package/package-catalog.json +2 -2
- package/package.json +1 -1
package/docs/configuration.md
CHANGED
|
@@ -50,6 +50,11 @@ launch-configs: # named ways this host starts a har
|
|
|
50
50
|
CLAUDE_CONFIG_DIR: { fromEnv: PERSONAL_CLAUDE_DIR }
|
|
51
51
|
model: opus
|
|
52
52
|
yolo: false
|
|
53
|
+
mine:
|
|
54
|
+
harness: claude
|
|
55
|
+
default: true # this host's baseline for every claude launch (0.32)
|
|
56
|
+
env:
|
|
57
|
+
CLAUDE_CONFIG_DIR: /home/ana/.claude-personal
|
|
53
58
|
```
|
|
54
59
|
|
|
55
60
|
Schema: [`oats-local.schema.json`](oats-local.schema.json). Unknown keys are
|
|
@@ -70,7 +75,7 @@ refused (`E_WORKSPACE_SCHEMA`).
|
|
|
70
75
|
| `host.name` | This machine's name. A workspace trigger or schedule runs only on the host named by its `runsOn` ([schedules.md](schedules.md)). |
|
|
71
76
|
| `automations.trust` | The workspace triggers and schedules (`<member>/<id>`) this host agrees to run, or `"*"` for every one the workspace places here (0.30). Absent or empty: none runs. See [Who runs workspace automations](#who-runs-workspace-automations). |
|
|
72
77
|
| `triggers.disabled`, `schedules.disabled` | Workspace triggers and schedules (`<member>/<id>`) this host does not run, without a commit. Written by `oats trigger disable` / `oats schedule disable`. |
|
|
73
|
-
| `launch-configs.<name>` | A named way to start a harness on this host, chosen at spawn or session start, never by the soul. See [Launch configurations](#launch-configurations). |
|
|
78
|
+
| `launch-configs.<name>` | A named way to start a harness on this host, chosen at spawn or session start, never by the soul. `default: true` makes it this host's baseline for its harness (0.32). See [Launch configurations](#launch-configurations). |
|
|
74
79
|
| `souls.launch` | This machine's launch preference per soul (0.30): `"*"` for every soul, a soul's own entry (its name, or `<package>/<soul>`) over it. A value is a `launch-configs` name or an inline `{ harness, model? }`. It overrides the soul's own `launch:`; explicit spawn flags win over both. See [Launch preferences](#launch-preferences). |
|
|
75
80
|
|
|
76
81
|
How teams are resolved, and what a messaging provider does with them, is in
|
|
@@ -81,8 +86,9 @@ How teams are resolved, and what a messaging provider does with them, is in
|
|
|
81
86
|
An entry has `harness` (`pi` \| `claude` \| `codex`, required), `executable`
|
|
82
87
|
(a bare name looked up on `PATH`, or a path relative to this deployment
|
|
83
88
|
directory), `args` (literal, no shell), `env` (a literal string, or
|
|
84
|
-
`{ fromEnv: NAME }` resolved on the host at start), `model
|
|
85
|
-
|
|
89
|
+
`{ fromEnv: NAME }` resolved on the host at start), `model`, `yolo` and
|
|
90
|
+
`default` (0.32; see [the harness default](#the-harness-default)). A launch
|
|
91
|
+
configuration is a host choice: a soul never names one.
|
|
86
92
|
|
|
87
93
|
- Select one with `--launch-config <name>` on `oats spawn`,
|
|
88
94
|
`oats session start` and `oats session restart`. A named configuration is
|
|
@@ -104,6 +110,50 @@ launch configuration is a host choice: a soul never names one.
|
|
|
104
110
|
- The old key `runtime` is still read as `harness`, with a
|
|
105
111
|
`deprecated-runtime-name` warning.
|
|
106
112
|
|
|
113
|
+
### The harness default
|
|
114
|
+
|
|
115
|
+
`default: true` makes a configuration this host's baseline for its harness
|
|
116
|
+
(0.32, feature `launch-config-default`). Use it for what every launch of a
|
|
117
|
+
harness on this machine needs, whatever soul or preference chose it: an
|
|
118
|
+
account directory (`CLAUDE_CONFIG_DIR`), a wrapper `executable`, an argument.
|
|
119
|
+
|
|
120
|
+
- **When it applies:** a new launch that picks the harness without naming a
|
|
121
|
+
configuration: a soul's `launch:`, an inline `souls.launch` preference,
|
|
122
|
+
`--harness` (on a spawn, or on `session start|restart` of an existing
|
|
123
|
+
home), `--reselect-launch`, and the host default (`pi`). It supplies the
|
|
124
|
+
executable, args, env and `yolo`. A `yolo` recorded from a default stays
|
|
125
|
+
with it: a later `--launch-config none` or another harness does not carry
|
|
126
|
+
it over.
|
|
127
|
+
- **The model** comes from whatever picked the harness (`--model`, then the
|
|
128
|
+
preference); the default's `model` is the last fallback, before the
|
|
129
|
+
harness's own.
|
|
130
|
+
- **A named configuration runs as declared**: `--launch-config <name>` or a
|
|
131
|
+
`souls.launch` name never inherits from the default. `--launch-config none`
|
|
132
|
+
(or a `souls.launch` entry of `none`) asks for the bare harness and
|
|
133
|
+
bypasses it.
|
|
134
|
+
- **One per harness.** A second `default: true` for the same harness is
|
|
135
|
+
refused (`E_LAUNCH_CONFIG_INVALID`, naming both); move it by clearing the
|
|
136
|
+
old one first.
|
|
137
|
+
- **Existing homes keep their launch** until `--reselect-launch` or a
|
|
138
|
+
respawn, like any change of preference. Declaring a default is such a
|
|
139
|
+
change: `oats readiness --home` warns `launch-changed` on each existing
|
|
140
|
+
home the default would now apply to, until it is restarted with
|
|
141
|
+
`--reselect-launch` or respawned.
|
|
142
|
+
- **It is visible.** `oats launch-config list` marks it; `spawn --preview`,
|
|
143
|
+
`launch-config preview`, `instance.json` and `oats inspect --home` say when
|
|
144
|
+
a launch's configuration came from the default (`launchConfigDefault`).
|
|
145
|
+
A default with `yolo: true` turns yolo on for every launch of that harness
|
|
146
|
+
here: the preview shows it.
|
|
147
|
+
- **Every kernel that reads the deployment needs OATS 0.32+.** OATS 0.31
|
|
148
|
+
and older refuse the whole `oats-local.yaml` (`E_WORKSPACE_SCHEMA`) once a
|
|
149
|
+
configuration declares `default`.
|
|
150
|
+
|
|
151
|
+
`oats-claude-config` (a one-line file naming the claude binary, found walking
|
|
152
|
+
up from the deployment) is no longer read. A new claude launch with one in
|
|
153
|
+
reach is refused (`E_CLAUDE_CONFIG_REMOVED`) naming the file: declare the
|
|
154
|
+
name it holds as the claude default (`executable: <name>`, `default: true`)
|
|
155
|
+
and delete the file. Homes launched with it keep their recorded executable.
|
|
156
|
+
|
|
107
157
|
### Launch preferences
|
|
108
158
|
|
|
109
159
|
A soul says what its role should run on, and each machine may override it
|
|
@@ -128,9 +178,11 @@ souls:
|
|
|
128
178
|
harness and replaces only its model.
|
|
129
179
|
- A **launch configuration** name runs that configuration's full recipe. An
|
|
130
180
|
**inline or soul preference** runs its harness the way this host starts it
|
|
131
|
-
without a configuration
|
|
132
|
-
|
|
133
|
-
|
|
181
|
+
without a configuration: [the harness default](#the-harness-default) if
|
|
182
|
+
one is declared, else the executable on `PATH` with no args and no env,
|
|
183
|
+
with the preference's `model`. A preference without `model` uses the
|
|
184
|
+
harness default's model, else the harness's own; it never borrows a lower
|
|
185
|
+
layer's.
|
|
134
186
|
- **A missing harness is refused**, never replaced: `E_HARNESS_UNAVAILABLE`
|
|
135
187
|
names the layer that chose it and the fix (install the harness, or override
|
|
136
188
|
it here in `souls.launch`). `oats souls` still lists the soul, with the
|
|
@@ -274,5 +326,10 @@ oats spawn <soul> --preview # the exact modules, teams and provider payloads
|
|
|
274
326
|
oats doctor # this deployment's files and the lock
|
|
275
327
|
```
|
|
276
328
|
|
|
277
|
-
Environment: `OATS_REMOTE_CACHE` relocates the fetch cache
|
|
278
|
-
|
|
329
|
+
Environment: `OATS_REMOTE_CACHE` relocates the fetch cache (which also holds
|
|
330
|
+
the bounded parsed-read cache and the observations `--max-age` reuses; all of
|
|
331
|
+
it is safe to delete); `OATS_PACKAGE_CATALOG` names an alternative package
|
|
332
|
+
catalog file. The read verbs (`status`, `workspace status`, `souls`,
|
|
333
|
+
`capabilities`, `inspect`, the read forms of `teams` and `soul teams`, and
|
|
334
|
+
`spawn --preview`) take `--max-age <seconds>` to reuse a remote head observed that recently
|
|
335
|
+
([Observation reuse](desktop-cli-api.md#observation-reuse-feature-observe-max-age-oats-0311)).
|
|
@@ -55,8 +55,9 @@ symlink enters as `symlink:<target>`; empty directories and a top-level `.git/`
|
|
|
55
55
|
### 1.3 Access, cache, failures
|
|
56
56
|
|
|
57
57
|
- Git uses the operator's own configuration and credentials; `GIT_TERMINAL_PROMPT=0`,
|
|
58
|
-
`GIT_ASKPASS=/usr/bin/false` and ssh `-o BatchMode=yes` mean nothing prompts. 30 s per git call.
|
|
59
|
-
- A commit is fetched depth 1
|
|
58
|
+
`GIT_ASKPASS=/usr/bin/false` and ssh `-o BatchMode=yes` mean nothing prompts. 30 s per git call; 10 minutes for the fetch of a commit.
|
|
59
|
+
- A commit is fetched depth 1 with its trees and its blobs up to 64 KiB (larger blobs on demand, when a read
|
|
60
|
+
needs them; whole trees from a server without partial fetches; awebai/oats#384) into a bare cache `<cacheDir>/<sha256(key)>/` (default
|
|
60
61
|
`~/.cache/oats/remotes`; the CLI honours `OATS_REMOTE_CACHE`) and pinned as `refs/oats/commits/<oid>`.
|
|
61
62
|
The cache may be wiped at any time. Operations on one cache repo are serialized.
|
|
62
63
|
- Failures: `E_REMOTE_UNREADABLE { url, key, reason }`, `reason` ∈ `auth`, `not-found`, `network`,
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -38,7 +38,7 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
|
|
|
38
38
|
"instance-events-2","schedule-history","schedule-read-2","spawn-preview-2","spawn-idempotency","spawn-idempotency-2","spawn-apply-2",
|
|
39
39
|
"workspace-v2","instance-modules","spawn-provider-payload","served-identity","packages-no-approval","spawn-name","settings-origins",
|
|
40
40
|
"team-model-2","settings-declared","capabilities-private","layers-from","harness","package-souls","triggers","automations","desktop-facts","launch-preference",
|
|
41
|
-
"preview-composed-from"],
|
|
41
|
+
"preview-composed-from","observe-max-age","spawn-preview-max-age"],
|
|
42
42
|
"automationsApi":1,"workspaceApi":2,"instanceGitApi":1,"spawnApplyApi":1,"soulsApi":2,"lifecycleApi":1,
|
|
43
43
|
"readinessApi":2,"spawnPreviewApi":2,"eventsApi":2,"scheduleHistoryApi":3,"scheduleApi":2,"operationsApi":2}
|
|
44
44
|
```
|
|
@@ -95,6 +95,8 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
|
|
|
95
95
|
| `desktop-facts` | the facts under [Desktop facts](#desktop-facts-feature-desktop-facts-oats-0290) | |
|
|
96
96
|
| `launch-preference` | soul and local launch preferences; `launch`, `launchCurrent`, `launchFrom`; `--reselect-launch`; `key` on soul and agent rows ([Launch preferences](#soul-launch-preferences-feature-launch-preference-oats-0300)) | |
|
|
97
97
|
| `preview-composed-from` | `composedFrom` on preview `modules[]` ([Composition](#the-preview)) | |
|
|
98
|
+
| `observe-max-age` | `--max-age <s>` on the read verbs and their `observation` block ([Observation reuse](#observation-reuse-feature-observe-max-age-oats-0311)) | |
|
|
99
|
+
| `spawn-preview-max-age` | `--max-age <s>` on `spawn --preview` and its `observation` block ([Observation reuse](#observation-reuse-feature-observe-max-age-oats-0311), [The preview](#the-preview)) | |
|
|
98
100
|
|
|
99
101
|
Payload-only integers, never in the probe: `onboardApi: 2`, `syncApi: 1`,
|
|
100
102
|
`workspaceStatusApi: 1`, `capabilitiesApi: 1`, the `oats souls` document's
|
|
@@ -181,6 +183,94 @@ an inherited `OATS_DEPLOYMENT` or `OATS_RESOLUTION` (`details.inherited`).
|
|
|
181
183
|
homes in `problems[]`: `legacy-captured-home {code, instances, homes,
|
|
182
184
|
message}` and `legacy-local-agents {code, dirs, instances, message}`.
|
|
183
185
|
|
|
186
|
+
<a id="observation-reuse-feature-observe-max-age-oats-0311"></a>
|
|
187
|
+
### Observation reuse (feature `observe-max-age`, OATS 0.32.0)
|
|
188
|
+
|
|
189
|
+
Every read asks each remote for its current head (`git ls-remote`). With
|
|
190
|
+
`--max-age <seconds>` a read verb reuses a head this machine observed at most
|
|
191
|
+
that many seconds ago instead, so a refresh right after another costs no
|
|
192
|
+
network round trip. Gate the flag on the feature: an older kernel may ignore
|
|
193
|
+
it and answer live, without the block.
|
|
194
|
+
|
|
195
|
+
```text
|
|
196
|
+
oats status | workspace status | souls | capabilities | inspect --soul|--home
|
|
197
|
+
| teams | soul teams <soul> … --max-age <seconds> --json
|
|
198
|
+
oats spawn <soul> … --preview --max-age <seconds> --json (feature spawn-preview-max-age)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
- **Values:** whole seconds, `0` to `86400`. `0` is live: it reuses nothing.
|
|
202
|
+
Anything else is `E_BAD_ARGS` (`--max-age needs a value: whole seconds from 0
|
|
203
|
+
to 86400`, `--max-age takes whole seconds from 0 to 86400, got "<v>"`).
|
|
204
|
+
- **The block:** with the flag (`0` included) the document gains one key,
|
|
205
|
+
`observation: {observedAt, reused, localRevision}`: in the result of an
|
|
206
|
+
envelope, at the top level of the roster (after `agents`). `observedAt` is the
|
|
207
|
+
OLDEST remote head the answer used, so the answer is at least that fresh
|
|
208
|
+
everywhere; `reused` is `true` when any head came from an earlier observation.
|
|
209
|
+
A command that read no remote head reports the time it started and
|
|
210
|
+
`reused: false`. Without the flag the key is absent and every document is
|
|
211
|
+
exactly as before. A refusal (`E_SOUL_UNKNOWN`, any error envelope) never
|
|
212
|
+
carries the block.
|
|
213
|
+
- **Spawn preview** (feature `spawn-preview-max-age`, OATS 0.33.0): the
|
|
214
|
+
preview's `result` gains the same block, so you can say "as of
|
|
215
|
+
`<observedAt>`". The `decision` covers the heads the preview used, reused
|
|
216
|
+
or live: a reused head the member has since moved from makes the apply
|
|
217
|
+
(which always observes live) refuse `E_DECISION_STALE`, and the apply
|
|
218
|
+
records the head it observed, so the next preview under `--max-age` shows
|
|
219
|
+
it with a new `decision.revision`. The apply itself refuses the flag.
|
|
220
|
+
- **`localRevision`:** 24 lowercase hex characters, opaque. It digests every
|
|
221
|
+
piece of local configuration the kernel read for this answer:
|
|
222
|
+
`oats-local.yaml` (and each closer `oats-local.yaml` it looked for and did
|
|
223
|
+
not find), `oats-lock.json`, an `OATS_PACKAGE_CATALOG` file, and the
|
|
224
|
+
automations snapshot. Only what the verb actually read counts. The same inputs
|
|
225
|
+
give the same revision; any byte change, or one of those files appearing or
|
|
226
|
+
disappearing, gives another. It names no path and no content. Different verbs
|
|
227
|
+
read different inputs (`workspace status` also reads the automations
|
|
228
|
+
snapshot; `inspect --home` reads no lock), so compare revisions of the same
|
|
229
|
+
verb and arguments only. Keep what you
|
|
230
|
+
hold (catalogs, inspect results) keyed on it: a different revision means the
|
|
231
|
+
deployment's configuration changed outside you (a `teams` edit, a sync, a
|
|
232
|
+
hand edit). A kernel upgrade shows through the probe (`oats version --json`),
|
|
233
|
+
not through `localRevision`: the bundled catalog is not an input. Instance
|
|
234
|
+
homes, member clones and tmux are not inputs either, because the answer
|
|
235
|
+
itself reports them.
|
|
236
|
+
- **What is reused:** only remote heads (the commit a branch or tag named),
|
|
237
|
+
never local state. Instances, `oats-local.yaml` and the lock are read afresh
|
|
238
|
+
by every command. A member's backlink (`oats-membership.yaml`) is read at
|
|
239
|
+
the member's observed head, which may be a reused one: a backlink removed
|
|
240
|
+
less than `<s>` seconds ago can still show the member `confirmed` under
|
|
241
|
+
`--max-age <s>`. Only the backlink's comparison with this workspace is made
|
|
242
|
+
afresh. A reused head's
|
|
243
|
+
`observedAt` (for example `workspace.observedAt` in `workspace status`) is
|
|
244
|
+
the time it was observed, not now.
|
|
245
|
+
- **When a head is not reused:** it is older than the flag allows (or dated
|
|
246
|
+
more than 5 s in the future); it was observed through a different URL
|
|
247
|
+
spelling of the same repository (ssh vs https) or for a different ref; its
|
|
248
|
+
commit can no longer be fetched. Each is observed live, as without the flag.
|
|
249
|
+
A live observation that fails is the usual error, never an older head.
|
|
250
|
+
- **Refusals:** every other command, a spawn apply (with or without
|
|
251
|
+
`--expect-decision`; the form named is `spawn`), every edit form (`teams add|remove|default`,
|
|
252
|
+
`soul teams --add|--remove|--default|--clear-default`) and any `--server`
|
|
253
|
+
invocation refuse the flag before reading or writing anything, with
|
|
254
|
+
`E_BAD_ARGS` "--max-age is not accepted by \`oats <form>\`: only the read
|
|
255
|
+
verbs reuse observations (status, workspace status, souls, capabilities,
|
|
256
|
+
inspect --soul|--home, spawn --preview, and the read forms of teams and soul
|
|
257
|
+
teams)" and,
|
|
258
|
+
with `--server`, "--max-age cannot be combined with --server: observation
|
|
259
|
+
reuse is local to this machine".
|
|
260
|
+
A capability command's argv (`oats <namespace> …`) is its provider's: the
|
|
261
|
+
kernel neither reads nor refuses `--max-age` there. The same holds for
|
|
262
|
+
`capture`, `recall`, `setup` and `experimental`, which parse their own argv:
|
|
263
|
+
`capture`, `setup` and `experimental` refuse it as an unknown argument (not
|
|
264
|
+
`E_BAD_ARGS`), and `recall` ignores unknown flags.
|
|
265
|
+
|
|
266
|
+
The observations are kept under the remote cache
|
|
267
|
+
(`$OATS_REMOTE_CACHE`, default `~/.cache/oats/remotes`), in `.observed/`,
|
|
268
|
+
beside the bounded parsed-read cache in `.parsed/`. An observation record
|
|
269
|
+
keeps a digest of the fetch URL, never the URL. A parsed entry keeps repository
|
|
270
|
+
content as committed (member refs included), and a value that carries a
|
|
271
|
+
credential-bearing URL (userinfo on http(s), or `user:password@` on any
|
|
272
|
+
scheme) is never written. Deleting either is always safe.
|
|
273
|
+
|
|
184
274
|
<a id="inspect-readiness-and-operation-run-on-the-workspace-model-operationsapi-2-soulsapi-2-readinessapi-2-oats-0260"></a>
|
|
185
275
|
## Inspect, readiness and operation run
|
|
186
276
|
|
|
@@ -214,7 +304,7 @@ A module's origin (`from`) is `{kind: "member", repoKey, commit}` or `{kind:
|
|
|
214
304
|
### `oats inspect`
|
|
215
305
|
|
|
216
306
|
```text
|
|
217
|
-
oats inspect (--home <abs> | --soul <name> [--dir <d>]) --json
|
|
307
|
+
oats inspect (--home <abs> | --soul <name> [--dir <d>]) [--max-age <s>] --json
|
|
218
308
|
```
|
|
219
309
|
|
|
220
310
|
An instance subject, abridged:
|
|
@@ -445,7 +535,25 @@ Feature `workspace-v2`, `workspaceApi: 2`. Model: [workspaces.md](workspaces.md)
|
|
|
445
535
|
`E_LOCAL_MISSING {dir, searched}`.
|
|
446
536
|
- The workspace is read over Git remotes with the operator's credentials,
|
|
447
537
|
never prompting: `E_REMOTE_UNREADABLE {url, reason: "auth" | "not-found" |
|
|
448
|
-
"network" | "timeout"}`.
|
|
538
|
+
"network" | "timeout" | "killed" | "cache" | "unknown"}`. `killed` (the
|
|
539
|
+
system killed git, for example out of memory) also carries `signal`.
|
|
540
|
+
`cache` (OATS 0.33.0) is local: the
|
|
541
|
+
remote cache on this machine could not be written (a git lock still held,
|
|
542
|
+
another oats process still writing it, or a lock file one left when it
|
|
543
|
+
died); `details.cacheDir` and, when known, `details.lock`,
|
|
544
|
+
`details.guard` or `details.holderPid` say which, and the message says
|
|
545
|
+
what to do.
|
|
546
|
+
- **How the Desktop reads it** (0.33.0). Of an `E_REMOTE_UNREADABLE` from
|
|
547
|
+
`status` / `workspace status`, the Desktop keeps the `message` (shown as
|
|
548
|
+
given) and only a bounded cause: `details.reason` (matching
|
|
549
|
+
`^[a-z][a-z-]{0,31}$`) and the host of `details.url`. No path, pid, lock,
|
|
550
|
+
`cacheDir` or other detail field crosses to the renderer. It keys only on
|
|
551
|
+
`code` + `details.reason`: `cache` words the roster "OATS cache
|
|
552
|
+
problem" with the message in full; `network` / `timeout` read "Couldn't
|
|
553
|
+
reach <host>"; any other reason keeps the generic wording. When the
|
|
554
|
+
deployment was observed before, the failed read keeps that observation:
|
|
555
|
+
`/api/panel` serves it with `error` (the message) and `errorCause {code,
|
|
556
|
+
reason, host?}`, and the roster shows it stale instead of empty.
|
|
449
557
|
- There is no package approval: declaring a package is the trust decision.
|
|
450
558
|
No payload carries `approvalNeeded`, `approval` or `approved`.
|
|
451
559
|
- A **standalone view** is a member repository whose workspace is not read
|
|
@@ -597,7 +705,7 @@ discovers the workspace over the network to check. Other errors: `E_USAGE`,
|
|
|
597
705
|
### `oats workspace status`
|
|
598
706
|
|
|
599
707
|
```text
|
|
600
|
-
oats workspace status [--dir <d>] --json
|
|
708
|
+
oats workspace status [--dir <d>] [--max-age <s>] --json
|
|
601
709
|
```
|
|
602
710
|
|
|
603
711
|
Read-only (it writes no lock):
|
|
@@ -646,8 +754,8 @@ Read-only (it writes no lock):
|
|
|
646
754
|
### `oats capabilities` and `oats souls`
|
|
647
755
|
|
|
648
756
|
```text
|
|
649
|
-
oats capabilities [--dir <d>] --json
|
|
650
|
-
oats souls [--dir <d>] --json
|
|
757
|
+
oats capabilities [--dir <d>] [--max-age <s>] --json
|
|
758
|
+
oats souls [--dir <d>] [--max-age <s>] --json
|
|
651
759
|
```
|
|
652
760
|
|
|
653
761
|
Every item of every confirmed member, the external souls, and the locked
|
|
@@ -835,7 +943,7 @@ soul subject), `""` when unknown.
|
|
|
835
943
|
### `oats teams`
|
|
836
944
|
|
|
837
945
|
```text
|
|
838
|
-
oats teams [--dir <d>] --json
|
|
946
|
+
oats teams [--dir <d>] [--max-age <s>] --json
|
|
839
947
|
oats teams add <label> --team <id> [--description <d>] --json
|
|
840
948
|
oats teams remove <label> --json
|
|
841
949
|
oats teams default <label> --json
|
|
@@ -874,6 +982,7 @@ oats teams default <label> --json
|
|
|
874
982
|
|
|
875
983
|
```text
|
|
876
984
|
oats soul teams <soul>|'*' [--add a,b] [--remove a,b] [--default <label> | --clear-default] [--dir <d>] --json
|
|
985
|
+
oats soul teams <soul>|'*' [--dir <d>] [--max-age <s>] --json (the read form only)
|
|
877
986
|
```
|
|
878
987
|
|
|
879
988
|
```json
|
|
@@ -1083,7 +1192,7 @@ the result with `oats inspect --soul <name> --json`.
|
|
|
1083
1192
|
### The preview
|
|
1084
1193
|
|
|
1085
1194
|
```text
|
|
1086
|
-
oats spawn <soul> [the flags of a real spawn] --preview --json
|
|
1195
|
+
oats spawn <soul> [the flags of a real spawn] --preview [--max-age <s>] --json
|
|
1087
1196
|
```
|
|
1088
1197
|
|
|
1089
1198
|
Feature `spawn-preview-2`, `spawnPreviewApi: 2`. The preview runs every
|
|
@@ -1135,6 +1244,12 @@ it to a temporary copy (`soulFetched: true`).
|
|
|
1135
1244
|
(absent when nothing sets it) are the resolved selection.
|
|
1136
1245
|
`backendStatus` is `{name, installed, started: false}`, `null` with
|
|
1137
1246
|
`--no-launch`. `executable` is the resolved harness binary.
|
|
1247
|
+
- `launchConfigDefault` (0.32, feature `launch-config-default`) is `true`
|
|
1248
|
+
when `launchConfig` is this host's default for the harness (its
|
|
1249
|
+
executable, args, env and `yolo` apply without being chosen), `false`
|
|
1250
|
+
otherwise. Show it, and `yolo`, whenever it is `true`. An explicit
|
|
1251
|
+
`--launch-config none` asks for the bare harness and bypasses the default;
|
|
1252
|
+
to run the host's default, omit `--launch-config`.
|
|
1138
1253
|
- `modelSource` is `"explicit"`, `"soul default"`, `"launch-config <name>"`,
|
|
1139
1254
|
`"native default"` or `"native default (explicit)"` (`--model
|
|
1140
1255
|
@native-default`). Omitting `--model` and asking for the native default are
|
|
@@ -1165,6 +1280,20 @@ it to a temporary copy (`soulFetched: true`).
|
|
|
1165
1280
|
`payloadRevision` (the merged payloads). `workspace` is the host key;
|
|
1166
1281
|
`standalone` marks a standalone view. `task` is the task text or `null`.
|
|
1167
1282
|
|
|
1283
|
+
**Observation reuse** (feature `spawn-preview-max-age`, OATS 0.33.0).
|
|
1284
|
+
- `--preview --max-age <s>` reuses member heads this machine observed at
|
|
1285
|
+
most `<s>` seconds ago, as the read verbs do ([Observation
|
|
1286
|
+
reuse](#observation-reuse-feature-observe-max-age-oats-0311): the same
|
|
1287
|
+
values, refusals and fallbacks to a live observation). With the flag (`0`
|
|
1288
|
+
included) the result gains `observation: {observedAt, reused,
|
|
1289
|
+
localRevision}`, shaped exactly as the read verbs' block; without it the
|
|
1290
|
+
preview is exactly as before, and no other field changes shape.
|
|
1291
|
+
- `decision.revision` covers the heads the preview used, reused or live.
|
|
1292
|
+
Apply never reuses (it refuses `--max-age`): a head that moved since the
|
|
1293
|
+
reused observation refuses `E_DECISION_STALE`, and the apply records what
|
|
1294
|
+
it observed, so re-preview under `--max-age` to get the new head and
|
|
1295
|
+
revision.
|
|
1296
|
+
|
|
1168
1297
|
**Provider settings.**
|
|
1169
1298
|
- `providers` is the `--provider` map as typed.
|
|
1170
1299
|
- `settings.<cap>`: the merged payload (manifest defaults, then workspace,
|
|
@@ -1346,10 +1475,11 @@ the keyed-spawn fields `decision`, `spawnIdempotencyKey`, `spawnCompleted` and
|
|
|
1346
1475
|
### The roster (`oats status --json`)
|
|
1347
1476
|
|
|
1348
1477
|
```text
|
|
1349
|
-
oats status [--dir <d>] --json
|
|
1478
|
+
oats status [--dir <d>] [--max-age <s>] --json
|
|
1350
1479
|
```
|
|
1351
1480
|
|
|
1352
|
-
Not an envelope: `{root, agents, workspace?, problems?, warnings?}
|
|
1481
|
+
Not an envelope: `{root, agents, observation?, workspace?, problems?, warnings?}`
|
|
1482
|
+
(`observation` only with [`--max-age`](#observation-reuse-feature-observe-max-age-oats-0311)).
|
|
1353
1483
|
|
|
1354
1484
|
```json
|
|
1355
1485
|
{"root":"/w/agents",
|
|
@@ -1812,26 +1942,45 @@ accept `--server <id>`.
|
|
|
1812
1942
|
{"context":"/w","level":"/w","file":"/w/oats-local.yaml","selected":null,
|
|
1813
1943
|
"configurations":[{"name":"reviewers","harness":"claude","executable":null,"args":["--permission-mode","plan"],
|
|
1814
1944
|
"env":{"ANTHROPIC_API_KEY":{"fromEnv":"REVIEW_KEY"},"REVIEW_MODE":{"redacted":true}},
|
|
1815
|
-
"model":"opus","yolo":null,"source":"/w/oats-local.yaml","shadows":[]}]}
|
|
1945
|
+
"model":"opus","yolo":null,"default":false,"source":"/w/oats-local.yaml","shadows":[]}]}
|
|
1816
1946
|
```
|
|
1817
1947
|
|
|
1818
1948
|
- **list**: `selected` is `null`, `{home, instance}` or `{soul, agentsRoot}`;
|
|
1819
1949
|
`level` and `file` are `null` without an `oats-local.yaml` (the set is then
|
|
1820
1950
|
empty). An environment literal is `{redacted: true}`, a reference
|
|
1821
|
-
`{fromEnv}`; values never leave the file.
|
|
1951
|
+
`{fromEnv}`; values never leave the file. `default` (0.32, feature
|
|
1952
|
+
`launch-config-default`) is always a boolean: `true` marks this host's
|
|
1953
|
+
default for the configuration's harness.
|
|
1822
1954
|
- **set**/**remove**: `{name, action, level, file, before, after,
|
|
1823
1955
|
effective}`. `set --file` takes `{harness, executable?, args?, env?, model?,
|
|
1824
|
-
yolo?}` (`-` reads stdin)
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
`
|
|
1956
|
+
yolo?, default?}` (`-` reads stdin); it replaces the whole entry and refuses
|
|
1957
|
+
a key it does not know, so an editor sends `default` back to keep it.
|
|
1958
|
+
`default: false` is written as its absence. A second `default: true` for a
|
|
1959
|
+
harness is `E_LAUNCH_CONFIG_INVALID` with `details: {harness,
|
|
1960
|
+
configurations: [<the declared one>, <this one>]}` and nothing is written;
|
|
1961
|
+
moving the default is two writes, never an automatic move. `--keep-env`
|
|
1962
|
+
keeps the declared environment when `env` is omitted. Routed with
|
|
1963
|
+
`--server`, a definition with `default: true` to a host that does not
|
|
1964
|
+
advertise `launch-config-default` is `E_REMOTE_INCOMPATIBLE` before
|
|
1965
|
+
anything is sent (`default: false` is dropped). Errors: `E_LOCAL_MISSING`,
|
|
1966
|
+
`E_BAD_ARGS` (including `--home`/`--soul`), `E_LAUNCH_CONFIG_UNKNOWN`,
|
|
1967
|
+
`E_LAUNCH_CONFIG_INVALID`, `E_CONFIG_BROKEN`, `E_HOME_UNKNOWN`,
|
|
1968
|
+
`E_REMOTE_INCOMPATIBLE`.
|
|
1828
1969
|
- **preview** (read-only) answers `{context, selected, selection: {source,
|
|
1829
1970
|
launchConfig, harness, model, yolo}, harness, model, modelSource, yolo,
|
|
1830
|
-
launchConfig, launchConfigSource, executable: {path,
|
|
1831
|
-
resolvedFrom}, argv, environment: [{name, fromEnv} | {name, redacted:
|
|
1971
|
+
launchConfig, launchConfigSource, launchConfigDefault, executable: {path,
|
|
1972
|
+
declared, resolvedFrom}, argv, environment: [{name, fromEnv} | {name, redacted:
|
|
1832
1973
|
true} | {name, reference: true}], command (redacted), prompt, hooks,
|
|
1833
1974
|
preflight: [{check, ok, detail}], ok}`.
|
|
1834
1975
|
|
|
1976
|
+
`launchConfigDefault` (0.32) is `true` when `launchConfig` is this host's
|
|
1977
|
+
default for the harness rather than a chosen configuration; the spawn preview
|
|
1978
|
+
carries the same top-level field, `instance.json` records it as
|
|
1979
|
+
`launch.launchConfigDefault: true`, and `inspect --json` of a home answers
|
|
1980
|
+
`instance.launchConfig` and `instance.launchConfigDefault`. The closed `Launch`
|
|
1981
|
+
object is unchanged: its `effective.launchConfig` names the default
|
|
1982
|
+
configuration.
|
|
1983
|
+
|
|
1835
1984
|
A successful envelope can carry `ok: false`: show the failed `preflight`
|
|
1836
1985
|
checks. The prompt is named, never the task body. A home predating launch
|
|
1837
1986
|
recipes answers its frozen command with `selection.source: "frozen-command"`
|
package/docs/desktop.md
CHANGED
|
@@ -110,6 +110,22 @@ registered remote workspace the timer and definitions live on that server, so
|
|
|
110
110
|
they do not depend on the Mac staying awake. See [Schedules](schedules.md) for
|
|
111
111
|
the CLI, cron semantics, observed outcomes and recovery commands.
|
|
112
112
|
|
|
113
|
+
## Instances on servers
|
|
114
|
+
|
|
115
|
+
Every instance a registered server reports shows in its workspace's roster, whoever spawned it. You
|
|
116
|
+
can open its terminal, start, restart, stop and remove it, and read its readiness, activity, Git
|
|
117
|
+
and diffs, as for a local one. Only its pull request is not read here, since the forge reads this
|
|
118
|
+
computer's clones. Every command goes to the server by the instance's home (`--server <id> --home
|
|
119
|
+
<path>`), never by a bare name. Stop and Remove show the plan the server makes, and confirm
|
|
120
|
+
against it.
|
|
121
|
+
|
|
122
|
+
A read waits for the server: the view says "Reading from <server>…", and gives up after about
|
|
123
|
+
45 seconds with "Couldn't reach <server>." When the server refuses, you see its code and message;
|
|
124
|
+
nothing is read from this computer in its place. A row that can't be opened says why on the row:
|
|
125
|
+
Herdr no longer supported, gone from <server>, not reachable on <server>, or <server> not reached.
|
|
126
|
+
For an instance a server no longer lists, the reason names the command that removes it from this
|
|
127
|
+
computer (`oats server forget <server> --instance <name>`).
|
|
128
|
+
|
|
113
129
|
## Remote terminals
|
|
114
130
|
|
|
115
131
|
A terminal for an instance on a registered server is a viewer over ssh
|
package/docs/first-team.md
CHANGED
|
@@ -80,6 +80,23 @@ Host-owned provider values (absolute paths, state roots) go under `settings:` in
|
|
|
80
80
|
`oats-local.yaml` afterwards — never in the workspace file, whose schema refuses
|
|
81
81
|
them. Do not commit `oats-local.yaml`.
|
|
82
82
|
|
|
83
|
+
**Trust the deployment once, for unattended launches.** Claude Code and Codex
|
|
84
|
+
ask before they work in a folder they have not seen, and every instance home is
|
|
85
|
+
new: a launch that stops at that prompt waits for a human. OATS never writes
|
|
86
|
+
the harnesses' configuration, so trust the deployment directory yourself, once
|
|
87
|
+
per harness you use:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
cd ~/acme && claude # accept the folder-trust prompt, then quit
|
|
91
|
+
cd ~/acme && codex # choose "Trust and continue", then quit
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
One entry covers every instance home under the deployment
|
|
95
|
+
([souls-and-instances.md](souls-and-instances.md#unattended-launches-folder-trust)
|
|
96
|
+
says how each harness applies it). Until then, a claude or codex spawn warns
|
|
97
|
+
that its session will stop at the folder-trust prompt, and so does
|
|
98
|
+
`oats readiness`.
|
|
99
|
+
|
|
83
100
|
## 3. Give the deployment a team
|
|
84
101
|
|
|
85
102
|
With a messaging capability in the soul's composition, every instance lives in
|
|
@@ -117,8 +134,9 @@ oats spawn backend-expert --purpose first-fix --task "Fix one small issue, run t
|
|
|
117
134
|
oats status
|
|
118
135
|
```
|
|
119
136
|
|
|
120
|
-
`--harness pi|claude|codex` picks the harness; complete any native
|
|
121
|
-
|
|
137
|
+
`--harness pi|claude|codex` picks the harness; complete any native
|
|
138
|
+
authentication prompt in the printed session (folder trust is the one-time step
|
|
139
|
+
in section 2). The instance home is
|
|
122
140
|
`agents/<soul>/instances/<instance>/`; `work/` is its repository view;
|
|
123
141
|
`.oats/modules/<cap>/` are the copied capabilities and `.agents/skills/<skill>/` their skills;
|
|
124
142
|
`instance.json` records `modules` (from, commit, digest), `providers` and
|
package/docs/implementation.md
CHANGED
|
@@ -30,7 +30,7 @@ published to npm. Its developer docs are in
|
|
|
30
30
|
| `lib/` | the kernel (below) |
|
|
31
31
|
| `injects/` | the kernel and work-mode instruction blocks composed into every instance |
|
|
32
32
|
| `skills/` | bootstrap skills shipped with the kernel |
|
|
33
|
-
| `capabilities/` | this repository's own member capabilities (`oats-workspace-experts`), discovered at the member's latest state |
|
|
33
|
+
| `capabilities/` | this repository's own member capabilities (`oats-desktop-ui`, `oats-workspace-experts`), discovered at the member's latest state |
|
|
34
34
|
| `mirrors/` | generated byte mirrors of the official packages' capabilities (for example `oats-okf*`, checked by `scripts/check-okf-mirror.mjs`): release-lane and test material, kept out of `capabilities/` so member discovery does not list them a second time; not shipped in the npm package |
|
|
35
35
|
| `oats-package/` | the `oats.framework` package |
|
|
36
36
|
| `souls/` | this repository's own souls (a workspace member) |
|
|
@@ -44,6 +44,7 @@ published to npm. Its developer docs are in
|
|
|
44
44
|
| module | owns |
|
|
45
45
|
|---|---|
|
|
46
46
|
| `remote.mjs` | repo refs, reading Git remotes, content digests |
|
|
47
|
+
| `local-inputs.mjs` | the local configuration a command read, for `observation.localRevision` |
|
|
47
48
|
| `workspace.mjs` | workspace, membership and soul files; discovery |
|
|
48
49
|
| `resolve.mjs` | a soul's resolution: capabilities, slots, provenance |
|
|
49
50
|
| `packages.mjs` | `packages:`, the catalog, `oats sync`, `oats-lock.json` |
|
|
@@ -57,11 +58,130 @@ published to npm. Its developer docs are in
|
|
|
57
58
|
| `tmux-config.mjs`, `session-*.mjs` | the tmux session backend and terminal input |
|
|
58
59
|
| `capability-contract.mjs`, `provider-binding.mjs` | manifest validation, the hook environment rules, the readiness wire |
|
|
59
60
|
| `servers.mjs` | routing commands to a registered server |
|
|
61
|
+
| `harness-trust.mjs` | reading (never writing) Claude's and Codex's folder trust for a launch |
|
|
60
62
|
|
|
61
63
|
The kernel is runtime-neutral: nothing in `lib/` depends on a harness or on
|
|
62
64
|
a provider. Provider behaviour lives in capabilities; the kernel supplies
|
|
63
65
|
their contracts ([layers](layers.md)).
|
|
64
66
|
|
|
67
|
+
### The remote read path
|
|
68
|
+
|
|
69
|
+
Every CLI command owns one read session (`createReadSession` in
|
|
70
|
+
`remote.mjs`, carried to every remote call as `remoteOptions.session`; the
|
|
71
|
+
CLI closes it when the command ends). A library caller without a session
|
|
72
|
+
gets the plain per-call behaviour. Within a session:
|
|
73
|
+
|
|
74
|
+
- a head is observed once per (cache repo, ref), and a commit peeled once; at
|
|
75
|
+
most eight observations run at once (`OBSERVE_LIMIT`), each holding its slot
|
|
76
|
+
for all its git work (the `ls-remote` and the fetch of the commit it names,
|
|
77
|
+
or the fetch of a reused record's commit);
|
|
78
|
+
- a whole-workspace discovery prefetches its members' heads together with the
|
|
79
|
+
host's (`prefetchMembers` in `workspace.mjs`): the member list comes from the
|
|
80
|
+
host's last observation record and the parsed `workspace` entry at that
|
|
81
|
+
commit, never from a git process, and the answer still uses the list at the
|
|
82
|
+
host commit observed now. A prefetched failure is adopted by the member's own
|
|
83
|
+
observation, not retried in the command. A prefetch no caller adopts (the
|
|
84
|
+
host could not be observed, or the member was dropped since) is abandoned:
|
|
85
|
+
one still queued runs no git. `observeWorkspace` alone (the `teams` reads,
|
|
86
|
+
`inspect --home`) never prefetches;
|
|
87
|
+
- closing the session (the end of the command, or `process.exit`) rejects
|
|
88
|
+
every queued observation and aborts every git child still running for it
|
|
89
|
+
(the session's `AbortSignal` rides every `runGit`). `runGit` starts git as
|
|
90
|
+
its own process group, so a timeout, an output overflow or an abort kills
|
|
91
|
+
git's ssh or remote helper with it; before a capability
|
|
92
|
+
command runs its provider, the CLI ends the idle batch readers
|
|
93
|
+
(`closeBatches`);
|
|
94
|
+
- a commit's tree is listed once (`git ls-tree -r -t -l`, bounded by
|
|
95
|
+
`TREE_INDEX_BUDGET`; anything odd falls back to the per-path reads), and
|
|
96
|
+
blobs come from one `git cat-file --batch` reader per cache repo (at most
|
|
97
|
+
12 open, ended through `process-group.mjs` on timeout and at close);
|
|
98
|
+
`fetchRemoteTree` copies a module through it too, once `ensureBlobs` has
|
|
99
|
+
fetched what was missing (git re-reads its packs on a miss, so a reader
|
|
100
|
+
opened earlier finds the new blobs; one still missing answers `missing`,
|
|
101
|
+
never a fetch), with what is left of `TREE_BUDGET` as each read's bound;
|
|
102
|
+
a blob the reader answers `missing` or over its bound is read once more
|
|
103
|
+
alone, so the error is the one a copy without a session gives. A
|
|
104
|
+
command that ends normally awaits the close, so its readers are reaped
|
|
105
|
+
before it exits; a `process.exit` (every refusal) ends them in the
|
|
106
|
+
exit hook (`closeNow`), and the system reaps them once the process is gone;
|
|
107
|
+
- every git child is ended with SIGTERM first and SIGKILL only after a
|
|
108
|
+
grace (`terminateGroup`): git removes its own lock files on SIGTERM, and
|
|
109
|
+
a git killed outright leaves one that blocks every later write. The
|
|
110
|
+
SIGKILL goes to the whole group even when git itself has exited, so a
|
|
111
|
+
descendant that ignores SIGTERM (ssh, a remote helper) still ends; but
|
|
112
|
+
never to a group seen empty, whose id may already lead an unrelated
|
|
113
|
+
group. Until git's `close` (`watchGroup`), a member holding its pipes
|
|
114
|
+
keeps the id ours; after it, the group is probed every 50 ms through the
|
|
115
|
+
grace (no pid is allocated while it is a live group's id): empty, and it
|
|
116
|
+
is never signalled again. git's pipes are drained on a kill, never
|
|
117
|
+
destroyed, so `close` keeps waiting for a pipe-holding descendant. The exit
|
|
118
|
+
hook cannot wait for a timer, so it waits a bounded 200 ms synchronously
|
|
119
|
+
(`reapOnExit`);
|
|
120
|
+
- discovery reads members eight at a time (`DISCOVERY_CONCURRENCY`) with
|
|
121
|
+
serial results: declaration order, the first failure in that order. The
|
|
122
|
+
observations and the member reads are two pools, so a discovery runs at
|
|
123
|
+
most sixteen short-lived git processes at once (eight of them fetches at
|
|
124
|
+
most), plus up to twelve cat-file readers: twenty-eight git processes. One
|
|
125
|
+
shared pool would deadlock: a member read holding a slot waits on its
|
|
126
|
+
member's observation, which needs a slot of its own.
|
|
127
|
+
|
|
128
|
+
The cache repos are partial: a commit is fetched with all its trees and
|
|
129
|
+
only the blobs up to `SMALL_BLOB_LIMIT` (64 KiB), which covers every file
|
|
130
|
+
discovery reads, so listings and discovery stay local after one fetch. A
|
|
131
|
+
read that needs a larger blob, or a `fetchRemoteTree` of a module, fetches
|
|
132
|
+
the missing blobs first in one fetch by id (`ensureBlobs`), then applies the
|
|
133
|
+
budgets to their real sizes before anything is written. git never fetches a
|
|
134
|
+
blob lazily (`GIT_NO_LAZY_FETCH=1`, and no url is stored in the cache: each
|
|
135
|
+
fetch passes it with `-c remote.origin.url=`). A server without partial
|
|
136
|
+
fetches gets whole trees; the cache records that (`oats.fetch = full` in its
|
|
137
|
+
config) and the CLI prints the session's notice once, on stderr. Partial
|
|
138
|
+
caches need git 2.45 or later (`PARTIAL_FETCH_GIT`, the first git with
|
|
139
|
+
`GIT_NO_LAZY_FETCH`): with an older git every cache fetches whole trees, a
|
|
140
|
+
partial cache it meets is deleted and fetched again whole, and the same
|
|
141
|
+
notice says why.
|
|
142
|
+
|
|
143
|
+
Every write to a cache repo (its `git init`, config, fetches and pins) holds
|
|
144
|
+
the repo's cross-process write lock, `<cache>/.locks/<repo>.lock`
|
|
145
|
+
(`withCacheWriteLock`): an exclusive file holding `{pid, token, startedAt}`,
|
|
146
|
+
waited for while its holder lives (bounded by a whole fetch, then
|
|
147
|
+
`reason: "cache"` naming the pid), reclaimed when the holder is dead, and
|
|
148
|
+
released only by its owner. Reclaimers take a short guard,
|
|
149
|
+
`<lock>.reclaim`, and check under it that the lock is still the dead
|
|
150
|
+
record before removing it, so a reclaimer that paused cannot delete a
|
|
151
|
+
live process's new lock. A guard whose holder died is never removed
|
|
152
|
+
automatically (that removal would race the same way, with nothing left to
|
|
153
|
+
serialize it): every write refuses at once, `reason: "cache"` naming the
|
|
154
|
+
guard (`details.guard`), until a human removes it once no oats process is
|
|
155
|
+
running. Reads take no lock. A cache repo appears whole
|
|
156
|
+
(`git init` into a private directory, then a rename), so processes making
|
|
157
|
+
the first fetch of one remote all succeed. A git `*.lock` a write meets is
|
|
158
|
+
judged under that lock (`cacheGit`): older oats kernels take no write lock,
|
|
159
|
+
so it is retried briefly, then removed only when it is inside the cache
|
|
160
|
+
repo, a regular file and older than the longest fetch
|
|
161
|
+
(`GIT_FETCH_TIMEOUT_MS` plus a margin): a git killed mid-write. A removal
|
|
162
|
+
is said once as a warning. Anything else is `reason: "cache"` naming the
|
|
163
|
+
file and when it is safe to remove; so is any other local write failure
|
|
164
|
+
(a `FETCH_HEAD` git cannot open, a read-only or full disk), with git's own
|
|
165
|
+
words.
|
|
166
|
+
|
|
167
|
+
Across commands, `memoAtCommit` keeps parsed reads under
|
|
168
|
+
`<cache>/.parsed/<kernel fingerprint>/`, keyed by (repo key, full commit,
|
|
169
|
+
item). The items: `workspace` (the workspace file), `membership` (a member's
|
|
170
|
+
backlink outcome), `enumerate` (a member's souls and capabilities),
|
|
171
|
+
`package-soul` and `external-soul` (one soul file each; their error handling
|
|
172
|
+
differs), `package-manifests` (a package's manifests), `list` (a skill
|
|
173
|
+
listing) and `tree-oids` (the tree ids of a set of directories). An entry is only ever a pure function of those bytes and this kernel's
|
|
174
|
+
code, never local state and never a transient error; it is written
|
|
175
|
+
atomically, a corrupt one is a miss, and `pruneStores` bounds the store
|
|
176
|
+
(`PARSED_LIMITS`, least recently used first). `--max-age` adds the observation
|
|
177
|
+
store `<cache>/.observed/` ([Observation reuse](desktop-cli-api.md#observation-reuse-feature-observe-max-age-oats-0311)):
|
|
178
|
+
one record per (repo key, ref args, url digest), so two spellings of one repo
|
|
179
|
+
keep a record each; the url itself is never written. Adding a cached item
|
|
180
|
+
means choosing an item name unique to its producer (the item string its
|
|
181
|
+
call site passes to `memoAtCommit`, or to `atCommit` in `workspace.mjs`) and
|
|
182
|
+
adding it to `test/parsed-cache.test.mjs`; `test/read-path-scale.test.mjs` pins the member
|
|
183
|
+
scaling by call count.
|
|
184
|
+
|
|
65
185
|
## Tests and gates
|
|
66
186
|
|
|
67
187
|
| command | what it checks |
|