Repository navigation
feat: M2 runtime (ConPTY, supervisor, hooks, terminal replay) - #2
Merged
Merged
Conversation
Section 19 now lists what the official documentation confirms and which real test (M2/M3) still has to run. Two corrections came out of it: - Absolute paths in permission rules need the `//` prefix and the POSIX form Claude Code uses on Windows (`//c/Users/...`). A single leading slash is relative to the settings file, so the old wording would have left the secrets folder readable. - The inbox message line format is not documented; the courier must not depend on it before the M3 real test.
- Prefixed ULID ids (crw_, bot_, msg_, dlv_, tsk_) that validate their prefix on parse and deserialize - Slugs and handles from display names: transliterated, max 32 chars, Windows device names avoided, numeric suffix for collisions - Avatar palette and #RRGGBB validation (spec 15.3) - Field validation for names, roles and instructions - M1 protocol types (hello, status, crews and bots) exported to app/src/lib/protocol.gen.ts by the export_bindings test; ts-rs is a dev-dependency only, and CI fails if the generated file is stale
- WAL, foreign keys and a 5 s busy timeout on every connection (spec 12) - Numbered migrations tracked in PRAGMA user_version; a database from a newer build is refused instead of being modified - 0001_init: crews, bots and settings. Handles are unique among a crew's active bots (partial index); slugs stay unique forever so an archived bot's folder is never reused. Bots get a color column for the avatar (spec 15.3) - Crew archiving cascades to its bots in one transaction
- config.toml with the spec 6 defaults; unknown keys are rejected - BOTLOFT_HOME / %LOCALAPPDATA%\Botloft and %USERPROFILE%\Botloft resolution, plus the //c/... form Claude Code expects in permission rules - Owner token: 32 random bytes in secrets\owner.token, only its SHA-256 kept in memory, compared in constant time - platform: protected DACL with a single user SID on Windows (tested against the real ACL), 0700/0600 elsewhere; Windows console close/logoff/shutdown signals - Single instance through an exclusive lock file per data folder instead of a fixed-name mutex, so a dev daemon can run next to an installed one; spec 14 updated
Writes the files of spec 5.1 into <root>\<crew>\<bot>: - .claude/settings.json: exec-form hooks calling botloftd.exe, crossSessionInbound=accept and a deny rule for the secrets folder in the //c/... form Claude Code matches on Windows - .botloft/mcp.json: the local MCP endpoint with the bot token expanded from the environment, so it never touches disk - .claude/rules/botloft.md: identity, role, instructions, shared folder and how to use the crew tools; regenerated on every change - CLAUDE.md: created once and then left to the bot Every write goes through a temp file and a rename.
- /rpc: JSON-RPC 2.0 over WebSocket. session.hello with the owner token must come first (10 s); anything else, a wrong token or another protocol version closes the connection. Browser origins other than the app get 403; clients without Origin still need the token - /health for the app's daemon check - crews.* and bots.* (spec 11.2) run under the store lock, write the workspace before the database and broadcast crew.changed and bot.changed to every connection; a client that falls 1024 events behind is dropped and reloads on reconnect - Handles follow the bot's name and must be unique among active bots of the crew; folders keep their slug. Bots get a palette color unless one is given - Integration tests run a real daemon on an ephemeral port with a WebSocket client: the M1 acceptance flow (create a crew and a bot via RPC and get a ready workspace), updates, archiving, auth and errors Spec: color in bots.create/update and the bots table, slug and handle rules, Origin and hello details, standard JSON-RPC codes.
`botloftd serve [--config <file>]` resolves the data folder, loads the config, starts daily-rotated logs (14 kept), takes the instance lock, prepares the owner token and database and listens on 127.0.0.1 until Ctrl+C or a Windows close/logoff/shutdown signal. log_level applies to Botloft's crates only; dependencies stay at warn because the WebSocket stack logs frames at debug and trace.
- runtime: ConPTY via portable-pty with one Job Object per bot
(kill-on-close), FakeRuntime for tests, Claude Code discovery and
version probe (>= 2.1.234, native claude.exe only)
- supervisor: states driven by hooks, reconcile loop, exponential
backoff with jitter, --continue after the first SessionStart, fresh
start after a quick death, auth_error waits for bots.restart, pause
and archive stop the process
- hooks: POST /hooks/{event} authenticated by a per-generation bot
token kept only in memory; `botloftd hook <event>` always exits 0
and never writes to stdout
- terminal: ring buffer per bot with replay by generation/offset,
8 ms / 32 KiB coalescing; the daemon answers ConPTY's startup cursor
query, without which the child never draws
- bots get the user's default environment block, not the daemon's
- rpc: bots.restart, terminal.attach/detach/write/resize, bot.state
and terminal.data notifications; runtimeError in system.status
Tested with Claude Code 2.1.283: trust dialog, launching -> idle ->
busy -> idle through real hooks, rules loaded, deny rule on secrets,
reattach replays only missing bytes, bot dies with the daemon.
Spec and CLAUDE.md updated with these findings.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Milestone M2 — runtime (spec section 17): ConPTY, supervisor with states and backoff, Job Objects, hooks, terminal with replay,
FakeRuntime.Done when (spec): a real bot opens on Windows, states change through hooks, reattach does not lose the screen. All three were checked with Claude Code 2.1.283 (see the test plan).
What's in it
runtime/): bots run in ConPTY throughportable-pty, each in its own Job Object with kill-on-close.FakeRuntimeimplements the same contract for tests. Claude Code is found throughclaude_pathorPATHand must be the nativeclaude.exe>= 2.1.234; when it is missing,system.status.runtimeErrorsays why.supervisor/): keeps active bots running, reconciles every 5 s or when woken, restarts with exponential backoff and jitter, uses--continueafter the firstSessionStart, starts fresh after a quick death, leavesauth_erroralone untilbots.restart, and stops bots on pause or archive.hooks/):POST /hooks/{event}authenticated by a per-generation bot token that exists only in memory.botloftd hook <event>always exits 0 and never writes to stdout.terminal.rs): a ring buffer per bot with replay by generation and offset, and 8 ms / 32 KiB coalescing. Generations are unique across daemon restarts.bots.restart,terminal.attach/detach/write/resize, plus thebot.stateandterminal.datanotifications.Found by running a real bot
portable-ptycreates the pseudo console withPSEUDOCONSOLE_INHERIT_CURSOR, so ConPTY sendsESC[6nand holds the child until something answers. With no terminal attached the screen stayed empty forever. The daemon now answers the first query of each generation.CLAUDECODE,CLAUDE_CODE_MESSAGING_SOCKET,ANTHROPIC_BASE_URLand more. Bots now get the user's default environment block (CreateEnvironmentBlock), not the daemon's.std::env::split_pathstreats quotes as grouping, so a single malformed entry hid every later directory, including.local\bin.PATHis now split on;only, as Windows does.launching. A restart beforeSessionStartdid not announce the new generation to the app. Every new process now emitsbot.state.launchingbehind the trust dialog. Spec 7.4.1 proposes that M4 onboarding trusts the workspaces root once.Spec and design changes
oauth_org_not_allowedalso meansauth_error; otherStopFailureerrors meanidle.--continueon a bot's first start.claude.exeonly (quoting throughcmd.exebreaks in the PTY), the user environment block,BOTLOFT_HOMEadded to the bot env, thePATHrule.bots.token_hashis unused.CLAUDE.md: new Windows pitfalls.Test plan
cargo fmt --all --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace(99 tests)FakeRuntime(backoff,--continue, fresh start,auth_error, pause, archive, restart, resize), repeated 5 times with no flakespnpm checkwith the regeneratedprotocol.gen.tsbotloftd servewithBOTLOFT_HOME=.dev\homeand workspaces outside any repository:launchinguntil it is acceptedlaunching->idle->busy->idledriven by realSessionStart,UserPromptSubmitandStophooks (exec form, no shell).claude/rules/botloft.mdsecrets\owner.tokenwas denied: "File is in a directory that is denied by your permission settings"botloftdalso killed the bot'sclaude.exe(Job Object)Still needs real Claude Code
--continueresuming a conversation after a crash.StopFailure(rate_limit) and a realNotificationwithpermission_prompt, to seerate_limitedandneeds_approvalend to end.${BOTLOFT_BOT_TOKEN}: needs the/mcpserver from M3.Not in this PR: deleting the workspace when a bot is archived (spec 14), keep-awake and the scheduled task (M5).