Previously a session tracked only one pending bypass request. If
agent A1 had a pending bypass, agent A2's bypass request would block
waiting to be read from toolsrv's channel, effectively blocking all
agents in the session.
Now bypassPending is a map keyed by request ID:
- SetBypassPending adds to the map instead of overwriting
- BypassPending returns all pending requests (slice)
- BypassPendingByID returns a specific request
- ResolveBypass removes from the map by ID
- 9P bypass file returns JSON array of all pending
The bypass loop reads requests continuously without waiting for
resolution, so multiple agents can have concurrent pending requests.
- Agent cwd is optional; empty = inherit session cwd (the common case)
- Session cwd is required at creation and is the inheritance root
- toolsrv maintains agentCWD map, resolved per call from agent= field
- Override set via agent cfg (cwd=...) or ctl (cwd [<dir>|-])
- Agent.SyncCwdToToolServer re-pushes on every (re)connect
- GUI NewAgentDialog shows '(inherit: <sessionCwd>)' as placeholder
- proc_test.go covers per-agent cwd isolation
- Kate and acme scripts updated for new session/agent creation flow
- AGENTS.md documents the architecture
Backend:
- Add proc.start/proc.exit events for background process lifecycle
- Add proc idx ctl command for machine-readable process listing (TSV)
- Add event.pub file for external event publishing
- Add ListProcsIdx to toolclient and toolsrv
- Fix bypass commands to use Setpgid for process group isolation
GUI:
- Add configurable bypass approval shortcuts (Ctrl+Y/Ctrl+N default)
- Add Keyboard Shortcuts section to Settings dialog
- Store shortcuts in theme.conf
Kate:
- Fix 'Start Session Here' - use rdwr for session/new endpoint
Changed from 0600 (owner only) to 0660 (owner + group):
- ctl: frontends need to send slash commands
- plan: frontends need to read/write plan
- fifo: frontends need to submit prompts via queue
The agent group includes frontends, so group permissions enable
frontend interaction while still maintaining ownership-based isolation.
Per-agent file ownership with Unix permission enforcement:
- Agent directories owned by agent ID (UID), group 'agent' (GID)
- Private files (plan, ctl, fifo): mode 0600 - owner only
- Group-readable (chat, log): mode 0440 - owner + agent group
- World-readable (state, id): mode 0444 - observable by all
- Prompt: mode 0220 - CLI and owner can write
virtfs: fix UID/GID inheritance through nested paths
- Added findChildWithInheritance() to accumulate inherited UID/GID
- Stat now correctly shows agent ID as owner for nested files
server: admin bypass for server owner
- serverAdmin variable captures the Unix user running olliesrv
- Admin bypass includes empty uname, 'admin', or server owner
Documentation updates:
- fs/doc.go: 'The Namespace IS the Security Model'
- registry/doc.go: capability-based tool access
- peer.go: capability-based peer access
- lessons-learned.md: 'Model compliance is not a security boundary'
- architecture-9p.md: per-agent file ownership section
Security evaluation:
- Added experiments/security-eval/ with NERV attack corpus adaptation
- Test scripts for Landlock sandbox validation
- RESULTS.md documenting 0% ASR on hostile operations
This implements the NERV thesis: 'An agent can only access resources
explicitly bound into its namespace.' Enforcement is structural via
file permissions, not behavioral via model compliance.
The pubsub library had issues:
- Published to literal '*' topic (nonsensical)
- Used TrySend which drops events
- Complex hierarchical wildcard publishing
New implementation:
- Simple eventHub with map of subscribers
- PublishEvent fans out to all subscribers (blocking send)
- SubscribeEvents returns channel, cleaned up on ctx cancel
- SubscribeEventsFiltered filters client-side with MatchTopic
- Removed simonfxr/pubsub dependency
The feed file was documented but never used by any frontend
or script. The observer agent pattern was never adopted.
- Remove feed.go, FeedWrite, ConsumeFeed
- Remove WatchFeed constant
- Remove feed file from 9P namespace
- Remove ConsumeFeed goroutine spawns from session
- Update docs (architecture-9p, architecture-ide, architecture, usage)
The event stream now covers real-time observation patterns better.
The event stream with filtering replaces statewait:
- echo filter | rdwrs event
Removed:
- statewait file from agent namespace
- All non-historical references in docs and code
The state file remains for simple polling reads.
- state: immediate read of current agent state
- statewait: blocks until state changes
Clearer semantics than overloading statewait with both behaviors.
EventValue was storing only the latest event and using hash comparison,
which caused events to be overwritten if they arrived faster than the
client could read them.
Now eventwait uses Stream mode with EventStream which delivers each
event as it arrives. Events won't be lost due to rapid arrival.
Server changes:
- Session tracks pending bypass request and exposes methods
- New 9P files: session/{sid}/bypass (read/write), bypasswait (blocking)
- Publish bypass.request events for GUI listeners
GUI changes:
- Handle bypass.request events from eventwait
- Show inline amber banner with command and cwd
- Approve/Deny buttons resolve via 9P
Desktop notifications still work in parallel for non-GUI usage.
CreateEmpty now returns (session, created, error): if a session with the
given name already exists it is returned untouched instead of erroring,
and only a freshly created session receives the provided cwd/workflow/
variant. The o script no longer suppresses errors from session/new, so
real failures surface while re-creating an existing session stays a
no-op success.
Make the namespace explain itself instead of requiring prior knowledge.
- ctl is self-describing: reading it (empty write) or writing 'help'
returns the valid verbs with one-line descriptions; an unknown verb
errors with the valid list. Refactor dispatch to an ordered []ctlCmd
carrying descriptions; drop the undocumented '.' alias and the
drift-prone hardcoded help verb. Keep 'i' (drop 'inject') for fast
injects. o's ctl usage now reads the live listing.
- Errors carry severity + remediation. backend.ClassifyError maps the
typed errors to transient/config/fatal with a one-line fix; the error
event renders [[[error:<severity>]]] and a 'remediation:' line so a
human knows whether to wait or intervene.
- Add a human status file: 'thinking · 12s', 'calling shell · 3s',
'idle' — distinct from the machine-facing raw state. Wire the TUI bar
to it.
- Bare 'o' shows an overview of running sessions/agents with status, so
you don't need to know any names to get oriented.
Tests: dispatch help/unknown/routing, ClassifyError severity table.
The autoLoad name implied an automatic tool-loading path that no longer
exists; tools now come only from agent config plus the /tool_load ctl
command. Rename the AgentConfig.AutoLoad field (json autoLoad) to Tools
(json tools), rename LoadAutoLoadTools to LoadTools, and update all 14
agent JSON profiles and the tool-not-loaded error message.
Add per-model pricing to /models output. Format:
backend<tab>model[<tab>in<tab>out<tab>cache_read<tab>cache_write]
Pricing sources:
- Anthropic: hardcoded from official pricing, includes cache rates
- OpenRouter: parsed from API response pricing field
- Other backends: static lookup table fallback, marked with (e)
Changes:
- backend: Add ModelPricing/ModelInfo types, ModelLister interface,
static price table (Claude, GPT, Gemini, DeepSeek), LookupStaticPricing()
- openai: Parse pricing from API, implement ModelsInfo()
- anthropic: Implement ModelsInfo() with hardcoded cache rates
- fs/cache: Use ModelsInfo when available, fall back to static lookup,
format prices per 1M tokens with (e) suffix for estimates
- agent/cost: Use shared LookupStaticPricing instead of duplicate table
Allows user to background the current foreground tool process
by writing 'detach' to the agent ctl file. The agent can then
continue working without waiting for the tool to complete.
When the 'agent <profile>' ctl command switches profiles, the tool
registry now clears old tools and loads the new profile's autoLoad
list. Previously, switching profiles left the old tools loaded.
Changes:
- registry: add ClearAgent(agentID) to remove all tools for an agent
- server/proc: add ClearAgent wrapper and 'clear <agentID>' ctl command
- toolclient: add ClearTools() method to ToolsrvConn
- agent: SwitchProfile now returns *AgentConfig for tool reload
- fs/spec: agent ctl handler clears and reloads tools after switch
Rewrote system prompt to enforce immediate tool use:
- New 'Autonomous Operation' section: act first, report results
- Explicit list of unacceptable behaviors (narrating intentions, asking
permission for routine ops, producing text when tools should be called)
- Tools section: 'Use them without hesitation', concrete examples
- Skills section: 'if the task needs it, load it' — no asking
- Stronger sub-agent prefix: 'Do NOT respond with a plan. Call tools.'
Timeout fix:
- Add Timeout field to ToolInfo (protocol) and MetaFile (metadata)
- proc.go respects tool-declared timeout before falling back to 30s default
- subagent_spawn.meta declares timeout=0 (no timeout) so the tool is
never killed prematurely while waiting for the sub-agent to finish
- Tool schema declares timeout with 'do not set' guidance to prevent
the LLM from adding a short timeout
Premature response fix:
- Inject behavioral prefix into sub-agent prompt: complete all work
before responding, report results not intentions
- Sub-agent's final text is returned to parent; this instruction ensures
it contains accomplished work, not a plan
- Workflow() no longer defaults to 'conductor' when empty
- runWorkflow() returns immediately for '' or 'none'
- 'none' listed first in the workflows file output
- QML dropdowns default to 'none' instead of 'conductor'
- C++ fallback uses 'none' when server unreachable
- Add session-level cfg file (read: name/cwd/remote/workflow/variant/yolo;
write: workflow, variant, cwd)
- Add readSessionConfig() and updateSessionConfig() to C++ client
- NewSessionDialog supports editMode: pre-populates fields from session cfg,
title becomes 'Session Settings', button becomes 'Save'
- Name and Remote fields disabled in edit mode (non-reconfigurable)
- YOLO checkbox hidden in edit mode
- Only changed values are written back on save
- Add 'Settings...' to session context menu in SessionTree
- Wire sessionSettingsRequested signal through to dialog
- runWorkflow accepts a variant parameter; sources {workflow}-{variant}.conf
as env vars before exec'ing the script
- Session stores variant; persisted and restored
- session/new accepts variant= parameter
- 'run' ctl command accepts optional variant as second arg
- workflows file now lists variants: name<TAB>default,variant1,...
- review workflow reads AUTHOR_PROFILE/REVIEWER_PROFILE env vars
- Add review-code.conf and review-writing.conf example variants
Add peer/ directory to each agent's 9P namespace. Agents communicate
by writing to peer/{name}, which delivers to the target's prompt handler.
Only declared peers can be messaged — the directory is the ACL.
Implementation:
- Agent struct: peers map + AddPeer/RemovePeer/Peers methods
- fs/spec.go: peer/ Each node (write-only entries), peeradd/peerdel/peers ctl commands
- Bidirectional: peeradd A on B also adds B on A
- Peers constrained to same session
- Persisted with session state (PersistedAgent.Peers field)
- peeradd/peerdel trigger immediate session save
Docs updated: system_prompt.md, AGENTS.md, README.md, architecture-9p.md,
architecture-core.md, architecture.md, usage.md.
- Add MEMO_TOOLS=1 env var to memo script; when set, all printed
instructions reference native tool names instead of memo paths
- Set MEMO_TOOLS=1 in all memory tool .meta wrappers
- Add memory_nap tool for compressions
- Add memory_zoom tool for tree navigation
- Add part/T pagination args to memory_wake
- Update system prompt to use memory_zoom tool call
- Fix inject ctl: submit as user message when agent is idle
Session creation now requires cwd= parameter (no fallback to daemon cwd).
Agent cwd is read-only — agents inherit from session, cannot override.
Frontends updated:
- NewSessionDialog: added Directory field with Browse button
- NewAgentDialog: removed Directory field
- createAgent(): removed cwd parameter
- Kate plugin: passes cwd when creating kate session
- Dolphin: removed cwd from agent/new call
Message history sanitization (backend.SanitizeMessages):
- Removes dangling tool calls (assistant with ToolCalls but missing results)
- Removes orphan tool results (tool message without preceding call)
- Applied before sub-agent context inheritance
- Applied before session persistence save
- Applied after compaction (defensive)
Includes unit tests for all sanitization cases.
- ollie-9p: Fall back to USER when OLLIE_UNAME not set (remove fatal error)
- goal/goalstatus: Use mode 0666 (world readable/writable) while keeping GID agent
Server:
- session/idx now outputs: session-id, session-name, paused, connected, remote, cwd
- session/{s}/agent/idx now outputs: session-id, agent-id, agent-name, parent-id, depth, state
- Add error if parent agent not found during sub-agent spawn
GUI:
- refreshSessions() reads session index, then agent/idx per session
- Auto-select first top-level agent (depth 0) instead of first in list
- SessionModel tracks agent expansion separately from session expansion
- Agents with children show expand/collapse arrows and are collapsible
- Add hasChildren role to SessionModel
Tools:
- subagent_spawn now passes parent= argument for proper depth tracking
- Remove hardcoded max_depth=5, use server default of 1
- Makefile installs shell script tools from data/tools/
KRunner:
- Update to parse new session/idx format
- Workflows are executable scripts in data/workflows/
- New 'workflows' 9P file lists available workflows
- Goal file stores text; writing triggers workflow if status allows
- goalstatus file for status read/write, goalwait for blocking
- Session ctl accepts 'run [workflow]' command
- Session now owns CWD; agents inherit via callback
- Conductor workflow: creates agent, primes with instructions, exits
- GUI workflow combo reads from workflows, not agents
- Persistence includes goal, goalstatus, workflow, and session CWD
Write to session/{s}/goal to set a session-level objective.
A conductor agent is spawned automatically in the background,
decomposes the goal, spawns sub-agents, and reports completion.
- goal file: write sets goal + starts conductor; read returns status
- goalwait file: blocks until goal status changes (BlockOnce)
- Conductor writes status=complete/blocked back to goal when done
- Session.Goal() / SetGoal() / GoalSignal() on Session struct
Enforce three limits on sub-agent spawning:
- depth (default 1): sub-agents cannot spawn their own sub-agents
- parallelism (default unlimited): cap concurrent children per parent
- timeout (default 600s): sub-agents are killed after 10 minutes
Top-level agents are never constrained by timeout.
Also: refactored parseAgentNewRequest to return a struct instead of
4 positional values. Added depth/activeChildren fields to Agent.
OLLIE_SUBAGENT_DEPTH env var set on sub-agents.
Deferred: remove maxSteps (replace entirely with timeout).
Sub-agents can now inherit parent context truncated at a specific
user turn: fork_at=5 means 'fork from the state after the 5th
user message.' Enables backtracking without losing unsummarized
context from before a failed approach.
Without fork_at (or fork_at=0), full history is inherited as before.