Repository navigation
Messages, permission levels and conversation history (#643, #652, #657) - #664
Merged
Merged
Conversation
Adds a durable per-agent inbox that the lodge's chat connectors, IWCL and terminal sends admit into, and a waker that tells the agent through its engine's own input path instead of pasting into a live session. Off unless woltspace.json sets inbox.enabled; with it off every path behaves as before.
A hint now says who the queued mail is from ("from pal-a, your human"),
never what it says. A busy engine no longer waits for its turn's end:
Claude gets the hint at its next tool step through a PostToolUse hook
(added context, nothing typed), Codex through turn/steer into its newest
turn while that turn is still in progress. Anything else, or a steer the
turn refuses, still hints at the turn's end.
…he inbox edges - Inbox off: no launch marker is written and none holds a stop, resume, reconcile or adoption (as before markers). Inbox on: any failed spawn, paste or new window clears its attempt when tmux answers that the session does not exist (TmuxSessionRuntime.session_exists); a tmux that did not answer leaves it unknown. - A Codex steer ends at the deliver budget however many notifications arrive: pending/busy before the steer was sent, unknown/timeout after. - Rearm refuses a name that is not a wolt before taking any lock, and any request carrying a session's inbox token. - A hint names "your human" first, never counted in "and N more"; other sender names keep only the wolt-name charset. - A hint is not placed when the session's attempt or locator changed after the wake read it. - Inbox request bodies are capped (413 too_large) before parsing. - Clients that send an inbox token never follow a redirect.
… the Inbox page A right-hand Inbox pill on wolt sites, the same pill as the site menu's, with the lodge-wide queued count. Its drawer lists the mail still waiting (queued, being read, held) with sender, time and full plain text, filtered to the site's wolt by default or to all wolts. The lodge Inbox page shows the same list, all wolts first; each wolt card links to its site and filters the list. One new read: Store.messages, a paged query that returns no key, claim, reader or route, served by GET /inbox/messages (refused to a session's inbox token, never cached). Viewing never fetches, acknowledges, releases or wakes.
…nly while it is visible
With the inbox on, a launch now records how its sessions hear of mail: a pasted body-free hint by default, or the engine's native hooks with inbox.native_hints true. Any other value refuses the launch before it creates a row or retires a locator. The mode rides in the launch marker and is published with the attempt; a live reuse keeps it, a relaunch chooses again. Paste mode prepares no native wiring. The waker pastes only after the agent takes input and nobody typed for two seconds (at most five per pass), then re-reads the senders and pastes through sessions.hint_live_agent, which checks the exact attempt, runtime, mode and pane under the agent's lifecycle lock and the paste lock. A changed session is not pasted into; a failed paste is unknown and never retried. A wake that starts a session counts its fetch instruction as that attempt's hint, so the start's pickup does not paste a second one.
Until now a session had two modes, prompt and auto, and every engine ran "auto" with its skip-every-check flag. "auto" now means the engine's own automatic mode, and the old behaviour has its own name, "bypass". - One table in harnesses.py (POLICY_FLAGS) gives each engine's flags per level, and per isolation where they differ. A level an engine does not list is refused (LevelNotOffered, a 400 on the lodge spawn route), at spawn and again in the command builders; nothing falls back to another level. Codex Auto is host only (its sandbox cannot start in the container); opencode and hermes have no Ask flag, so their own settings decide and the label says so; Pi offers Bypass only. - A stored policy has its own version (RECORD_VERSION 2), separate from the host grant's (POLICY_VERSION stays 1, so grants are untouched). ExecutionPolicy.from_record reads version 1 "auto" (and a bare string or no policy) as bypass and refuses an unknown version. - Migration 6 rewrites every stored session policy to version 2 and is breaking: an older build refuses the file. - Defaults are unchanged in effect: Bypass where it was "auto", Ask on a host without a grant. One grant allows both Auto and Bypass, and its wording says so. - Capabilities report record_version and the levels each engine offers. The CLI has --policy, the TUI a level picker, and both list a version 1 auto record as "bypass*".
Codex's approval policy leaves it to the model when to ask, and the only values this Codex accepts are on-request and never. Only its sandbox makes it ask, for what the sandbox would block, and in the box container that sandbox cannot start. Run there with on-request and full access, the model ran a command without asking. So the container offers Codex Bypass only, and the refusal says why; on the host the level is labelled "Ask (sandbox escalations)", in capabilities, the refusal text and the TUI picker.
Creating a wolt with a level its engine does not offer scaffolded the wolt first and was refused only when its session started: a 409, the folder left behind, and a retry with the same name taken. The create route now checks the level, or the default a new wolt gets, before anything is written, and answers 400. Capabilities name the default engine, so the TUI's create picker offers only that engine's levels.
woltspace.json "sessions": {"default_execution_policy": "auto"} makes every
session that asks for no level (the bot, wolf, IWCL and lodge spawns) start
on that level instead of Bypass, where the engine offers it here and, for
Auto or Bypass on the host, the wolt's grant exists. Elsewhere the built-in
default applies. A level asked for by name still wins; unset changes nothing.
Supervisor.prepare() writes os.environ directly (WOLTSPACE_ISOLATION and more), and monkeypatch.delenv() of an unset key leaves nothing to undo. TestEnvFileLoading and TestContainerEntrypoint prepared host layouts, so every later test ran as "host": test_boot_context_cap[pi] then resolved Ask with no grant, which pi does not offer, and failed only in the full CI run.
# Conflicts: # container/lib/lodge_db.py
With conversation_log.enabled in woltspace.json, each new inbox admission also writes its exact payload to conversation_log in the same durable write, so a message and its copy commit together or not at all. A dedup retry returns before the archive and adds nothing; ack drops only the operational body. The archive keeps at most 128 MiB of text and 80,000 rows lodge-wide, evicting the oldest rows in the same write, with the totals in conversation_usage. Migration 7 is additive and has no foreign keys and no inbox prefix, so the restore guard treats it as passive data; backup's known schema moves to 7 (it was still 5, below the floor 6 that migration 6 sets). A reader reads its mailbox's history with the new inbox history verb: rows it sent or received, matched by mailbox id, newest first, in pages bounded by rows and by serialized bytes, with an omission marker that moves the cursor past a row larger than the page.
With conversation history on, notify writes the outgoing text (after any speech fallback wording) as an unknown attempt in a durable write, in a worker thread, before the sender runs; if that write fails nothing is sent. The row is tied to the calling session's open mailbox, also when an explicit route clears the routing session. A provider success marks it accepted; a failed outcome write only logs, so the result stands and nothing is resent, and an error or a cancel leaves it unknown.
The flag, what is kept and when, the history command and its scope, the limits and what they do not bound, and how backup and restore treat it.
max_bytes sums the record objects' own compact JSON, not the array or the rest of the reply, so the CLI's whole output can be larger. A cancel during the send leaves the attempt unknown; a cancel while the outcome is written may still end accepted, as the worker finishes. Wording only.
A lodge with the inbox on keeps acknowledged mail for 30 days, so every backup taken since it went live was refused at restore (safe_state_present). Restore now keeps every inbox row: acknowledged keys still dedupe, pending mail is still owed, sequences continue. The disarm write revokes every reader (as before), puts mail claimed by the old lodge's readers back in the queue with a 'restored' event, and clears each mailbox's wake state. The restore report counts the mail and says to keep the old lodge stopped, since restored pending mail may run again. A layout this restore cannot read, a schema floor it does not know, and usage charged without mail are still refused (safe_state_unknown).
# Conflicts: # container/lib/lodge_db.py
# Conflicts: # src/woltspace/backup.py
… the inbox's rule
jerpint
approved these changes
Nov 4, 2026
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.
This lands three of @asharakeh's PRs together, with merge commits so their history and authorship are kept:
Merge it with "Create a merge commit", not squash, so #643, #652 and #657 show as merged and
git revert -m 1can undo all three at once.What we did on top
lodge_db.pyhas migrations 5, 6 and 7 in order with_BREAKING = {6}.backup.pytakes Messages: durable inbox with pasted hints and native opt-in #643's rule that the inbox restores as it is, with_KNOWN_SCHEMA = 7.Review: two independent reviews of the combined branch found no blocker. Both upgraded a 0.5-shaped lodge.db through 5, 6 and 7, checked rollback on failure, checked that 0.5 refuses the upgraded db without writing to it, and checked backup and restore. The follow-ups they found (a launch marker that can stick, deleting an archived wolt that still has mail, user docs and a changelog entry for the breaking floor) come in a follow-up PR after this one.