typeclaw 0.9.0 → 0.9.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.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/scripts/require-parallel.ts +41 -15
  3. package/src/agent/live-subagents.ts +0 -1
  4. package/src/agent/session-origin.ts +10 -0
  5. package/src/agent/subagent-completion-reminder.ts +4 -1
  6. package/src/agent/subagents.ts +72 -13
  7. package/src/agent/system-prompt.ts +5 -5
  8. package/src/agent/tools/channel-reply.ts +47 -7
  9. package/src/agent/tools/channel-send.ts +43 -11
  10. package/src/agent/tools/restart.ts +13 -2
  11. package/src/agent/tools/runtime-notice.ts +41 -0
  12. package/src/agent/tools/spawn-subagent.ts +0 -1
  13. package/src/agent/tools/subagent-output.ts +3 -51
  14. package/src/bundled-plugins/memory/README.md +11 -11
  15. package/src/bundled-plugins/memory/dreaming-state.ts +51 -2
  16. package/src/bundled-plugins/memory/index.ts +77 -26
  17. package/src/bundled-plugins/memory/memory-retrieval.ts +7 -1
  18. package/src/bundled-plugins/memory/migration.ts +91 -16
  19. package/src/bundled-plugins/memory/stream-io.ts +71 -1
  20. package/src/channels/adapters/kakaotalk-classify.ts +4 -1
  21. package/src/channels/adapters/kakaotalk.ts +1 -1
  22. package/src/channels/manager.ts +7 -0
  23. package/src/channels/router.ts +260 -15
  24. package/src/channels/schema.ts +1 -1
  25. package/src/cli/compose.ts +23 -2
  26. package/src/cli/logs.ts +17 -2
  27. package/src/compose/logs.ts +8 -4
  28. package/src/config/config.ts +8 -0
  29. package/src/container/index.ts +1 -1
  30. package/src/container/logs.ts +38 -11
  31. package/src/init/dockerfile.ts +147 -4
  32. package/src/inspect/live.ts +32 -1
  33. package/src/inspect/render.ts +32 -0
  34. package/src/inspect/replay.ts +44 -0
  35. package/src/inspect/types.ts +26 -0
  36. package/src/run/index.ts +28 -11
  37. package/src/server/index.ts +59 -19
  38. package/src/shared/protocol.ts +30 -0
  39. package/src/skills/typeclaw-codex-cli/SKILL.md +324 -0
  40. package/src/skills/typeclaw-codex-cli/references/auth-flow.md +131 -0
  41. package/src/skills/typeclaw-codex-cli/references/stop-hook.md +92 -0
  42. package/src/skills/typeclaw-codex-cli/references/tmux-driving.md +239 -0
  43. package/src/skills/typeclaw-config/SKILL.md +32 -31
  44. package/src/test-helpers/wait-for.ts +15 -7
  45. package/typeclaw.schema.json +24 -11
