Repository navigation
rolebox v2.0.0-alpha.1
Pre-releaseBreaking Changes
-
Declared outcome graphs replace the legacy graph executor — graph declarations now use the version-3 outcome protocol and its
graph_declare,graph_submit_outcome,graph_status,graph_auditandgraph_controltool face. Legacy graph declarations, persisted v2 execution state, the incremental node and edge construction tools, and signal-driven settlement are no longer read or run. Existing graph authors must rewrite declarations for the version-3 grammar and start new runs; the old state has no automatic migration. See docs/graph-outcome-protocol.md and docs/graph-v3-operations.md. -
Diagnostics moved to a typed logging platform, and the log files changed shape — every diagnostic rolebox produces now travels one pipeline (
src/log/**): a record carries its level, its channel, an optional registered event code, the ambient scope (session / agent / graph / run / node / attempt / effect / tool), the caller's id-state-reason-count fields and the process identity, and the sinks behind it are the console, one JSON-lines file per channel and an in-memory ring buffer. Three things an operator can see change. File records are flat:{"time":…,"level":"warn","channel":"graph:host","code":"watch.settlement-threw","message":…,"scope":{…},"fields":{…},"process":{"pid":…,"role":"host"}}, where the retired logger nested the message under"0"and the fields under"1", so ajq '.["1"].x'recipe becomesjq '.fields.x'.warnand above now reach the console by default, where the previous file-only logger printed nothing at all;ROLEBOX_LOG_CONSOLE_LEVEL=error(orfatal) restores a quiet console. And there is one file per channel —graph:hostwritesgraph-host.log— instead of a singlerolebox.log, which is what keeps two processes from racing over one rotation; historicalrolebox.logfiles are left exactly as they are, neither migrated nor read back as records nor deleted.ROLEBOX_LOG_FILE=/path/to/rolebox.logrestores the single-file layout for every channel at once, rotation included. The variables areROLEBOX_LOG_LEVEL(file threshold, defaultinfo),ROLEBOX_LOG_CONSOLE_LEVEL(console threshold, defaultwarn),ROLEBOX_LOG_LEVEL_<CHANNEL>(one channel's threshold, e.g.ROLEBOX_LOG_LEVEL_GRAPH_HOST=debug),ROLEBOX_LOG_DIR,ROLEBOX_LOG_MAX_BYTES(rotation size, default 10 MB),ROLEBOX_LOG_RETAIN(rotated copies kept, default 3) andROLEBOX_LOG_FILE. Details: docs/logging.md.
Features
-
Live log views react to source changes — the log reader now watches the resolved file or directory and emits a debounced change signal. The dsh web panel and TUI Logs view refresh on that signal while retaining a fallback poll for platforms that do not report every append.
-
Logs are queryable, prunable and watchable —
rolebox logs, a dsh web panel and a TUI view — the same records are now readable back withoutjqarchaeology.rolebox logsanswers the newest 100 records and takes--level,--channel,--code,--graph,--session,--text,--since/--until,--limit,--orderand--json, while--followtails live;rolebox logs fileslists every file's channel, rotation, size and modification time, androlebox logs pruneremoves rotated copies under--keep/--days/--max-total-bytesgates without ever touching the file being written. The dsh web UI gains a right-Sidebar Logs tab that polls the read-onlyGET /rolebox/logsroute (registered only when the host provides a web server), and the TUI sidebar gains a Logs view —ctrl+lopens it,ctrl+ppauses and resumes,ctrl+up/ctrl+downmove the level threshold,ctrl+ncycles the channel filter andctrl+gfollows. Structured diagnostics also moved into a closed, typed event vocabulary (src/log/registry.ts): a registered code owns its level, its channel and its one-sentence message, so it is filterable by--codeand re-worded in exactly one place, and the compatibility shell the older call sites still import reports a field value its type cannot admit — by key name, never by value — as the throttledlog.field.narrowedevent instead of dropping it silently. Command reference: docs/cli.md; pipeline map: docs/logging-architecture.md. -
The dsh
enabledNamespacestool filter composes allows with!exclusions — a profile that had to narrow the tool list could only enumerate the namespaces it wanted, which silently dropped every rolebox tool it did not name. Entries may now be exclusions (!nameor!namespace-prefix), matched exactly as allows are, and an exclusion always beats an allow,"*"included, while a negatives-only list subtracts from a baseline of everything instead of registering nothing. That makesenabledNamespaces: ["*", "!web"]the documented mitigation for dsh's globalweb_search/web_fetchcollision: every other rolebox namespace stays registered —interactive_terminaland any later tool included — without enumerating namespaces, and the boot log now reports which compiled tool names the filter dropped so a narrowed profile is visible instead of silent. Absent/empty and positive-only lists keep their previous behavior. -
The dsh plugin never fails a boot on a tool name the host already registered — rolebox now probes the host registry's own lookup (
ctx.tools.get(name, scope?), optional: a host without it keeps the direct registration path) before registering each assembled tool, so a name a composition already owns — dsh's globalweb_search/web_fetchamong them — is registered by rolebox asrb_<name>with a single warning naming both, instead of throwingtool "<name>" is already registeredand taking the whole row down. The copy carries only the new name: execute and the native-presentation callbacks are keyed by the canonical name at compile time, so the renamed tool behaves exactly like the canonical one. When the prefixed name is taken too the tool is skipped with a warning (a probe that throws counts as free, and a skip is never reported as a namespace-filter drop), and the two graph-worker tool names (graph_worker_exec,graph_submit_outcome) are never renamed — the worker guard and the dispatch filter match them by literal, so an occupied one is warned about and skipped rather than silently broken. -
opencode v2 is a supported host — opencode v2 (
@opencode/cli@2.0.18) is a separate product from the 1.x line and does not load 1.x plugins, so rolebox now ships the plugin entrypoint v2 resolves —rolebox/server(dist/entries/opencode2.js, fromsrc/entries/opencode2.ts) — default-exporting{ id, setup }. On setup it runs the same initialization and builds the same rolebox service graph as the 1.x entry, then projects that graph onto the v2 plugin domains: one registration per canonical tool, the toolexecute.before/execute.afterhooks (the latter rebuilding the 1.x{ title, output, metadata }output object from the v2result/errorevent, so the recovery guards still read the tool text), the sessionprompt/context/compactionhooks, roles and sub-agents through the in-process agent transform, role skills,/stop-loop, and the event stream normalized into rolebox's canonical events, with a returned cleanup that disposes every registration and flushes loop, function and signal state. The package root keeps the 1.x{ id, server }shape, so an opencode 1.x install keeps working untouched; both opencode entries also register the declared-graph tool face described in the next bullet. Where the v2 plugin surface is smaller than the 1.x host the loss is explicit rather than silent: the plugin session domain has no session list, child listing, todos, diff, fork, status polling or compact (those calls and their tools degrade to empty results instead of throwing), agents register in process with no agent file sync and no in-session role switch, a created session cannot be given a parent (SessionCreateInputhas noparentID), so every dispatched session starts without the parent conversation's history, the compaction hook's context travels on the compaction request because v2's compaction event has nocontextfield (a compaction another plugin has already resolved keeps its summary verbatim and carries the context inresult.metadata["rolebox.context"]), the prompt hook carries no agent id so rolebox resolves the acting agent before the hook runs, asking the session first and falling back to the mapping a previous turn recorded (per turn and never cached, so aswitchAgentbetween turns is observed on the next one, and an unknown session degrades to no agent), system parts rolebox rewrites lose v2 cache hints (untouched parts keep theirs), toolaskresolves without prompting, and the canonicalmessage.updatedprogress heartbeat has no v2 counterpart. A registration the host rejects is logged and skipped, so no single surface can take the host down. Details: docs/compatibility.md. -
Both opencode entries register the declared-graph tool face — opencode 1.x and opencode v2 now assemble the shared host capability layer (
src/platform/adapters/opencode/graph-host.ts, opened for v2 bysrc/platform/adapters/opencode2/graph-host.ts) and register its five bound tools —graph_declare,graph_submit_outcome,graph_status,graph_audit,graph_control— throughctx.outcomeGraphTools, the composition seam that carries a host-assembled face intobuildCanonicalTools. The graph store root is host-owned and deliberately outside the workspace (graphStoreRoot(getDataDir(), directory)resolves to<data-dir>/host/<workspace-hash>), because a dispatched worker runs with the workspace as its root. opencode registers nograph_worker_execand needs none: every canonical tool is registered globally, so a dispatched worker keeps the canonical face while its graph face is exactlygraph_submit_outcome— a bound worker's call to any other graph tool is refused with the stable codeworker-tool-forbidden. Completion is watched through the platform's own signals (v2 armsctx.session.waitper dispatched session, both entries feedsession.idle/session.errorends and the typedsession.statusstate to the host): a recorded end is reportedcompletedfor a normal end andfailedfor an errored end or a failing terminal outcome, while an idle state answersunknown, and neither the end nor acompletedreading settles an attempt — only the plan's own acceptance of a submission does. Run notifications are installed rather than degraded: each entry hands the host a wake-up channel (notifyClient) and the host installs the shipped sender (src/platform/graph-notifications.ts), which prompts the session that declared the graph with the shipped<system-reminder>[GRAPH COMPLETE] / [GRAPH BLOCKED]render and resumes its loop, while a platform that rejects the prompt leaves the notice pending and retried instead of reporting it delivered. The degradations are explicit: no execution query port and no last-turn reader, and — unlike dsh and Pi — no boot recovery, because an opencode plugin loads for every CLI invocation of a shared configuration and an automatic re-dispatch would fire from unrelated commands (a stranded run stays visible ingraph_statusand is re-armed withgraph_control {command:"retry"}); the execution reading itself is real on both platforms, from whatever each one exposes: v2 has nosession.statuscall (the client's argument-lessactivereports only running sessions and is not plugin-exposed), so its activity state comes from the typedsession.statusevent (busy/retry answer running; idle answers unknown) and its session's terminaloutcomefromctx.session.get({ sessionID })(succeeded completes; failed/interrupted are reported on the attempt as a failed execution in the platform's own word), while the 1.x SDK'sSessioncarries no outcome field at all, so there the reading stays state-only and no outcome is invented from it; an idle state still answersunknown— the turn is over, not that the attempt reached a declared outcome — where a recorded end is reportedcompletedfor a normal end andfailedfor an errored end or a failing terminal outcome, and only the plan's own acceptance of a submission settles an attempt; an execution whose reads are missing or failed answersunknownnaming the read that could not answer, and a cancel is answeredrequestedorunsupported, neverconfirmed.
Bug Fixes
-
Graph worker task headings are separated from preceding instructions — the dispatched
## Assigned tasksection now starts after a blank line when a host combines it with the worker's role context. -
Log watching and rotation withstand filesystem errors and competing processes — watcher errors no longer crash the host, throttle windows are scoped to the affected subject, and a writer waits for a live rotation holder instead of appending into a file still being renamed.
-
Graph store opening tolerates competing writers — the SQLite busy timeout now allows a loaded host more time to finish its write before another host opens the same workspace store.
-
A session rolebox creates for a role agent runs that role's model on opencode v2 — v2 does not inherit an agent's model into a session it creates (
SessionCreateInput.modelis what the created session runs), so a v2 dispatch fell back to the host's default model and failed on its first model request whenever that default was unusable. The v2 entry now derives the resolved model of every registered role and sub-agent from the same registration assembly the agent transform writes (collectOpencode2AgentModels,src/platform/adapters/opencode2/agents.ts) and passes it to the session adapter, which forwards it — role variant included — onsession.create; a role that resolves no model stays out of the map and keeps the host default. The prompt paths also switch an agent only whenctx.session.getreports the session is not already running it, so a graph notification or a copilot continuation no longer appends a redundantagent-switchedmessage, and they never switch the session's model, which on those paths belongs to the user's own session; a session read that is missing or fails falls back to the previous unconditional switch.