Skip to content

JSON-lines headless mode (no-tty): stream correlated statements to stdout#2

Open
tejusarora wants to merge 2 commits into
yeet-src:masterfrom
tejusarora:json-lines
Open

JSON-lines headless mode (no-tty): stream correlated statements to stdout#2
tejusarora wants to merge 2 commits into
yeet-src:masterfrom
tejusarora:json-lines

Conversation

@tejusarora

Copy link
Copy Markdown

Implements #1, following your guidance exactly: a plain-JS entry that branches on typeof tty === "undefined", the dashboard moved behind an exported Root, and yeet run -T . (or piping) now streams one JSON object per completed, correlated execution on stdout — the same records the TUI renders:

{"process":"python3","pid":34412,"sql":"INSERT INTO users (username) VALUES (?)","params":[{"idx":1,"type":"text","value":"alice"}],"rows":0,"steps":1,"rc":101,"total_ns":3100000,"max_ns":3100000}

Design notes:

  • The correlation logic is extracted from the statements producer into attachStatements({onStatement, onStatus, onActivity}) and shared by both sinks — I avoided observing the signal headlessly since a from() producer only runs when watched (per AGENTS.md). The signal's behavior is byte-for-byte the same (windowed publish, row shape, CAP, rate stats).
  • tty.on registrations in the (renamed) app.jsx are guarded so the module loads on the headless path; main.js mounts explicitly, so the bundle no longer relies on component auto-mount and moves to src/index.js (needed anyway for -T).
  • stdout is pure JSON lines; status/attach errors go to stderr.

Honest caveat: I develop on macOS, so I verified the bundle builds with your exact esbuild flags but could not run yeetd/eBPF end-to-end. Two things worth a smoke test on your side: (1) that the entry ladder picks up src/index.js the way it did index.jsx, and (2) the headless path against live traffic (demo/run.sh). Happy to iterate on anything.

Context for the use case: piping this into sqlsure gives live semantic checking (fan-out, join keys, sensitive columns) of every SQLite statement on a box with zero app cooperation — I'll publish that pairing once this lands.

🤖 Generated with Claude Code

…llocated

- probes/sqlite.js: extract attachStatements() — the existing correlation
  (prepare/bind/step -> one frozen record per execution) behind callbacks,
  shared by the TUI signal and the new headless sink. The statements
  signal's behavior is unchanged (same windowed publish, same row shape).
- src/main.js: new entry — branches on typeof tty === 'undefined' per
  maintainer guidance; tty -> mount(Root), headless -> JSON lines.
- src/app.jsx (was main.jsx): exports Root, no self-mount; tty.on
  registrations guarded so the module is loadable on the headless path.
- src/json.js: one JSON object per completed execution on stdout
  ({process,pid,tid,sql,params,rows,steps,rc,total_ns,max_ns,error?});
  status/errors to stderr so stdout stays pure.
- Makefile: entry src/main.js, bundle to src/index.js (plain .js so
  yeet run -T can force headless).
- README: JSON output section with jq example.
@karans4

karans4 commented Jul 10, 2026

Copy link
Copy Markdown

Thanks for this, and for flagging what you couldn't run. I tested it end to end on a Linux box against latest yeet master: builds clean, the entry ladder picks up src/index.js fine (it's actually the first name tried, before index.jsx), -T on the project dir works, piped auto-detect works, TUI still renders, and the JSON stream held up against live traffic. Both of your smoke-test worries are clear.

One real bug though, in the attachStatements extraction. The old BIND handler was

let e = cur.get(stmtId);
if (!e || e.steps > 0) e = startExec(stmtId, comm, pid, tid);
e.comm = comm; e.pid = pid; e.tid = tid;

and the PR collapsed it to cur.get(stmtId) ?? startExec(...). The dropped e.steps > 0 branch is what detects reset+rebind of a cached statement: stepped but never hit DONE, then bound again means a new execution. With the PR, the two executions merge into one record, the first bind set is silently overwritten, and steps/rows/latency accumulate across both. Python's sqlite3 module hits this on every re-execute of a cached cursor statement. I confirmed it by feeding an identical synthetic event stream through both versions: master emits two records (?1='A' 1 row, then ?1='B'), your branch emits one merged record with only ?1='B'. Restoring that branch (and the comm/pid/tid refresh) inside attachStatements fixes it; the rest of the extraction looks faithful.

Other than that, looks good to me.

The extraction dropped the 'e.steps > 0' branch that detects a bound-
again cached statement (stepped but never DONE -> new execution), plus
the comm/pid/tid refresh. Two executions merged into one record with
the first bind set overwritten. Restored verbatim from master.
@tejusarora

Copy link
Copy Markdown
Author

Great catch — and thank you for the synthetic-stream verification, that's exactly the failure mode. Confirmed against master: the BIND handler's e.steps > 0 reset+rebind branch (and the comm/pid/tid refresh) got dropped in my extraction. Restored verbatim in 64641c3, with the original comment kept so the next extractor doesn't make my mistake.

Also appreciate you running the entry-ladder and -T checks I couldn't — good to know index.js is actually first in the ladder.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants