@awebai/oats 0.40.1 → 0.40.2

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/bin/oats.mjs CHANGED
@@ -490,7 +490,7 @@ function finishOperation({ r, bail, address, provider, op, argFlags, cwd, home,
490
490
  // (a partial receipt such as result.instance of something it launched
491
491
  // before failing, and any details it gave) travels in error.details so a
492
492
  // scheduler can keep an unconfirmed outcome and reconcile that target.
493
- if (!envelope.ok) bail(envelope.error?.code || "E_OPERATION_FAILED", `${address}: ${envelope.error?.message || "failed"}`, { exit: r.status, envelope, ...(reportsRetainedEffectsText(envelope.error?.message) ? { unconfirmed: true } : {}) });
493
+ if (!envelope.ok) bail(envelope.error?.code || "E_OPERATION_FAILED", `${address}: ${envelope.error?.message || "failed"}`, { exit: r.status, envelope, ...(envelope.error?.details?.unconfirmed === true || reportsRetainedEffectsText(envelope.error?.message) ? { unconfirmed: true } : {}) });
494
494
  if (r.status !== 0) bail("E_OPERATION_RESULT", `${address} (${provider.capability} ${op.command}) answered ok but exited ${r.status}; the receipt is not trusted and its effects are unconfirmed${stderr ? `: ${stderr.slice(0, 400)}` : ""}`, observed(envelope));
495
495
  const result = envelope.result && typeof envelope.result === "object" ? envelope.result : {};
496
496
  if (op.kind === "view") {
@@ -2413,8 +2413,8 @@ async function spawnCmd() {
2413
2413
  if (e?.code === "E_PLACEMENT_TAKEN") { bail(e.code, e.message, { instance: e.instance, home: e.home }); throw e; }
2414
2414
  if (e?.code === "E_INSTANCE_NAME_TAKEN") { bail(e.code, e.message, { instance: e.instance, home: e.home ?? null, ...(e.session ? { session: e.session } : {}) }); throw e; }
2415
2415
  if (e?.code === "E_INSTANCE_NAME_INVALID") { bail(e.code, e.message, e.details); throw e; }
2416
- if (e?.code === "E_SPAWN_INCOMPLETE") { bail(e.code, e.message, { instance: e.instance, home: e.home, launched: e.launched }); throw e; }
2417
- bail(["E_BAD_ARGS", "E_RELATIVE_AMBIGUOUS"].includes(e.code) ? e.code : "E_SPAWN_FAILED", e.message || e); throw e;
2416
+ if (e?.code === "E_SPAWN_INCOMPLETE") { bail(e.code, e.message, { ...e.details, instance: e.instance, home: e.home, launched: e.launched }); throw e; }
2417
+ bail(["E_BAD_ARGS", "E_RELATIVE_AMBIGUOUS"].includes(e.code) ? e.code : "E_SPAWN_FAILED", e.message || e, e.details?.unconfirmed === true ? e.details : undefined); throw e;
2418
2418
  }
2419
2419
  // The instance exists from here on: a failed wake save is reported beside
2420
2420
  // the full receipt, never hidden, and never causes a second spawn.
@@ -3818,7 +3818,7 @@ else if (cmd === "session") await sessionCmd();
3818
3818
  else if (cmd === "schedule") await scheduleCmd();
3819
3819
  else if (cmd === "trigger") await triggerCmd();
3820
3820
  else if (cmd === "automations") await automationsCmd();
3821
- else if (cmd === "spawn") { try { await spawnCmd(); } catch (e) { if (TYPED_CLI_FAILURES.has(e?.code)) throw e; if (JSON_MODE) jsonFail("E_SPAWN_FAILED", e.message || e); throw e; } }
3821
+ else if (cmd === "spawn") { try { await spawnCmd(); } catch (e) { if (TYPED_CLI_FAILURES.has(e?.code)) throw e; if (JSON_MODE) jsonFail("E_SPAWN_FAILED", e.message || e, e.details?.unconfirmed === true ? e.details : undefined); throw e; } }
3822
3822
  else if (cmd === "retire") retireCmd();
3823
3823
  else if (cmd === "capture" || cmd === "recall" || cmd === "setup") await recordCmd(cmd);
3824
3824
  else if (cmd === "experimental") await experimentalCmd();
@@ -4062,7 +4062,7 @@ Usage:
4062
4062
  typed lifecycle events (spawned, launched, stopped,
4063
4063
  restarted, retired, worktree-retained…) written by
4064
4064
  the action that made them true; nothing inferred
4065
- oats instance waiting <set|clear> --producer <id> [--reason permission|question|attention] [--message <text>] [--home <abs>] [--json]
4065
+ oats instance waiting <set|clear> --producer <id> [--reason permission|question|attention] [--message <text>] [--home <abs>] [--dir <d>] [--json]
4066
4066
  a producer's claim that the instance needs input
4067
4067
  from a human; appended only on change; display only
4068
4068
  oats instance attention [--message <text>] [--clear] [--json]
@@ -4135,7 +4135,7 @@ Layers: ${LAYERS.join(", ")}. Workspace model v2: docs/design/2026-09-23-workspa
4135
4135
  // Same two renderings as every other typed failure: one envelope on stdout in
4136
4136
  // --json mode, one `oats: <message>` line on stderr otherwise. The message
4137
4137
  // already names the offending file — the readers re-raise it with one.
4138
- if (JSON_MODE) jsonFail(e.code, e.message);
4138
+ if (JSON_MODE) jsonFail(e.code, e.message, e.details?.unconfirmed === true ? e.details : undefined);
4139
4139
  die(e.message);
4140
4140
  } finally {
4141
4141
  // The command's read session: every `git cat-file --batch` child ends before the process does.
@@ -544,6 +544,18 @@ runs `bin/claude-waiting.sh` from the home's module copy, which calls
544
544
  | `PostToolUseFailure` | `*` | clear, unless a subagent made the call |
545
545
  | `UserPromptSubmit`, `Stop`, `SessionEnd` | none | clear, not debounced (a turn boundary) |
546
546
 
547
+ **One home per settings file.** The launch hook bakes the home it writes the
548
+ settings for into every command, as its real path (oats.core 2.4.1). The
549
+ script acts only when the session's `$OATS_INSTANCE_HOME` names that same
550
+ home, through any spelling (a symlinked deployment resolves to the same
551
+ path). A Claude process that loads one home's settings while carrying
552
+ another instance's environment (a nested `claude -p`, a `claude -p` started
553
+ with its working directory in another home, a pane that inherited the
554
+ variables) does nothing at all: no CLI call and no marker write, for either
555
+ home. Before 2.4.1 it set and cleared the claim of the home its environment
556
+ named, and its `Stop` and `SessionEnd` clears could erase that instance's
557
+ real claim.
558
+
547
559
  Claude Code shows an AskUserQuestion through its permission dialog, so that
548
560
  dialog's own `permission_prompt` follows the question's set: the script keeps
549
561
  the current reason in its marker, and a permission prompt never relabels an
@@ -597,7 +609,8 @@ clears it.
597
609
  after the hook began, the worst case is about 4 s, well under Claude's 5 s
598
610
  hook timeout.
599
611
  - **Debounce.** The script keeps private state outside the home, in a file
600
- per home: `<dir>/<first 16 hex of sha256(home)>.claude`, where `<dir>`
612
+ per home: `<dir>/<first 16 hex of sha256(home)>.claude` (the home's real
613
+ path, so each spelling of a home has the same file), where `<dir>`
601
614
  is per user: `$XDG_RUNTIME_DIR/oats-waiting` when that is set and
602
615
  absolute, else `$TMPDIR/oats-waiting-<uid>` when `TMPDIR` is absolute,
603
616
  else `/tmp/oats-waiting-<uid>`. The spawn and launch
@@ -715,6 +728,11 @@ again" permission rules there.
715
728
  - A `Stop` or `UserPromptSubmit` always clears, even one the main thread
716
729
  produces while a subagent's prompt is open (a background subagent's
717
730
  completion is submitted as a prompt).
731
+ - The one-home rule separates homes, not two sessions of one home: a second
732
+ Claude process started inside the same home with that home's own
733
+ `$OATS_INSTANCE_HOME` (a `claude -p` the agent runs there) still matches,
734
+ so its `Stop` and `SessionEnd` clears can erase the home's own live
735
+ `oats.core` claim ([#557](https://github.com/awebai/oats/issues/557)).
718
736
  - A Claude instance spawned before the upgrade gets the emitter only when
719
737
  it is respawned. Its launch hook comes from its recorded module copy.
720
738
 
@@ -752,7 +770,22 @@ with no provider or a home operation without `--home`
752
770
  The provider must exit 0 with exactly one JSON envelope on stdout. Otherwise
753
771
  the outcome is **unconfirmed**: `E_OPERATION_TIMEOUT` (after 240 s) or
754
772
  `E_OPERATION_RESULT`, with `error.details { unconfirmed: true, exit, envelope?, stderr? }`.
755
- A provider's own `ok: false` is relayed with its code. A schedule of kind
773
+ A provider's own `ok: false` is relayed with its code and full envelope. When
774
+ the provider sets `error.details.unconfirmed: true` (the boolean), the operation
775
+ wrapper also sets its outer `error.details.unconfirmed: true`. Providers should
776
+ set that marker when dispatched effects or their compensation cannot be
777
+ confirmed, and preserve it through wrappers. Producers should leave ordinary
778
+ refusals and fully compensated failures unmarked: naming a home or retained
779
+ evidence is not itself uncertainty. The operation wrapper and scheduler still
780
+ apply their existing message-based compatibility checks during this additive
781
+ migration, including for copied providers in older homes; their text-based
782
+ false positives are not removed by this change.
783
+
784
+ The kernel marks incomplete keyed spawns (`E_SPAWN_INCOMPLETE`) and spawn
785
+ failures whose rollback cannot finish with the same field, through the CLI.
786
+ A completed rollback remains an unmarked failure. This adds structural evidence;
787
+ it does not remove text fallbacks or change scheduler slot and retry rules.
788
+ A schedule of kind
756
789
  `operation` runs the same command ([schedules.md](schedules.md)). The JSON
757
790
  shapes are in [desktop-cli-api.md](desktop-cli-api.md#inspect-readiness-and-operation-run-on-the-workspace-model-operationsapi-2-soulsapi-2-readinessapi-2-oats-0260).
758
791
 
@@ -543,7 +543,10 @@ oats operation run <layer>:<name> (--home <abs> | --soul <name> [--dir <d>]) [--
543
543
  home operation without `--home`), `E_CAPABILITY_REQUIRES`, `E_BAD_ARGS`
544
544
  (undeclared or missing `--arg`), `E_CAPABILITY_BROKEN`. A provider's `ok:
545
545
  false` is relayed with its code and `details: {exit, envelope,
546
- unconfirmed?}`. `E_OPERATION_TIMEOUT` (240 s) and `E_OPERATION_RESULT` are
546
+ unconfirmed?}`. A literal provider `error.details.unconfirmed: true` is
547
+ promoted to the outer details while its full envelope stays nested. Existing
548
+ retained-effect text checks remain for compatibility during migration.
549
+ `E_OPERATION_TIMEOUT` (240 s) and `E_OPERATION_RESULT` are
547
550
  unconfirmed outcomes: `details: {exit, signal, unconfirmed: true,
548
551
  envelope?, stderr?, cleanup?}`.
549
552
 
@@ -1617,7 +1620,7 @@ with `--expect-decision` records the key and decision in `instance.json`.
1617
1620
  `E_IDEMPOTENCY_CONFLICT {instance, home}`.
1618
1621
  - `spawnCompleted` is `false` until launch, lineage and events are done; a
1619
1622
  retry of an unfinished spawn is `E_SPAWN_INCOMPLETE {instance, home,
1620
- launched}` (recover through the session surface).
1623
+ launched, unconfirmed: true}` (recover through the session surface).
1621
1624
  - The key lives in the home. Mint it on the first confirmation and keep it
1622
1625
  for that intent's retries.
1623
1626
  - `wake: {requested, saved, error}` is recorded and replayed; `saved: null`
@@ -1686,11 +1689,17 @@ Feature `spawn-name`. `--name <slug>` is the exact name, with no prefix.
1686
1689
  | `E_INSTANCE_NAME_INVALID`, `E_INSTANCE_NAME_TAKEN` | see above | |
1687
1690
  | `E_DECISION_STALE` | `{decision}` | |
1688
1691
  | `E_PLACEMENT_TAKEN`, `E_IDEMPOTENCY_CONFLICT` | `{instance, home}` | |
1689
- | `E_SPAWN_INCOMPLETE` | `{instance, home, launched}` | |
1692
+ | `E_SPAWN_INCOMPLETE` | `{instance, home, launched, unconfirmed: true}` | |
1690
1693
  | `E_LAUNCH_*`, `E_MODEL_UNKNOWN`, `E_UNSUPPORTED_HARNESS` | | the launch selection is refused |
1691
1694
  | `E_LAUNCH_SHIM` | | the home's `oats` (`<home>/.oats/bin/oats`) cannot be written; the spawn is rolled back |
1692
1695
  | `E_SCHEDULE_INVALID` | | a bad wake (`--wake-json`, `--wake-file`, `--wake-*`) |
1693
- | `E_SPAWN_FAILED` | | anything else |
1696
+ | `E_SPAWN_FAILED` | `{unconfirmed: true}` when compensation cannot finish | anything else |
1697
+
1698
+ Spawn failure envelopes carry `error.details.unconfirmed: true` when an existing
1699
+ keyed spawn is incomplete (`E_SPAWN_INCOMPLETE`) or compensation cannot confirm
1700
+ cleanup. The keyed-spawn details retain `instance`, `home` and `launched`.
1701
+ Confirmed completed compensation does not set this marker. This is an additive
1702
+ producer migration; existing text-based compatibility checks remain.
1694
1703
 
1695
1704
  ## `instance.json` and the roster
1696
1705
 
@@ -1922,6 +1931,15 @@ route target:
1922
1931
  `relativeTo` and `spawnOrigin` are always present, `null` when the host
1923
1932
  does not supply them (a host before 0.31, a fact it never recorded, or a
1924
1933
  saved route the host no longer lists). Nothing is derived on this side.
1934
+ - **`waitingOnYou`** (0.40.2, [Waiting on you](#waiting-on-you)) is on a row
1935
+ only when the host's kernel reports it: a row from a host before 0.40.0,
1936
+ and a saved route the host did not list, have no such key. Absent means
1937
+ "not reported", which is not `null` ("no claim"). When present it is `null`
1938
+ or `{since, producer, reason, message}`, passed through the kernel's read
1939
+ rule again on this side: a value that is not a claim (no valid `since` or
1940
+ `producer`) is `null`, and an unknown `reason` or an invalid `message` is
1941
+ `null` inside a claim that still counts. As on a local row, the host
1942
+ reports a claim only for a running instance.
1925
1943
  - **`addressable`** (0.31): `true` for every row the host reports. Routed
1926
1944
  session and lifecycle commands reach it by `--home`, or by name when the
1927
1945
  name is unique on the host ([addressing](servers.md#run-there); a shared
@@ -2160,7 +2178,9 @@ oats instance events <instance> [--limit <n>] [--since <iso>] [--home <abs>] [--
2160
2178
 
2161
2179
  - **Sources.** `home` is `<home>/.oats-events.jsonl`; `workspace` is
2162
2180
  `<deployment>/.agents/events/<agent>--<instance>.jsonl` (it survives the
2163
- home). Each is `{path, status: "ok" | "absent" | "refused" | "tail",
2181
+ home). The deployment is the one the home's spawn recorded, when that
2182
+ directory really holds the home at `agents/<agent>/instances/<instance>`;
2183
+ otherwise it is the fourth ancestor of the home as it was addressed. Each is `{path, status: "ok" | "absent" | "refused" | "tail",
2164
2184
  bytes}`. Only a regular file is opened (no symlinks, same device and inode
2165
2185
  after open), and at most its last 4 MiB is read (`"tail"`).
2166
2186
  - **Kinds:** `spawned`, `launched`, `restarted`, `stopped`, `stop-refused`,
@@ -2182,12 +2202,35 @@ oats instance events <instance> [--limit <n>] [--since <iso>] [--home <abs>] [--
2182
2202
  `null` for old rows); the top-level `incarnation` is the current home's (or
2183
2203
  `null`). Earlier incarnations are returned as this address's history.
2184
2204
  - **Address.** `--home` must be a home of `<instance>` (`E_HOME_MISMATCH`).
2205
+ A home has one address in storage, its real path (since 0.40.2): rows are
2206
+ written and matched under it, whatever spelling a writer or reader used (a
2207
+ deployment reached through a symlink, a symlinked agents root). The answer
2208
+ keeps the spelling it was asked in: the top-level `home` and every
2209
+ returned row's `home` are the home as the caller addressed it (`--home`,
2210
+ or the home found under `--dir`), the same string a status row carries.
2185
2211
  Rows for another address are dropped and counted in
2186
2212
  `integrity.foreignRows`; torn or invalid lines are counted in
2187
2213
  `integrity.unreadableRows`. A row present in both logs is returned once;
2188
2214
  identical rows repeated within one log (a set, a clear and the same set in
2189
2215
  one millisecond) are all returned, as many as the log holding the most
2190
2216
  copies has.
2217
+ - **Rows from before 0.40.2**, in a deployment addressed through a symlink
2218
+ only. A stored `home` is never rewritten, and never matched under another
2219
+ spelling. A row an earlier kernel wrote under the lexical spelling (a
2220
+ spawn's rows, and the claims of a session that was spawned and never
2221
+ restarted) is foreign after the upgrade, and counted in
2222
+ `integrity.foreignRows`. What that means for a claim:
2223
+ - A claim that was live under the lexical spelling stops showing. Nothing
2224
+ brings that row back: the claim shows again only when a producer makes it
2225
+ anew (the next permission prompt or question, the agent's next
2226
+ `oats instance attention`). A restart starts a new session with no
2227
+ claim, as always.
2228
+ - A claim that stayed set because the restart that should have voided it
2229
+ was recorded under the real path (#583) is gone.
2230
+ - The rows a started or restarted session wrote under the real path, which
2231
+ `oats status --dir <symlink>` could not see, are read now. They cannot
2232
+ surface a stale claim: such a session wrote its clears and its session
2233
+ boundaries under the real path too, so that history is complete.
2191
2234
  - **Window.** `count` is the rows after `--since`; `returned` the window
2192
2235
  (`--limit`, default 200, 1–2000); `truncated` means rows were cut or a
2193
2236
  source was a tail. `lastEvent` is `{kind, at, producer, incarnation}` of
@@ -2251,14 +2294,17 @@ oats instance attention [--message <text>] [--clear] --json
2251
2294
  message (a hand-edited log) reads as `null`, and the claim still counts.
2252
2295
  A stored `reason` outside `permission`, `question`, `attention` reads as
2253
2296
  `null` too, and a row whose `producer` is neither `kernel` nor a valid
2254
- producer id is no claim at all.
2297
+ producer id, or whose `at` is not a date, is no claim at all.
2255
2298
  - **Idempotent.** The verb reads the producer's live claim first and appends
2256
2299
  only on a change: a `set` whose reason or message differs from the live
2257
2300
  positive claim appends (`changed: true`), an identical one does not; a
2258
2301
  `clear` appends only over a live positive claim. The answer is
2259
2302
  `{eventsApi, instance, home, producer, changed, waitingOnYou}`, where
2260
2303
  `waitingOnYou` is that producer's resulting claim (`null` when it holds
2261
- none). Concurrent writers append whole lines; the latest row decides.
2304
+ none) and `home` is the home as the caller addressed it (`--home`,
2305
+ `$OATS_INSTANCE_HOME` or the enclosing home); the row is stored under the
2306
+ real path, so a set and a clear through different spellings of one home
2307
+ meet. Concurrent writers append whole lines; the latest row decides.
2262
2308
  Each log is judged on its own, and success means both took the row: a
