Skip to content

0.117.0: Long participant waits, OpenAI wait length, serve.build fails fast, TUI confirmations

Choose a tag to compare

@danielgwilson danielgwilson released this 09 Oct 03:50
· 208 commits to main since this release
3f3204e

humanish 0.117.0 lets a participant wait through a long hosted study, gives an OpenAI
participant's wait a usable length, and fails a broken serve.build within seconds. A
participant's wait longer than one desktop call now lasts as long as it asked, up to the new study
field actor.maxWaitMs (1 second to 10 minutes; 2 minutes by default, 30 seconds on a desktop with
speech), and goes to the desktop in steps of at most 30 seconds. A Codex participant's longer wait
was cut to 30 seconds before. An openai-computer-use participant's wait names no duration and
lasted half a second on a hosted desktop; it now lasts actor.idleWaitMs (5 seconds by default) in
a turn that only waits, and 500 ms after an action in the same turn. The participant's actor.json
records these lengths as waitSettings. A serve.install, serve.build or seed step that bash
cannot parse fails within seconds with bash's error, where the run waited out the step's limit (10
minutes for serve.build), and a failed serve.build on the computer-use route has its own error
code. In humanish tui, a two-Enter confirmation ends when the cursor leaves it. The terminal
route's check after its sandbox kill carries the run's E2B key, and browser Back in the Observer
clears a stale saved-moment message.

This is a minor release because it adds two study fields and behaviors change for the same input.
actor.maxWaitMs and actor.idleWaitMs are new on computer-use and shared-world studies; 0.116.0
refuses a study that sets either. A Codex participant's wait over 30 seconds, which 0.116.0 cut to
30 seconds, now lasts as long as it asked. An OpenAI participant's wait in a turn that only waits
lasts 5 seconds where it lasted half a second, so the same study can take fewer turns and
screenshots. A serve.build, serve.install or seed step that bash cannot parse fails within
seconds where the run waited out the step's limit, and a failed serve.build on the computer-use
route now ends with HUMANISH_COMPUTER_USE_SUBJECT_BUILD_FAILED. Every TUI confirmation disarms
when the cursor or screen leaves it. The library's exported ComputerUseTurn type loses
shortenedWaits, which a consumer that reads it no longer typechecks against; ActorTrace gains
an optional waitSettings, and runComputerUseLoop takes maxWaitMs and idleWaitMs. The
package exports the same 13 values and 31 types as 0.116.0, and removes or renames no command,
flag, study field or bundle field. These outputs change for the same input, which a script could
notice:

# 0.116.0 0.117.0 What to do
1 A study that sets actor.maxWaitMs or actor.idleWaitMs was refused: "Unknown study field in actor: maxWaitMs. Known fields: ...", HUMANISH_STUDY_INVALID, exit 2. A computer-use or shared-world study plans with them. A value outside 1000 to 600000 is refused: "actor.maxWaitMs, the longest one wait action lasts, must be a whole number of milliseconds from 1000 to 600000." An actor.idleWaitMs above an explicit actor.maxWaitMs is refused naming both values. A terminal, scripted or preview study that sets either is refused: "route: terminal does not read actor.maxWaitMs (...). Remove it." study show --json reports both under config.actor. (#1724, #1736) A study that sets either field needs 0.117.0 or later to run, and review on 0.116.0 cannot read its study file (it says "No analysis has run for this run." for a study that turned analysis off).
2 A Codex participant's wait over 30 s was cut to 30 s; the trace recorded a wait shortened notice and the participant was told on its next turn. In the smoke, a participant asked for one 70 s wait got wait 30000ms and stopped as blocked. The wait lasts as long as asked, up to actor.maxWaitMs (120 s unset; 30 s on a desktop with speech), sent to the desktop in steps of at most 30 s. Its title gives its length (wait 70000ms); in the smoke it lasted 70.2 s with no notice, and the participant read the code and finished. A wait over actor.maxWaitMs is still shortened with the notice. The Codex humanish_ui tool description states the study's longest wait, and its input schema no longer publishes a 30-second maximum. (#1724) Set actor.maxWaitMs to bound how long one wait may hold a session.
3 An openai-computer-use participant's wait (it names no duration) lasted 500 ms on a hosted desktop and 250 ms on a guest desktop; its title read wait. In the smoke's lobby study, the participant sent 80 waits over 6 turns. It lasts actor.idleWaitMs (5000 unset) when its turn only waits or takes screenshots, and 500 ms when it follows another action in the same turn. Its title gives its length (wait 5000ms), and actor.json records waitSettings: {maxWaitMs, idleWaitMs, settleWaitMs}. The same lobby study took 10 waits over 3 turns and half the priced cost. (#1736) Compare turn counts, screenshots and session time across this version with that in mind. Read waitSettings for the lengths a run used.
4 A serve.install, serve.build or seed step that bash could not parse waited out the step's limit, then failed with no bash message. In the smoke a broken serve.build ended after 10 min 8 s: subject build timed out: (no output), HUMANISH_COMPUTER_USE_FAILED. It fails at the next poll with bash's message: subject build failed (exit 2): [REDACTED_LOCAL_PATH] line 8: syntax error: unexpected end of file, 13 s in the smoke. Any failed or timed-out serve.build on the computer-use route ends with HUMANISH_COMPUTER_USE_SUBJECT_BUILD_FAILED on the run and the participant. (#1734) Match a failed build by the new code.
5 The exported ComputerUseTurn had an optional shortenedWaits, and a ComputerUseExecutor passed to runComputerUseLoop could receive a wait with no ms. ComputerUseTurn.shortenedWaits is removed; the 0.116.0 consumer check's read of it fails to typecheck (TS2339: Property 'shortenedWaits' does not exist on type 'CuaTurn'). runComputerUseLoop takes maxWaitMs and idleWaitMs, throws a RangeError for a value outside 1000 to 600000 (or an idleWaitMs above an explicit maxWaitMs), and hands the executor every wait with ms set. ActorTrace gains an optional waitSettings. (#1724, #1736) Drop shortenedWaits from a provider's turns; the loop shortens a wait itself and records the notice.
6 In humanish tui, Stop, Cancel analysis, Run again (after g or G) and Set up humanish here (after ?) stayed armed when the cursor or screen left them, so a later Enter confirmed: arming Stop, moving to Open in Observer and back, then Enter stopped the run. Any key other than Enter or the right arrow disarms, as do a change of the action under the cursor or of the screen, and 30 s. An Enter less than 400 ms after the one before it is ignored and restarts that wait. Esc cancels an arming without going back. (#1727) Press Enter twice on the action itself.
7 In the Observer, browser Back and Forward left the Saved moments message ("Moment saved." or why a moment could not open) in place. They clear it, as opening a participant or the grid already did. (#1728) Nothing.
8 A terminal run's check by id after its sandbox kill used E2B_API_KEY from process.env. A library caller with the key only in RunStudyOptions.env got the SDK's "API key is required" error and an errored re-verification in terminal-ledgers.json. The check uses the key the create used. (#1726) Nothing.

Added

  • actor.maxWaitMs sets how long one wait action of a computer-use or shared-world participant
    may last, from 1000 to 600000. When unset it is 120000 (two minutes), or 30000 on a desktop with
    speech, where heard speech reaches the participant only with a screenshot. A longer wait is
    shortened to it, the trace records a wait shortened notice with the requested and applied
    durations, and the participant is told on its next turn. A terminal, scripted or preview study
    that sets it is refused, as for any field its route does not read (row 1). (#1724, fixes #920)
  • actor.idleWaitMs sets how long a computer-use or shared-world participant's wait that names
    no duration lasts when its turn only waits or takes screenshots, from 1000 to 600000 and no
    longer than actor.maxWaitMs. When unset it is 5000. A study that sets it longer than its
    actor.maxWaitMs is refused (rows 1 and 3). (#1736, fixes #1731)
  • A computer-use participant's actor.json records the wait lengths its session used as
    waitSettings (maxWaitMs, idleWaitMs, settleWaitMs), and each wait action's title gives
    the length it lasted (wait 500ms). A dry run writes no participant trace, so it records none
    (row 3). (#1736)

Changed

  • A participant's wait longer than 30 seconds now lasts as long as it asked, up to
    actor.maxWaitMs. humanish sends it to the desktop as consecutive waits of at most 30 seconds,
    the longest one browser-control request carries; when one of them stalls, the rest of the wait
    is skipped and the session goes on. A Codex participant's wait was shortened to 30 seconds
    before, and still is on a desktop with speech unless the study sets actor.maxWaitMs. Its
    humanish_ui tool description now states the study's longest wait, and the tool's input schema
    no longer publishes a 30-second maximum. ComputerUseTurn.shortenedWaits is removed: the loop
    shortens a wait itself, for every provider (rows 2 and 5). (#1724, fixes #920)
  • An openai-computer-use participant that only waits now waits 5 seconds for each wait it
    sends. OpenAI's computer-use wait action names no duration, and it lasted 500 ms on a hosted
    E2B desktop and 250 ms on a guest desktop, so a participant waiting in a video lobby or for an
    email code spent a model turn and a screenshot every half second. A wait that names no duration
    now lasts actor.idleWaitMs (5 seconds by default) when the participant's turn only waits or
    takes screenshots, and 500 ms on both desktops when it follows an action in the same turn, as
    nearly every such wait does. A ComputerUseExecutor passed to runComputerUseLoop now receives
    every wait with its ms set (rows 3 and 5). (#1736, fixes #1731)
  • In the Observer, browser Back and Forward now clear the message under Saved moments ("Moment
    saved." or why a moment could not open), as opening a participant or the participants grid
    already did (row 7). (#1728)
  • observer/app.tsx is split into navigation, player and saved-moment hooks and goes from 1,029
    to 508 lines; nine Observer navigation tests passed on the old file before the split. (#1728,
    from #1677)

Fixes

  • A serve.install, serve.build or subject.state seed step that bash cannot parse now fails
    within seconds and shows bash's error. Each command was pasted into the script that records the
    step's exit status, so a syntax error stopped that script before it recorded one, and the run
    waited out the step's time limit: 10 minutes for serve.build. A serve.build ending in a
    heredoc hit this every time: the study parser trims each command, which put the heredoc's
    closing word on the same line as the script's own text. Each command now runs as its own
    script, and a heredoc may close on its last line. A failed serve.build on the computer-use
    route gives the run the error code HUMANISH_COMPUTER_USE_SUBJECT_BUILD_FAILED, and its message
    carries the build's redacted log tail. A serve.start that bash cannot parse still waits out
    readyTimeoutMs; the server log tail in its error now shows bash's message (row 4). (#1734,
    fixes #1732)
  • A terminal run's check by id after its sandbox's kill (Sandbox.getInfo) now carries the API
    key the create used. With the key only in RunStudyOptions.env, the check failed with the SDK's
    "API key is required" error and terminal-ledgers.json recorded the re-verification as errored.
    With another key in process.env, the check asked that key's account, and a not-found answer
    from it would be recorded as confirming the sandbox was gone (row 8). (#1726, follows #1717)
  • In humanish tui, an armed action no longer stays armed after the cursor leaves it. Arming Stop,
    moving to Open in Observer and back, then pressing Enter stopped the run at once. Cancel
    analysis did the same, as did Run again on a live run after g or G, and Set up humanish here
    after ?. The five actions that take two Enters (those four and a live start) now follow one
    rule: the second Enter counts only on the same action of the same screen, within 30 seconds of
    the first. An Enter less than 400 ms after the one before it is ignored and restarts that wait.
    The wait used to count from the first Enter, so a held Enter confirmed on every repeat past it;
    it now confirms at most once, on its first repeat when the system's repeat delay is 400 ms or
    longer (#1730). Any other key cancels the first Enter, and Esc cancels it without going back
    (row 6). (#1727, fixes #1722)

Known gaps

  • A held Enter in humanish tui can still confirm an armed action on its first key repeat. The
    400 ms floor ignores repeats that come sooner, and a system's initial repeat delay is often
    longer (500 ms by default in GNOME). (#1730)
  • A computer-use study stops at 16 participants, and analysis refuses more than 16, so a study of a
    whole day at a shared app has to be split across studies. (#1737)
  • A serve.start that bash cannot parse still waits out readyTimeoutMs. The scripted and
    shared-world routes give a failed build their generic failure code; the message carries bash's
    error. (#1734)
  • On a desktop with speech, heard speech reaches the participant only with a screenshot and the
    guest holds 8 utterances between two screenshots, so a long wait in a talking call can overflow
    it; a wait there stays 30 seconds unless the study raises actor.maxWaitMs. (#1724)
  • Two contained-file races from #1669 remain. They need a second local process, running as the
    same user, to swap a directory in the run between two filesystem calls: temporary-file cleanup
    still unlinks by path (item 1), and the reviewer-notes listing still lists notes/ by path and
    can follow a swapped directory (item 3). Storage validation and the scan still list each folder
    whole before the 10,000-entry cap applies, verify has no total read budget, and a legitimate
    sandbox-receipts.ndjson over 32 MiB is refused. (#1669)
  • Coding agents still do not tell their person about humanish tui. (#1701)
  • Path redaction in terminal transcripts still removes the text after a path up to the next space,
    such as a line break and the next header after cwd:. Nothing leaks; text a reviewer needs is
    lost. (#1702)
  • A hosted computer-use participant cannot type keysyms outside US printable ASCII (é, €) as
    keys; it has to type them as text. (#1713)
  • study check passes a study whose sandbox deadline passes the ceiling; humanish run refuses it
    at plan time. A shared-world study with an explicit execution.timeoutMs whose participant
    sandbox passes the ceiling has no plan-time refusal. #1733 fixes both and is held until this
    release merges. (#1707)
  • The routes still take E2B_DOMAIN from process.env on create and ignore one in
    RunStudyOptions.env. (#1717)
  • A desktop-cli dry run still says Dry run: 1 participant would use ., a local-app subject on
    the in-process route still records app-url, and verify's message for a scan stopped at 10,000
    entries counts the stopped folder as "1 file". #1733 fixes these. (#1716, #1691)
  • The TUI's live median leaves out analysis spend, where a finished run's card shows the total with
    analyses; the analysis line's expected range can pass its own admission limit; after a key is
    entered on the keys screen, Esc returns to the studies list; and while any action is armed, the
    hint line still reads "Enter select esc back". #1735 fixes these. (#1720)
  • From #1646, after #1684: a 4-byte value inside a longer base64 run is not found, two further
    decodings at a marker edge are not read, RunSecrets.spans (the terminal route's per-chunk
    replacement) has no final check, and a value that is part of a marker nests markers. (#1646)
  • Design findings cannot say that text or a click target is too small by measure. (#1638)
  • The impressions ask describes its kinds in visual terms, which can steer a participant toward
    visual critique whatever its persona. (#1654)
  • Nothing Mac-specific has run on a Mac: the Lima VM sizing and runtime setup --memory --cpus.

Package

humanish 0.117.0 packs to 2.5 MB, 7.8 MB unpacked, 964 files (npm pack on release/0.117.0 at
af0b19f, shasum fb4fbfcf791540e17b373baea82dbefdf9167db8). Against 0.116.0 it adds
dist/actors/computer-use/wait.js and wait.d.ts and drops nothing. The package exports only .,
with the same 13 values and 31 types as 0.116.0 (pnpm api:proof); the exported
ComputerUseTurn loses shortenedWaits (row 5). Its dependencies are the same as 0.116.0's. It
needs Node 22.19 or newer, as before, and @e2b/desktop 2.3.2 or newer as an optional peer.

Checked before release

A live smoke on a tarball packed from main 882d47f in fresh projects under /tmp with a fresh HOME
and npm cache, DO_NOT_TRACK=1, and @e2b/desktop 2.4.0 beside humanish. Keys came from the user
key store through key discovery, with XDG_CONFIG_HOME pointing at it; no --dotenv was passed.
The Codex participants used the signed-in Codex CLI 0.162.0 through CODEX_HOME. The store's
OpenAI organization is now non-ZDR, so every OpenAI participant ran threaded. Controls ran the
same study files on npm 0.116.0, or the same CLI commands on copies of the main runs. Priced
spend: $1.28 for the smoke's live runs ($1.02 on main, $0.64 of it in try-live's analysis, and $0.26
on npm 0.116.0), then $0.21 for the dogfood run and $6.04 for the benchmark.

  • init --yes writes the same files as npm 0.116.0, and doctor, doctor --study try-live and
    keys print the same.
  • try-live, as init writes it: reached the goal in 9 participant turns, input growing every turn
    from 2,888 to 16,439 tokens, then 16,787 for the impressions request; cached input on each turn
    from the second was the previous turn's input minus 3. $0.23 run. Its automatic analysis was
    admitted at an expected $1.00 (admittedCostUsd $1.10, worst case $1.99, cap $3) and billed
    $0.64, with 2 findings and 3 design findings. It gave 5 impressions (unclear, unfinished,
    unclear, liked, liked).
  • A lobby page that shows a room code 60 s after it loads, with a mission to wait for it, on
    openai-computer-use (row 3): npm 0.116.0's participant sent 80 waits of 500 ms (10, 10, 30 and
    30 per turn) over 6 turns and 6 screenshots in 69.7 s, $0.16; main's sent 10 waits of 5 s in one
    turn, 3 turns and 3 screenshots in 66.1 s, $0.08. Both read the code. Main's actor.json records
    waitSettings {maxWaitMs: 120000, idleWaitMs: 5000, settleWaitMs: 500}.
  • A Codex participant asked for one 70 s wait on the same page (row 2): with actor.maxWaitMs: 90000 on main, one wait 70000ms lasted 70.2 s with no wait shortened notice and the
    participant read the code; the installed planWait gives its steps as 30000, 30000 and 10000.
    npm 0.116.0, without the field it cannot read, cut the wait to wait 30000ms with a notice, and
    the participant stopped as blocked. A second main run with actor.maxWaitMs: 180000 waited 150
    s in one action.
  • A local-tree study whose serve.build ends in an if with no fi (row 4): main failed in 13 s
    with bash's line 8: syntax error: unexpected end of file and
    HUMANISH_COMPUTER_USE_SUBJECT_BUILD_FAILED; npm 0.116.0 failed after 10 min 8 s with subject build timed out: (no output) and HUMANISH_COMPUTER_USE_FAILED. The same study with that line
    removed, so the build ends on its heredoc's EOF, built and passed on main.
  • verify, review and runs (text and --json) on copies of the nine live runs: byte-identical
    on main and npm 0.116.0, including the main runs whose actor.json carries waitSettings,
    except review of the two main Codex runs, whose study files set actor.maxWaitMs: npm 0.116.0
    cannot parse them and says "No analysis has run for this run." where main names
    review.analysis: false (row 1).
  • The TUI under a pseudo-terminal sized on that pty only, replayed through @xterm/headless: on a
    running live run, Enter armed "stop this run?", j to Open in Observer and k back cleared it,
    and the next Enter armed it again; 5 s later the run was still working, and Esc cleared the
    prompt. In an empty directory, "Set up humanish here" armed, ? and ? returned to it unarmed on
    main and still armed on npm 0.116.0 (row 6).
  • After every run, reclaim --check exited 0 with clean. Raw sandbox ids appear only in
    sandbox-receipts.ndjson, E2B listed 0 tagged sandboxes after the smoke and after the benchmark,
    and no key value from the store appears in any run or kit file, as written, base64 or hex.

On the release branch:

  • pnpm release:check on the release head 54ff159, which adds the benchmark result: exit 0 in 4.3 min with the same counts as below, a public-surface scan of 3,035 text files and the same shasum. On
    the release commit af0b19f before it: exit 0 in 4.0 min, 504 test files passed and 10 skipped,
    7,932 tests passed and 12 skipped; TUI 184 tests; lint 426 warnings at its cap of 426; api:proof
    "13 values and 31 types match"; public-surface scan of 3,033 text files and 527 binary assets;
    npm pack --dry-run 964 files with shasum fb4fbfcf791540e17b373baea82dbefdf9167db8. pnpm docs:generate there wrote 0.117.0 into cli.mdx.
  • pnpm release:dogfood on af0b19f (the script run with Node, keys read from the user store by
    Node's --env-file into its environment, and a temporary HOME): "release:dogfood ok", verdict
    blocked (blocked_approval), no-spend satisfied, $0.21. The participant ran init, the free
    four-participant dry run, verify ("Passed all 16 evidence checks; status was share_ready") and
    review on the installed 0.117.0, and stopped at the keys and the local VM devices the gate
    withholds; the script reads that verdict as the expected stop. verify on a copy of the run:
    share_ready, 16 of 16; reclaim --check clean.
  • The post-publish check on the release tarball from af0b19f (TMPDIR=/tmp, HUMANISH_SPEC the
    tarball): 111 of 111 passed. Its 5 new rows cover actor.maxWaitMs planned at 90000 and refused at
    999 and 600001 with its range (#1724); a terminal study that sets either wait field refused as a
    field its route does not read (#1724, #1736); actor.idleWaitMs planned at 15000 and refused above
    actor.maxWaitMs (#1736); both fields in study show --json, with no waitSettings from a dry
    run, which writes no participant trace (#1724, #1736); and the TUI's Set up humanish here disarmed
    by ? (#1727). The same script on npm 0.116.0 passed 106 and failed exactly those 5 rows. A first
    pass on the release tarball passed 110 of 111; the failure was in the new row's own assumption
    that a dry run records waitSettings, and both versions ran again with the row rewritten.
  • The consumer check: the 0.116.0 check's library and CLI consumer, plus 9 CLI lines for this
    release. On the release tarball the consumer fails to typecheck with 1 error, TS2339: Property 'shortenedWaits' does not exist on type 'CuaTurn' (row 5); after the one migration row that drops
    that read, it typechecks with 0 errors, as it does on npm 0.116.0. The library steps, at baseline
    and migrated, and every CLI line from the 0.116.0 check are the same on npm 0.116.0 and the
    release tarball. The new lines differ as row 1 says: npm 0.116.0 refuses every study that sets a
    wait field as an unknown field. A clone study whose serve.build bash cannot parse passes study check and its dry run on both versions, since a dry path runs no build.
  • pnpm bench on af0b19f with its defaults (3 runs per arm, openai-computer-use, neutral
    mission), committed in 54ff159: report recall 8/15, analysis recall 7/15, 0 invented on either
    arm, $6.04 estimated. 0.116.0 scored 9/15 and 8/15, and 10/15 and 9/15 read by hand. One line the
    rubric left unresolved, the second planted analysis's "Repeated clearing attempts left completed
    tasks visible", is D1, which puts analysis recall at 8/15 read by hand; report recall stays 8/15.
    Against 0.116.0's table the drop is D5: the reports and the analyses each found it once, where
    0.116.0's found it twice by the rubric and three times read by hand. The analyses found D1 once
    read by hand, where 0.116.0's found it in none, and no report gave a false assurance (0.116.0:
    one). Reports and analyses found D3 and D4 3/3 on both versions. Each analysis's cap was the dry
    run's admittedCostUsd ($1.14 to $1.23 against bills of $0.66 to $0.76), and none was refused or
    skipped. The third clean participant gave up after a reload emptied the list, so its run reads as
    failed; it was scored as the others were. The wait changes plausibly moved nothing: every wait the
    participants sent followed another action in the same turn and lasted 500 ms on both versions
    (titles wait 500ms), 10 waits in all against 14 on 0.116.0, and no participant had a turn that
    only waited. No planted participant typed more than 30 characters in one action, so none met D2.
    Every participant ran threaded, ended on its own and gave 5 or 6 impressions; E2B listed 0
    tagged sandboxes after it.

Not verified

  • macOS and Windows. Every check ran on Linux x64 with Node 24.12.
  • A live run on the published package: the live smoke ran on a main tarball, and the post-publish
    rows are offline dry runs, refusals and local runs.
  • The 30-second steps of a long wait as separate desktop calls: the bundle records one wait action
    and its duration, so the 30000, 30000 and 10000 split comes from planWait and the 70.2 s total.
    A step that stalls, and the skipped rest of its wait, were seen only in tests.
  • A guest desktop (execution.target: local), where an OpenAI wait with no duration lasted 250 ms,
    a desktop with speech, the shared-world route and a Codex wait with no duration: not run live.
  • A wait over actor.maxWaitMs on a live run, so the wait shortened notice on 0.117.0 was seen
    only in tests.
  • The terminal route's check with the run's key for a library caller whose key is only in
    RunStudyOptions.env (#1726): the CLI puts the key in process.env, where the old check found it
    too. tests/substrates/e2b/connection.test.ts covers it with a fake SDK.
  • A failed serve.build that exits nonzero without a syntax error, a seed step bash cannot parse,
    and a serve.start bash cannot parse: tests on local bash cover them, and no live run tried them.
  • The Observer's Back and Forward clearing a saved-moment message (#1728): the Observer tests and
    its browser proofs in release:check cover it; nobody tried it by hand in a browser.
  • In the TUI: Cancel analysis, Run again after g or G, the 30 s expiry and the 400 ms floor
    were not driven at a pseudo-terminal; the TUI tests cover them.