Skip to content

session-id wire + attach-or-start TUI - #348

Open
EdwardIrby wants to merge 8 commits into
mainfrom
agent/working-session
Open

EdwardIrby wants to merge 8 commits into
mainfrom
agent/working-session

Conversation

@EdwardIrby

Copy link
Copy Markdown
Member

Context

  • Day's working branch carrying the session-id wire slice plus the full attach-or-start + TUI slice, built per .prompts/attach-or-start-tui.md (TDD, one commit per slice). plan.md (gitignored by design) is the navigator's decision log for the design chain.

Summary

  • TraceBase gains sessionId beside instanceId (accept-only, host-supplied, defaults to the instanceId; behavioral() return unchanged) — the dormant audit axis, landed with the wire-wide schemas + specs
  • Instance lock (<home>/instance.pid) + unix-socket attach lane (<home>/instance.sock) speaking the existing JSON-RPC vocabulary through the same dispatcher as serve (one protocol, one dispatcher, multiple carriers)
  • The minimal TUI (src/cli/tui.ts, 190 lines): three primitives (emit/prompt/select), two ingress-only tui_* constants with AJV schemas; the root guard pack derives tui_* entries from TUI_DETAIL_SCHEMAS so malformed tui events block visibly
  • Bare behavioral is attach-or-start: attaches to a running instance over the socket (with the notice), or starts the foreground host+TUI in one process; the instance's own TUI rides the socket like every client; SIGINT/SIGTERM cleanup
  • Static GUI serving on the same listener (/.behavioral/connect.js), --dev (start-time only) flips to per-request rebundling; bundleController promoted to src/controller/
  • Docs cleanup: dropped stale ACP/Tauri references from comments

Changed Files

  • src/behavioral/ — behavioral.ts, behavioral.types.ts, behavioral.utils.ts (sessionId wire), tests (session-id.spec.ts, schemas.spec.ts, add-b-thread.spec.ts)
  • src/faculties/ — frontier/faculty.ts (sessionId on synthetic traces + replay schemas), faculties.threads.ts (TUI guard entries), instance-lock.ts, store/faculty.ts (doc line)
  • src/cli/ — tui.ts, socket-host.ts, attach.ts, attach-or-start.ts, serve.ts (dispatcher extraction), cli.ts (lazy default entry), json-rpc.ts, load-config.ts, trace-consumer.ts + suites
  • src/controller/ — bundle-controller.ts (promoted bundler), controller.types.ts (doc lines)
  • bin/behavioral.ts — default command registration
  • skills/behavioral/references/behavioral.md — factory signature doc

Known Failures / Drift

  • Attach notice learns the instanceId from the first received trace — a fresh, idle instance can delay the notice until the first event (watch item: an immediate hello-with-id on connect would fix it cheaply)
  • Two Bun runtime findings shaped the code: readline with terminal: false drops buffered lines between question() calls (explicit queue); Bun.file().exists() returns false for socket files (socket checks use node:fs existsSync)
  • One pre-existing router spec asserting "no args exits 1" was updated deliberately — it pinned the pre-slice behavior the bare-command decision supersedes; it now asserts attach-or-start + clean detach on closed stdin

Review Notes / Residual Risks

  • Fullstack dev server (--dev) is Bun-WIP upstream; degrade path is full page reloads, and the flag is opt-in start-time only
  • sessionId is a dormant axis: landed and defaulted but currently degenerate (always equal to instanceId) — no consumer supplies a real one yet; removing it later requires revisiting the frontier-analysis grouping use
  • 377 specs green, bun --bun tsc --noEmit clean

