Skip to content

rolebox v2.0.0-alpha.1

Pre-release
Pre-release

Choose a tag to compare

@EricMoin EricMoin released this 09 Oct 08:43
· 102 commits to main since this release

Breaking 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_audit and graph_control tool 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 a jq '.["1"].x' recipe becomes jq '.fields.x'. warn and above now reach the console by default, where the previous file-only logger printed nothing at all; ROLEBOX_LOG_CONSOLE_LEVEL=error (or fatal) restores a quiet console. And there is one file per channel — graph:host writes graph-host.log — instead of a single rolebox.log, which is what keeps two processes from racing over one rotation; historical rolebox.log files are left exactly as they are, neither migrated nor read back as records nor deleted. ROLEBOX_LOG_FILE=/path/to/rolebox.log restores the single-file layout for every channel at once, rotation included. The variables are ROLEBOX_LOG_LEVEL (file threshold, default info), ROLEBOX_LOG_CONSOLE_LEVEL (console threshold, default warn), 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) and ROLEBOX_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 without jq archaeology. rolebox logs answers the newest 100 records and takes --level, --channel, --code, --graph, --session, --text, --since/--until, --limit, --order and --json, while --follow tails live; rolebox logs files lists every file's channel, rotation, size and modification time, and rolebox logs prune removes rotated copies under --keep/--days/--max-total-bytes gates without ever touching the file being written. The dsh web UI gains a right-Sidebar Logs tab that polls the read-only GET /rolebox/logs route (registered only when the host provides a web server), and the TUI sidebar gains a Logs view — ctrl+l opens it, ctrl+p pauses and resumes, ctrl+up/ctrl+down move the level threshold, ctrl+n cycles the channel filter and ctrl+g follows. 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 --code and 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 throttled log.field.narrowed event instead of dropping it silently. Command reference: docs/cli.md; pipeline map: docs/logging-architecture.md.

  • The dsh enabledNamespaces tool 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 (!name or !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 makes enabledNamespaces: ["*", "!web"] the documented mitigation for dsh's global web_search / web_fetch collision: every other rolebox namespace stays registered — interactive_terminal and 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 global web_search / web_fetch among them — is registered by rolebox as rb_<name> with a single warning naming both, instead of throwing tool "<name>" is already registered and 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, from src/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 tool execute.before/execute.after hooks (the latter rebuilding the 1.x { title, output, metadata } output object from the v2 result/error event, so the recovery guards still read the tool text), the session prompt/context/compaction hooks, 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 (SessionCreateInput has no parentID), 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 no context field (a compaction another plugin has already resolved keeps its summary verbatim and carries the context in result.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 a switchAgent between 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), tool ask resolves without prompting, and the canonical message.updated progress 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 by src/platform/adapters/opencode2/graph-host.ts) and register its five bound tools — graph_declare, graph_submit_outcome, graph_status, graph_audit, graph_control — through ctx.outcomeGraphTools, the composition seam that carries a host-assembled face into buildCanonicalTools. 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 no graph_worker_exec and needs none: every canonical tool is registered globally, so a dispatched worker keeps the canonical face while its graph face is exactly graph_submit_outcome — a bound worker's call to any other graph tool is refused with the stable code worker-tool-forbidden. Completion is watched through the platform's own signals (v2 arms ctx.session.wait per dispatched session, both entries feed session.idle / session.error ends and the typed session.status state to the host): a recorded end is reported completed for a normal end and failed for an errored end or a failing terminal outcome, while an idle state answers unknown, and neither the end nor a completed reading 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 in graph_status and is re-armed with graph_control {command:"retry"}); the execution reading itself is real on both platforms, from whatever each one exposes: v2 has no session.status call (the client's argument-less active reports only running sessions and is not plugin-exposed), so its activity state comes from the typed session.status event (busy/retry answer running; idle answers unknown) and its session's terminal outcome from ctx.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's Session carries no outcome field at all, so there the reading stays state-only and no outcome is invented from it; an idle state still answers unknown — the turn is over, not that the attempt reached a declared outcome — where a recorded end is reported completed for a normal end and failed for 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 answers unknown naming the read that could not answer, and a cancel is answered requested or unsupported, never confirmed.

Bug Fixes

  • Graph worker task headings are separated from preceding instructions — the dispatched ## Assigned task section 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.model is 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 — on session.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 when ctx.session.get reports the session is not already running it, so a graph notification or a copilot continuation no longer appends a redundant agent-switched message, 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.