2263
2309
  write either log refused is `E_EVENTS_FAILED` naming it, and the next call
2264
2310
  (a retry) appends again to repair it.
@@ -2461,6 +2507,42 @@ stderr, not envelopes.
2461
2507
 
2462
2508
  ## Sessions and launch configurations
2463
2509
 
2510
+ ### Input
2511
+
2512
+ ```text
2513
+ oats session input --home <abs> [--text-file <path>] --json
2514
+ ```
2515
+
2516
+ Input bytes come from stdin or the named file. The existing version-1 success
2517
+ answer is `{schemaVersion: 1, ok: true, result: {home, backend: "tmux",
2518
+ present: true, state, paneId, submitted: true, verified}}`. Session input runs
2519
+ on the execution host, including when the wake broker invokes it there;
2520
+ `--server` is not supported for input. The adapter sends one literal
2521
+ bracketed paste and one Enter after the existing input/authority/target checks.
2522
+
2523
+ `submitted` means terminal-operation success, **not model acceptance or
2524
+ processing**. `verified` is display observation only: `true` means a bounded
2525
+ look changed, possibly because of unrelated output or a dialog; `false` means
2526
+ unchanged, unreadable or exhausted observation. False never authorizes retry
2527
+ and is not proof of a pending draft or absence of effects. No `reason` is
2528
+ emitted; the `enter-not-taken` result from 0.39.4 is removed.
2529
+
2530
+ Read-only settling before Enter shares one monotonic 2-second budget starting
2531
+ when paste returns; up to two post-Enter looks share a 1-second budget. Probe
2532
+ timeouts and sleeps use the remaining budget. Observation failures produce
2533
+ `verified: false`, not input errors or extra keys. The original paste/key
2534
+ command timeouts and `E_SESSION_INPUT_FAILED` errors remain; the observation
2535
+ budgets do not bound those commands, failed buffer cleanup or OS scheduling.
2536
+ No busy-pane submission or exactly-once guarantee is provided. Generic command
2537
+ errors can still be uncertain after partial effects.
2538
+
2539
+ The Desktop terminal's authorized PTY writes are a separate stream; they do not
2540
+ consume this `verified` field. The Pi bridge does not interpret this result.
2541
+ Scheduler wake still records a nonthrowing input operation as delivered without
2542
+ adding acceptance/history fields. Actual broker acknowledgement and retry
2543
+ policy require their own consumer qualification; this result is not a native
2544
+ harness receipt. See [execution targets](execution-targets.md).
2545
+
2464
2546
  ### Start and restart
2465
2547
 
2466
2548
  ```text
@@ -143,35 +143,36 @@ oats session attach --home /abs/home
143
143
  live claim that the instance needs input from a human, `{since, producer,
144
144
  reason, message}`, or `null`; it is non-null only for a running harness
145
145
  (docs/desktop-cli-api.md, "Waiting on you").
146
- - **input** submits UTF-8 text (stdin or `--text-file`, at most 256 KiB, no
147
- NUL) followed by Enter, as a bracketed paste. The text is never run by a
148
- shell. A fallback shell, a stopped session or a split
149
- tmux window is refused. The text is pasted once. Enter waits for the pane to
150
- settle (two identical captures, at least about 200 ms, longer for a larger
151
- paste, at most 2 s), and is judged by whether it changed the bottom 15 lines
152
- of the pane. The comparison is of bytes only, and the pane's text is never
153
- interpreted. Trailing spaces are ignored. When the pane was seen to change
154
- size (a resize, or a second client attaching), a reflow of the same content
155
- is not a change either: each side's content must already be on the other
156
- side's screen or in its history, so content that appeared or disappeared
157
- still counts. An Enter that changed nothing was swallowed, and is resent
158
- after a backoff, at most 3 Enters in total. The answer adds:
159
- - `submitted: true, verified: true`: an Enter was taken.
160
- - `submitted: false, verified: true, reason: "enter-not-taken"`: none of the
161
- 3 Enters changed the pane. The text stays in the agent's input box; it is
162
- not pasted again.
163
- - `submitted: true, verified: false`: a capture failed, so no further Enter
164
- was sent and the last one was not judged. As before, this means the
165
- terminal accepted the keys. When the pane cannot be read before the first
166
- resend, exactly one Enter was sent.
167
-
168
- `submitted` never means the agent processed the text. A pane that changes
169
- for another reason after Enter (a spinner, a clock, a human typing) reads as
170
- taken. A harness that shows no visible reaction to Enter receives up to two
171
- extra Enters; real harnesses (claude, codex, pi) redraw on submit. A call takes at most about 4 s plus its tmux calls. A failed paste or
172
- key send is `E_SESSION_INPUT_FAILED`, with no retry. Wake schedules and
173
- messaging capabilities use this command ([schedules.md](schedules.md)); a
174
- wake schedule records any answer as delivered.
146
+ - **input** sends UTF-8 text (stdin or `--text-file`, at most 256 KiB, no
147
+ NUL) as exactly one bracketed paste followed by exactly one Enter. The text
148
+ is never run by a shell. Existing endpoint authority, fallback-shell,
149
+ stopped-session and split-window checks still refuse before input.
150
+
151
+ Before Enter, a size-based floor (200 ms plus 3 ms per KiB) and read-only
152
+ settling polls share one **monotonic 2-second observation budget**, starting
153
+ immediately after the paste command returns, before buffer cleanup. Sleeps
154
+ and capture timeouts are clipped to the remaining budget; a failed capture
155
+ or exhausted budget ends settling and proceeds to the single Enter. After
156
+ Enter, at most two read-only looks share a separate 1-second observation
157
+ budget. These deadlines do not bound the original paste/key commands, buffer
158
+ cleanup or OS scheduling. They authorize no further keys.
159
+
160
+ Successful terminal commands return `submitted: true` and `verified`, with
161
+ no `reason`. `verified: true` means only that the bounded display comparison
162
+ saw a changed look; `false` means unchanged, unreadable or exhausted
163
+ observation. The comparison retains its whitespace and resize/reflow
164
+ handling. Display movement can be unrelated output, a spinner or a dialog;
165
+ an unchanged display is not proof that no effects occurred or that a draft
166
+ is pending. **Neither value authorizes retry or proves model acceptance.**
167
+
168
+ A failed paste or key command remains `E_SESSION_INPUT_FAILED`; an
169
+ observational failure does not turn successful terminal operations into a
170
+ refusal. Command errors may themselves be uncertain after partial effects.
171
+ This transport offers no exactly-once guarantee and does not guarantee that
172
+ a busy pane accepts the input. Wake schedules retain their existing rule:
173
+ any nonthrowing input answer is recorded as delivered, meaning terminal
174
+ operations, not model processing. Broker delivery/ack policy and harness
175
+ acceptance evidence remain separate contracts.
175
176
  - **attach** is interactive and takes no `--json`. It opens a temporary tmux
176
177
  session linked to the agent's window alone.
177
178
  Closing the viewer leaves the agent running.
@@ -65,11 +65,54 @@ published to npm. Its developer docs are in
65
65
  | `harness-trust.mjs` | reading (never writing) Claude's and Codex's folder trust for a launch |
66
66
 
67
67
  The schedule registry stores explicit concurrency caps, leaving the schedule
68
- cap absent for its effective default of five. Reads migrate legacy stored one
69
- to absent once, under `registry.lock`, and record `capsVersion: 2`. Registration
70
- and cap updates use that same short lock; later explicit one stays explicit.
71
- `readRegistry()` returns stored choices; scheduling and status apply the default
72
- without writing it back. The trigger cap is independent and absent means no cap.
68
+ cap absent for its effective default of five. `readRegistry()` is a pure,
69
+ bounded atomic-file snapshot: it takes no lock and writes nothing, projecting
70
+ legacy implicit one to absent and `capsVersion: 2` only in memory. Its no-follow
71
+ regular-file descriptor accepts an atomic rename between stat and open, so it
72
+ returns a complete old or new snapshot; other bounded readers retain their
73
+ identity check. Invalid present schedule or trigger caps are refused.
74
+ Registration, unregistration and cap updates reread under `registry.lock` and
75
+ compare against the raw snapshot, so even unchanged membership persists the
76
+ migration. A successful write removing legacy one emits a stderr notice naming
77
+ the one-slot restoration command. `writeRegistry()` remains a raw atomic writer;
78
+ read-modify-write callers must supply the lock and reread. Later explicit one
79
+ stays explicit, including one reintroduced by an older writer: the stored
80
+ marker cannot distinguish its provenance. The trigger cap stays independent.
81
+
82
+ The shared mkdir lock retries every occupied-directory window within the
83
+ caller's retry deadline, including owner-file publication and removal. It never
84
+ removes a contender's lock or steals from a live, dead or unreadable owner;
85
+ persistent contention ends in the caller's existing busy error.
86
+
87
+ Spawn compensation returns its diagnostic and a structural uncertainty flag;
88
+ only an incomplete result marks the original error's `details.unconfirmed`.
89
+ CLI wrappers preserve that field, and the operation runner promotes a provider's
90
+ literal true marker while retaining the nested envelope. Message-based consumers
91
+ remain during the additive producer migration; do not replace stage evidence
92
+ with a substring check or infer uncertainty from a retained home alone.
93
+
94
+ The schedule child supervisor installs SIGINT/SIGTERM/SIGHUP handlers before
95
+ launch and removes them on settlement. All catchable shutdown signals share
96
+ its idempotent TERM/KILL path; the first stop cause is retained. The private
97
+ receipt records interruption independently of both the first stop cause and
98
+ the direct child's observed exit, so a signal during overflow cleanup still
99
+ keeps an otherwise valid envelope unconfirmed. Group probes
100
+ start at leader exit; an observed-empty group is permanently excluded from
101
+ later probes/signals. Polling cannot eliminate the gap before observation or
102
+ prove away PID reuse. Escaped sessions are outside the owned group, and
103
+ SIGKILL/OOM or unrecoverable supervisor death cannot be cleaned up by handlers;
104
+ a missing receipt supplies no child-exit evidence and cannot release a slot.
105
+
106
+ Session input keeps terminal operations separate from display observation.
107
+ The pre-Enter budget starts at paste completion, before best-effort buffer
108
+ cleanup; post-Enter observation starts after the single key command returns.
109
+ Both use a monotonic clock, clip sleeps and read-only capture subprocess
110
+ budgets, and stop observing on failure or exhaustion. Capture subprocesses use
111
+ SIGKILL on timeout so an ignored TERM cannot extend a probe; terminal command
112
+ timeouts/errors are unchanged. Screen comparison can set only the observational
113
+ `verified` boolean, never send another key or report terminal failure. Tests
114
+ use inert command runners and clocks; they do not qualify broker delivery or
115
+ harness acceptance.
73
116
 
74
117
  The kernel is runtime-neutral: nothing in `lib/` depends on a harness or on
75
118
  a provider. Provider behaviour lives in capabilities; the kernel supplies
@@ -99,7 +99,7 @@
99
99
  "disabled": {
100
100
  "description": "Workspace schedules this host does not run, by qualified id <member>/<id>, without a commit (`oats schedule disable <member>/<id>` writes it). Local schedules are enabled and disabled in oats-schedules.json.",
101
101
  "type": "array", "uniqueItems": true,
102
- "items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*/[a-z0-9-]{1,40}$" }
102
+ "items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*/[a-z0-9-]{1,100}$" }
103
103
  }
104
104
  }
105
105
  },
@@ -111,7 +111,7 @@
111
111
  "description": "The workspace triggers and schedules this host agrees to run, by qualified id <member>/<id>, or \"*\" for every one the workspace places on this host (0.30). A workspace automation runs only when its runsOn is host.name, its owner is this host's gh account AND trust admits it; absent or empty, none runs. A host fact: the committed workspace file refuses it. Local triggers and schedules need no trust.",
112
112
  "oneOf": [
113
113
  { "const": "*" },
114
- { "type": "array", "uniqueItems": true, "items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*/[a-z0-9-]{1,40}$" } }
114
+ { "type": "array", "uniqueItems": true, "items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*/[a-z0-9-]{1,100}$" } }
115
115
  ]
116
116
  }
117
117
  }
@@ -9,7 +9,7 @@ or workspace membership alone does not make a package official.
9
9
 
10
10
  | package | release | capabilities | package souls |
11
11
  |---|---|---|---|
12
- | `oats.framework` | `oats-framework/v1.6.0` (this repository) | `oats.core`, `oats.setup`, `oats.knowledge-theory` | `knowledge-theory-expert` |
12
+ | `oats.framework` | `oats-framework/v1.6.1` (this repository) | `oats.core`, `oats.setup`, `oats.knowledge-theory` | `knowledge-theory-expert` |
13
13
  | `oats.okf` | `v4.1.1` | `oats.okf` (knowledge), `oats.okf-harvest`, `oats.okf-maintenance` | `knowledge-harvester`, `knowledge-maintainer` |
14
14
  | `oats.aweb` | `v1.21.1` | `oats.aweb` (messaging) | |
15
15
  | `oats.engineering` | `v1.8.1` | `oats.engineering-expert`, `oats.developer`, `oats.code-review`, `oats.maintainer` | `code-reviewer` |
package/docs/packages.md CHANGED
@@ -51,7 +51,7 @@ packages:
51
51
  - **Bare version** (`v4.1.1`, `4.1.1`, `1.0.0-rc.1`): the id is looked up in
52
52
  the official catalog — `package-catalog.json` in the `oats` repo, or the file
53
53
  named by `OATS_PACKAGE_CATALOG` — which supplies the repo url, the tag
54
- convention (`v4.1.1` or `oats-framework/v1.6.0`) and the payload path. An id
54
+ convention (`v4.1.1` or `oats-framework/v1.6.1`) and the payload path. An id
55
55
  the catalog does not know is `E_PACKAGE_MISSING` ("use `git:<repo>@<ref>` for
56
56
  a package outside the catalog"). The catalog is the reviewed official list
57
57
  ([official-catalog.md](official-catalog.md)) and the only way a
@@ -74,7 +74,7 @@ members:
74
74
  - git:github.com/acme/agents
75
75
  - git:github.com/acme/platform
76
76
  packages:
77
- oats.framework: v1.6.0
77
+ oats.framework: v1.6.1
78
78
  oats.okf: v4.1.1
79
79
  oats.aweb: v1.21.1
80
80
  teams:
@@ -333,11 +333,11 @@ A soul that names one of the package's capabilities with
333
333
  "policy": "docs/official-catalog.md",
334
334
  "packages": {
335
335
  "oats.okf": { "url": "https://github.com/awebai/oats-okf.git", "ref": "v4.1.1", "path": "oats-package" },
336
- "oats.framework": { "url": "https://github.com/awebai/oats.git", "ref": "oats-framework/v1.6.0", "path": "oats-package" }
336
+ "oats.framework": { "url": "https://github.com/awebai/oats.git", "ref": "oats-framework/v1.6.1", "path": "oats-package" }
337
337
  }
338
338
  }
339
339
  ```
