Logger.Sub(tag) derives a child logger with a new tag from the same
writers, with level inherited from the parent but independently
overridable via OLLIE_{TAG}_LOG.
The agent creates an "audit" sub-logger at startup. Key events are
logged at debug level through it:
audit: call: <tool> <args (truncated)>
audit: result: <tool> <content (truncated)>
audit: error: <message>
audit: turn: start/end with cost and session ID
audit: compact: messages removed, trigger, session ID
Enable with OLLIE_AUDIT_LOG=debug.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Usage events now carry a 4th field (costUSD) so OpenRouter's reported
cost flows through unchanged. For backends that only report token counts
(Anthropic, direct OpenAI, Ollama), cost is computed from a prefix-keyed
pricing table. The higher-fidelity source always wins.
Per turn:
- turnInputTokens/turnOutputTokens/turnCostUSD reset before each turn
- recordTurnCost() stores LastTurnCostUSD, adds to SessionCostUSD
- "costLast=$X.XXXX" emitted as an info event at turn end
Session file "cost" returns costLast/costSession key=value pairs.
Agent interface gains Cost() string.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Emits "context at N% — will auto-compact at 75%" on the turn that
crosses the warn threshold, then suppresses further warnings until
the next compaction resets the flag.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously only HTTP 429 triggered a retry. Network failures, 5xx
responses, and mid-stream drops all hard-failed the turn.
- Add TransientError for 5xx and network-level failures; streamRequest
wraps client.Do errors and >=500 responses in it
- Merge pre-stream and mid-stream retry into one loop (maxTransientRetries=3)
covering rate limits, transient errors, and pure stream drops
- On stream drop with ctx cancelled: record partial state and return
(existing pause behaviour preserved)
- On stream drop without cancellation: close any open <think> block,
emit retry countdown, restart ChatStream from the same history
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New session file 'env' (mode 0444) returns OLLIE_SESSION_ID and all
OLLIE_ environment variables from the server process. Frontends can
read it to populate shell environments without depending on core.
Also fix pre-existing race in TestSessionFileStoreWritePrompt.
Commands like /compact arriving via ctl and prompts arriving via prompt
both call Core.Submit in separate goroutines. handleCommand for mutating
commands (compact, clear, agent, backend) modifies session state without
setting currentAction, so IsRunning() returns false and a concurrent
executeTurn can race.
Add submitMu held during the serialized path of Submit. The fast path
(inject/FIFO push while running) remains lock-free.
Also fix pre-existing race in TestSessionFileStoreWritePrompt: replace
time.Sleep synchronization with a channel signal.
With mid-turn SaveSession in place, interrupt keeps in-memory session
state matching the on-disk state rather than restoring the pre-turn
snapshot. The model resumes naturally from the saved history on the
next turn.
Remove sessionClean — no longer needed now that all interrupted sessions
are kept rather than conditionally discarded.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds SaveSession callback to agentConfig, called after every state.update()
in the run loop. On restart, the session JSON contains all completed tool
rounds; the model continues naturally from current history.
SIGTERM is already handled in olliesrv via InterruptAll + Shutdown, which
cancels all session contexts and lets the loop exit cleanly after the
current state.update(). Worst-case (SIGKILL) loses one tool round, not
the whole turn.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The function only appends to a slice and can never fail. Removing the
dead error return eliminates the nolint:errcheck suppression on the
interrupted-stream path in loop.go.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add tests for FormatParams/ParseParams round-trip and error paths,
Session.Interrupt, createSession via NewCore injection, and openEntry/
OpenStore not-found paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers detectParallelClass, sanitizeLockName, acquireFlock, classifyStep,
SetLockDir, and runReadBatch — bringing pkg/tools/execute from 70.9% to 92.4%.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NewWithName and newFromEnv now error when OLLIE_BACKEND is unset
instead of falling back to ollama silently
- createSession reads OLLIE_MODEL as fallback when model= is not given
- Update test to assert error on missing OLLIE_BACKEND
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
System prompt is now assembled via x/prime in the agent prompt array,
same as all other prompt fragments. Remove expandSystemPrompt and
the agentPrompt field — resolved prompt goes directly into preamble.
Prompt field in agent JSON now accepts either a string (existing
behavior) or an array of strings. Each array element is executed as a
shell command; their combined stdout becomes the agent prompt.
- Add config.Prompt type with custom JSON unmarshaling
- Rewrite resolvePrompt to handle both modes
- Wire resolvePrompt into BuildAgentEnv
Keep session state after an interrupt if the last message is a clean
assistant text response (no dangling tool calls). Roll back to pre-turn
snapshot if the history is inconsistent — mid-tool-call interrupts would
send malformed history to the backend on the next turn.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Interrupting a long turn previously cleared the session (first-turn case)
or rolled back messages (subsequent turns). An interrupt is intentional —
keep whatever state was built so the user can continue.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DeepSeek's native tool-call format uses <|DSML|invoke> tokens. When
OpenRouter fails to convert them to standard tool_calls chunks, they
arrive as raw content text. Detect the marker, buffer the content, and
parse it into ToolCall structs at [DONE] if no structured tool calls
accumulated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Session.plan field and SessionFile handler for 9p endpoint s/{id}/plan.
Agents write markdown checklists here via task_add/task_check/task_clear;
peers and parent sessions can read it directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Snapshot session state before each turn. On any error from run(),
restore to the pre-turn snapshot so the session is clean for the next
attempt. Remove recordInterruption — those messages were appended to
state that gets rolled back anyway, making them dead code.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
preTool fires before each tool execution; exit 2 blocks the call and
returns the hook's stderr as the tool error result. postTool fires after;
exit 0 appends context to the result, exit 2 replaces it entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Non-blocked postTurn hook output is now appended to session history so
the model sees it at the start of the next turn without triggering
autonomous continuation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract runCompact() to handle the full compact cycle (pre-hook, compact,
spawn-context re-injection, post-hook) consistently across all three call
sites. Wire AutoCompact into agentConfig so loop.go triggers it after each
tool round, not just before turns.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rename loopConfig→agentConfig, agentCore→agent, loopcfg→cfg, systemPrompt→preamble
for clarity: the struct represents agent runtime config, the field is its config,
and preamble accurately describes the compiled system+agent prompt sent as system role
- Separate preamble from agent context: preamble now contains only SYSTEM_PROMPT.md;
agent prompt and agentSpawn hook output are injected into session history at
context refresh points instead of being bolted onto the system role
- Replace fireAgentSpawn (mutated preamble once, guarded by agentSpawnFired flag)
with spawnContext (assembles agent prompt + hook output, returns string for caller
to inject); remove agentSpawnFired entirely — session==nil is the sufficient guard
- Three refresh points inject spawn context into session history:
session start, post-clear (session=nil triggers same path), post-compaction
(auto and manual)
- Move all preTurn tool primes (tools-file, tools-reasoning, tools-memory,
tools-subagent, tools-elevate) to agentSpawn in all agent configs so they are
injected at session refresh points alongside bd prime, not repeated every turn
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Default to $XDG_RUNTIME_DIR/ollie/elevate.sock (falling back to
/run/user/<uid>/ollie/elevate.sock) so the daemon sets and propagates
the value without requiring explicit configuration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add paths.ExpandHome() helper for tilde expansion
- Apply in NewAgentCore, SetCWD (agent + execute server), and hook runner
- Fixes 'chdir ~/path: no such file or directory' errors
- Report non-zero size for wait files so FUSE clients read them
- Return current state on timeout instead of nil
- Set ChatOffset after user message so offset points past it
- Comment out hooksRan info events (TODO: route to debug file)
Batch jobs were a separate concept (one-and-done lifecycle, distinct
dirspec) that turned out to be a front-end concern. s/b, s/bfg, and
s/bbg now compose session primitives directly: create session, submit
prompt, loop statewait until idle, extract chat[offset:], kill.
Also renames spec -> cfg throughout (session config file) and removes
the parallel/output fields that previously lived in the batch spec;
parallel is now a shell loop, output=json is a prompt suffix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove state, backend, model, agent, cwd, params, cwdwait, usagewait,
ctxszwait as individual files. Add spec (0666) which on read returns a
KV snapshot of all config and current state; on write, dispatches
partial KV pairs to the appropriate handlers (backend/model/agent via
Submit, cwd via SetCWD, generation params via SetGenerationParams).
Read-only fields (state, usage, ctxsz, offset) are silently ignored on
write. Only statewait survives from the *wait set — it has genuine
control-flow semantics; usagewait/ctxszwait/cwdwait are better served
by post-turn hooks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SessionStore now embeds a *BatchStore and presents a unified
interface. s/new detects batch format (--- delimiter) and
delegates accordingly. list/stat/del/OpenStore/index all check
both session and batch maps. Shutdown/InterruptAll delegate to
both. Batch scripts (job, q, sched, cleanup) added to session
store file table.
This is the store-side half of Option C: single namespace with
composable file tables. The 9p server changes are in the parent
repo.
Introduce FileSpec and RunFileStore as a shared, table-driven
implementation of RunnableStore. Both SessionFileStore and
BatchJobStore are now type aliases for RunFileStore, constructed
from a list of FileSpecs that define per-file read/write/wait
behavior via function pointers.
This eliminates ~94 lines of duplicated store plumbing (stat/list/
open/entryFor methods) while preserving the existing filesystem
layout and all public API signatures.
No behavioral changes. Tests pass unchanged.
assistant events are streamed without trailing newlines, causing
line-buffered 9P reads to lag one turn behind. Close the assistant
block with \n on role transitions and after Submit returns. Set
ChatOffset at EOF before the user event so offset spans the full
exchange. Remove 500-char truncation of tool call args in FormatEvent.
Adds a synthetic `tail` file (mode 0555) to each session directory
that runs `tail -f chat`, allowing users to follow a session's chat
log from any working directory.
expandSystemPrompt fell through to os.Getenv("PWD") which always
returned the process startup directory. Map "PWD" explicitly to the
cwd parameter so the system prompt reflects the session's working
directory.
- Test all expandPath branches (HOME, XDG_*, OLLIE_CFG/DATA_PATH)
- Test checkPath symlink resolution and ROX read/write
- Test LoadSandbox reader error
- Test WrapCommand: landrun unavailable, ROX/RWX entries, sort tiebreaker, superpowerd wrapping
- Test isSuperpowerdRunning: all socket dir paths, successful connection
- Make isAvailableFn/isSuperpowerdRunningFn overridable for testing
- Add Warning field to HookResult
- runHookCmd sets Warning on timeout and start failure
- Hooks.Run collects warnings across commands
- All hook call sites emit warnings via infoEvent
- Test verifies timeout warning is surfaced