The engine self-mints `instanceId = ueid('bp_')` and stamps it on every
trace. With an ACP ingress landing later, the host owns session identity
policy (mints on `session/new`, passes a client's old id on `session/load`),
so the engine must ACCEPT a host-supplied session id — it never mints one
and never returns ids. No rename: the two ids are separate axes and both
live on the wire.

- `TraceBase` gains `sessionId: string` alongside `instanceId`; a new
  exported `TraceBaseSchema` is the one home for the trace wire's common
  shape, so per-kind validators derive from it instead of hand-mirroring.
- `behavioral()` gains `{ sessionId?: string }`; the frozen return object
  is unchanged. Session id is computed once at factory time
  (`options?.sessionId ?? instanceId`) and stamped on every trace next to
  `instanceId`; `resumePendingThreadsForSelectedEvent` stamps it on
  interrupt traces too.
- The frontier faculty speaks the same wire with no drift: replay/explore/
  verify inputs accept `sessionId` (AJV `nullable`, defaulting to the
  `instanceId`), and every synthetic trace it builds (frontier, selection,
  deadlock) carries both ids.
- `serve.ts`/`trace-consumer.ts` are pass-through only — no construction
  sites existed in production code; only spec fixtures gained the field.
- `skills/behavioral/references/behavioral.md` factory signature updated.

Validation: `bun --bun tsc --noEmit` plus the behavioral, frontier
faculty, and cli serve/trace-consumer suites (211 specs).
The direction was dropped from the plan; these comment-only references
survived the sweep. No behavior change — doc lines and one doc-comment
re-wrap only.

- behavioral.ts / behavioral.types.ts / skill reference: session identity
  is owned by "the host layer", no protocol names
- controller.types.ts: Transport seam examples now generic ("native IPC")
- store/faculty.ts: swap-backings list drops the Tauri engine example

Validation: bun --bun tsc --noEmit clean; comment-only changes, no
executable surface touched.
Slice 1 of the attach-or-start lifecycle: the running instance owns
<home>/instance.pid. acquireInstanceLock takes the lock when the home is
free, blocks with the live holder pid when one runs, and reaps a stale
pidfile (pid no longer alive) before acquiring. Injectable home + pid;
release removes the pidfile for the SIGINT/SIGTERM terminate path.
Slice 2: the host gains its single listener, <home>/instance.sock — a
Bun.serve with the unix option whose WebSocket carrier speaks the same
JSON-RPC lane as stdio (one text frame = one message). The host-side
dispatcher and egress wiring are extracted from serve.ts (dispatchToRuntime,
wireRuntimeEgress) and reused, one protocol one dispatcher multiple
carriers. Redacted traces and ui_* selections fan out to every client;
socket file removed on close, stale file reaped at start. Transport-shaped
clients exercise the real server over ws+unix in the specs.
Slice 3: a single-file readline TUI (src/cli/tui.ts, no framework) with
exactly three primitives: emit (egress-only, Bun.color-resolved kind
severity colors that never touch the wire or a prompt), prompt (a raw
line resolves to a tui_command ingress event), and select (a numbered
choice resolves to tui_select). The tui_* family is ingress-only with AJV
detail schemas at the top of tui.ts; the root guard pack now derives its
guard entries from TUI_DETAIL_SCHEMAS alongside the controller wire, so
malformed tui_* details block visibly. Lines are consumed through an
explicit queue (terminal:false readline drops buffered lines between
questions).
Slice 4: bare `behavioral` (no subcommand) is the attach-or-start entry.
A live instance (pidfile held) is attached to over <home>/instance.sock
with "attached to running instance <id>"; a free or stale home starts the
foreground instance — engine + socket host + TUI in one process, the
shell supervising. The instance own TUI rides the socket like every other
client (no in-process fast path); SIGINT/SIGTERM terminate the engine and
remove the pidfile and socket (the daemon-door discipline). The router
gains a lazy `default` entry; --help and subcommands still never load the
composition graph. Two-process specs drive the real lifecycle across
Bun.spawn children: attach notice, cross-process trigger, no second
instance, stale-pidfile reap, SIGTERM cleanup. File IO is Bun-native
(Bun.file/Bun.write/Bun.file().delete()); node:fs only where Bun has no
equivalent (mkdir, socket-file existence).
Slice 5: the host single listener now serves the bundled controller GUI
at /.behavioral/connect.js alongside the WebSocket upgrade — the browser
carrier and the TUI carrier are two clients of one Bun.serve
(Carriers/H). bundleController is promoted from the controller fixture to
src/controller/ (one home for the bundle; the fixture re-exports it). In
prod the AOT bundle is built once and cached; --dev (start-time only,
attachers cannot flip a running instance) rebuilds per request for source
editing. Engine and wire are identical in both modes. The router default
forwards argv so the bin entry recognizes the flag.
export const behavioral = (options?: { sessionId?: string }) => {
const instanceId = ueid('bp_')
/** @internal Host session identity — accepted at factory time, never minted. */
const sessionId = options?.sessionId ?? instanceId
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