340
340
 
341
- `ref` carries the tag convention: a workspace's `oats.framework: v1.6.0`
342
- resolves to tag `oats-framework/v1.6.0`. Resolving through the catalog never
341
+ `ref` carries the tag convention: a workspace's `oats.framework: v1.6.1`
342
+ resolves to tag `oats-framework/v1.6.1`. Resolving through the catalog never
343
343
  advances a lock by itself: `oats sync` does, and says so.
@@ -47,8 +47,8 @@ override; report the risk you accepted). It never touches the checkout it
47
47
  runs from: it exports the SHA into a detached worktree under the system
48
48
  temporary directory (recorded in `MANIFEST.json`, `--export <dir>` overrides)
49
49
  and runs every build step there. The bumped manifests exist only in that
50
- export; the version-bump commit to `main` remains the workflow's job, or a
51
- manual PR.
50
+ export; the version-bump commit reaches `main` through the pull request the
51
+ workflow opens (or a manual one), merged by a maintainer.
52
52
 
53
53
  `publish-npm` and `release-github` print their plan and refuse without
54
54
  `--yes`. `tag` creates the local tag without `--yes` but pushes only with
@@ -117,8 +117,12 @@ Everything after `build` reads `MANIFEST.json` and the files already staged:
117
117
  runs `release.yml`, whose steps are idempotent: it skips the live npm
