Changelog
Changelog
Section titled “Changelog”All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Section titled “[Unreleased]”Capture redaction
Section titled “Capture redaction”- Optional write-path redaction in
@rivetos/capture-core(createCaptureWriter). Off by default: withredactionunset andRIVETOS_CAPTURE_REDACTIONunset, posted/spooled bytes are unchanged. When enabled (options orRIVETOS_CAPTURE_REDACTION=1), built-in detectors plus optional operator regexes run on messagecontent,tool_result, andtool_argsbefore the 16k cap; logs report a span count only. Config surface:memory.capture.redaction(enabled,builtins,patterns) — validated in boot and documented in CONFIG-REFERENCE. YAML→hook injection is not wired yet; use the env flag or passredactionexplicitly.
Memory
Section titled “Memory”- Opt-in SQLite memory backend (
plugins/memory/sqlite,@rivetos/memory-sqlite) behind theMemorycontract: WAL file store, append, session/task history, settings, and FTS5 search. Config:memory.sqlite.path. Mutually exclusive withmemory.postgres. With the block unset, behaviour is unchanged. Phase 1 implements the in-processMemorywrite/search path (chat append works); HTTP/api/captureand memory MCP/HTTP tool parity remain Postgres-gated and are deferred with vectors, compaction, wiki, multi-user routing, and Postgres import/export.
Providers and embeddings — token_command, wire-shape, model catalog
Section titled “Providers and embeddings — token_command, wire-shape, model catalog”- New leaf package
@rivetos/token-command: argv-only bearer mint with TTL cache and invalidate-on-401 (createTokenSource,createAuthorizedFetch), embeddings wire-shape helpers (openai/native), and a model catalog with a static floor plus background endpoint refresh. Dual-consume leaf (no"type": "module", same shape as@rivetos/types) so CJS-compiled plugins can import it. - Providers
anthropic,xai,vllm, andllama-serveraccept optionaltoken_command/token_ttl_ms/token_command_timeout_ms. Unset keeps today’s staticapi_keypath. When set, the mint wins overapi_key, is never logged, and AI SDK / availability probes remint once on HTTP 401. vllmandllama-serveraccept optionalmodels(static floor) andmodels_ttl_ms;listModels()returns floor + discovered ids from the models endpoint (last-known on outage). Building block: no den-server / web roster consumer reads the merge yet.- Embedding worker and
memory.postgresquery-time embed supportembed_api_key/embed_token_command,embed_wire_shape(openaidefault, ornativepassthrough), and optionalembed_expected_dims(must equal thehalfvec(1024)column width when set; longer vectors null out rather than silent-slice). Worker env:RIVETOS_EMBED_API_KEY,RIVETOS_EMBED_TOKEN_COMMAND(JSON argv),RIVETOS_EMBED_WIRE_SHAPE,RIVETOS_EMBED_EXPECTED_DIMS. NoOPENAI_API_KEYfallback (opt-in only). - Config validation and
docs/CONFIG-REFERENCE.mdcover the new keys. Opt-in: with these keys unset, behaviour is unchanged.
Den URL guards
Section titled “Den URL guards”- A plain-http den URL is no longer dropped when the CA file is missing.
rivetos_resolve_denunsetRIVET_DEN_URLwhenever the CA path did not exist, including forhttp://URLs that never use a CA, so on a den without TLS and a node with no shared CA the hooks and sidecars lost the den and fell back or spooled. The CA check (and theNODE_EXTRA_CA_CERTSexport) now apply only to URLs that are nothttp://. One trade to know: a stalehttp://line left in~/.rivetos/.envon a node with no den at all used to be dropped by that same check and is now kept, so captures would target the dead port instead of falling back;rivetos doctorreports a presetRIVET_DEN_URL. An http loopback URL only reaches that point when the den really serves http: the https rewrite for a TLS den runs first (#1053). - A pre-set
RIVET_DEN_URLis checked before the memory sidecar and the capture hooks dial it. A comma list (the old den-hook fallback form) is not one origin: the first entry is used. A plain-http loopback URL against a den that serves https is rewritten to https. Each guard prints one line to stderr naming the value to put in~/.rivetos/.env, or says to remove the line. Shared between the shell launcher (rivetos_guard_den_urlinintegrations/shared/rivet-paths.sh) and@rivetos/capture-core(guardDenUrl;resolveCaptureTransportreturns the warnings and the claude-cli hook logs them). “Serves https” mirrors boot’sresolveDenTls:den.tls_cert/tls_key, thenRIVETOS_DEN_TLS_CERT/KEY, then the mesh issue-node files formesh.node_name. A non-loopback http URL is left alone. Background:~/.rivetos/.envon three nodes keptRIVET_DEN_URL=http://127.0.0.1:5174from before gateway TLS; nothing dialed it until #1012 and #1013/#1014 moved the sidecar and every hook onto den transport, and the launcher loads that file after the den-injected env, so the stale line overrode the correct https the den hands its own sessions. Reads failed on every call and captures spooled for four hours with nothing else showing red. rivetos doctorgains adengroup:RIVET_DEN_URLpresent in~/.rivetos/.envis reported (fail on a comma list or an http scheme against a TLS den, warn when redundant or a differing override; the den injects the URL and the launcher derives it, so the line is drift by definition), the den is dialed once at/healthzwith the CA and a failure that the other scheme answers is named a scheme mismatch, and the capture spool is reported (warn when batches wait, fail when the oldest is over an hour old, dead-lettered count included).memory_statsreports the local capture spool in the alerts block, right after queue health:✅ empty, or the waiting count, oldest age, dead-lettered count and theRIVET_DEN_URL/rivetos doctorhint.createStatsTooltakescaptureSpool(a reader, ornullto omit the block).
Agent registry
Section titled “Agent registry”- Killing a running task now stops it. The runner aborts the in-flight turn when the row flips to
killed(it used to let the turn finish and discard the result, so a killed task kept running tools and spending tokens), and re-checks the row at each turn boundary so a kill issued from another node is caught too. Theclaude-clitask spawn leads its own process group and is signalled as a group (SIGTERM, then SIGKILL after 3 s, plus one sweep when the CLI exits), so the CLI’s own children — stdio MCP servers, Bash tool shells — no longer outlive a killed or timed-out task; the sweep runs when the CLI exits, so a descendant holding its output pipe can no longer hang the turn (#1053). The capture hook worker is unaffected: it detaches into its own session by design. The CLI now runs in its own session (everyclaude -pspawn, the model path included), so a signal aimed at the runtime’s process group no longer reaches it directly; the runtime kills its live CLI groups when it shuts down instead, and its lifecycle now treats SIGHUP (a closed terminal) as a shutdown like SIGINT and SIGTERM, since Node’s default handling of a signal runs no exit hook. POSIX only: on Windows the pid is signalled as before. The Kimi, OpenCode, Pi and Qwen executors still signal the pid alone. When a user kill and a budget trip land in the same turn, the recorded verdict iskilled. POST /api/tasks(the route a den-transportdelegate_taskuses): a preset with no harness configured no longer shadows a runtime agent of the same name, andagent@nodepins a runtime agent to an online node that hosts it (400 otherwise). #1052 fixed the same shadowing in the in-process mesh engine and the Postgres-transport sidecar but missed this third path, sodelegate_taskfrom a CLI harness session still answeredpreset "Grok" has no harness configuredfor runtime agents. With no runtime agent of that name the preset still answers with its own refusal. The den-transport tool description now advertisesagent@node.delegate_task: a preset wins over a runtime agent of the same name only when it can run. A preset with no harness configured no longer shadows a same-named runtime agent (every runtime agent tends to have one — “Deepseek” next to runtimedeepseek— which made those agents unreachable by bare name); with no runtime agent of that name the preset still answers with its own “no harness configured” refusal. Applies to the in-process mesh engine and the MCP sidecar;agent@node(#1039) still pins a node explicitly.POST /term { agentId }spawns that preset’s harness in its directory (materialized, with therivet-sharedlink) and uses the preset’s roster command, model, and effort unless the request sets them. Per-agent cwd for interactive sessions: a session cwd store records a non-default directory and a later resume starts there. PTY drivers acceptcwd.SessionSummary.cwdis filled for every PTY harness. OnedefaultSpawnCwdrule is shared by the term manager and the harness roster getter. Clients never send a raw cwd.@rivetos/agent-registry(taggeddomain:shared) is the shared preset store den, core, and the hub will import: file and Postgres backends, directory materialization, a short-lived cache, and a one-shotagents.jsonimporter. Migration0017_agent_presets.sqladds the DataHub tableros_agent_presets.AgentPresetgains optionalnode,directory, andsharedLink(nodeBaseUrlis optional and deprecated); roster-command helpers move into@rivetos/typesso den and RivetHub share one map. No runtime behaviour change.- den serves presets from the DataHub (
ros_agent_presetsonconfig.pgUrl/RIVETOS_PG_URL) and falls back to the per-nodeagents.jsonwhen Postgres is absent or the table is not ready yet. The first time the table answers ready, that file is imported once and renamed aside; imported rows get a directory on disk. A name that already exists (case-insensitively) is imported as"<name> (<node>)", then"<name> (<node> 2)", and if those are taken"<name> (<node> <first 8 of id>)", instead of being dropped. Only an id conflict is skipped. A row that still cannot be imported leaves the source file in place and is reported asunresolved. The import result countsrenameddisambiguations next toimportedandskipped. Preset names are unique case-insensitively on every den, including file-only dens. - Presets carry
node,directory, andsharedLink. The hosting den materializes the directory (mode 0700) and arivet-sharedsymlink to the shared directory.sharedLink: falsedoes not create that symlink. On PATCH,sharedLink: falseremoves an existingrivet-sharedlink only when this patch flips it off and the directory stays the same; a directory move withsharedLink: falseskips creating the link and does not unlink a different directory. Turning it back on links it again. The directory is not deleted with the preset. nodeBaseUrlis optional and deprecated — placement isnode+directory; kept only for rows written by pre-registry dens (ones that answer 400nodeBaseUrl is required). Create stores a client’s trimmednodeBaseUrl(at most 512 characters) when one is sent and does not require it. A create that omits it, including from a new hub, writes an empty URL (Postgres included; no migration). Update does not patch it. List and GET still echo the URL on a legacy file row written by a pre-registry den. A clientnodeother than this den’s is rejected;nodecannot be changed later.RIVETOS_DEN_NODE_NAME(thenRIVETOS_DEN_NODE_ID, then the hostname) is this den’s mesh node name. Boot sets it fromnodeNameFor(config)(mesh.node_name, thenHOSTNAME, thenlocal) after prefix passthrough, so the config-derived name wins for an embedded den and a process-envRIVETOS_DEN_NODE_NAMEis ignored there. The mesh registrar registers that same string: a whitespace-paddedmesh.node_nameis trimmed, and whenmesh.node_nameis unset the fallback is the hostname, not'unknown'.RIVETOS_DEN_AGENTS_DIRoverrides the default preset directory root (~/.rivetos/agents)./mesh.jsonattacheslatestusing that sameconfig.nodeName. On this box the hostname isrivet-claudeand the mesh id isnode-f; matching the hostname leftlateston the wrong roster entry./healthzincludesnode(the mesh node name) next toname(the hostname).- Presets are
delegate_tasktargets (by id or name) from both delegation engines. A hit runs as a durableros_tasksrow (executor: harness-session,executorTargetthe preset harness, pinned to the hosting node) with the preset’s directory, model, effort, and system prompt. The caller never names a node. A harness with no headless executor on that node fails pre-flight with the gap text and creates no row. The task runner forwardsworkingDir,effort, andsystemPromptAppend. It materialises the directory and therivet-sharedsymlink only for a preset the runner resolved (resolvePreset, boot passes the preset resolver’sfind) — not from a clientpresetIdorworkingDir— so a workflow step’s case directory is not given a symlink into the shared tree. AworkingDirthat disagrees with that preset fails the task (working_dir_mismatch) and creates nothing. Chat-loop workflow steps still run withworkspaceDirset to that case directory.POST /api/tasksfor a preset (when the body does not setexecutor) creates that same harness-session row; no harness or an unimplemented one is 400, and a hosting node that is offline or unknown is 409. An explicitexecutoris left alone. The HTTP mesh fallback carriestoAgent. The catalog lists presets (kind: 'preset'), and nodes advertisemetadata.harnessExecutors. POST /api/tasksstrips clientpresetId,presetName,sharedLink,delegation, andmeshFromunless the route itself resolved a preset (the preset branch writes them back). On that branch a non-blank bodymodelwins, a blankmodelis omitted, and the preset ownseffortand the system prompt; API rows are not delegations. Fresh roster reads (rosterEntriesFresh({ timeoutMs }), default 2s) and the catalog race the preset store and mesh registry against that bound and fall back to the last-known list and mesh snapshot, so a hung read cannot pin the request.- The task runner bounds each preset lookup at 2s (the same window as roster reads). A timeout or a rejection materialises nothing and runs with the spec working directory, and the liveness heartbeat starts before that lookup so a hung resolver cannot strand the claimed task.
- A mesh node with no
node_nametakes its registry id, TLS cert lookup name, and task affinity fromnodeNameFor(HOSTNAME, thenlocal) instead of the literalunknown. - MCP sidecar
delegate_task/list_agentsfor CLI harnesses. WithRIVETOS_PG_URLset, a preset name or id becomes the sameharness-sessionrow the runtime engine creates (Postgres direct — no gateway mTLS client), and a runtime agent id becomes achat-looprow pinned to the newest online mesh node that hosts it.RIVETOS_TASK_IDis the chain guard (refuse when the next depth would pass 3) and is stored asparentTaskId.RIVETOS_MCP_ENABLE_DELEGATE=0turns both tools off. A row nobody claims is killed, and the reply names the node whose runtime never picked it up.PresetDelegationEngine.delegatetakes an optionalparentTaskIdso that sidecar row can record its parent; in-process callers omit it. - MCP sidecar
delegate_taskacceptsagent@nodeto pin a runtime agent to one online mesh node when several host the same id. The fullto_agentstring is matched against presets first, so a preset whose name is exactlyagent@nodestill wins; with no node, a preset name or id still wins over a runtime agent with the same id and the newest online host is still chosen. The den HTTPS delegate tool does not parseagent@node(the gateway is givento_agentverbatim) and keeps the pre-pin description. - Migration
0018adds a nullablesort_order.AgentPreset.sortOrderis the user-chosen sidebar position.PATCH /api/agents/:idaccepts{ sortOrder }as an integer 0–1,000,000 ornull; anything else is 400sortOrder must be an integer 0-1000000 or null. A DataHub without 0018 still lists presets, and saving an order there fails with an error naming the migration.
den.allowed_harnesses— optional allow-list of harness ids this node offers for new launches. Unset = every registered harness (unchanged). When set,GET /api/harnessesstampsallowedon each row; Agents preset create/PATCH,POST /term(fresh spawn), control-plane session create, and harness-session task create refuse off-list ids withharness_not_allowed(HTTP 403). Existing sessions / real resumes still work (sessionId/nativeSessionIdon session create when the target exists;/termresume of the same harness). A never-seen resume key on either path does not mint an off-list spawn. The task runner re-checks on the claiming node (pre-list rows including legacyclaude-clitargets, internalstore.create, peer affinity)./api/catalogomits off-list harness-session executors;/term/configdrops off-list roster commands. RivetHub web and Android new-conversation / Agents pickers filter oninstalledandallowedtogether (absent = true); existing-session drawers still list off-list harnesses so resumes stay visible.- The 202 from
POST /api/harness-sessions/:enc/turnsand the legacyPOST /term/injectcarriesdismissedDialog: truewhen the inject button’s send found an open dialog and queued Esc ahead of the paste. The flag has the same delivery guarantee as the turn: accepted into the inject path (it may still be sitting in the pre-ready buffer), not proven written to the pane. Flag only — no pane text leaves the node. AgentPreset.nodeBaseUrlis deprecated — placement isnode+directory; kept only for rows written by pre-registry dens (ones that answer 400nodeBaseUrl is required). The den stops requiring or patching it. On create it stores a client’s trimmednodeBaseUrl(at most 512 characters) when one is sent, and an empty URL when the client sends none (the Postgres column stays; there is no migration). Update does not patch the field. List and GET still echo whatever URL a legacy file row carries.- The gateway enforces a browser origin policy. A request or WebSocket upgrade that carries an
Originis answered only when that origin is the gateway itself, the RivetHub desktop app, another den in the mesh roster, or listed in the newden.allowed_origins; anything else gets403. Responses echo the allowed origin instead ofAccess-Control-Allow-Origin: *. Clients that send noOrigin(Android, hooks, CLI, mesh peers) are unaffected. A plain-HTTP gateway also requires loopback callers to use a loopbackHostname or one listed in the newden.allowed_hosts. The mesh agent channel now refuses requests that carry anOrigin. See CONFIG-REFERENCE →den→ Browser origin policy. POST /term/injectwrites only into sessions whose roster entry is an agent harness (room: true). Terminal-only sessions (room: false) answer 409session is not an agent harnessand nothing is written to the PTY (#810). Custom harness entries must setroom: trueto receive chat; terminal-only entries are typed through the terminal. Typing into a shell remains the terminal websocket.- When a herdr-backed coding agent exits, den reaps the PTY and mux session after a short grace so chat inject 409s instead of writing into the leftover shell (#791). Adopted harness panes stay closed to inject until a pane-scoped
pane list/agent listprobe (or a working|idle|blocked frame, orpane.agent_detectedwith a live agent) proves a live agent; a fresh create stays not-ready and re-probes instead of killing a still-booting harness;POST /termduring the grace mints a new pty immediately. - Term ready-gate waits for output quiescence or herdr agent-idle (not first-chunk + delay). The first buffered inject on an agent pane is confirmed only by a herdr
workingframe; unconfirmed turns are recorded on the pty and never retried (#796). - Claude transcript adapter strips Claude Code’s
<pasted_content id="…">…</pasted_content id="…">framing from a parsed user turn before it reaches the transcript, and from the session drawer title (#818). The den injects chat as a bracketed paste, which Claude Code wraps and stores verbatim; left as-is the wrapper showed as raw tags in the user bubble and, because the web client retires an optimistic bubble by exact-text match with no id, duplicated the turn. The strip is paired and id-anchored, so literal user text that merely mentions the tag is left untouched. - The herdr agent kind is resolved from the roster entry’s own
argv[0]instead of its key, so a renamed entry (claude-coderunningclaude) keeps the agent-idle ready-gate and first-turn confirm instead of silently falling back to output quiescence. The restart sweep (status subscription only) lets the@rivet_agent_panestamp — written at create and on adopt — win and, with no stamp, retains a survivor when its@rivet_commandtag is a kind or its roster entry’sargv[0]is a kind. Reattach lets the stamp win too; a session with no@rivet_agent_panestamp (created by an older den) keeps the previous key-based rule — the kind comes from the roster key andargv[0]must equal it — so a renamed-key or pinned-path plain pane is not reclassified and ended. Wrapper scripts and absolute paths remain plain panes by design —--kindwould launch herdr’s own PATH binary rather than the pinned one — and aroom: trueentry that resolves to a plain pane now logs once at spawn. - The harness model/effort sheet carries an explicit
launchModelflag, and the PTY driver stamps it onto the advertised capabilities:claude-codesets it (its--modelis the launch-time switch and the sheet’s alias set is what the CLI accepts). A sheet withmodels+modelFlagdoes NOT imply it — the pre-spawn picker gates on the flag, not the sheet’s shape (#814).POST /termstill validates the model/effort tokens against the resolved sheet either way. - Model and effort flags are only added when the roster entry still runs the built-in program for that key.
- Chat inject refuses to paste a turn onto unsent text in Claude Code’s input box (
reason: 'harness_draft', 409turn_in_flight). Claude only: the PTY driver’sdialogGate, andPOST /term/injectwhen the roster command isclaudeorclaude-code. The inject button is refused too. Empty-box ghost text (the eightTry "…"examples and the other Claude Code 2.1.280 placeholders, read from that version’s bundle:Message @<agent>…,Press Enter to edit the selected message, or up again for history,Press Enter to edit the selected message, or up again for an older one,Press up to select a queued message to edit, or Enter to send them now,Press up to edit queued messages, Enter to send them immediately,Press up to select a queued message, then Enter to edit it, andPress up to edit queued messages) is treated as an empty box, because the pre-send capture is plain text and cannot see dim SGR. A draft that wraps past the pane width still fails open. - Model ids may contain
~after the first character (OpenRouter aliases such asopenrouter/~z-ai/glm-latest):POST /termand preset spawns no longer answer 400model must be a 1-64 token, and harness model sheets keep those ids. A leading~is still rejected.
RivetHub client
Section titled “RivetHub client”apps/rivethub-web: when a send’s 202 carriesdismissedDialog, the composer shows “sent: a picker or prompt was open in the Terminal; it is cancelled (Esc) before the paste” for 8 seconds.- Copying an agent no longer seeds
nodeBaseUrl. The hub still resolves a legacy preset URL whennodeand the roster cannot name the den, until no pre-registry den (one that 400snodeBaseUrl is required) remains on the roster. Creating an agent still retries once withnodeBaseUrlwhen an old den answersnodeBaseUrl is required. - Agents: the sidebar shows each preset’s hosting node by name, edits its working directory and the
rivet-sharedlink, and opening it sendsagentIdonPOST /termso the den spawns in that directory with the preset’s command, model, and effort. An explicit per-thread model or effort still wins. Settings shows the connected node’s registry backend, node name, directory root, and shared directory (fromRIVETOS_SHARED_DIR/mesh.storage_dir). An old den that still answersnodeBaseUrl is requiredis retried once with that field. A file-only preset with nonodestill resolves throughnodeBaseUrl. A harness-less preset spawns withoutagentId(the node’s default directory). A 404agent not foundfor a preset that is no longer in the active agents queries clears that id and opens once without it, with the notice “Preset not found on this node; opened without it”; a preset that is still listed there is not recovered, the 404 stays the thread error, and those queries are refreshed so the next attempt is not stuck on a stale snapshot. The command-404 fallback now also carries the thread’s model and effort (it used to send the session only). AnodeBaseUrl-only row opens only when that URL is on the saved roster. - Composer model picker for conversations whose harness supports choosing a model per turn.
apps/rivethub-web: the composer autofocuses on landing in a conversation and when switching to another, so you can type without clicking first. Waits for the socket (the textarea is disabled while reconnecting) and fires once per session. It never pulls focus from something you’re already using — an inline rename, a filter, a dialog (unless the dialog has closed and gone inert), or the terminal — and skips touch devices so a tap can’t raise the keyboard over the transcript.- A model picker appears before a harness’s first launch in a conversation. The choice is then fixed for that harness in that conversation. Switching the conversation’s agent clears it and offers the new agent’s models. claude-code only today (#814).
apps/rivethub-web: prevent sidebar and composer node pickers flashing while discovery is pending or failed with one saved node; keep multiple saved nodes available immediately (#809).apps/rivethub-web: hide node pickers only when connected to the sole saved node (or the app origin with no saved nodes) and mesh discovery confirms no peers; keep discovery and first-peer saving available.apps/rivethub-web: a send refused because the Terminal input holds unsent text (reason: 'harness_draft') showsDRAFT_NOTE. The outbound pump retries that 409 up to six times (about 57s) and then leaves the turn queued; press the inject button again after the draft is sent or cleared.- Agents: drag a row, or Alt+↑/↓ on a focused row, to reorder. The order is saved per preset on its hosting den. A den that does not support ordering is reported.
- Sidebar Agents: the agent bound to the active chat is marked (accent bar, ringed swatch and highlighted name; a folded list or the collapsed rail keeps a dot for it, and the row carries
aria-current). - Ctrl+Tab / Ctrl+Shift+Tab cycle agents in the sidebar roster order; Ctrl+Shift+E toggles the rail (wide) or the narrow drawer. Capture-phase listeners handle the chords before the terminal sees them. Agent cycling works in the RivetHub desktop app only (browsers reserve Ctrl+Tab).
Harness integrations
Section titled “Harness integrations”- Grok Bot capture (
integrations/grok-bot/rivet-memory, plugin 0.3.0): one normalizer on@rivetos/capture-corefor on-disk jsonl and ReadTranscript pages. Strips wrapper noise, stamps real message times (UTC offset + N ms inheritance), tags each bot from its profile name (historical session keys unchanged), stores hidden turns asrole=system, readstool_result.result, and caps atcapForStorage16,000. Re-clean writes new-v3sessions only — never DELETE/UPDATE. opencodeharness (idopencode, provideropencode-cli, rosteropencode) surfaced in RivetHub web, Android, and docs. Defaultmodeliszai/glm-5.3-flash. The installed OpenCode CLI owns backend, endpoint, and credentials.pimemory capture (integrations/pi/rivet-memory): v3 session jsonl watcher underagent=rivet-deepseek/channel=pi, systemd user unitpi-memory-capture.service/ launchddev.rivetos.pi-capture, wired intorivetos plugins installand doctor.
Tooling
Section titled “Tooling”scripts/worktree-reap.sh: the post-merge cleanup step. Lists (and with--applyremoves) worktrees whose branch’s PR is merged or closed, or whose detached HEAD is onorigin/mainor belongs to a mergedwt-pr-NNNNreview checkout, when the tree is clean and no process is inside; then deletes those branches and prunes remote refs. Dirty trees and open PRs are skipped and listed. Added after an audit found ~200 abandoned worktrees (~25 GB) across two checkouts.
Harness
Section titled “Harness”claude-codetask isolation, opt-in:tasks.harnesses.claude-code.isolationisinherit(default, unchanged) orisolated, and a task can tighten it withspec.isolation: isolated(the node setting is a floor: a spec cannot loosen an isolated node). A delegated run used to load the service user’s whole personal Claude Code setup — settings, permission rules and default mode, hooks, plugins and their MCP servers, and the user-levelCLAUDE.md.isolateddrops all of it (--setting-sources project), loads only the embedded RivetOS bridge (--strict-mcp-config), supplies the RivetOS capture hooks itself so task transcripts keep working, and denies every permission prompt explicitly whenpermission_promptsis unset — which closes the gap where MCP tool prompts escaped the auto-deny through the operator’s allow rules.tasks.harnesses.claude-code.allowed_toolsis passed as--allowedToolsfor what a headless run may call without a prompt. See CONFIG-REFERENCE →tasks.harnesses→ Task isolation (#1053).- Harness model lists are discovered from the harness itself, with config able to override or supply them. Codex’s sheet now comes from the CLI’s own catalog (
codex debug models, run in the background with a bounded timeout, with<codex home>/models_cache.jsonas the floor until the listing lands): listed models in priority order, per-model reasoning efforts (max/ultrawhere the catalog has them), andconfig.toml’smodelas the default (added as a row when the catalog does not know it, the custom-gateway case). The Codex sheet carries--model <id>and-c model_reasoning_effort=<effort>so a model picked for a Codex agent reaches a term-spawned TUI, and declareslaunchModel; the old placeholderdefaultrow is gone. Async sources (the Hermes endpoint, the Codex listing) share one background cache with its own deadline (the subprocess is SIGKILLed at the timeout): a hung or failing listing leaves/api/harnessesserving the last-known or static list, is retried on the next TTL, and logs one line per outage.tasks.harnesses.<id>.models_modeisdiscover(default),replace, ormerge(discovered plus config entries, deduped by id, config winning on label / default / efforts); amodelslist with nomodels_modekeeps today’s replace meaning.capabilities.modelsSource(discovered|config|merged|static) says where the list came from. An agent preset naming a model that is not on the resolved list is refused (400) under a non-emptyreplacelist (modelsSource: config) and stored with one warning otherwise (only when the harness/model pair changes, so a round-tripped form save is never refused), and the spawn log names the harness and the list’s source when it omits--model. Behavior change for existing configs: atasks.harnesses.<id>.modelslist was always a replace and now also pins preset saves to it. For Codex, the legacy placeholder iddefaultmeans “the CLI’s own default” (no flag, never vetted). A config-supplied Codex list declareslaunchModeleven before discovery has rows, and on the Codex app-server driver a configdefault: trueis the only default. A Codex preset’s effort now reaches the TUI as-c model_reasoning_effort=<effort>, overridingconfig.toml’s value for that session (presets default tomedium). See CONFIG-REFERENCE →tasks.harnesses→ Model lists. cursorharness (idcursor, roster commandcursor) in RivetHub web and Android. The den spawnsagent --force --trustand resumes withagent --resume <chatId>. Transcripts are read from~/.cursor/projects/<slug>/agent-transcripts/. No pin flag and no headless task executor.grok-cliprovider:max_turnsdefaults to unset, so--max-turnsis only passed when configured (#951). The old default of1ended every tool-using grok turn aserror_max_turns— grok heartbeats and mesh tasks failed with a baregrok result is_error. A failed result now carries grok’ssubtypein the error (e.g.grok result is_error: error_max_turns).claude-cliprovider: theclaude --versionavailability probe is bounded (15 s, then SIGKILL, reported unavailable) and shared between concurrent callers. It had no timeout, andRuntime.start()awaitsrouter.healthCheck()before starting channels, so aclaudethat never exits (e.g. a wrapper script whoseexec clauderesolves back to itself) held the gateway channel and health endpoint down indefinitely.piharness (earendil-works/pi, providerpi-cli, roster commandpi) on RivetHub web + Android, with a commented@rivetos/provider-pi-cliconfig example (recommended default backend z.ai GLM).- feat(harness): add qwen-code — Qwen Code CLI as the eighth first-class harness (driver, provider, executor, hooks-driven memory capture via a qwen extension, web + Android).
hermes-cli:modelis still passed tohermes chat -m, exceptcustom:<provider>:<model>, which is sent as--provider <provider> -m <model>before--inand--resume. The model part keeps further colons (custom:local:qwen3.5:27bis providerlocaland modelqwen3.5:27b).-mof the wholecustom:…string is a model name on the default endpoint, which does not resolve it.- Den-spawned Hermes (interactive PTY,
appendModelEffortArgvon the hermes sheet) applies the samecustom:<provider>:<model>split. Other harness sheets are unchanged, including qwen-code, which also uses-m. - RivetHub client — new chats and registered harness sessions open in Chat; legacy sessions wait for registry resolution and fall back to Terminal on failure, with the settled view remembered across navigation.
RivetHub client
Section titled “RivetHub client”- Sidebar Agents: pre-fill the existing create form from unsaved edits when a preset’s node fails to load harnesses, validate settings against another reachable node, and create a copy; the original stays on its node and can be deleted when it is reachable again. Surface save and delete failures.
RivetHub client
Section titled “RivetHub client”- Chat: preserve in-flight sends, ordering, and visible failure/retry across draft adoption and native-id rotation (#795).
- Chat: a committed user turn retires the optimistic bubble of the send it confirms — by item id when the echo carries one, otherwise the accepted/sending bubble before a failed twin of the same text — so repeating a message can no longer retire the wrong bubble or strand the committed send’s own.
Breaking
Section titled “Breaking”- Per-user memory routing reads only the users.json registry (
RIVETOS_USERS_FILE, else$RIVETOS_SHARED_DIR/rivetos/users.json, else~/.rivetos/users.json). TheRIVETOS_USER_DBSandRIVETOS_DEN_DEVICE_USERSenv maps are removed — leftover values do not route. - Removed the
deepseek-harness(dsh) harness —HARNESS_IDStoken,@rivetos/harness-deepseek,integrations/deepseekcapture plugin, and thedeepseek/dshpreset aliases (presets using them no longer map to a harness); nodes with adshroster entry in~/.rivetos/den-term.jsonneedrivetos plugins install --forceor a hand edit.
[0.5.0] - 2026-08-30
Section titled “[0.5.0] - 2026-08-30”First stable release. Everything since the 0.4.0 public beta: gateway + RivetHub, den, harness control plane, memory wiki, device mTLS, and per-user tenancy. Workspace packages align at 0.5.0 (RivetHub Electron keeps its own 0.5.4 updater cadence).
Runtime / mesh
Section titled “Runtime / mesh”- Self-registering plugin manifests (
PluginManifest+register(ctx));transportcategory; in-process@rivetos/mcp-server. - Providers: dedicated
@rivetos/provider-vllmand@rivetos/provider-llama-serverreplaceopenai-compat;@rivetos/provider-claude-clidrives localclaudewith an embedded MCP bridge. - Agent loop on the AI SDK (
@rivetos/aisdk); providers migrated to official AI SDK packages. - Mesh mTLS (shared CA, HTTPS agent channel,
mesh.tls,.meshDNS). Breaking: all mesh nodes must upgrade together.mesh.secretis ignored for agent-channel auth (warning on load); remove it from config. rivetos mesh enroll/mesh sync/mesh renew(SSH hub helper, unpack issued certs +mesh.json); doctor warns when the leaf expires within 30 days. Breaking:mesh join <host>without--manualexits non-zero — usemesh enrollormesh join --manual(#599).- Durable task engine (
ros_tasks): chat-loop executor, heartbeats, subagents, mesh delegation over shared Postgres, evaluation/retry/escalation. - Gateway embedded in the rivetos process:
/api/tasks, catalog, sessions, notifications WS, uploads, wiki, memory, workflows. - Workflows v1: journal-replay engine, step SDK, budget/
parallel, gateway + RivetHub runs UI (#438, #441–#446). - MCP unification: core + sidecar split, era-negotiating stdio, MCP 2026-07-28 final / SDK 2.0 (#275, #276, #435, #451).
RIVETOS_INSTALL_ROOTandRIVETOS_SHARED_DIRreplace hardcoded install-root and shared-dir defaults (#595, #590).- Identity contract in workspace templates — verify before assuming, maintain the user roster (#594).
- Removed: Pulumi IaC, split container images, Telegram/Discord/voice-discord channels (Phase 5, #490), unused circuit-breaker/audit-rotation exports (#463), Rivet Team product (#530).
Memory / wiki
Section titled “Memory / wiki”- Memory v5 compaction (leaf/branch/root + tool-call synthesis) and hybrid search (FTS + trigram + vector/RRF).
- Compaction and embedding moved onto graphile-worker (
services/{compaction,embedding}-worker). - Memory wiki: page model, extract/consolidate/recompile, durable topics and Wikipedia-style articles,
/api/wiki+ hub UI,wiki_search/wiki_read(#285–#292, #414, #416). memory_get_fulldisk-pointer recall;window=shortcuts; tool rows excluded from browse by default (#546); tool_result search/embed (#440, #482).- Per-user memory routing (device identity → per-user database) (#561);
/api/memoryrouted by the den-stamped user (#571). - Compaction skips heartbeats (#518); deadlock retry + wiki enqueue after commit (#542);
rivetos memory retry-failed(#508).
RivetHub client
Section titled “RivetHub client”apps/rivethub-web: chat-first hub over the gateway — transcript, composer, node switcher, terminal, files, memory Search/Browse/Stats, wiki, workflows/flows canvas (#295–#307, #428–#433, #502, #550).- Desktop shell migrated from Tauri to Electron (
apps/rivethub-electron) (#555); in-app updates from the mesh filestore (#562). Electron stays on its own 0.5.x updater cadence (0.5.4 as of #587). - Composer attachments, interactive ask card, voice mode (#576); per-session node binding (#583); agents roster in the sidebar (#549).
- Loopback mTLS pipe so the desktop client presents device certs to the gateway (#494).
apps/rivet-bots-android: Grok Bot-style client where every bot is a mesh agent node (#541, #544, #545).
- New stack:
@rivetos/den-protocol,@rivetos/den-server(harness event contract, PTY terminals, mesh overview). - Den embedded in the rivetos process; harness adapters (Claude Code, Grok Build, later Hermes/Kimi/DeepSeek); MicBridge host-mic input (#418); voice transcribe/speak proxy (#574).
- Harness control-plane fencing per session owner (#580, #582); idle auto-close of unattached harness PTYs (#439, #514).
- DeepSeek
dshTUI driver (#539).
Harness integrations
Section titled “Harness integrations”- Harness control-plane contract +
SessionIdcodec (#456); drivers for Claude Code (#458), Grok Build (#472), Hermes (#477), kimi-code (#480), DeepSeek (#539). - Rotation re-keying and superseded lifecycle (#470); reasoning-delta on the contract (#469); gateway attachment staging (#465).
- Capture plugins: Grok Build, Hermes, Kimi Code, Grok Bot memory bridge (#517); kimi-code headless executor + wire.jsonl backfill (#474, #481).
- RivetHub and Android chat bound to the harness control plane (#466, #479).
Tenancy / multi-user
Section titled “Tenancy / multi-user”- Mesh device enrollment (QR pairing) and per-device datahub credentials (#357, #366).
- Gateway Rivet CA device mTLS; bearer tokens removed (#491). Breaking: clients must present a device cert.
- Resolve the user at the den edge and filter RivetHub by owner (#565); wiki/memory/harness surfaces honor the routed user (#571, #579, #580, #581).
- Early per-user identity (profile
USER.md+ memory tag) generalized into the owner-user model.
Release / infra
Section titled “Release / infra”- Unified
rivetoscontainer image (--role); GHCR publish;@rivetos/cliinstallable vianpm install -g. - Versioned SQL migrations as source of truth; Nx module-boundary enforcement; secret scanner (#363); commit authorship check (#552).
docs/RELEASES.mdrelease policy; docs truth-sweep to Phase-5 reality (#589, #593).- Workspace versions normalized to 0.5.0 for the first stable tag (this release). Lockfile refresh (
npm install) must land in the PR — CI runsnpm ci.
[0.4.0] - 2026-04-05
Section titled “[0.4.0] - 2026-04-05”First public beta. Containerized distribution, reliability hardening, and launch documentation across three internal milestones (M6 containers, M7 reliability, M8 docs).
- Containerized distribution: agent + datahub Dockerfiles, root
docker-compose.yaml, Nx container build targets,DATA-PERSISTENCE.mdmodel, CI pipeline. rivetos build,rivetos initinteractive wizard,rivetos config,rivetos agent add/remove/list,rivetos updatesource-based update flow.- Pulumi infrastructure components and
rivetos infra up/preview/destroy(later removed in 0.4.x). - Reliability primitives:
RivetErrorhierarchy, channelReconnectionManager, provider circuit breaker, memory connection pooling, structured logging, audit log rotation. - Observability:
rivetos logs, runtime metrics,/health,/health/live,/metricsendpoints, enhancedrivetos statusandrivetos doctor,rivetos testsmoke suite. - Secret management:
redactSecrets(),.envpermission enforcement, config secret validation, 1Passwordop://resolution. - Multi-agent mesh:
FileMeshRegistry,MeshDelegationEngine, mesh HTTP endpoints,rivetos meshCLI,rivetos init --join,rivetos update --mesh. - Launch docs:
GETTING-STARTED,CONFIG-REFERENCE,PLUGINS,SKILLS,DEPLOYMENT,TROUBLESHOOTING, example configs,rivetos plugin init,rivetos skill init,rivetos skill validate.
Changed
Section titled “Changed”- Node.js requirement: 22 → 24.
- All package versions normalized to 0.4.0 (was unreleased 1.0.0 placeholders).
- Containers moved from
containers/toinfra/containers/. - Plugin discovery is convention-based via
package.jsonrivetosfield; all plugins exportcreatePlugin()factory. - Root
package.jsonno longer leaks plugin dependencies (onlyyamlremains).
Removed
Section titled “Removed”- Backward-compat runtime shim
core/src/runtime.ts. - Architecture violation:
memory-postgres/review-loop.tsno longer imports from@rivetos/core.
Long-form release narrative archived outside the repo:
0.4.0-milestone-6-containerized-distribution.md,0.4.0-milestone-7-reliability-polish.md,0.4.0-milestone-8-documentation-launch.md.
[0.0.8] - 2026-04-03
Section titled “[0.0.8] - 2026-04-03”Changed
Section titled “Changed”- License — changed from MIT to Apache License 2.0. NOTICE file added.
- Documentation overhaul — updated all markdown files to reflect current architecture and features.
- Deleted
CODE_OF_CONDUCT.md,REFACTOR_PROGRESS.md,docs/PHASE2.md,docs/MILESTONE-2-3-ANALYSIS.md(obsolete).
[0.0.7] - 2026-04-03
Section titled “[0.0.7] - 2026-04-03”Changed
Section titled “Changed”- Runtime decomposition —
runtime.ts(576 lines) split into focused modules:runtime.ts(296 lines) — thin compositor, registration, routing, lifecycleturn-handler.ts(263 lines) — single message turn processingmedia.ts(105 lines) — attachment resolution, download, multimodal contentstreaming.ts,sessions.ts,commands.ts— already extracted, unchanged
- Delegation/subagent/skills registration moved from
Runtime.start()toboot/registrars/agents.tsfor consistency with other registrars. - Net -280 lines from runtime. Runtime no longer knows about images, base64, content parts, history management, hook execution, or memory appending.
[0.0.6] - 2026-04-03
Section titled “[0.0.6] - 2026-04-03”- Boot package (
@rivetos/boot) — composition root properly decomposed:config.ts— YAML config loading with env var resolutionvalidate.ts— schema validation with structured error/warning reportinglifecycle.ts— PID file, signal handlers, shutdownregistrars/providers.ts— provider instantiationregistrars/channels.ts— channel instantiationregistrars/hooks.ts— safety, fallback, auto-action, session hook wiringregistrars/tools.ts— tool plugin registrationregistrars/memory.ts— memory backend wiringregistrars/agents.ts— delegation, subagent, skills registration
typechecktarget on all 21 nx packages —tsc --noEmitcatches type errors independently per package.- Typing indicators for Discord channel plugin (same pattern as Telegram — channel-managed, runtime-agnostic).
- Message splitting in channel plugins — Discord (2000 char) and Telegram (4096 char) handle overflow internally. Runtime has zero knowledge of message length limits.
- Safety cap fix — when agent hits tool iteration limit, preserves the accumulated response text instead of replacing it with a generic message.
Changed
Section titled “Changed”- CLI rewired — imports from
@rivetos/bootinstead of../../../../src/boot.js. No more rootDir violations. - Telegram typing refactored — typing indicator management moved from internal
handleMessage()wrapping to publicstartTyping()/stopTyping()methods, then back to channel-internal management (matching Discord’s pattern). Runtime doesn’t touch typing. - 21/21 packages typecheck clean — fixed ~138 type errors across the monorepo (config types, tool result types, delegation types, missing tsconfigs).
Removed
Section titled “Removed”src/boot.ts— 500-line god file replaced by@rivetos/bootpackage with 7 focused files.src/config.ts,src/validate.ts— moved topackages/boot/src/.
[0.0.5] - 2026-04-02
Section titled “[0.0.5] - 2026-04-02”rivetos logs— tail runtime logs with filtering (--lines,--follow,--since,--grep,--json). Wrapsjournalctlfor systemd service, falls back to log file reading.rivetos skills list— discovers all skills fromskill_dirs, parses SKILL.md frontmatter, shows name/description/trigger count.rivetos plugins list— enumerates configured providers, channels, memory backends, and tools with status (configured / available / missing-key).rivetos login— OAuth login for Anthropic subscription auth.
Changed
Section titled “Changed”- CLI extracted to
@rivetos/cli(packages/cli/) — independent Nx package with ownpackage.json,tsconfig.json, build/test targets. Enablesnx run cli:build,nx run cli:test, affected-only testing, and Nx caching. Oldsrc/cli/removed. @rivetos/clipath alias added totsconfig.base.json.- Root
binentry updated to point topackages/cli/src/index.ts.
Milestone
Section titled “Milestone”- 0.5 — CLI Tools: Complete. All planned CLI commands shipped.
mesh list/ping/removemoved to Milestone 6.6 (Fleet Management).
[0.0.4] - 2026-04-02
Section titled “[0.0.4] - 2026-04-02”- Config validation engine (
packages/boot/src/validate.ts) — schema validation on startup with structured error/warning reporting- Missing required fields, invalid types, unknown keys
- Cross-reference validation: agents ↔ providers, heartbeats, channel bindings, coding pipeline
- Warns on hardcoded API keys/tokens in config (use env vars)
- Warns on out-of-range values (temperature, max_tokens)
- Human-readable error messages with config path and available options
rivetos config validateCLI command — dry-run config validation without starting the runtime- Upgraded
rivetos doctor— now runs schema validation, config-aware env var checks, and provider connectivity tests - 62 unit tests for config validation covering all sections, cross-references, edge cases
ConfigValidationErrorthrown on boot with formatted output when config is invalid
Changed
Section titled “Changed”loadConfig()now validates schema before resolving env vars — catches structural issues earlyrivetos doctorversion bumped to match package version- Root test script now includes validation tests alongside Nx project tests
[0.0.1] - 2026-03-28
Section titled “[0.0.1] - 2026-03-28”- Core runtime with agent loop, router, workspace loader, message queue
- Streaming-first provider interface (
AsyncIterable<StreamEvent>) - Domain-driven design with clean architecture (types → domain → application → plugins)
- Providers: Anthropic (with OAuth subscription auth), Google Gemini, xAI Grok, Ollama, llama-server
- Channels: Telegram (grammY) with typing indicator, inline buttons, reactions
- Memory: PostgreSQL adapter with full transcript archive, summary DAG, hybrid FTS+vector search
- Tools: Shell execution with safety categorization and AbortSignal support
- Full command surface:
/stop,/interrupt,/steer,/new,/status,/model,/think,/reasoning - Message queue with deterministic behavior (commands immediate, messages queued)
- Session persistence across restarts via Memory plugin
- Thinking level control (off/low/medium/high) mapped to provider-specific parameters
- CLI:
rivetos start/stop/status/doctor/config/version+ provider commands - Toggleable structured logging via
RIVETOS_LOG_LEVELenvironment variable - YAML configuration with
${ENV_VAR}resolution - GitHub Actions CI
- Apache 2.0 license