@@ -0,0 +1,239 @@
1
+ # Tmux driving — full reference (Codex CLI)
2
+
3
+ Deep dive for driving `codex` (interactive) through tmux. Read it when the main `SKILL.md` workflow hits an edge case: a hung session, a missing JSONL, an unexpected ANSI burst, a "tmux says session exists but codex isn't responsive" situation.
4
+
5
+ The shape of this reference is intentionally a mirror of `typeclaw-claude-code`'s `references/tmux-driving.md`. The differences are noted inline; everything not flagged is the same.
6
+
7
+ ## Why tmux
8
+
9
+ The agent process has no TTY. `codex` (interactive) is a TUI built on `ratatui` + `crossterm` that uses raw terminal modes — it reads stdin one byte at a time with echo off, draws with ANSI escapes, and refuses to start sensibly if stdin/stdout aren't terminals. Piping into `codex` doesn't work; the CLI detects the absence of a TTY and either falls back to non-interactive semantics or fails outright.
10
+
11
+ `tmux` solves this by spawning the process with a pty allocated by tmux itself. You then drive the pty via `send-keys` (write) and `capture-pane` (read).
12
+
13
+ ## Why `/tmp/cx-<id>` and not `workspace/cx-<id>`
14
+
15
+ Same reasoning as Claude Code's `cc-<id>`:
16
+
17
+ 1. **Worktree-vs-scratch:** the `cx-<id>` directory is a real git checkout managed by `git worktree`, with refs in `/agent/.git/worktrees/cx-<id>/`. Putting it under `workspace/` would mean the agent folder contains a worktree of itself, which works mechanically but is recursive and confusing.
18
+ 2. **The global SessionStart and Stop hooks write their per-session files into cwd.** Both hook scripts read `$PWD` (the literal cwd Codex CLI was invoked with) and write into it: SessionStart writes `.session-id` containing the UUID; Stop writes `sentinel-<uuid>.json` and `.done-<uuid>`. `$PWD` resolves to the worktree because `tmux new-session -c /tmp/cx-<id>` sets codex's cwd there.
19
+ 3. **The worktree IS the codebase.** Codex can read every file at `HEAD` directly — it doesn't need a separate scratch area.
20
+
21
+ The `cx-` prefix (vs Claude Code's `cc-`) keeps the two CLIs' worktrees, branches, and tmux sessions in parallel namespaces. An agent running both can delegate concurrently without collision; `git worktree list` shows both kinds clearly; `tmux ls` is unambiguous.
22
+
23
+ ## Spawning the session
24
+
25
+ The canonical spawn (per `SKILL.md`):
26
+
27
+ ```sh
28
+ tmux new-session -d -s cx-<task-id> -c /tmp/cx-<task-id> codex
29
+ ```
30
+
31
+ Flags worth knowing:
32
+
33
+ - `-d` — detached. The session runs in the background; your shell doesn't attach.
34
+ - `-s cx-<task-id>` — explicit session name. Required. Without `-s`, tmux picks `0`, `1`, … and a sibling delegation will clobber yours.
35
+ - `-c /tmp/cx-<task-id>` — start directory. Must be the worktree path. The global Stop hook at `~/.codex/hooks.json` always fires regardless of cwd, but the hook script writes its sentinel to `$PWD`; if cwd is wrong, the sentinel lands somewhere your polling loop isn't watching.
36
+ - `codex` — the command. Just `codex`, not `codex exec`. The interactive TUI is the whole point.
37
+
38
+ Common mistakes:
39
+
40
+ - Forgetting `-c` and getting cwd `/agent` by default. The Stop hook still fires (it's global), but `sentinel.json` + `.done` end up under `/agent/`, your polling loop watches `/tmp/cx-<id>/`, and the loop times out at its wall-clock budget. Worse: codex in `/agent` operates on the live working tree instead of the worktree.
41
+ - Passing flags Codex CLI doesn't recognize. Codex is a Rust binary with a strict clap parser — unknown flags fail at startup, not silently.
42
+
43
+ ### Alternate-screen vs inline mode
44
+
45
+ Codex CLI's TUI defaults to **alternate-screen mode** (the same mode `vim` uses) — it takes over the entire terminal and restores the previous screen on exit. This means `tmux capture-pane -S -` only captures what's currently visible, not the full conversation history.
46
+
47
+ For our workflow this is fine: we use the Stop sentinel as the done-signal, not pane-content parsing. But if you ever need to capture the full TUI history (e.g. for a post-mortem when the JSONL is missing), spawn codex with `--no-alt-screen`:
48
+
49
+ ```sh
50
+ tmux new-session -d -s cx-<id> -c /tmp/cx-<id> codex --no-alt-screen
51
+ ```
52
+
53
+ `--no-alt-screen` runs the TUI inline, preserving terminal scrollback. The trade-off is that the rendering is slightly less stable (the TUI's redraws collide with normal scrollback flow), but `capture-pane -S -` will then see the full history.
54
+
55
+ Default to alternate-screen mode (no flag); reach for `--no-alt-screen` only when you genuinely need scrollback capture for debugging.
56
+
57
+ ## The init wait
58
+
59
+ `codex` prints a brief Welcome animation, then renders its TUI. The animation takes ~1 second; after that the input composer appears immediately if no dialogs fire, or one or more dialog modals appear in sequence. You must wait for the input composer (and clear any dialogs) before sending the first prompt.
60
+
61
+ The skill body's flow uses dialog-polling rather than a fixed sleep: every 500ms, `tmux capture-pane -t cx-<id> -p -S -15` and check for the input composer (bottom-of-pane prompt indicator) OR a known dialog (Auth picker / TrustDirectory / hook trust). Clear dialogs as they appear, exit the loop when the input composer is visible. Give up after ~10s and surface to the user.
62
+
63
+ Unlike Claude Code, Codex's `.session-id` IS a reliable readiness signal in most cases: Codex's SessionStart hook does NOT get suppressed while the TrustDirectory dialog is pending (different upstream architecture than Claude Code's #11519 issue). The fast path — read `.session-id` after ~1 second — usually wins. But the dialog-polling loop is still required to clear TrustDirectory before sending the first prompt; otherwise the prompt is interpreted as a dialog answer.
64
+
65
+ ## Sending input
66
+
67
+ ```sh
68
+ tmux send-keys -t cx-<id> "<text>" Enter
69
+ ```
70
+
71
+ Notes:
72
+
73
+ - **Quote carefully.** The text is interpreted by tmux's send-keys before reaching the pty. Embedded `"` and `\` need escaping. For complex prompts, write the prompt to a file and use `tmux load-buffer + paste-buffer` instead of `send-keys`:
74
+
75
+ ```sh
76
+ echo "<prompt>" > prompt.txt
77
+ tmux load-buffer -t cx-<id> prompt.txt
78
+ tmux paste-buffer -t cx-<id>
79
+ tmux send-keys -t cx-<id> Enter
80
+ ```
81
+
82
+ - **Bracketed paste is enabled** by default in Codex's TUI (`EnableBracketedPaste` in `tui.rs`), so `paste-buffer` content arrives intact even with newlines.
83
+
84
+ - **`Enter` is the literal key name**, not the text "Enter". Other useful key names: `Escape`, `Tab`, `BSpace`, `Up`, `Down`, `C-c` (Ctrl+C), `C-d` (Ctrl+D for EOF). Codex's composer also honors `Ctrl-M` as submit (configurable per `tui.keymap.composer.submit` in `~/.codex/config.toml`, but our flow assumes the default).
85
+
86
+ - **Multi-line prompts**: send the body, then `Enter`. Codex's composer treats Enter as submit by default, so newlines in your text become submitted lines. For genuinely multi-line input, use the paste-buffer flow above — bracketed paste passes the newlines through to the composer's input buffer without submitting until you press Enter explicitly.
87
+
88
+ ## Discovering `cx_session_id` and polling for `.done-<session_id>`
89
+
90
+ Same edge-triggered polling pattern as Claude Code. Track which sentinels you've already processed by UUID; on every poll, enumerate ALL `.done-*` files, ignore the ones you've already processed, and pick the **newest by mtime** of what remains.
91
+
92
+ ### Phase 1 — first-turn discovery
93
+
94
+ Fast path: after spawning codex and waiting ~1s, check `/tmp/cx-<id>/.session-id`. If it exists and contains a real UUID, that's `cx_session_id`. Otherwise fall through to the same first-turn polling as Claude Code (translate to your tool calls; the shell snippet is illustrative):
95
+
96
+ ```sh
97
+ budget=600
98
+ elapsed=0
99
+ processed=""
100
+ cx_session_id=""
101
+ # Fast path
102
+ if [ -f /tmp/cx-<id>/.session-id ]; then
103
+ candidate="$(cat /tmp/cx-<id>/.session-id 2>/dev/null)"
104
+ case "$candidate" in
105
+ [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]-*) cx_session_id="$candidate" ;;
106
+ malformed) echo "SessionStart fired but session_id was bad" ; exit 1 ;;
107
+ esac
108
+ fi
109
+ # Discovery path (fallback or wait for the first Stop)
110
+ while [ -z "$cx_session_id" ]; do
111
+ newest=""
112
+ for f in $(ls -t /tmp/cx-<id>/.done-* 2>/dev/null); do
113
+ [ -f "$f" ] || continue
114
+ uuid="${f##*/.done-}"
115
+ case " $processed " in *" $uuid "*) continue ;; esac
116
+ if [ "$uuid" = "malformed" ]; then
117
+ malformed_fallback="$f"
118
+ continue
119
+ fi
120
+ newest="$f"
121
+ cx_session_id="$uuid"
122
+ break
123
+ done
124
+ if [ -n "$cx_session_id" ]; then break; fi
125
+ if [ -n "${malformed_fallback:-}" ]; then
126
+ echo "Stop hook fired but couldn't extract a UUID-shape session_id"
127
+ exit 1
128
+ fi
129
+ if [ "$elapsed" -ge "$budget" ]; then echo "Timeout reached"; break; fi
130
+ if ! tmux has-session -t cx-<id> 2>/dev/null; then echo "tmux session died"; break; fi
131
+ sleep 0.5
132
+ elapsed=$((elapsed + 1))
133
+ done
134
+ ```
135
+
136
+ ### Phase 2 — per-turn polling (turns 2 onward)
137
+
138
+ Same shape as Claude Code's phase 2. `processed` carries over from phase 1; add the just-consumed UUID before entering phase 2. On every iteration, glob `.done-*`, pick newest unprocessed real-UUID, update `cx_session_id` if it differs (rotation), read the sentinel, `rm` ONLY the specific `.done-<uuid>` you processed.
139
+
140
+ ```sh
141
+ budget=600
142
+ elapsed=0
143
+ processed="$processed $cx_session_id"
144
+ new_sid=""
145
+ while [ -z "$new_sid" ]; do
146
+ for f in $(ls -t /tmp/cx-<id>/.done-* 2>/dev/null); do
147
+ [ -f "$f" ] || continue
148
+ uuid="${f##*/.done-}"
149
+ case " $processed " in *" $uuid "*) continue ;; esac
150
+ if [ "$uuid" = "malformed" ]; then echo "bad session_id"; exit 1; fi
151
+ new_sid="$uuid"
152
+ break
153
+ done
154
+ if [ -n "$new_sid" ]; then break; fi
155
+ if [ "$elapsed" -ge "$budget" ]; then echo "Timeout"; break; fi
156
+ if ! tmux has-session -t cx-<id> 2>/dev/null; then echo "tmux died"; break; fi
157
+ sleep 0.5
158
+ elapsed=$((elapsed + 1))
159
+ done
160
+
161
+ if [ "$new_sid" != "$cx_session_id" ]; then
162
+ echo "Detected session_id rotation: ${cx_session_id} → ${new_sid}"
163
+ cx_session_id="$new_sid"
164
+ fi
165
+ cat "/tmp/cx-<id>/sentinel-${cx_session_id}.json"
166
+ rm -f "/tmp/cx-<id>/.done-${cx_session_id}"
167
+ ```
168
+
169
+ ### Why edge-triggered, not level-triggered
170
+
171
+ Same reasoning as Claude Code. Level-triggered polling on a fixed `.done-<sid>` filename breaks on session rotation and on the "old marker still exists when new one arrives" race. Newest-by-mtime + processed-set is the only correct pattern.
172
+
173
+ ### Why 500ms cadence
174
+
175
+ - Faster polling (50–100ms) wastes CPU and is invisible to the user.
176
+ - Slower polling (2s+) adds visible latency.
177
+ - 500ms is the sweet spot — same calibration as Claude Code, same reasoning.
178
+
179
+ ### Session-died recovery
180
+
181
+ `tmux has-session` returns non-zero when the session is gone. Three reasons:
182
+
183
+ 1. **Codex crashed**: panic, segfault. `capture-pane` before tmux GC'd would show the panic message.
184
+ 2. **Auth failed**: `codex` exited cleanly with "Unauthorized" or similar. Pane would have shown the error briefly.
185
+ 3. **User killed it externally**: someone ran `tmux kill-session -t cx-<id>` outside your control.
186
+
187
+ Recovery: surface to the user with whatever you have — `git diff main..cx-<id>` (which still works because the branch exists), `sentinel.json` from any prior turn, the JSONL if it exists. Then clean up: `git worktree remove --force /tmp/cx-<id>` + `git branch -D cx-<id>`. Ask whether to retry.
188
+
189
+ ## Capturing the pane (fallback path)
190
+
191
+ When the JSONL is missing or you need to see what the TUI showed:
192
+
193
+ ```sh
194
+ tmux capture-pane -t cx-<id> -p -S - -E -
195
+ ```
196
+
197
+ Flags:
198
+
199
+ - `-p` — print to stdout.
200
+ - `-S -` — start from the beginning of the scrollback.
201
+ - `-E -` — end at the current line.
202
+
203
+ If codex was spawned in alternate-screen mode (the default), `-S -` only captures the current screen. For full history, spawn with `--no-alt-screen`.
204
+
205
+ ### ANSI gotchas
206
+
207
+ - **Codex uses crossterm + ratatui** with bracketed paste, focus tracking, and keyboard enhancement modes enabled. Captures will contain ANSI escapes for color, cursor positioning, and bracketed-paste markers (`\x1b[200~` / `\x1b[201~`).
208
+ - **Progress indicators** in Codex's tool-use UI redraw in place. Captures show the final state of each cell.
209
+ - **Box-drawing characters** for input frames are Unicode (`╭ ╮ ╰ ╯ │ ─`). Preserve as UTF-8.
210
+ - **Color codes**: standard 8-color and 256-color. Strip with `s/\x1b\[[0-9;]*[a-zA-Z]//g` if you need plain text.
211
+
212
+ ## Cleaning up
213
+
214
+ ```sh
215
+ tmux send-keys -t cx-<id> "/exit" Enter
216
+ sleep 1
217
+ tmux kill-session -t cx-<id> 2>/dev/null || true
218
+ git -C /agent worktree remove --force /tmp/cx-<id>
219
+ git -C /agent branch -D cx-<id>
220
+ ```
221
+
222
+ - `/exit` is Codex CLI's built-in exit command. Cleaner than `C-c` — it lets the CLI flush the JSONL and close cleanly.
223
+ - `sleep 1` gives the CLI time to flush. Skip it and you may lose the last few JSONL lines.
224
+ - `kill-session ... || true` because the session may have already exited cleanly after `/exit`.
225
+ - `worktree remove --force` because the working tree has un-cherry-picked changes. `--force` is correct here because we're explicitly discarding.
226
+ - `branch -D` to delete the throwaway branch.
227
+
228
+ ## Things you must not do in tmux driving
229
+
230
+ - **Do not omit `-s <name>` on `new-session`.** Anonymous sessions race across delegations.
231
+ - **Do not omit `-c /tmp/cx-<id>` on `new-session`.** The global Stop hook writes its sentinel into `$PWD`; wrong cwd means the sentinel lands somewhere your polling loop isn't watching. Worse: codex in `/agent` operates on the live working tree instead of the worktree.
232
+ - **Do not skip the init wait.** Sending input before the TUI is ready loses the input silently — or worse, the input lands inside a dialog modal and gets interpreted as a yes/no answer.
233
+ - **Do not use `send-keys` with raw user-supplied strings without escaping.** Tmux's send-keys is mildly shell-like; embedded special chars get interpreted. Use `load-buffer + paste-buffer` for anything untrusted or complex.
234
+ - **Do not poll `capture-pane` as your primary done-signal.** Use the sentinel. `capture-pane` is for content retrieval, not lifecycle.
235
+ - **Do not kill the session with `C-c` if you can avoid it.** `/exit` is cleaner.
236
+ - **Do not assume `tmux has-session` returning success means codex is responsive.** A session can exist while codex is wedged. Pair with a wall-clock budget.
237
+ - **Do not `rm -rf /tmp/cx-<id>` instead of `git worktree remove`.** Pure rm leaves orphan refs and a dangling branch.
238
+ - **Do not mix `cx-` and `cc-` prefixes within one delegation.** They're parallel namespaces by design — Codex sessions use `cx-`, Claude Code sessions use `cc-`. Cross-using leaves orphans in both worktree lists.
239
+ - **Do not pass `codex --no-alt-screen` reflexively.** Alternate-screen mode is the default for good reason (cleaner TUI rendering). Reach for the flag only when you need full-scrollback capture for debugging.
@@ -39,18 +39,18 @@ You yourself cannot run `typeclaw restart` — that is a host-stage command and
39
39
 
40
40
  `typeclaw.json` is a single JSON object with these fields:
41
41
 
42
- | Field | Required | Type | Notes |
43
- | ------------- | -------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
- | `$schema` | no | string | Path to `typeclaw.schema.json` for editor autocompletion. Scaffolded as `./node_modules/typeclaw/typeclaw.schema.json`. Leave it alone unless the user moves it. |
45
- | `port` | no | integer | 1–65535. Defaults to `8973` (T9 spelling of "TYPE"). Change only if the default collides with something on the user's host. **Restart-required.** |
46
- | `model` | no | string | Must be one of the values listed in the **Allowed models** section below. Defaults to `openai/gpt-5.4-nano`. **Live-reloadable.** |
47
- | `mounts` | no | array of objects | Host directories bind-mounted into your container. Defaults to `[]` (no host paths exposed). Omitted from scaffolded `typeclaw.json` — add it only when the user wants host paths exposed. See **Mounts** section below. **Restart-required.** |
48
- | `plugins` | no | array of strings | Plugin package names loaded at server boot. Defaults to `[]`. **Restart-required.** Plugin-owned config blocks live alongside as additional top-level keys; see **Plugin config blocks**. |
49
- | `alias` | no | array of strings | Additional names the agent answers to in channel engagement, on top of the implicit `basename(agentDir)`. Each entry is a non-empty trimmed string matched case-insensitively as a substring of the inbound text. Defaults to `[]`. Hatching populates this with the agent's chosen name. See **Alias** section below. **Live-reloadable.** |
50
- | `channels` | no | object | Per-adapter engagement triggers and history-prefetch knobs for external messengers. Defaults to `{}` (no adapters configured). `typeclaw init` scaffolds an empty block per requested adapter (e.g. `"discord-bot": {}`) and the schema fills in defaults. Channel access control lives in `roles` — see the `typeclaw-permissions` skill. **Live-reloadable.** See **Channels** section below. |
51
- | `portForward` | no | object | Allow/deny policy for the host-stage portbroker that auto-forwards container LISTEN ports to `127.0.0.1` on the host. Defaults to `{ "allow": "*" }` (forward everything). Omitted from scaffolded `typeclaw.json`. **Restart-required.** See **portForward** section below. |
52
- | `docker` | no | object | Namespace for Docker-related blocks. Today the only child is `docker.file` — toggles (`tmux`, `gh`, `python`, `ffmpeg`, `cjkFonts`, `cloudflared`, `claudeCode`) gate opinionated package installs; `append` adds custom Dockerfile lines just before `ENTRYPOINT`. `docker.file` defaults to `{ ffmpeg: false, gh: true, python: true, tmux: true, cjkFonts: true, cloudflared: true, claudeCode: false, append: [] }`. Omitted from scaffolded `typeclaw.json`. **Restart-required** (next `typeclaw start` rebuilds the image). See **Dockerfile** section below. |
53
- | `git` | no | object | Namespace for git-related blocks. Today the only child is `git.ignore` — extra patterns spliced into the autogenerated `.gitignore` before TypeClaw's protected rules. `git.ignore` defaults to `{ "append": [] }`. Omitted from scaffolded `typeclaw.json`. **Restart-required** (next `typeclaw start` refreshes `.gitignore`). See **Gitignore** section below. |
42
+ | Field | Required | Type | Notes |
43
+ | ------------- | -------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
+ | `$schema` | no | string | Path to `typeclaw.schema.json` for editor autocompletion. Scaffolded as `./node_modules/typeclaw/typeclaw.schema.json`. Leave it alone unless the user moves it. |
45
+ | `port` | no | integer | 1–65535. Defaults to `8973` (T9 spelling of "TYPE"). Change only if the default collides with something on the user's host. **Restart-required.** |
46
+ | `model` | no | string | Must be one of the values listed in the **Allowed models** section below. Defaults to `openai/gpt-5.4-nano`. **Live-reloadable.** |
47
+ | `mounts` | no | array of objects | Host directories bind-mounted into your container. Defaults to `[]` (no host paths exposed). Omitted from scaffolded `typeclaw.json` — add it only when the user wants host paths exposed. See **Mounts** section below. **Restart-required.** |
48
+ | `plugins` | no | array of strings | Plugin package names loaded at server boot. Defaults to `[]`. **Restart-required.** Plugin-owned config blocks live alongside as additional top-level keys; see **Plugin config blocks**. |
49
+ | `alias` | no | array of strings | Additional names the agent answers to in channel engagement, on top of the implicit `basename(agentDir)`. Each entry is a non-empty trimmed string matched case-insensitively as a substring of the inbound text. Defaults to `[]`. Hatching populates this with the agent's chosen name. See **Alias** section below. **Live-reloadable.** |
50
+ | `channels` | no | object | Per-adapter engagement triggers and history-prefetch knobs for external messengers. Defaults to `{}` (no adapters configured). `typeclaw init` scaffolds an empty block per requested adapter (e.g. `"discord-bot": {}`) and the schema fills in defaults. Channel access control lives in `roles` — see the `typeclaw-permissions` skill. **Live-reloadable.** See **Channels** section below. |
51
+ | `portForward` | no | object | Allow/deny policy for the host-stage portbroker that auto-forwards container LISTEN ports to `127.0.0.1` on the host. Defaults to `{ "allow": "*" }` (forward everything). Omitted from scaffolded `typeclaw.json`. **Restart-required.** See **portForward** section below. |
52
+ | `docker` | no | object | Namespace for Docker-related blocks. Today the only child is `docker.file` — toggles (`tmux`, `gh`, `python`, `ffmpeg`, `cjkFonts`, `cloudflared`, `claudeCode`, `codexCli`) gate opinionated package installs; `append` adds custom Dockerfile lines just before `ENTRYPOINT`. `docker.file` defaults to `{ ffmpeg: false, gh: true, python: true, tmux: true, cjkFonts: true, cloudflared: true, claudeCode: false, codexCli: false, append: [] }`. Omitted from scaffolded `typeclaw.json`. **Restart-required** (next `typeclaw start` rebuilds the image). See **Dockerfile** section below. |
53
+ | `git` | no | object | Namespace for git-related blocks. Today the only child is `git.ignore` — extra patterns spliced into the autogenerated `.gitignore` before TypeClaw's protected rules. `git.ignore` defaults to `{ "append": [] }`. Omitted from scaffolded `typeclaw.json`. **Restart-required** (next `typeclaw start` refreshes `.gitignore`). See **Gitignore** section below. |
54
54
 
55
55
  > **Top-level keys not in this table are not "ignored unknowns" anymore** — they are reserved for **plugin config blocks**. The schema's `catchall(z.unknown())` preserves them, and the plugin loader hands each block to its owning plugin's `configSchema` for validation. The bundled memory plugin owns `memory` at the top level — see the `typeclaw-memory` skill for that block's semantics. Do not write a top-level key unless you know which plugin owns it.
56
56
 
@@ -138,7 +138,7 @@ Each entry in `channels` is keyed by adapter id and has this shape:
138
138
 
139
139
  | Field | Required | Type | Notes |
140
140
  | ------------ | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
141
- | `engagement` | no | object | When the agent should auto-reply vs. stay silent. Defaults to mention/reply/dm with 5-minute reply stickiness. See **Engagement** below. |
141
+ | `engagement` | no | object | When the agent should auto-reply vs. stay silent. Defaults to mention/reply/dm with 15-minute reply stickiness. See **Engagement** below. |
142
142
  | `history` | no | object | Cold-start prefetch windows for `(thread.head, thread.tail, channel.tail)`. Set any to `0` to disable that side. Defaults to `{ thread: { head: 3, tail: 10 }, channel: { tail: 10 } }`. |
143
143
  | `enabled` | no | boolean | Defaults to `true`. Set `false` to disable the adapter entirely without removing its config. |
144
144
 
@@ -151,7 +151,7 @@ To stop the agent answering in a specific channel, narrow the `roles` block so t
151
151
  ```json
152
152
  "engagement": {
153
153
  "trigger": ["mention", "reply", "dm"],
154
- "stickiness": { "perReply": { "window": 300000 } }
154
+ "stickiness": { "perReply": { "window": 900000 } }
155
155
  }
156
156
  ```
157
157
 
@@ -159,7 +159,7 @@ To stop the agent answering in a specific channel, narrow the `roles` block so t
159
159
  - `mention` — explicit `@bot` mentions.
160
160
  - `reply` — message is a Discord reply pointed at the agent's own message.
161
161
  - `dm` — any message in a DM channel.
162
- - **`stickiness`** — either the literal string `"off"`, or `{ perReply: { window: <ms> } }`. Default: 5-minute reply stickiness (`window: 300000`).
162
+ - **`stickiness`** — either the literal string `"off"`, or `{ perReply: { window: <ms> } }`. Default: 15-minute reply stickiness (`window: 900000`).
163
163
  - `perReply` means: after the agent replies to a user, follow-up messages from that same user in that same channel within the window also wake the loop, even without a mention. The window is bounded server-side (`1` to `86_400_000` ms — 1 ms to 24 hours).
164
164
  - `"off"` disables stickiness — the agent only wakes on explicit triggers.
165
165
 
@@ -174,7 +174,7 @@ There is also a **solo-human fallback** built into the runtime that is **not con
174
174
  "discord-bot": {
175
175
  "engagement": {
176
176
  "trigger": ["mention", "reply", "dm"],
177
- "stickiness": { "perReply": { "window": 300000 } }
177
+ "stickiness": { "perReply": { "window": 900000 } }
178
178
  },
179
179
  "enabled": true
180
180
  }
@@ -337,22 +337,23 @@ Off switch — the broker is constructed but never opens a WS, no LISTEN gets fo
337
337
 
338
338
  The `docker.file` block has two layers of customization:
339
339
 
340
- 1. **Toggles** for opinionated package installs typeclaw knows how to layer correctly (`tmux`, `gh`, `python`, `ffmpeg`, `cjkFonts`, `cloudflared`, `claudeCode`). Most are apt packages — boolean for on/off, version string for an apt pin (e.g. `"gh": "2.40.0"` → `gh=2.40.0`) — and benefit from BuildKit cache mounts. `cloudflared` and `claudeCode` are the exceptions: `cloudflared` downloads the pinned GitHub release, `claudeCode` runs Anthropic's `curl | bash` installer; both are boolean-only. Use a toggle whenever it covers what the user wants over a hand-rolled `append` entry.
340
+ 1. **Toggles** for opinionated package installs typeclaw knows how to layer correctly (`tmux`, `gh`, `python`, `ffmpeg`, `cjkFonts`, `cloudflared`, `claudeCode`, `codexCli`). Most are apt packages — boolean for on/off, version string for an apt pin (e.g. `"gh": "2.40.0"` → `gh=2.40.0`) — and benefit from BuildKit cache mounts. `cloudflared`, `claudeCode`, and `codexCli` are the exceptions: `cloudflared` downloads the pinned GitHub release, `claudeCode` runs Anthropic's `curl | bash` installer, `codexCli` `bun install`s the `@openai/codex` npm package; all three are boolean-only. Use a toggle whenever it covers what the user wants over a hand-rolled `append` entry.
341
341
  2. **`append`** is the escape hatch for everything the toggles don't cover. An array of single-line Dockerfile instructions spliced in right before `ENTRYPOINT`, prefixed with a `# Custom lines from typeclaw.json#docker.file.append.` comment.
342
342
 
343
343
  ### Fields
344
344
 
345
- | Field | Required | Type | Notes |
346
- | ------------- | -------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
347
- | `tmux` | no | boolean \| string | Default `true`. `false` omits tmux from the apt install. String pins the Debian package version (e.g. `"3.3a-3"` → `tmux=3.3a-3`). |
348
- | `gh` | no | boolean \| string | Default `true`. `false` omits **both** the `gh` package and the GitHub CLI keyring bootstrap layer (skipping the network roundtrip on cold builds). String pins the version. |
349
- | `python` | no | boolean | Default `true`. Fans out to `python3 python3-pip python3-venv python-is-python3` (the bundle that makes `python` and `pip` resolve correctly inside the container). Boolean-only — no version pin, because Debian's `python3` is a meta-package that doesn't accept a useful pin. |
350
- | `ffmpeg` | no | boolean \| string | Default `false`. `true` apt-installs ffmpeg (~80 MB of codecs). String pins the version. |
351
- | `cjkFonts` | no | boolean | Default `true`. Installs `fonts-noto-cjk` (~56 MB) so Chromium (used by `agent-browser`) renders Korean/Japanese/Chinese glyphs correctly in screenshots, `page.pdf()`, and other raster output. `false` skips the layer entirely (DOM/innerText scraping is unaffected by font absence — only raster output shows tofu boxes). Boolean-only: the package is a metapackage tracking upstream Noto, no useful apt pin. |
352
- | `cloudflared` | no | boolean | Default `true`. Downloads the pinned `cloudflared` GitHub release (~35 MB) into the image so `cloudflare-quick` tunnels work on the next `start` without a separate Dockerfile edit. `false` skips the layer entirely on agents that don't use tunnels. Boolean-only — pinning is owned by the typeclaw release. |
353
- | `xvfb` | no | boolean | Default `true`. Installs `xvfb` (~5 MB) so the entrypoint shim can spawn a virtual X server and export `DISPLAY=:99`, giving headed Chrome (agent-browser `--headed`, headful Playwright) a real X11 display to defeat headless-mode WAF fingerprinting. `false` skips the layer; the shim self-heals (no `Xvfb` on PATH → execs the agent without `DISPLAY`). Boolean-only — xvfb tracks the upstream X server release with no useful apt pin. |
354
- | `claudeCode` | no | boolean | Default `false`. `true` runs Anthropic's official `curl -fsSL https://claude.ai/install.sh \| bash` in a dedicated layer (between agent-browser and the entrypoint shim) and pre-seeds `~/.claude.json` to skip the TTY-only theme picker on first launch (without it the agent's `tmux send-keys` would be eaten by the picker). Not apt: no version-pin variant; the upstream installer manages channels via env vars. Pairs with the `typeclaw-claude-code` skill, which documents the auth + tmux-driven usage flow including how to clear the post-seed API-key/trust dialogs. |
355
- | `append` | no | array of strings | Each entry is a single Dockerfile line schema **rejects** entries containing `\n` or `\r`. Defaults to `[]`. Splice happens just before `ENTRYPOINT`, after `ENV NODE_ENV=production`. |
345
+ | Field | Required | Type | Notes |
346
+ | ------------- | -------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
347
+ | `tmux` | no | boolean \| string | Default `true`. `false` omits tmux from the apt install. String pins the Debian package version (e.g. `"3.3a-3"` → `tmux=3.3a-3`). |
348
+ | `gh` | no | boolean \| string | Default `true`. `false` omits **both** the `gh` package and the GitHub CLI keyring bootstrap layer (skipping the network roundtrip on cold builds). String pins the version. |
349
+ | `python` | no | boolean | Default `true`. Fans out to `python3 python3-pip python3-venv python-is-python3` (the bundle that makes `python` and `pip` resolve correctly inside the container). Boolean-only — no version pin, because Debian's `python3` is a meta-package that doesn't accept a useful pin. |
350
+ | `ffmpeg` | no | boolean \| string | Default `false`. `true` apt-installs ffmpeg (~80 MB of codecs). String pins the version. |
351
+ | `cjkFonts` | no | boolean | Default `true`. Installs `fonts-noto-cjk` (~56 MB) so Chromium (used by `agent-browser`) renders Korean/Japanese/Chinese glyphs correctly in screenshots, `page.pdf()`, and other raster output. `false` skips the layer entirely (DOM/innerText scraping is unaffected by font absence — only raster output shows tofu boxes). Boolean-only: the package is a metapackage tracking upstream Noto, no useful apt pin. |
352
+ | `cloudflared` | no | boolean | Default `true`. Downloads the pinned `cloudflared` GitHub release (~35 MB) into the image so `cloudflare-quick` tunnels work on the next `start` without a separate Dockerfile edit. `false` skips the layer entirely on agents that don't use tunnels. Boolean-only — pinning is owned by the typeclaw release. |
353
+ | `xvfb` | no | boolean | Default `true`. Installs `xvfb` (~5 MB) so the entrypoint shim can spawn a virtual X server and export `DISPLAY=:99`, giving headed Chrome (agent-browser `--headed`, headful Playwright) a real X11 display to defeat headless-mode WAF fingerprinting. `false` skips the layer; the shim self-heals (no `Xvfb` on PATH → execs the agent without `DISPLAY`). Boolean-only — xvfb tracks the upstream X server release with no useful apt pin. |
354
+ | `claudeCode` | no | boolean | Default `false`. `true` runs Anthropic's official `curl -fsSL https://claude.ai/install.sh \| bash` in a dedicated layer (between agent-browser and the entrypoint shim) and pre-seeds `~/.claude.json` to skip the TTY-only theme picker on first launch (without it the agent's `tmux send-keys` would be eaten by the picker). Not apt: no version-pin variant; the upstream installer manages channels via env vars. Pairs with the `typeclaw-claude-code` skill, which documents the auth + tmux-driven usage flow including how to clear the post-seed API-key/trust dialogs. |
355
+ | `codexCli` | no | boolean | Default `false`. `true` runs `bun install -g @openai/codex` in a dedicated layer (after `claudeCode`, before the entrypoint shim) and pre-writes `~/.codex/hooks.json` registering `SessionStart` + `Stop` hooks so the operator can detect turn boundaries the same way as Claude Code (sentinel files, `.session-id` discovery). Not apt: no version-pin variant. Codex CLI has NO theme picker so no onboarding seed is needed, but auth (`codex login` or `OPENAI_API_KEY`) and the per-project trust dialog are still required at runtime — handled by the `typeclaw-codex-cli` skill. |
356
+ | `append` | no | array of strings | Each entry is a single Dockerfile line — schema **rejects** entries containing `\n` or `\r`. Defaults to `[]`. Splice happens just before `ENTRYPOINT`, after `ENV NODE_ENV=production`. |
356
357
 
357
358
  Toggle version strings reject whitespace and `=` (apt-injection guard) — pass just the version, not `pkg=ver`.
358
359
 
@@ -400,7 +401,7 @@ The toggle-driven apt install benefits from BuildKit `--mount=type=cache` on `/v
400
401
  ### When the user asks "install <package> in the container" / "add a Dockerfile line"
401
402
 
402
403
  1. **Read `typeclaw.json`.**
403
- 2. **Check if a toggle covers it.** If the package is `tmux`, `gh`, `python`, `ffmpeg`, `cjkFonts` (CJK glyph rendering for `agent-browser` screenshots), `cloudflared` (Cloudflare Quick tunnels), or Anthropic's Claude Code CLI (`claudeCode`), prefer the toggle: `"docker": { "file": { "ffmpeg": true } }`. For a pinned version of an apt toggle, pass the version string: `"gh": "2.40.0"`. This is faster (BuildKit cache mount) and clearer than `append`. `cjkFonts`, `cloudflared`, and `claudeCode` are boolean-only — no version-pin variant.
404
+ 2. **Check if a toggle covers it.** If the package is `tmux`, `gh`, `python`, `ffmpeg`, `cjkFonts` (CJK glyph rendering for `agent-browser` screenshots), `cloudflared` (Cloudflare Quick tunnels), Anthropic's Claude Code CLI (`claudeCode`), or OpenAI's Codex CLI (`codexCli`), prefer the toggle: `"docker": { "file": { "ffmpeg": true } }`. For a pinned version of an apt toggle, pass the version string: `"gh": "2.40.0"`. This is faster (BuildKit cache mount) and clearer than `append`. `cjkFonts`, `cloudflared`, `claudeCode`, and `codexCli` are boolean-only — no version-pin variant.
404
405
  3. **Otherwise, use `append`.** Decide on a single-line entry — for apt installs, prefer one `RUN apt-get update && apt-get install -y --no-install-recommends <pkg> && rm -rf /var/lib/apt/lists/*` line. For env vars, one `ENV` line per variable.
405
406
  4. **Validate no embedded newlines** (`append` only). Multi-step logic must be `&&`-chained on one line, not split across array entries unless those entries are independent Dockerfile instructions.
406
407
  5. **Append to `docker.file.append`** (creating the field if it doesn't exist). Preserve existing entries.
@@ -626,7 +627,7 @@ Never echo, log, or commit values from `secrets.json` or `.env`. Both are gitign
626
627
  - `channels.<adapter>.allow` (legacy) is silently dropped on parse; `migrateLegacyConfigShape` lifts it into `roles.member.match` on load. See the `typeclaw-permissions` skill.
627
628
  - If `portForward` is set: `allow` is either `"*"` or an array of integers (1–65535); `deny`, if present, is an array of integers and **only valid when `allow` is `"*"`** (the schema rejects `deny` paired with a number-array `allow`)
628
629
  - If `docker.file.append` is set: array of strings, each with no embedded `\n` or `\r` (multi-step shell logic goes in a single `&&`-chained `RUN` entry)
629
- - If any `docker.file` toggle is set: `tmux`/`gh`/`ffmpeg` are boolean or version string (no whitespace, no `=`); `python`, `cjkFonts`, `cloudflared`, and `claudeCode` are boolean only
630
+ - If any `docker.file` toggle is set: `tmux`/`gh`/`ffmpeg` are boolean or version string (no whitespace, no `=`); `python`, `cjkFonts`, `cloudflared`, `claudeCode`, and `codexCli` are boolean only
630
631
  - No unknown top-level keys you invented — keys outside the well-known ten are interpreted as **plugin config blocks** and only do something if a plugin owns them. Inventing one means the user thinks it took effect and it did not.
631
632
 
632
633
  ## Things you must not do
@@ -645,7 +646,7 @@ Never echo, log, or commit values from `secrets.json` or `.env`. Both are gitign
645
646
  - **Do not promise to post to a channel the speaker's role does not cover.** The router drops every inbound where the speaking author resolves to a role without `channel.respond`. If the user wants you to post somewhere new, the prerequisite is a `roles` edit + restart, not a retry.
646
647
  - **Do not conflate "stop replying" with "remove the role's match-rule".** Removing the match-rule cuts off both inbound visibility and outbound posting. If the user just wants quieter behavior, edit `engagement` instead.
647
648
  - **Do not edit the `Dockerfile` directly.** It is autogenerated and rewritten on every `typeclaw start` from `src/init/dockerfile.ts` in the typeclaw repo. Manual edits will be silently overwritten (and auto-committed away if the working tree is dirty). Customizations belong in the `docker.file` block (toggles or `append`).
648
- - **Do not reach for `docker.file.append` when a toggle covers it.** If the user wants tmux, gh, python, ffmpeg, fonts-noto-cjk (cjkFonts), cloudflared, or Anthropic's Claude Code CLI (claudeCode) installed (or removed, or pinned), use the toggle. The apt toggles are the cache-mounted path; `cloudflared` and `claudeCode` are non-apt boolean toggles, and the `claudeCode` toggle pairs with the `typeclaw-claude-code` skill that documents auth + usage. `append` for any of these is slower and harder to read.
649
+ - **Do not reach for `docker.file.append` when a toggle covers it.** If the user wants tmux, gh, python, ffmpeg, fonts-noto-cjk (cjkFonts), cloudflared, Anthropic's Claude Code CLI (claudeCode), or OpenAI's Codex CLI (codexCli) installed (or removed, or pinned), use the toggle. The apt toggles are the cache-mounted path; `cloudflared`, `claudeCode`, and `codexCli` are non-apt boolean toggles, and the `claudeCode`/`codexCli` toggles pair with the `typeclaw-claude-code`/`typeclaw-codex-cli` skills that document auth + usage. `append` for any of these is slower and harder to read.
649
650
  - **Do not use `docker.file.append` for things that belong in the template.** If the user wants a system package _every_ typeclaw user should have, that's a typeclaw release, not a per-agent `append`. Suggest filing an issue.
650
651
  - **Do not put multiline strings in `docker.file.append`.** The schema rejects entries with embedded `\n`/`\r`. Use one entry per Dockerfile instruction; chain shell logic with `&&` on one line.
651
652
  - **Do not pass `pkg=ver` as a toggle version string.** The schema rejects `=` in version strings. Pass just the version (`"gh": "2.40.0"`); the renderer prepends `pkg=` itself. Same for whitespace — version strings cannot contain spaces.
@@ -4,13 +4,21 @@ export type WaitForOptions = {
4
4
  description?: string
5
5
  }
6
6
 
7
- // 5s, not 1s. 1s was tight enough to be the dominant cause of `bun test --parallel`
8
- // flakes on macOS: under 18-worker concurrent shell-spawn load, the kernel can
9
- // take >1s to drain a child process's stderr pipe past the libuv JS boundary,
10
- // so a `waitFor` for "fake-cloudflared printed a URL" loses the race. 5s costs
11
- // nothing on the happy path (the polled predicate returns truthy as soon as it
12
- // can; this is just the timeout, not the wait), and absorbs realistic load.
13
- const DEFAULT_TIMEOUT_MS = 5_000
7
+ // 30s, not 5s. 5s was tight enough to flake on heavy-load callers (the WS
8
+ // pipeline assembly in src/portbroker/broker.test.ts is the canonical case:
9
+ // host-side `Bun.listen` accept broker connect port discoverysnapshot
10
+ // fanout data round-trip is a 5-hop chain across two event loops, and the
11
+ // 5s deadline was reached when libuv contention queued one of those hops).
12
+ // The original 1s 5s bump (commit b6a3ef9-era) acknowledged the same
13
+ // failure mode for fake-cloudflared stderr drain; the WS pipeline pushed
14
+ // the bound one tier higher. 30s costs nothing on the happy path (the
15
+ // polled predicate returns truthy as soon as it can; this is just the
16
+ // timeout, not the wait), absorbs realistic 18-worker contention, and
17
+ // matches the global `setDefaultTimeout(30_000)` in
18
+ // scripts/require-parallel.ts so a wedged waitFor surfaces as a clear
19
+ // "waitFor: ... did not become truthy within 30000ms" message before the
20
+ // outer test-level timeout fires with no context.
21
+ const DEFAULT_TIMEOUT_MS = 30_000
14
22
  const DEFAULT_INTERVAL_MS = 1
15
23
 
16
24
  export async function waitFor<T>(
@@ -134,7 +134,7 @@
134
134
  ],
135
135
  "stickiness": {
136
136
  "perReply": {
137
- "window": 300000
137
+ "window": 900000
138
138
  }
139
139
  }
140
140
  },
@@ -159,7 +159,7 @@
159
159
  "stickiness": {
160
160
  "default": {
161
161
  "perReply": {
162
- "window": 300000
162
+ "window": 900000
163
163
  }
164
164
  },
165
165
  "anyOf": [
@@ -275,7 +275,7 @@
275
275
  ],
276
276
  "stickiness": {
277
277
  "perReply": {
278
- "window": 300000
278
+ "window": 900000
279
279
  }
280
280
  }
281
281
  },
@@ -300,7 +300,7 @@
300
300
  "stickiness": {
301
301
  "default": {
302
302
  "perReply": {
303
- "window": 300000
303
+ "window": 900000
304
304
  }
305
305
  },
306
306
  "anyOf": [
@@ -448,7 +448,7 @@
448
448
  ],
449
449
  "stickiness": {
450
450
  "perReply": {
451
- "window": 300000
451
+ "window": 900000
452
452
  }
453
453
  }
454
454
  },
@@ -473,7 +473,7 @@
473
473
  "stickiness": {
474
474
  "default": {
475
475
  "perReply": {
476
- "window": 300000
476
+ "window": 900000
477
477
  }
478
478
  },
479
479
  "anyOf": [
@@ -589,7 +589,7 @@
589
589
  ],
590
590
  "stickiness": {
591
591
  "perReply": {
592
- "window": 300000
592
+ "window": 900000
593
593
  }
594
594
  }
595
595
  },
@@ -614,7 +614,7 @@
614
614
  "stickiness": {
615
615
  "default": {
616
616
  "perReply": {
617
- "window": 300000
617
+ "window": 900000
618
618
  }
619
619
  },
620
620
  "anyOf": [
@@ -730,7 +730,7 @@
730
730
  ],
731
731
  "stickiness": {
732
732
  "perReply": {
733
- "window": 300000
733
+ "window": 900000
734
734
  }
735
735
  }
736
736
  },
@@ -755,7 +755,7 @@
755
755
  "stickiness": {
756
756
  "default": {
757
757
  "perReply": {
758
- "window": 300000
758
+ "window": 900000
759
759
  }
760
760
  },
761
761
  "anyOf": [
@@ -932,6 +932,7 @@
932
932
  "cloudflared": true,
933
933
  "xvfb": true,
934
934
  "claudeCode": false,
935
+ "codexCli": false,
935
936
  "append": []
936
937
  }
937
938
  },
@@ -947,6 +948,7 @@
947
948
  "cloudflared": true,
948
949
  "xvfb": true,
949
950
  "claudeCode": false,
951
+ "codexCli": false,
950
952
  "append": []
951
953
  },
952
954
  "type": "object",
@@ -1007,6 +1009,10 @@
1007
1009
  "default": false,
1008
1010
  "type": "boolean"
1009
1011
  },
1012
+ "codexCli": {
1013
+ "default": false,
1014
+ "type": "boolean"
1015
+ },
1010
1016
  "append": {
1011
1017
  "default": [],
1012
1018
  "type": "array",
@@ -1177,7 +1183,8 @@
1177
1183
  "idleMs": 60000,
1178
1184
  "bufferBytes": 500000,
1179
1185
  "injectionBudgetBytes": 16384,
1180
- "spawnTimeoutMs": 50000
1186
+ "spawnTimeoutMs": 50000,
1187
+ "retrievalSpawnTimeoutMs": 30000
1181
1188
  },
1182
1189
  "type": "object",
1183
1190
  "properties": {
@@ -1205,6 +1212,12 @@
1205
1212
  "minimum": 1,
1206
1213
  "maximum": 9007199254740991
1207
1214
  },
1215
+ "retrievalSpawnTimeoutMs": {
1216
+ "default": 30000,
1217
+ "type": "integer",
1218
+ "minimum": 1,
1219
+ "maximum": 9007199254740991
1220
+ },
1208
1221
  "dreaming": {
1209
1222
  "type": "object",
1210
1223
  "properties": {