118
118
  versions, re-uploads the same assets, and attaches the attestations. That
119
119
  later pass is the way to add provenance; nothing is republished.
120
- - **The version-bump PR.** The workflow's final step; open it by hand if the
121
- workflow does not run.
120
+ - **The version-bump PR.** The workflow's final step opens it and stops: the
121
+ run never merges into `main`. A maintainer reviews that the diff is the
122
+ version lines only and merges it. The PR is opened with the workflow token,
123
+ so no checks run on it by themselves; close and reopen it to run them, and
124
+ do not read missing checks as green. Open the PR by hand if the workflow
125
+ does not run.
122
126
  - **Legs for hosts you do not have.** The Linux AppImage/DEB need a Linux
123
127
  host; the lane says so and `stage` lists what is missing.
124
128
 
@@ -0,0 +1,120 @@
1
+ # OATS 0.40.2
2
+
3
+ ## Added
4
+
5
+ - **Unconfirmed spawn and operation failures carry structural evidence**
6
+ ([#548](https://github.com/awebai/oats/issues/548)). Incomplete keyed spawns
7
+ and failed spawn compensation set `error.details.unconfirmed: true`; the
8
+ operation wrapper promotes a provider's literal true marker while retaining
9
+ its complete envelope. Completed compensation stays unmarked. Existing
10
+ text fallbacks and scheduler slot/reconcile behavior remain during this
11
+ additive migration; older copied providers are not upgraded by this change.
12
+
13
+ ## Fixed
14
+
15
+ - **Schedule registry reads are lock-free and do not mutate state**
16
+ ([#579](https://github.com/awebai/oats/issues/579)). Status and other readers
17
+ use coherent atomic-file snapshots, including during registry writes. Legacy
18
+ cap migration is projected in memory and persisted only by locked mutations,
19
+ with a one-slot restoration notice on stderr. Invalid stored schedule caps
20
+ now refuse instead of silently increasing capacity; explicit CLI cap options
21
+ can repair them. Old hand-set and implicit ones cannot be distinguished, nor
22
+ can current kernels identify a one reintroduced by an older writer sharing
23
+ the registry; stop mixed-version writes and explicitly set the desired cap.
24
+ Shared directory locks wait through transient owner publication/removal within
25
+ their retry deadline without stealing locks.
26
+ - **OATS Desktop: "Needs input" shows on a remote instance, and a withheld
27
+ note reads as the reason.** Desktop hid every remote claim: a remote roster
28
+ row carries no `runtimeState`, and Desktop read "not reported" as "not
29
+ running" (#582). An unreported state no longer hides the claim; a row
30
+ reported stopped, unreachable or unsupported, or one whose server did not
31
+ answer, still shows none. When a claim's note holds a link or text such as
32
+ `token: …`, Desktop does not show it. The row's description, the terminal
33
+ tab's title and the hover card now show the reason in words, such as "Asked
34
+ you a question", where they showed "[Detail withheld]" (#584). The activity
35
+ view still marks such a note as withheld, and `oats status` prints it in
36
+ full.
37
+ - **A remote instance's "needs input" reaches the roster**
38
+ ([#582](https://github.com/awebai/oats/issues/582)). `oats server roster`
39
+ built a remote row from a fixed list of facts and dropped the host's
40
+ `waitingOnYou`. A row now carries it when, and only when, the host's kernel
41
+ reports it: a row from a host before 0.40.0, and a saved route the host did
42
+ not list, have no such key, so "not reported" stays distinct from `null`
43
+ ("no claim"). The value passes the kernel's read rule again on this side;
44
+ anything that is not a claim is `null`. See
45
+ [the remote roster](../desktop-cli-api.md#the-remote-roster-oats-server-roster---json).
46
+ - **"Needs input" in a deployment addressed through a symlink**
47
+ ([#583](https://github.com/awebai/oats/issues/583)). A home there has two
48
+ spellings: `oats status --dir <symlink>` addressed it lexically, while a
49
+ started or restarted session carries the real path. Event rows are keyed by
50
+ the home, so the session's claims never showed on status, a restart did not
51
+ void a claim written under the other spelling, and a clear through one
52
+ spelling found nothing. Rows are now stored and matched under the home's
53
+ real path, whichever spelling a writer or reader uses, and both spellings
54
+ use one workspace log, the deployment's own `.agents/events`. Answers keep
55
+ the spelling they were asked in: the `home` in `oats instance events --json`
56
+ (the top-level field and each row's) and in the `oats instance waiting` and
57
+ `oats instance attention` answers is the home as the caller addressed it.
58
+ Deployments not reached through a symlink see no change.
59
+
60
+ Rows an earlier kernel wrote under the lexical spelling are not rewritten
61
+ and not matched: after the upgrade they are foreign, counted in
62
+ `integrity.foreignRows`. In a symlinked deployment that means:
63
+ - a claim that was live under the lexical spelling (a session that was
64
+ spawned and never restarted) stops showing. It shows again only when it
65
+ is made anew: at the agent's next permission prompt, question or
66
+ `oats instance attention`. A restart starts a new session with no claim;
67
+ - a claim that stayed set because its restart was recorded under the real
68
+ path is gone, not stuck;
69
+ - the rows a started or restarted session wrote under the real path are
70
+ read now. They cannot show a stale claim: such a session wrote its clears
71
+ and its session boundaries under the real path too.
72
+ - **oats.core 2.4.1: the Claude Code emitter no longer speaks for another
73
+ instance** ([#584](https://github.com/awebai/oats/issues/584)). Its hook
74
+ script took the home from `$OATS_INSTANCE_HOME`. A Claude process that
75
+ loaded one home's `.claude/settings.json` while carrying another instance's
76
+ environment (a nested `claude -p`, a `claude -p` started with its working
77
+ directory in another home, a pane that inherited the variables) set and
78
+ cleared that other instance's claim, and its `Stop` and `SessionEnd` clears
79
+ could erase a real one. The launch hook now writes the home's real path
80
+ into each hook command, and the script acts only when `$OATS_INSTANCE_HOME`
81
+ names that home, through any spelling; otherwise it does nothing. Ships in
82
+ oats.framework 1.6.1 (compatibility unchanged; catalog and workspace pin
83
+ `oats-framework/v1.6.1`): a workspace's `oats.framework: v1.6.1` resolves
84
+ to that tag through the official catalog, and a workspace pinned to v1.6.0
85
+ keeps oats.core 2.4.0 until it moves the pin and syncs. A home picks it up
86
+ at its next spawn. See [capabilities](../capabilities.md).
87
+ - **A waiting row whose time is not a date is no claim**
88
+ ([#584](https://github.com/awebai/oats/issues/584)). The reader validated a
89
+ stored claim's producer, reason and message but passed its time through.
90
+ And `oats help` lists `[--dir <d>]` for `oats instance waiting`.
91
+ - **Catchable scheduler-supervisor shutdown cleans up its owned child group**
92
+ ([#580](https://github.com/awebai/oats/issues/580)). SIGINT, SIGTERM and SIGHUP
93
+ enter the existing bounded TERM/KILL cleanup once, preserving observed child
94
+ exit evidence and treating interrupted envelopes as unconfirmed. The group
95
+ is checked when its leader exits and never signalled again after it is seen
96
+ empty, reducing but not eliminating process-group ID reuse races. Escaped
97
+ sessions and unrecoverable supervisor deaths such as SIGKILL/OOM remain
98
+ outside that cleanup guarantee; no receipt means no proven child exit, so
99
+ the unknown attempt and host slot remain pending reconciliation.
100
+ - **Workspace schedule opt-outs and named trust accept 100-character IDs**
101
+ ([#581](https://github.com/awebai/oats/issues/581)). Host configuration now
102
+ accepts the full schedule name limit in `schedules.disabled` and
103
+ `automations.trust`, so enabling/disabling and trusting schedules with
104
+ 41–100-character IDs works through the CLI and workspace placement checks.
105
+ Trigger definitions and `triggers.disabled` retain their 40-character limit;
106
+ member syntax, allowed characters and uniqueness rules are unchanged.
107
+
108
+ ## Changed
109
+
110
+ - **Session input restores single-paste, single-Enter terminal semantics**
111
+ ([#562](https://github.com/awebai/oats/issues/562)). Corrects 0.39.4's
112
+ screen-derived extra Enters and `enter-not-taken` refusal after successful
113
+ terminal commands. Pre-Enter settling remains, with a shared monotonic
114
+ 2-second observation budget; at most two read-only post-Enter looks share
115
+ 1 second. Each probe and sleep is limited by its remaining budget.
116
+ `submitted: true` reports terminal-operation success; `verified` reports
117
+ display change only, and false never authorizes retry. Neither proves model
118
+ acceptance, pending draft state or absence of effects. Command errors and
119
+ authority checks are unchanged; busy-pane submission, exactly-once delivery
120
+ and generic-error retry uncertainty are not solved by this correction.