@with-logic/elwood 0.1.1 → 0.1.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/CHANGELOG.md +17 -3
- package/CONTRIBUTING.md +104 -0
- package/README.md +23 -17
- package/SECURITY.md +42 -0
- package/dist/claude/model-picker.d.ts.map +1 -1
- package/dist/claude/model-picker.js +4 -3
- package/dist/claude/model-picker.js.map +1 -1
- package/dist/claude/model-switch-confirmation.js +9 -1
- package/dist/claude/model-switch-confirmation.js.map +1 -1
- package/dist/claude/screen-table.js +1 -1
- package/dist/claude/screen-table.js.map +1 -1
- package/dist/claude/session/build.d.ts.map +1 -1
- package/dist/claude/session/build.js +3 -0
- package/dist/claude/session/build.js.map +1 -1
- package/dist/claude/session/start-or-resume.js +1 -0
- package/dist/claude/session/start-or-resume.js.map +1 -1
- package/dist/cli/interactive/spawn.d.ts.map +1 -1
- package/dist/cli/interactive/spawn.js +30 -9
- package/dist/cli/interactive/spawn.js.map +1 -1
- package/dist/cli/models/index.d.ts.map +1 -1
- package/dist/cli/models/index.js +6 -3
- package/dist/cli/models/index.js.map +1 -1
- package/dist/cli/request/layers.d.ts +2 -0
- package/dist/cli/request/layers.d.ts.map +1 -1
- package/dist/cli/request/layers.js +9 -5
- package/dist/cli/request/layers.js.map +1 -1
- package/dist/cli/sessions/index.d.ts +9 -2
- package/dist/cli/sessions/index.d.ts.map +1 -1
- package/dist/cli/sessions/index.js +2 -6
- package/dist/cli/sessions/index.js.map +1 -1
- package/dist/cli/sessions/settings.d.ts +11 -0
- package/dist/cli/sessions/settings.d.ts.map +1 -0
- package/dist/cli/sessions/settings.js +26 -0
- package/dist/cli/sessions/settings.js.map +1 -0
- package/dist/codex/model-picker.d.ts.map +1 -1
- package/dist/codex/model-picker.js +3 -2
- package/dist/codex/model-picker.js.map +1 -1
- package/dist/codex/session/build.d.ts.map +1 -1
- package/dist/codex/session/build.js +5 -7
- package/dist/codex/session/build.js.map +1 -1
- package/dist/codex/session/start-or-resume.js +1 -0
- package/dist/codex/session/start-or-resume.js.map +1 -1
- package/dist/core/input/index.d.ts.map +1 -1
- package/dist/core/input/index.js +15 -3
- package/dist/core/input/index.js.map +1 -1
- package/dist/core/models/input.d.ts +3 -0
- package/dist/core/models/input.d.ts.map +1 -0
- package/dist/core/models/input.js +9 -0
- package/dist/core/models/input.js.map +1 -0
- package/dist/core/models/picker.d.ts +2 -1
- package/dist/core/models/picker.d.ts.map +1 -1
- package/dist/core/models/picker.js +6 -5
- package/dist/core/models/picker.js.map +1 -1
- package/dist/core/models/tui-screen.d.ts +1 -1
- package/dist/core/models/tui-screen.d.ts.map +1 -1
- package/dist/core/models/tui-screen.js +26 -3
- package/dist/core/models/tui-screen.js.map +1 -1
- package/dist/runtime/session/base.d.ts.map +1 -1
- package/dist/runtime/session/base.js +4 -1
- package/dist/runtime/session/base.js.map +1 -1
- package/dist/runtime/session/commands.d.ts +2 -1
- package/dist/runtime/session/commands.d.ts.map +1 -1
- package/dist/runtime/session/commands.js +10 -4
- package/dist/runtime/session/commands.js.map +1 -1
- package/dist/runtime/session/lifecycle.d.ts +3 -4
- package/dist/runtime/session/lifecycle.d.ts.map +1 -1
- package/dist/runtime/session/lifecycle.js +6 -5
- package/dist/runtime/session/lifecycle.js.map +1 -1
- package/dist/state/directories.d.ts +4 -0
- package/dist/state/directories.d.ts.map +1 -0
- package/dist/state/directories.js +41 -0
- package/dist/state/directories.js.map +1 -0
- package/dist/state/files.d.ts.map +1 -1
- package/dist/state/files.js +25 -11
- package/dist/state/files.js.map +1 -1
- package/dist/state/private-session.d.ts.map +1 -1
- package/dist/state/private-session.js +2 -0
- package/dist/state/private-session.js.map +1 -1
- package/dist/state/store.d.ts.map +1 -1
- package/dist/state/store.js +7 -1
- package/dist/state/store.js.map +1 -1
- package/docs/cli-behavior.md +1 -1
- package/docs/codex/cli.md +57 -0
- package/docs/codex/config.md +47 -0
- package/docs/codex/hooks.md +70 -0
- package/examples/README.md +56 -0
- package/examples/full.ts +113 -0
- package/examples/minimal.ts +15 -0
- package/examples/stream.ts +39 -0
- package/examples/support.ts +71 -0
- package/package.json +10 -5
- package/prd/01-overview.md +21 -0
- package/prd/02-goals.md +47 -0
- package/prd/03-terminology.md +32 -0
- package/prd/04-architecture.md +121 -0
- package/prd/05-api/5.1-starting-claude.md +108 -0
- package/prd/05-api/5.2-resuming-claude.md +86 -0
- package/prd/05-api/5.3-claude-session-api.md +658 -0
- package/prd/05-api/5.4-events.md +145 -0
- package/prd/05-api/5.5-starting-codex.md +106 -0
- package/prd/05-api/5.6-resuming-codex.md +48 -0
- package/prd/05-api/5.7-codex-session-api.md +425 -0
- package/prd/05-api/5.8-session-api.md +122 -0
- package/prd/05-api/5.9-loops.md +170 -0
- package/prd/05-api/README.md +19 -0
- package/prd/06-claude-hook-bridge.md +164 -0
- package/prd/07-claude-policy.md +15 -0
- package/prd/07a-codex-hook-bridge.md +76 -0
- package/prd/08-state.md +127 -0
- package/prd/09-lifecycle.md +222 -0
- package/prd/10-errors.md +55 -0
- package/prd/11-test-app.md +53 -0
- package/prd/12-e2e.md +23 -0
- package/prd/12a-cli.md +375 -0
- package/prd/13-latitude.md +12 -0
- package/prd/14-conformance.md +333 -0
- package/prd/15-open-notes.md +8 -0
- package/prd/README.md +45 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
## 5.3 ClaudeSessionApi (the raw live session)
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
interface ClaudeSessionApi {
|
|
5
|
+
readonly elwoodSessionId: string;
|
|
6
|
+
readonly cwd: string;
|
|
7
|
+
readonly status: ElwoodSessionStatus;
|
|
8
|
+
readonly terminal: ElwoodTerminal;
|
|
9
|
+
|
|
10
|
+
statusDecisions(): readonly ElwoodStatusDecision[];
|
|
11
|
+
waitForStatus(
|
|
12
|
+
match: (status: ElwoodSessionStatus) => boolean,
|
|
13
|
+
timeoutMs?: number,
|
|
14
|
+
): Promise<ElwoodSessionStatus>;
|
|
15
|
+
waitForActivity(
|
|
16
|
+
match: (event: ElwoodActivityEvent) => boolean,
|
|
17
|
+
timeoutMs?: number,
|
|
18
|
+
): Promise<ElwoodActivityEvent>;
|
|
19
|
+
|
|
20
|
+
on<E extends ClaudeEventName>(event: E, handler: ClaudeEventHandler<E>): Unsubscribe;
|
|
21
|
+
off<E extends ClaudeEventName>(event: E, handler: ClaudeEventHandler<E>): void;
|
|
22
|
+
|
|
23
|
+
sendPrompt(prompt: string, options?: SendOptions): Promise<void>;
|
|
24
|
+
sendMessage(message: string, options?: SendOptions): Promise<void>;
|
|
25
|
+
sendGuidance(message: string, options?: SendOptions): Promise<void>;
|
|
26
|
+
sendKeys(input: string | Uint8Array): Promise<void>;
|
|
27
|
+
resize(size: TerminalSize): Promise<void>;
|
|
28
|
+
interrupt(options?: { readonly timeoutMs?: number }): Promise<void>;
|
|
29
|
+
compact(options?: { readonly timeoutMs?: number }): Promise<void>;
|
|
30
|
+
listModels(options?: { readonly timeoutMs?: number }): Promise<readonly AgentModelOption[]>;
|
|
31
|
+
setModel(id: string, options?: { readonly timeoutMs?: number }): Promise<void>;
|
|
32
|
+
createLoop(request: ElwoodLoopRequest): Promise<ElwoodLoopSnapshot>;
|
|
33
|
+
listLoops(): Promise<readonly ElwoodLoopSnapshot[]>;
|
|
34
|
+
cancelLoop(loopId: string): Promise<void>;
|
|
35
|
+
// Claude-only re-authentication recovery; NOT part of CodexSession or the
|
|
36
|
+
// common ElwoodAgentSession surface (C-API-43).
|
|
37
|
+
login(options: ClaudeLoginOptions): Promise<void>;
|
|
38
|
+
|
|
39
|
+
stop(): Promise<void>;
|
|
40
|
+
kill(): Promise<void>;
|
|
41
|
+
teardown(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface SendOptions {
|
|
45
|
+
// Images to attach to this submission. Attached content: the submission is the
|
|
46
|
+
// text plus this ordered list of images (images have no position within the
|
|
47
|
+
// text, but array order determines attachment/chip order), attached by the
|
|
48
|
+
// adapter's native path before the text is submitted (C-API-44).
|
|
49
|
+
readonly images?: readonly ImageInput[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// An image to attach, given either as a filesystem path or as in-memory bytes
|
|
53
|
+
// with an explicit format (never both — the alternatives are mutually exclusive).
|
|
54
|
+
// Bytes are written to a short-lived temp file so the adapter attach paths (which
|
|
55
|
+
// are file- or clipboard-based) have something to read; the temp file is removed
|
|
56
|
+
// (best-effort) after the submission is attached.
|
|
57
|
+
type ImageInput =
|
|
58
|
+
| { readonly path: string }
|
|
59
|
+
| { readonly data: Uint8Array; readonly format: "png" | "jpeg" | "gif" | "webp" };
|
|
60
|
+
|
|
61
|
+
type ClaudeLoginMethod = "claudeai" | "console" | "third_party";
|
|
62
|
+
|
|
63
|
+
interface ClaudeLoginOptions {
|
|
64
|
+
// Which method to pick from the "Select login method" list if it renders;
|
|
65
|
+
// defaults to "claudeai". Skipped when the list never appears.
|
|
66
|
+
readonly method?: ClaudeLoginMethod;
|
|
67
|
+
// Called with the (validated https, approved-host) browser authorization URL
|
|
68
|
+
// once it is scraped off screen, so the caller can open/show it. Optional.
|
|
69
|
+
readonly onAuthUrl?: (url: string) => void;
|
|
70
|
+
// REQUIRED for the default account flow: called when the CLI reaches its
|
|
71
|
+
// "paste code here" prompt; the returned code is validated then submitted.
|
|
72
|
+
readonly provideCode: () => string | Promise<string>;
|
|
73
|
+
readonly timeoutMs?: number; // default 300000
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`login` is Claude-only and drives the interactive `/login` flow to recover a
|
|
78
|
+
session whose login lapsed (C-CLAUDE-18); it is NOT on `CodexSession` or the
|
|
79
|
+
common `ElwoodAgentSession`. It runs as an EXCLUSIVE control-queue transaction:
|
|
80
|
+
it serializes behind and ahead of all other queued operations and no message,
|
|
81
|
+
prompt, model-picker command, or second `login` may interleave with its picker
|
|
82
|
+
keys, authorization code, or Enters. It writes exactly one library-controlled
|
|
83
|
+
Enter after a submitted code, never sends raw keystrokes into a blocking dialog,
|
|
84
|
+
and validates both untrusted inputs — the scraped URL (rejected unless `https:`
|
|
85
|
+
on an approved Anthropic host with no embedded credentials) and the human code
|
|
86
|
+
(rejected as `login_failed` if empty, oversized, or control-bearing). Screen
|
|
87
|
+
stages are recognized only when they NEWLY appear after this `/login` submission,
|
|
88
|
+
so stale on-screen text cannot settle the wrong attempt or disclose the code. It
|
|
89
|
+
resolves only after the CLI reports success AND a fresh `ready` transition proves
|
|
90
|
+
the session is usable again; it rejects with `login_failed` on an explicit
|
|
91
|
+
failure/invalid code or a failing `provideCode` callback, `login_timeout` after
|
|
92
|
+
`timeoutMs`, and `session_not_running` if the session terminates first or is
|
|
93
|
+
already terminal. A flow that self-completes without a code prompt never calls
|
|
94
|
+
`provideCode`.
|
|
95
|
+
|
|
96
|
+
`login` defends against untrusted on-screen text with layered checks — exclusive
|
|
97
|
+
queue ownership, approved-host/`https:` URL validation, active-tail + baseline
|
|
98
|
+
scoping of stage markers, blocking-dialog write holds, and a re-check that the
|
|
99
|
+
paste prompt is still current immediately before the code is written. Two
|
|
100
|
+
residual limitations are accepted by design: a single frame that renders BOTH a
|
|
101
|
+
genuine approved-host OAuth URL and a paste-code prompt in the active region can
|
|
102
|
+
still advance the flow (coherent per-frame dialog identity on a text-only TUI is
|
|
103
|
+
not attempted — callers should invoke `login` only when the session is genuinely
|
|
104
|
+
at `/login`, not while untrusted output streams), and the version-coupled login
|
|
105
|
+
screen matchers are covered by unit tests against real captured CLI strings
|
|
106
|
+
rather than a live-CLI e2e (which would mutate real auth and cannot run in CI).
|
|
107
|
+
|
|
108
|
+
`sendPrompt` submits a user prompt through the terminal input path, as if a
|
|
109
|
+
human typed or pasted it into Claude and pressed Enter. It MUST support
|
|
110
|
+
multi-line prompts. The implementation should use bracketed paste or an
|
|
111
|
+
equivalent robust terminal-input strategy so newline characters are entered as
|
|
112
|
+
prompt content rather than premature submissions.
|
|
113
|
+
|
|
114
|
+
`sendMessage` is the adapter-neutral API for the common chat-loop operation:
|
|
115
|
+
send a user message when the wrapped agent can accept one. Parent apps that only
|
|
116
|
+
need to send a user message and wait for activity/status should prefer
|
|
117
|
+
`sendMessage`. If the session is ready, Elwood submits the message immediately
|
|
118
|
+
through the same PTY input path as `sendPrompt`. If the session is alive but not
|
|
119
|
+
ready, Elwood queues the message and submits it when the adapter reports semantic
|
|
120
|
+
readiness through adapter-specific lifecycle signals. Initial readiness MUST be
|
|
121
|
+
hook-backed whenever the CLI emits a pre-input readiness event: Claude's
|
|
122
|
+
`InstructionsLoaded` and Codex's `SessionStart` both fire only once the CLI is
|
|
123
|
+
initialized and accepting input, so Elwood submits the first queued message on
|
|
124
|
+
that hook. The rendered terminal is NOT a reliable pre-input signal on Codex:
|
|
125
|
+
the input composer marker (a line-leading `›`) is painted as a boot-time
|
|
126
|
+
placeholder roughly a second before the TUI accepts input, and it is
|
|
127
|
+
byte-for-byte identical to the ready composer, so a message submitted on the
|
|
128
|
+
composer marker alone races Codex's input loop and is silently swallowed
|
|
129
|
+
(observed: the queued persona never reaches `SessionStart`/`UserPromptSubmit`).
|
|
130
|
+
Elwood therefore never treats the rendered composer as a readiness signal on a
|
|
131
|
+
COLD START. On RESUME the contract differs: the CLI is reattaching to an
|
|
132
|
+
already-established conversation, so it does not re-fire its pre-input readiness
|
|
133
|
+
hook (`SessionStart` for Codex; observed), and its input loop is live the moment
|
|
134
|
+
the composer paints — a message submitted when the composer marker first appears
|
|
135
|
+
on resume is accepted, not swallowed (verified against the real CLI). So on a
|
|
136
|
+
resumed session Elwood treats the first rendered composer marker as the readiness
|
|
137
|
+
signal, submitting the first queued message on it rather than waiting out the
|
|
138
|
+
deadline; this brings a resumed session to readiness in roughly a second instead
|
|
139
|
+
of the full deadline. (A resumed adapter whose readiness hook DOES re-fire —
|
|
140
|
+
Claude's `InstructionsLoaded` re-fires on resume — still marks ready on that hook,
|
|
141
|
+
whichever comes first.) The bounded deadline remains the ultimate fallback for
|
|
142
|
+
BOTH start and resume: a continuously animating screen region (for example a
|
|
143
|
+
failing MCP server's retry spinner) or a session whose readiness hook never
|
|
144
|
+
arrives (a missing or failed hook bridge) must not starve readiness forever, so
|
|
145
|
+
initial readiness fires no later than a fixed deadline after the first rendered
|
|
146
|
+
frame even when neither a hook nor (on cold start) any composer signal arrives.
|
|
147
|
+
Subsequent turn readiness comes from
|
|
148
|
+
completion signals such as an unblocked `Stop` hook. Queued messages are
|
|
149
|
+
submitted in FIFO order, one message per ready transition, so back-to-back calls
|
|
150
|
+
do not accidentally paste multiple user turns into one active agent prompt. A
|
|
151
|
+
queued `sendMessage` promise resolves when that message has been written to the
|
|
152
|
+
PTY. If the session terminates before a queued message is written, the promise
|
|
153
|
+
rejects with `session_not_running`.
|
|
154
|
+
|
|
155
|
+
The ergonomic `stream`/`send` turn owner additionally requires positive turn
|
|
156
|
+
acceptance before an idle `ready` transition may complete a turn. A matching
|
|
157
|
+
`UserPromptSubmit`, turn content, or `Stop` hook is positive acceptance. This
|
|
158
|
+
closes the cold-start deadline race: under load, Codex may repaint a boot spinner
|
|
159
|
+
and composer after the deadline-released paste, swallow that paste, and otherwise
|
|
160
|
+
look exactly like a completed empty turn. When `ready` returns without positive
|
|
161
|
+
acceptance, Elwood waits the normal no-oracle quiet window and replays the same
|
|
162
|
+
submission at the now-idle composer, at most twice. A still-unaccepted turn fails
|
|
163
|
+
with `wait_timeout`; it never reports a false empty success. A positive acceptance
|
|
164
|
+
arriving during the quiet window cancels recovery and lets the observed `ready`
|
|
165
|
+
settle normally.
|
|
166
|
+
|
|
167
|
+
`sendGuidance` is the adapter-neutral intervention API. Before the session has
|
|
168
|
+
ever reached semantic readiness, and while a blocking dialog is visible, it has
|
|
169
|
+
the same readiness-safe behavior as `sendMessage`. During a real running turn
|
|
170
|
+
after initial readiness, it bypasses turn readiness and enters the live TUI
|
|
171
|
+
immediately, like `sendPrompt`. Guidance may overtake readiness-waiting messages,
|
|
172
|
+
but queue-backed prompt, message, and command submissions remain serialized so
|
|
173
|
+
their bracketed pastes and delayed Enter keystrokes never interleave. Its promise
|
|
174
|
+
resolves only after the first submitting Enter is dispatched.
|
|
175
|
+
|
|
176
|
+
Prompt submission MUST NOT race the TUI's paste ingestion. The CLIs ingest a
|
|
177
|
+
bracketed paste asynchronously, and an Enter concatenated into the same PTY
|
|
178
|
+
write can be consumed by the paste boundary — leaving the prompt staged in
|
|
179
|
+
the composer, never submitted (long multi-line prompts such as personas hit
|
|
180
|
+
this reliably). Elwood therefore writes the bracketed paste and the
|
|
181
|
+
submitting Enter as separate PTY writes, with the Enter following after a
|
|
182
|
+
short settle delay, and then confirms submission against the rendered
|
|
183
|
+
screen: while the composer still shows staged content (Claude's
|
|
184
|
+
"[Pasted text" chip; for Codex the paste's final line still in the
|
|
185
|
+
composer), Elwood re-sends Enter a bounded number of times. A surplus Enter
|
|
186
|
+
on an empty composer is a no-op on both CLIs, so the recovery cannot
|
|
187
|
+
double-submit.
|
|
188
|
+
|
|
189
|
+
Message text is DATA, never terminal control. `sendPrompt`, `sendMessage`, and
|
|
190
|
+
`sendGuidance` submit caller- or model-controlled text as the content of a
|
|
191
|
+
bracketed paste, so that text MUST NOT be able to escape paste mode or inject
|
|
192
|
+
live keystrokes. Before framing the paste, Elwood sanitizes the text: it removes
|
|
193
|
+
the bracketed-paste sentinels (`ESC [ 200~` and `ESC [ 201~`) so an embedded end
|
|
194
|
+
sentinel cannot terminate the paste early and turn following bytes — an Enter, an
|
|
195
|
+
Escape, a permission-dialog confirmation — into live input, and it strips other
|
|
196
|
+
C0/C1 control characters except the horizontal tab, newline, and carriage return
|
|
197
|
+
that are legitimate multi-line text. This sanitization applies only to the
|
|
198
|
+
queued text-submission APIs; `sendKeys` is the explicit raw-input escape hatch
|
|
199
|
+
and is never sanitized. Newlines inside a sanitized paste remain paste content
|
|
200
|
+
(the whole point of bracketed paste) rather than premature submissions.
|
|
201
|
+
|
|
202
|
+
`sendPrompt`, `sendMessage`, and `sendGuidance` accept an optional `images`
|
|
203
|
+
list (file paths or in-memory bytes with an explicit format). A submission is
|
|
204
|
+
then the text plus that list of attached images: images have no position WITHIN
|
|
205
|
+
the text (an attached-content model, not interleaving), but the list is ORDERED
|
|
206
|
+
— array order determines attachment order and the CLI's `[Image #N]` chip
|
|
207
|
+
numbering. Elwood attaches every image through the SAME native ingestion path a
|
|
208
|
+
human would use in that CLI, as part of the SAME serialized queue operation that
|
|
209
|
+
submits the text, BEFORE the text's submitting Enter, so a single call produces
|
|
210
|
+
a single agent turn carrying both. An image given as bytes is written to a
|
|
211
|
+
short-lived temp file (the adapter attach paths are file- or clipboard-based);
|
|
212
|
+
Elwood attempts to remove every such temp file after the submission is attached,
|
|
213
|
+
on success or failure (best-effort — see the residual limitations below).
|
|
214
|
+
|
|
215
|
+
Inputs are validated and bounded BEFORE any temp file is written or any input
|
|
216
|
+
reaches the composer, and a validation failure rejects the whole call so nothing
|
|
217
|
+
is partially attached. The limits are: at most 16 images per submission; each
|
|
218
|
+
byte input at most 25 MiB and each path input's file at most 25 MiB; at most 50
|
|
219
|
+
MiB total across a submission. A `format` that is not one of `png`/`jpeg`/`gif`/
|
|
220
|
+
`webp`, empty bytes, a `path` that is not a readable regular file (its readability
|
|
221
|
+
is checked, not merely its existence), or any count/size past those limits
|
|
222
|
+
reject with `invalid_image`. Malformed inputs from JavaScript callers (a non-object
|
|
223
|
+
entry, or one lacking a valid `path`/`data`) also reject with `invalid_image`
|
|
224
|
+
rather than a raw type error.
|
|
225
|
+
|
|
226
|
+
Image attachment is CONFIRMED, never assumed. For each image Elwood waits for the
|
|
227
|
+
CLI's `[Image #N]` chip to appear in the composer region; if the chip is not
|
|
228
|
+
observed within the per-image confirmation timeout (10 s), or the OS clipboard
|
|
229
|
+
cannot be read or written, the call rejects with `image_attach_failed` and Elwood
|
|
230
|
+
submits NO text and attaches no further images. A confirmation timeout therefore
|
|
231
|
+
surfaces (e.g. on TUI-wording drift) rather than silently degrading to a text-only
|
|
232
|
+
turn. A session that TERMINATES mid-attach rejects the call with
|
|
233
|
+
`session_not_running`, like any other queued operation. Chip confirmation is
|
|
234
|
+
scoped to the composer region so unrelated on-screen text containing a literal
|
|
235
|
+
`[Image #N]` cannot spoof it.
|
|
236
|
+
|
|
237
|
+
The attach path is adapter-specific because the two CLIs ingest images
|
|
238
|
+
differently, and this is a version-coupled TUI behavior verified against real
|
|
239
|
+
CLIs (Claude 2.1.215, codex-cli 0.144.6):
|
|
240
|
+
|
|
241
|
+
- Claude reads a pasted ABSOLUTE image path itself: Elwood bracketed-pastes each
|
|
242
|
+
image's absolute path into the composer (the same delivery a terminal produces
|
|
243
|
+
on drag-and-drop), Claude reads and encodes the file, and the composer shows an
|
|
244
|
+
`[Image #N]` chip. This is pure PTY text and works on every platform.
|
|
245
|
+
- Codex ingests an interactive image ONLY from the OS clipboard via Ctrl+V (it
|
|
246
|
+
reads `public.tiff` off the macOS `NSPasteboard`); a pasted path is treated as
|
|
247
|
+
pasted TEXT, not an image. Elwood snapshots the user's clipboard ONCE before
|
|
248
|
+
the attachment, then for each image in order writes it onto the clipboard as a
|
|
249
|
+
native image, sends Ctrl+V, and waits for the `[Image #N]` chip; after all
|
|
250
|
+
images it restores the single snapshotted clipboard text. The whole
|
|
251
|
+
snapshot/set/paste/confirm/restore sequence holds a process-wide clipboard lock
|
|
252
|
+
so two concurrent Codex sessions cannot cross-attach each other's images. If
|
|
253
|
+
the initial clipboard snapshot itself fails, the attach rejects with
|
|
254
|
+
`image_attach_failed` before mutating the clipboard. Because this drives the
|
|
255
|
+
macOS clipboard, Codex image attachment is macOS-only; a `sendMessage`/
|
|
256
|
+
`sendPrompt`/`sendGuidance` with `images` on a non-macOS Codex session rejects
|
|
257
|
+
with `unsupported_platform` and submits nothing. Restoring the clipboard is
|
|
258
|
+
best-effort and covers text contents; a brief window where the injected image
|
|
259
|
+
is the clipboard contents is accepted by design. When the restore itself fails,
|
|
260
|
+
the attach result is unchanged (still success) but Elwood surfaces a content-free
|
|
261
|
+
`clipboard_restore_failed` warning (no clipboard contents) so the possible loss
|
|
262
|
+
of the user's prior clipboard is diagnosable rather than silent.
|
|
263
|
+
|
|
264
|
+
Image attachment carries several accepted residual limitations. First, temp-file
|
|
265
|
+
removal after an attach is best-effort: the primary attach/materialize error is
|
|
266
|
+
always preserved (a cleanup failure never masks it), but a failed removal is not
|
|
267
|
+
retried and leaves the file in the OS temp directory for the OS to reap rather
|
|
268
|
+
than surfacing a distinct error. Second, the
|
|
269
|
+
Codex clipboard lock serializes attachments within a SINGLE Elwood process only;
|
|
270
|
+
two Elwood processes sharing one OS clipboard are not coordinated, so a
|
|
271
|
+
cross-process cross-attach remains possible (callers running concurrent
|
|
272
|
+
Elwood processes that attach Codex images should serialize that themselves).
|
|
273
|
+
Third, session close during an in-flight attach rejects the operation and blocks
|
|
274
|
+
the text write, but does not join the attach's temp-cleanup and clipboard-restore
|
|
275
|
+
finalizers; on an abrupt teardown those may not complete before process exit
|
|
276
|
+
(clipboard restore is already best-effort, and temp files fall to OS reaping).
|
|
277
|
+
Fourth, composer isolation on a failed attach is best-effort: when an attach
|
|
278
|
+
fails after staging began, Elwood clears the composer draft (kill-to-line-start
|
|
279
|
+
plus kill-to-line-end) so the rejected images do not leak into a later caller's
|
|
280
|
+
turn, but it does not run an adapter-specific transactional confirm-and-restore
|
|
281
|
+
of the exact prior composer baseline; a partially-staged draft that those keys do
|
|
282
|
+
not fully clear could still carry over (mitigated because a caller does not
|
|
283
|
+
interleave submissions on one session). Fifth, chip confirmation identifies the
|
|
284
|
+
composer by the adapter's prompt-glyph line (`›`/`❯`) rather than adapter cursor
|
|
285
|
+
metadata: this defeats stray transcript/assistant `[Image #N]` text on
|
|
286
|
+
non-composer lines, but a running-turn render that puts a prompt-glyph line
|
|
287
|
+
on-screen at the confirmation moment, or a chip on a wrapped composer
|
|
288
|
+
continuation row, is a known edge the glyph heuristic does not perfectly resolve.
|
|
289
|
+
|
|
290
|
+
Turn boundaries MUST be observable even when no completion hook fires. Claude
|
|
291
|
+
does not fire a `Stop` hook when a running turn is interrupted (Escape), so
|
|
292
|
+
hook-driven readiness alone leaves the session in `running` forever after an
|
|
293
|
+
interrupt. Elwood therefore also derives turn state from the rendered TUI:
|
|
294
|
+
each adapter has a documented working indicator (a footer token rendered only
|
|
295
|
+
while a turn is running) and an idle indicator (the input composer). When the
|
|
296
|
+
working indicator appears after initial readiness, the session transitions to
|
|
297
|
+
`running`; when it disappears and the composer is visible again, the session
|
|
298
|
+
transitions to `ready` — emitting the standard `status` lifecycle activities
|
|
299
|
+
on every real turn boundary regardless of how the turn ended (completion,
|
|
300
|
+
interrupt, or otherwise). A screen that shows neither indicator (for example
|
|
301
|
+
a modal permission dialog) holds the current state so queued messages are
|
|
302
|
+
never submitted into a dialog. Detection MUST be robust to terminal width:
|
|
303
|
+
Claude elides the footer working token below roughly 66 columns, so each
|
|
304
|
+
adapter also has a documented interrupt end banner (Claude renders
|
|
305
|
+
"⎿ Interrupted", Codex renders "Conversation interrupted") whose appearance
|
|
306
|
+
fires the turn-end transition even when the working token was never visible.
|
|
307
|
+
Together the sources cover every width. Turn start is a rendered-state signal:
|
|
308
|
+
the on-screen working token where it fits, plus the OSC window-title braille
|
|
309
|
+
spinner (C-TURN-05) which is width-independent and reveals a running turn even
|
|
310
|
+
when the footer token is elided; a caller `sendMessage`/`sendPrompt` also moves
|
|
311
|
+
the session to `running`. Normal completion comes from the `Stop` hook, and
|
|
312
|
+
interrupts from the working-token edge on wide screens or the end banner
|
|
313
|
+
anywhere. Hook-driven readiness (`Stop`) remains a redundant turn-end source;
|
|
314
|
+
transitions are idempotent, and rendered-state watching only activates after
|
|
315
|
+
initial readiness so startup spinners that borrow the same wording cannot
|
|
316
|
+
fabricate turns. On a RESUME the same guard extends to the transcript replay: a
|
|
317
|
+
resumed session that reached readiness on its first composer frame then repaints
|
|
318
|
+
prior turns, whose footer lines carry the working token — so rendered turn edges
|
|
319
|
+
are suppressed until the composer has stayed quiet and non-blocking (composer
|
|
320
|
+
visible, no working token, no blocking dialog) for a SUSTAINED run of consecutive
|
|
321
|
+
frames, not merely one. The replay repaints those footers in bursts separated by
|
|
322
|
+
brief quiet gaps, so a single quiet frame does not prove the replay finished; any
|
|
323
|
+
working or blocking frame during suppression resets the run. Once the quiet run is
|
|
324
|
+
reached, detection behaves exactly as it does post-readiness on a cold start.
|
|
325
|
+
Explicit running evidence (a caller submission,
|
|
326
|
+
a hook) releases that suppression IMMEDIATELY — a real turn has begun, so the replay
|
|
327
|
+
is over by definition — and synchronizes the watcher into the running state without
|
|
328
|
+
re-announcing a start; the CURRENT frame is still evaluated, so if the turn ended
|
|
329
|
+
exactly as the evidence arrived (the first post-release frame is already the quiet
|
|
330
|
+
composer or the interrupt banner) that frame's end edge is retained, not discarded.
|
|
331
|
+
This ensures a resume never fabricates a phantom rendered turn from replayed history
|
|
332
|
+
and never swallows a real turn's only end edge. These
|
|
333
|
+
indicators (working token, title spinner, composer markers, interrupt banners, and
|
|
334
|
+
blocking-dialog shapes) are version-coupled TUI wording and MUST live in the
|
|
335
|
+
documented per-adapter screen-fact tables, each recording the CLI version it was
|
|
336
|
+
verified against.
|
|
337
|
+
|
|
338
|
+
Elwood MUST NOT require callers or examples to inspect the terminal screen or
|
|
339
|
+
match adapter-specific prompt text before calling `sendMessage`. Adapter-specific
|
|
340
|
+
terminal behavior belongs inside Elwood. Because terminal prompts are a rendered
|
|
341
|
+
UI rather than a stable machine protocol, Elwood should prefer lifecycle hooks
|
|
342
|
+
and startup health checks over fragile prompt-text regular expressions.
|
|
343
|
+
|
|
344
|
+
`sendPrompt` does not gate on readiness. If a caller sends a prompt while Claude
|
|
345
|
+
is busy, Elwood does NOT wait for `ready`: the prompt bypasses readiness and
|
|
346
|
+
dispatches as soon as no other submission is in flight, matching human terminal
|
|
347
|
+
behavior. Like guidance, it may overtake readiness-waiting messages, but it never
|
|
348
|
+
interleaves with an in-flight submission — any paste/Enter sequence already
|
|
349
|
+
underway completes first (C-API-07, queue ordering above).
|
|
350
|
+
This readiness guarantee applies while the agent process is alive, including
|
|
351
|
+
`starting`, `running`, and `ready` states. It does not require Elwood to write to
|
|
352
|
+
terminated sessions; calls made after `stopped`, `exited`, `killed`, or
|
|
353
|
+
`torn_down` should fail with `session_not_running`. Promise-returning session
|
|
354
|
+
methods MUST report that failure by rejecting the returned promise; they never
|
|
355
|
+
throw synchronously, so `promise.catch` alone is a complete error-handling
|
|
356
|
+
strategy for callers.
|
|
357
|
+
|
|
358
|
+
`sendKeys` is the low-level escape hatch for raw terminal input. String input
|
|
359
|
+
flows through the headless xterm input path so terminal semantics match visual
|
|
360
|
+
input. `Uint8Array` input is written to the PTY as bytes instead of being decoded
|
|
361
|
+
as UTF-8, because callers use this overload for raw escape/binary input. It
|
|
362
|
+
bypasses the control queue intentionally so interrupts remain immediate, and
|
|
363
|
+
may therefore interleave with a prompt's paste and delayed Enter.
|
|
364
|
+
|
|
365
|
+
`interrupt` cancels the in-flight turn: the programmatic equivalent of a human
|
|
366
|
+
pressing Escape in the adapter's TUI. An interrupt exists to stop work already
|
|
367
|
+
in flight, so like `sendKeys` it bypasses the control queue and writes Escape
|
|
368
|
+
to the terminal immediately; it never waits behind queued submissions. Once the
|
|
369
|
+
session has reached initial readiness at least once and is `running` or
|
|
370
|
+
`blocked`, Elwood sends a single Escape and the returned promise resolves when
|
|
371
|
+
the session next reaches `ready`, driven by the same turn-end and
|
|
372
|
+
dialog-dismissal signals as a human interrupt (turn-state watching and blocked
|
|
373
|
+
detection above). When no turn is in flight, `interrupt` resolves immediately
|
|
374
|
+
and MUST NOT write anything: Escape at an idle composer is not neutral (Claude
|
|
375
|
+
clears staged composer text; Codex arms its edit-previous-message affordance),
|
|
376
|
+
so an idle interrupt is a safe no-op. "No turn in flight" covers both an idle
|
|
377
|
+
`ready` session AND the pre-readiness startup window, where the session is
|
|
378
|
+
`running` only because startup is usable, not because a user turn has begun —
|
|
379
|
+
Escape into a booting TUI MUST NOT fire. Concurrent `interrupt` calls coalesce:
|
|
380
|
+
while one interrupt is in flight, a second joins it rather than writing a second
|
|
381
|
+
Escape, which could otherwise land on the composer the first interrupt just made
|
|
382
|
+
idle. If the session does not reach `ready` within `timeoutMs` (default 10000
|
|
383
|
+
ms), the promise rejects with `interrupt_failed`; if the session terminates
|
|
384
|
+
first, it rejects with `session_not_running` — as does an `interrupt` on an
|
|
385
|
+
already-terminated session.
|
|
386
|
+
|
|
387
|
+
`listModels` and `setModel` drive the adapter's own `/model` picker UI through
|
|
388
|
+
the headless terminal, because neither CLI exposes a stable machine protocol
|
|
389
|
+
for session model selection. The row model is:
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
type AgentModelOption = {
|
|
393
|
+
readonly id: string;
|
|
394
|
+
readonly label: string;
|
|
395
|
+
readonly description?: string;
|
|
396
|
+
readonly isCurrent: boolean;
|
|
397
|
+
readonly isDefault: boolean;
|
|
398
|
+
readonly raw: string;
|
|
399
|
+
};
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
`id` is a stable lowercase selector (the Claude option label, such as `haiku`,
|
|
403
|
+
or the Codex model slug, such as `gpt-5.4`). `isCurrent` reflects the model the
|
|
404
|
+
session is using now; `isDefault` reflects the CLI's configured default. When
|
|
405
|
+
Codex renders only a `(current)` marker, that row is also the default.
|
|
406
|
+
|
|
407
|
+
`listModels` opens the picker, parses the rendered rows, cancels the picker
|
|
408
|
+
with Escape, and MUST leave the session's model unchanged. `setModel` selects
|
|
409
|
+
the requested row through cursor navigation. Elwood itself MUST NOT persist a
|
|
410
|
+
new default model into the user's configuration (§4.5): for Claude it applies
|
|
411
|
+
with the session-only key rather than Enter, and it never selects rows via
|
|
412
|
+
number keys because Claude's number shortcut immediately saves the choice as
|
|
413
|
+
the user's default. Claude may next show its built-in `Switch model?` or
|
|
414
|
+
`Change effort level?` cache warning before applying the session-only change.
|
|
415
|
+
When the dialog carries Claude's standard cache-warning copy, Elwood MUST find
|
|
416
|
+
the rendered affirmative row across numbered and unnumbered layouts, navigate
|
|
417
|
+
to it from the rendered cursor position, confirm it, and resolve `setModel`
|
|
418
|
+
only after the dialog has cleared and the idle composer has returned. Recognition
|
|
419
|
+
MUST use one bottom-most, contiguous dialog region and revalidate that live
|
|
420
|
+
region before confirmation; transcript text elsewhere in the viewport cannot
|
|
421
|
+
supply cache-warning copy, action rows, or a composer signal. A confirmation
|
|
422
|
+
requested by a `PreModelSwitch` hook is NOT the built-in cache warning: Elwood
|
|
423
|
+
MUST leave that decision to the human and keep it classified as a blocking
|
|
424
|
+
prompt. For Codex, the Codex CLI itself persists the confirmed
|
|
425
|
+
picker selection (model and reasoning effort) into the user's `config.toml` —
|
|
426
|
+
there is no session-only affordance — so Elwood compensates: it snapshots
|
|
427
|
+
`config.toml` before opening the picker and, after the switch is confirmed,
|
|
428
|
+
restores the prior contents if and only if the file differs solely in the
|
|
429
|
+
root-level `model` and `model_reasoning_effort` keys the picker writes
|
|
430
|
+
(compare-and-swap on the untouched remainder). The restore never affects the
|
|
431
|
+
live session, which keeps the switched model because Codex reads its config
|
|
432
|
+
at launch. When the compare fails — for example the user edited `config.toml`
|
|
433
|
+
concurrently — Elwood MUST NOT clobber the file; it leaves the CLI's write in
|
|
434
|
+
place and emits the `codex_default_model_persisted` warning so the deviation
|
|
435
|
+
is surfaced rather than silent. Elwood confirms the CLI's own default
|
|
436
|
+
reasoning level for the chosen Codex model. The whole snapshot/apply/restore
|
|
437
|
+
transaction holds a process-wide lock so two concurrent `setModel` switches in
|
|
438
|
+
the SAME Elwood process cannot interleave and leave the wrong default. This lock
|
|
439
|
+
is process-local: two SEPARATE Elwood processes sharing one `config.toml` can
|
|
440
|
+
still interleave their transactions. That cross-process race is an accepted
|
|
441
|
+
current limitation (the compare-and-swap still refuses to clobber an unrelated
|
|
442
|
+
edit; the residual risk is a transiently-persisted picker model surviving as the
|
|
443
|
+
default). Closing it requires an inter-process lock on the resolved config path
|
|
444
|
+
and is scoped as separate work.
|
|
445
|
+
`listModels` and `setModel` are transient TUI control: they open and drive the
|
|
446
|
+
adapter's own picker overlay and MUST dispatch even while a turn is in flight
|
|
447
|
+
(for example an MCP-server boot spinner at startup), rather than waiting for
|
|
448
|
+
the session to reach `ready`. Waiting would stall picker automation behind a
|
|
449
|
+
turn that has not completed. Failures to recognize or drive the rendered
|
|
450
|
+
picker reject with `model_automation_failed`; an unknown `id` rejects with the
|
|
451
|
+
same error and includes the available ids in the error details.
|
|
452
|
+
|
|
453
|
+
Elwood also exposes the model list WITHOUT an active session, for callers that
|
|
454
|
+
only want to enumerate available models (for example to populate a UI selector):
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
type ListModelsOptions = {
|
|
458
|
+
readonly cwd: string;
|
|
459
|
+
readonly stateDir?: string;
|
|
460
|
+
readonly autoupdate?: boolean;
|
|
461
|
+
readonly hookTimeoutMs?: number;
|
|
462
|
+
readonly strictVersionCheck?: boolean;
|
|
463
|
+
readonly timeoutMs?: number;
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
declare function listClaudeModels(
|
|
467
|
+
options: ListModelsOptions,
|
|
468
|
+
): Promise<readonly AgentModelOption[]>;
|
|
469
|
+
declare function listCodexModels(
|
|
470
|
+
options: ListModelsOptions,
|
|
471
|
+
): Promise<readonly AgentModelOption[]>;
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Each starts a throwaway session and, as a documented and deliberate exception to
|
|
475
|
+
the opt-in workspace-trust default (§5.1), implies `autotrust: true` so a
|
|
476
|
+
workspace-trust prompt cannot silently stall the probe at `blocked`. This
|
|
477
|
+
exception is scoped: the probe never runs a user turn — it only opens and cancels
|
|
478
|
+
the `/model` picker, then tears the session down — so a trusted workspace's
|
|
479
|
+
integrations are loaded but never exercised. A caller that must NOT auto-trust an
|
|
480
|
+
untrusted workspace should probe from a directory it already trusts. Elwood then
|
|
481
|
+
waits for the session to reach first readiness, calls `listModels`, and ALWAYS
|
|
482
|
+
tears the session down — including when readiness or the picker fails, AND when
|
|
483
|
+
adapter startup itself fails after allocating a state directory — so no probe
|
|
484
|
+
session, PTY process tree, or state directory is ever leaked. The returned
|
|
485
|
+
options are exactly the `listModels` rows, so
|
|
486
|
+
`isCurrent` reflects the throwaway session's launch model and `isDefault`
|
|
487
|
+
reflects the user's configured default. Because the probe opens and cancels the
|
|
488
|
+
picker only (it never selects a row), it leaves the user's saved model default
|
|
489
|
+
untouched, exactly as `listModels` does on a live session (Codex's own boot-time
|
|
490
|
+
config bookkeeping is outside Elwood's control and is not a model change). A
|
|
491
|
+
start failure surfaces the adapter's normal typed start error
|
|
492
|
+
(`claude_not_found`, `codex_start_failed`, and so on); a picker failure rejects
|
|
493
|
+
with `model_automation_failed`. The `timeoutMs` bounds the picker automation as
|
|
494
|
+
it does on a live session; readiness uses the normal startup deadline.
|
|
495
|
+
|
|
496
|
+
Command submissions (`compact`, `listModels`, `setModel`) MUST NOT consume the
|
|
497
|
+
session's ready transition: typing a slash command does not start a user turn,
|
|
498
|
+
no completion hook will re-arm readiness afterwards, and consuming readiness
|
|
499
|
+
would deadlock every later queued submission.
|
|
500
|
+
|
|
501
|
+
Queue ordering is FIFO by default, with two documented overtaking exceptions.
|
|
502
|
+
An operation is *readiness-waiting* (a `sendMessage`, a persona message, or a
|
|
503
|
+
`compact`) when it may only reach the terminal once the session is ready; the
|
|
504
|
+
rest are *immediate* (a `sendPrompt`; a `listModels`/`setModel` picker command;
|
|
505
|
+
and a `sendGuidance` that was submitted during an already-ready running turn —
|
|
506
|
+
see §5.3 guidance). When the head of the queue is readiness-waiting and the
|
|
507
|
+
session is not ready, it holds every OTHER readiness-waiting operation behind it
|
|
508
|
+
in FIFO order, but an immediate operation queued behind it overtakes the head
|
|
509
|
+
and reaches the terminal first. Among operations of the same class, and once the
|
|
510
|
+
session is ready, order is strict FIFO. Overtaking never interleaves two
|
|
511
|
+
submissions: the bracketed paste and delayed Enter of one operation always
|
|
512
|
+
complete before the next operation writes. Guidance's eligibility to overtake is
|
|
513
|
+
fixed when it is submitted (§5.3), so guidance queued before first readiness or
|
|
514
|
+
while blocked stays readiness-waiting for its whole lifetime and never reclassifies
|
|
515
|
+
into a later turn.
|
|
516
|
+
|
|
517
|
+
`compact` asks the wrapped agent to compact its conversation. The compact
|
|
518
|
+
command submission goes through the same readiness queue as `sendMessage`, so
|
|
519
|
+
a compact requested mid-turn runs after the active turn completes. Elwood
|
|
520
|
+
types the adapter's `/compact` command through terminal input and resolves the
|
|
521
|
+
returned promise when the adapter reports completion through its `PostCompact`
|
|
522
|
+
hook. If no `PostCompact` hook arrives within `timeoutMs` (default 120000 ms),
|
|
523
|
+
the promise rejects with `compact_failed`. If the session terminates first,
|
|
524
|
+
the promise rejects with `session_not_running`. Hook and activity events for
|
|
525
|
+
`PreCompact`/`PostCompact` flow to the parent app exactly as for any other
|
|
526
|
+
hook.
|
|
527
|
+
|
|
528
|
+
`ElwoodSessionStatus` values are `starting`, `running`, `ready`, `blocked`,
|
|
529
|
+
`stopped`, `exited`, `killed`, and `torn_down`.
|
|
530
|
+
|
|
531
|
+
After startup health checks pass, a newly started or resumed session may enter
|
|
532
|
+
`ready` only when that adapter's first-input readiness signal has fired.
|
|
533
|
+
Otherwise it remains `running` until an adapter lifecycle hook or terminal
|
|
534
|
+
lifecycle signal proves the interactive prompt is ready. Sending a message
|
|
535
|
+
through `sendMessage` or a prompt through `sendPrompt` keeps or moves the session
|
|
536
|
+
in `running`. Adapter readiness signals move the session back to `ready`.
|
|
537
|
+
|
|
538
|
+
`blocked` reports that the wrapped agent is waiting on a human decision it
|
|
539
|
+
cannot answer itself: a tool permission or approval dialog, or a workspace
|
|
540
|
+
trust prompt Elwood was not authorized to answer (`autotrust: false`).
|
|
541
|
+
Blocked detection derives from the rendered screen via the adapter's
|
|
542
|
+
documented screen-fact rules. A session enters `blocked` from any live status
|
|
543
|
+
(`running` or `ready`) when a blocking dialog appears — an adapter may finish
|
|
544
|
+
its working indicator before the dialog settles, so the block can follow a
|
|
545
|
+
`ready` beat — and settles to `ready` when the dialog resolves, since the
|
|
546
|
+
composer is then waiting for input again. Prompts Elwood answers automatically
|
|
547
|
+
(startup prompt automation, or workspace trust with `autotrust: true`) MUST
|
|
548
|
+
NOT produce a `blocked` transition. Terminal statuses absorb blocking
|
|
549
|
+
evidence. On the transition into `blocked`, Elwood also emits an `activity`
|
|
550
|
+
event with `source: "terminal"`, `kind: "attention"`, a label naming the
|
|
551
|
+
matched screen-fact rule ids, and text describing that the agent is waiting on
|
|
552
|
+
a human decision.
|
|
553
|
+
|
|
554
|
+
`statusDecisions()` returns a live-only log of at most the 50 most recent
|
|
555
|
+
status decisions, oldest first. Each `ElwoodStatusDecision` records the evidence that
|
|
556
|
+
arrived, the status it moved `from`, the status it moved `to` (undefined when
|
|
557
|
+
the evidence was ignored rather than applied), and a human-readable `reason`.
|
|
558
|
+
It explains why lifecycle transitions did or did not fire — including which
|
|
559
|
+
evidence was ignored — for diagnostics and the local dev app. The log is never
|
|
560
|
+
persisted and never contains prompt, terminal, or hook payload content.
|
|
561
|
+
|
|
562
|
+
`waitForStatus(match, timeoutMs?)` and `waitForActivity(match, timeoutMs?)` are
|
|
563
|
+
typed helpers over the session's own `status` and `activity` event streams.
|
|
564
|
+
`waitForStatus` resolves with the current status if `match` already accepts it,
|
|
565
|
+
otherwise on the first matching `status` event. `waitForActivity` resolves on
|
|
566
|
+
the first `activity` event `match` accepts. Both reject with `wait_timeout`
|
|
567
|
+
after `timeoutMs` (default 60000 ms), and reject with `session_not_running` if
|
|
568
|
+
the session reaches a terminal status the caller was not waiting for before the
|
|
569
|
+
condition is met (for `waitForActivity`, this includes a session that is
|
|
570
|
+
already terminal when the call is made). The helpers unsubscribe on settle and
|
|
571
|
+
add no persistence; they are convenience wrappers over events parent apps could
|
|
572
|
+
observe directly.
|
|
573
|
+
|
|
574
|
+
If `initialSize` is omitted, Elwood MUST default the terminal to 189 columns by
|
|
575
|
+
48 rows. Parent apps with a visible terminal should still pass and maintain the
|
|
576
|
+
visual xterm size explicitly.
|
|
577
|
+
|
|
578
|
+
Claude sessions requested below 100 columns MUST bootstrap the PTY and headless
|
|
579
|
+
terminal at 100 columns because Claude Code can silently discard input when it
|
|
580
|
+
initializes below that width. Elwood holds only the PHYSICAL pty/terminal resize
|
|
581
|
+
during this bootstrap; a caller `resize` in this window is recorded in memory as
|
|
582
|
+
the latest requested size (terminal size is never persisted, §8.2). Elwood
|
|
583
|
+
restores that latest requested size at the one-shot initial-ready transition,
|
|
584
|
+
before the queued persona or caller message is submitted. Later resizes retain
|
|
585
|
+
their ordinary exact behavior. Codex sessions do not apply this bootstrap.
|
|
586
|
+
|
|
587
|
+
If restoring the latest requested size at the initial-ready transition fails with
|
|
588
|
+
a real (non-closed) native PTY resize error, Elwood MUST NOT report that resize as
|
|
589
|
+
applied: the session stays at its safe bootstrap width, the failure is surfaced as
|
|
590
|
+
a typed `resize_restore_failed` warning, and the queued persona or caller message
|
|
591
|
+
is still released so a failed restore never starves input. A closed-fd resize at
|
|
592
|
+
this transition is the ordinary benign race and is a no-op, not a warning.
|
|
593
|
+
|
|
594
|
+
`terminal` exposes the session's headless xterm.js terminal handle. The handle
|
|
595
|
+
MUST expose the underlying headless xterm instance, current size, the latest
|
|
596
|
+
OSC window title, and a snapshot API with visible lines, joined visible text,
|
|
597
|
+
and cursor position. Parent apps may
|
|
598
|
+
use this handle for diagnostics or to bridge Elwood's terminal state into their
|
|
599
|
+
own renderer, while `terminal:data` remains the live raw stream for browser
|
|
600
|
+
renderers. The handle also exposes terminal-control methods used internally
|
|
601
|
+
(`sendInput`, `writeOutput`, `resize`, `settled`, and `dispose`); callers should
|
|
602
|
+
prefer the session-level `sendPrompt`, `sendMessage`, `sendKeys`, `resize`,
|
|
603
|
+
`stop`, `kill`, and `teardown` APIs unless they are building advanced terminal
|
|
604
|
+
integrations. Consumers that need diagnostics may await `terminal.settled()` and
|
|
605
|
+
read `terminal.snapshot()`, but visual terminal renderers SHOULD render the raw
|
|
606
|
+
`terminal:data` byte stream instead of repainting from snapshots.
|
|
607
|
+
`sendInput` resolves after input has been forwarded to the PTY and rejects when
|
|
608
|
+
that forwarding fails.
|
|
609
|
+
Because parent apps often attach listeners after `startClaude` or `startCodex`
|
|
610
|
+
resolves, sessions MUST maintain a bounded live-only replay buffer of raw PTY
|
|
611
|
+
output emitted during the current process lifetime (128 KB by default). New
|
|
612
|
+
`terminal:data` subscribers receive that buffer immediately, then receive future
|
|
613
|
+
live chunks. When the bound trims the oldest retained output, it trims on a UTF-8
|
|
614
|
+
code-point boundary so the replayed data never begins with a split multibyte
|
|
615
|
+
sequence (U+FFFD).
|
|
616
|
+
This replay buffer MUST NOT be written to Elwood session state. Parent apps that
|
|
617
|
+
embed their own visual xterm.js instance should feed it the replayed and live
|
|
618
|
+
`terminal:data` chunks; Elwood does not require sharing the same JavaScript
|
|
619
|
+
terminal object between Node and the browser.
|
|
620
|
+
|
|
621
|
+
`stop` attempts graceful process termination while preserving Elwood metadata and
|
|
622
|
+
loop definitions for resume. It should wait for process exit for a bounded grace period, then escalate
|
|
623
|
+
to force termination so callers do not get a terminal status while the process is
|
|
624
|
+
still indefinitely alive. `kill` force-terminates the process and permanently
|
|
625
|
+
clears its loop definitions. `teardown` removes
|
|
626
|
+
Elwood-owned state for the session and must not remove Claude-owned transcripts,
|
|
627
|
+
auth, or project/user settings that Elwood did not create.
|
|
628
|
+
|
|
629
|
+
`stop`, `kill`, and `teardown` MUST reap the full PTY process tree, not only the
|
|
630
|
+
PTY leader. The adapter CLI spawns helper subprocesses that are descendants of
|
|
631
|
+
the PTY leader — notably the per-session hook bridge (`node hook-bridge.mjs`) the
|
|
632
|
+
CLI runs on each hook — and signaling only the leader can orphan them, leaking a
|
|
633
|
+
process tree that accumulates across launch/quit cycles. Elwood MUST reap these
|
|
634
|
+
by SIGKILLing the leader's process group: node-pty starts the leader as its own
|
|
635
|
+
session leader, so every descendant inherits its process group, and a group
|
|
636
|
+
SIGKILL reaches them even after a descendant has been reparented to PID 1 by the
|
|
637
|
+
leader's exit — which a parent-pid walk cannot. The reap MUST run for `stop()`,
|
|
638
|
+
`kill()`, and `teardown()`, including when the leader has already exited on its
|
|
639
|
+
own (an already-terminal session still reaps the group). A dead or empty group
|
|
640
|
+
is a no-op. A `teardown()` that rejects in-flight control-queue operations MUST
|
|
641
|
+
still complete: rejecting a pending `sendMessage`/`compact`/model operation does
|
|
642
|
+
not skip the group reap or the rest of teardown.
|
|
643
|
+
|
|
644
|
+
### Automated input and pending decisions
|
|
645
|
+
|
|
646
|
+
All queued text, including slash-command text and its delayed Enter, MUST wait
|
|
647
|
+
while a human-decision dialog is visible. Permission menus remain blocking
|
|
648
|
+
regardless of the highlighted option or wrapped question. Model commands may
|
|
649
|
+
bypass startup readiness but never this dialog guard. Their timeout includes a
|
|
650
|
+
held submission and cancels it, so no expired command or retry executes later.
|
|
651
|
+
Input blocking follows live rendered facts even before startup usability or
|
|
652
|
+
semantic readiness. Timeout cleanup MUST NOT write control keys into a blocking
|
|
653
|
+
dialog; text already staged may remain for the caller to inspect. Picker navigation
|
|
654
|
+
and apply keystrokes MUST revalidate their expected current screen immediately
|
|
655
|
+
before writing; only the existing first-party cache-confirmation policy may own
|
|
656
|
+
its specific blocking screen. Compaction recovery Enter must also respect the dialog guard. Explicit caller
|
|
657
|
+
`sendKeys` and narrowly scoped login/picker confirmation automation retain their
|
|
658
|
+
existing contracts. C-API-23/24/37 and C-ATTN-01.
|