Expand description
@arch:layer(kg_store) @arch:role(substrate) @arch:see(.yah/docs/working/yah-task-runs.md)
PTY subprocess driver — spawn commands, capture output as append-only
chunks, handle SIGTERM/SIGKILL with a grace period, and mark stale
Running runs as Lost when the daemon restarts.
§Tier 2 side-channel (yah-log shims)
When SpawnOpts::log_fd_enabled is true (the default), the driver creates
a named pipe (FIFO) and exports two env vars into the child:
YAH_TASK_RUN— theTaskRunIdas a hyphenated UUID string.YAH_LOG_PIPE— absolute path to the FIFO.
The child opens YAH_LOG_PIPE for writing and emits JSON-lines. The
driver reads those lines in a background thread and stores them as
[EventSource::Shim] events.
Why FIFO instead of a raw fd? portable-pty calls close_random_fds()
in its pre_exec hook, closing every fd ≥ 3 before exec. A raw-pipe write
fd is always ≥ 3 and would be closed before the child could use it. Opening
a FIFO by path requires no fd inheritance.
Wire format — one JSON object per line:
{"level":"info","target":"myapp::module","msg":"text","fields":{"key":"val"}}Optional shim-identity keys: "_lib" (string), "_lib_ver" (string).
Unknown keys in fields pass through as freeform JSON.
The driver holds the write end of the FIFO open until the run lifecycle task completes, which triggers EOF for the receiver thread. The FIFO file is deleted after the receiver thread drains the last line.
On non-Unix platforms YAH_TASK_RUN and YAH_LOG_PIPE are not exported.
Shim libraries must treat absent YAH_TASK_RUN as “not inside a TaskRun”.
@yah:ticket(R617-F6, “Reattach-by-run_id replaces Lost-on-disappear for origin=terminal shells”)
@yah:at(2026-07-20T18:38:27Z)
@yah:status(open)
@yah:phase(P3)
@yah:parent(R617)
@yah:next(“TaskDriver::new (driver.rs:199) marks every leftover Running run Lost on construction — correct for ordinary jobs, fatal for a shell meant to survive a restart. Split the behaviour on origin: a terminal shell whose host process is still alive is re-adopted (control channel rebuilt, reader thread restarted against the surviving PTY) rather than tombstoned.”)
@yah:verify(“Manual: open a shell, run sleep 300, quit and relaunch the desktop — the run is still Running, not Lost”)
@yah:gotcha(“This is an oss/qed crate — changes land in-tree under oss/task-runs and flow outward via scripts/export-oss.sh. Keep the reattach seam generic (origin-agnostic policy hook), not yah-terminal-specific, since the crate ships standalone.”)
@yah:gotcha(“Reattach only makes sense once the PTY outlives the desktop (S5 decides the host). Landing it before that gives a reattach path with nothing to reattach to.”)
@arch:see(.yah/docs/working/W280-durable-terminal-sessions.md)
@yah:depends_on(R617-F13)
@yah:ticket(R617-B9, “Pre-existing: task-runs log_pipe_events_land_in_store never completes (233 pass / 1 fail)”)
@yah:status(review)
@yah:assignee(agent:bundle-anthropic-ashguard)
@yah:at(2026-07-22T19:50:25Z)
@yah:phase(P1)
@yah:parent(R617)
@yah:handoff(“Root cause: not the FIFO, not the PTY. The whole pipeline completed correctly every time (child wrote the JSON line, receiver drained it, reader hit EOF, child.wait returned 0) — but the lifecycle’s terminal store.update_status returned Sql(Busy(\"database is locked\")) and run_lifecycle swallowed it with let _ =, so the run stayed Running forever and the 20s poll deadline blew. A live run has three concurrent turso writers (PTY chunk appends, shim-FIFO event appends, lifecycle status) on independent connections with no busy handling at all.”)
@yah:handoff(“Fix in oss/qed/crates/task-runs/src/store.rs: (1) conn() now sets busy_timeout(5s) on every connection; (2) new exec_retry() wraps writes in an outer exponential-backoff retry on the Busy/BusySnapshot class, because turso caps its internal backoff and then hands Busy back; (3) insert_run / update_status / update_beholder_status / append_chunk / append_event all routed through it.”)
@yah:handoff(“driver.rs run_lifecycle no longer swallows the terminal status write — a genuine failure after retries now prints [yah task-runs] failed to record terminal status for run <id>, matching the crate’s existing eprintln convention.”)
@yah:handoff(“New regression test store.rs::concurrent_writers_do_not_lose_the_terminal_status — two background tasks hammer append_chunk/append_event while update_status lands. Verified it has teeth: with busy_timeout and the retry disabled it fails 3/3 with the exact Busy(\"database is locked\"); with them it passes 5/5.”)
@yah:verify(“cd oss/qed && cargo test -p task-runs –lib — 237 passed / 0 failed (was 235 pass / 1 fail)”)
@yah:verify(“log_pipe_events_land_in_store run 8x sequentially: 8/8 green in ~0.58s each. Before the fix the same loop was 11/12 red at the 20s timeout.”)
Structs§
- Driver
Channels - Optional side-channels a driver can publish to. Both are fire-and-forget: a closed receiver never stalls or fails a run.
- Spawn
Opts - Options for
TaskDriver::spawn_run. - Task
Driver - Manages in-flight task runs for a single camp.