Sessions, memory, goals
Three different things survive the end of a turn, and they survive it in three different ways.
Sessions
Every conversation is written to disk as it happens, message by message, as a JSONL file under ~/.local/state/task-agent/projects/<project>/sessions/, one folder per repository and shared by its worktrees. Sessions are keyed by working directory: listing and resuming only ever consider conversations started in the same place.
| Command | Effect |
|---|---|
revolve --list | The named sessions for this directory |
revolve <name> | Resume the latest session with that name, or start one under it |
revolve --resume | Resume the latest session, named or not |
/name <name> | Name the session you are in, at any time |
Unnamed sessions are saved too. They are reachable with --resume but are not listed, which is the difference naming makes. An interactive session that you do not name still gets one — amber, birch, cedar, the first word no other session of this directory is using, neither a running one nor one whose session file is still on disk — so it has an address for revolve attach and for a message from another agent, and a generated name can never resume a conversation you thought was over.
A turn also appends the running totals — tokens spent, turns taken, the last context reading — so a resumed session continues the count and fills the status bar before the first turn rather than starting blank. A line that is not a message is skipped when reading, which is what lets an older session file without those totals still open. On resume the previous conversation is replayed into the log and the model keeps its full history.
The session files are plain JSONL and can be read directly, which is the usual way to find out what an agent actually saw.
Compaction
A context window fills up. /compact summarizes the history into working notes and continues in a fresh session file, and the same happens automatically once the last request came within one turn of filling the declared context window. The split is always at a turn boundary, never between a tool call and its result. The standing goal is not in the history, so a compaction cannot lose it.
The summary is the second step, not the first. Before anything is sent to a model, every tool
result over prune-result-chars in the part that would be dropped anyway is cut down to
its first 4096 and its last 1024 characters, with a line in the middle saying how much is gone; the
history is then measured again, and only what still does not fit is summarized. Most of a long
session's weight is a handful of oversized outputs — a wide grep, a build log, a file read whole —
so the step that costs no model call and no message usually does the work, and the conversation
stays where it was instead of becoming notes about itself. A pruned result is still a result: the
call it answers keeps it, which is the same invariant the split at a turn boundary protects.
The other half of the same idea works before a result ever enters the conversation. A tool result
over spill-result-chars — a grep across a large tree, the output of a long command, a
file read whole — is written to a file under the state directory, and what the model gets is the
same head and tail plus that file's absolute path, so the rest is one Read away with an
offset rather than a third of the window spent up front. The file is granted to the running session
the way request_file_grant grants one, which is what lets a path outside the working
directory be read at all; the grant lives in memory only, so a resumed session no longer reaches the
files its earlier run spilled.
What survives is not the message count. Three turns are kept verbatim and everything before them becomes working notes, so a compaction of two hundred messages leaves a few dozen — which looks like a heavy loss and is not one, because the summary is asked for names, paths and numbers verbatim. Measured on a session of eleven facts spread over eleven turns: sixteen of twenty-two messages were summarized away, and every one of five values from the summarized half — a key fingerprint, a port and database name, an expiry date, a region, a retention pair — came back exactly. Compaction also starts before the window is full, at the window minus the room one full turn can still need to answer, which is why the reading names the point it compacts at once that point is near.
Memory
Memory is markdown files that are loaded into the system prompt of every turn. They live under a .task-agent/ workspace when one exists in a parent directory — shared with the task-agent program, memory/ for what is true everywhere and projects/<name>/memory/ for one repository — and under the config directory otherwise, in ~/.config/task-agent/projects/<repository path with the slashes turned into dashes>/memory/. The folder is named after the full path of the main repository, so two checkouts sharing a directory name do not share a memory.
Two things write them:
- The
remembertool, mid-turn, when the model learns one durable fact: one label, the whole file as content, an empty content to delete. A label that already exists is refused unlessreplaceis passed, so adding a memory cannot silently overwrite another one. /refine, which has the model read the whole session and rewrite the memory files from it.
Refinement also runs on its own — after 25 turns or after a compaction, with a twenty-minute cooldown — which means the one nobody is watching is the common case.
Every rewrite is reversible
Before the model rewrites anything, the current memory files are copied to ~/.local/state/task-agent/refinements/<memory directory>/<timestamp>/files/, stamped in a meta.cfg with the session name, how many messages it held, and whether the refinement was asked for or ran on its own. A refinement that changes nothing leaves no entry behind, so an entry exists exactly when memory changed. A remember call takes the same snapshot, so undo walks back over it too.
The model names what it changed per file, and that sentence is stored with the snapshot, so /refine history reads as a log rather than as a list of timestamps:
2026-08-12 12:23 UTC · session 'refine-check' · after 4 messages · manual
updated project/worktree-preferences.md — added the lint table constraint
/refine undo restores the newest entry still standing and names every file it put back or removed. A second undo walks one refinement further back rather than undoing the undo, so repeated undo is a way backwards through the history; a rolled-back entry is marked (undone) and skipped afterwards. The restored memory reaches the running session on the next turn.
The snapshots live under the state directory rather than beside the memory files, because the memory directory is something you read.
The memory directory is shared with task-agent and with every other session on the machine, and the model call takes seconds. So a refinement re-reads each target before writing and drops the single edit whose file moved underneath, naming it to you while the rest of the refinement still lands. It is the same read-before-write rule the file edits follow, one layer up.
What a refinement can rewrite is bounded by construction. The base system prompt cannot be changed by anything the agent does; a refinement only ever adds to a supplemental block appended after it. A write goes to a global/<name>.md or project/<name>.md label resolved against the two memory directories, and any other path, extension, or dot file is refused.
Standing instructions belong here, not in the prompt
A habit you want the agent to keep — check for a refactoring opportunity whenever code
changes, keep the skills and the memory files under the same review, never leave a dead branch
behind — is a memory file, written once by hand or through /refine. It is not a
sentence for the base prompt, and that is the point of the boundary rather than an accident of
where it was easier to put: the base prompt is compiled in and can only be added to, so anything
placed there is permanent for every project and every user of the program, while a memory file is
read, edited and deleted by the person it belongs to. Rules that some projects want and others
would fight are exactly the ones that must stay removable.
What ships in the memory directory
A fresh install would otherwise start with an empty memory directory and answer from the compiled base prompt alone, so the first start writes five short behavior documents into the global memory: verify-before-commit.md (a change is finished when the project's own checks pass), task-discipline.md (one task one file, delete when done, wait to defer), worktree-isolation.md (one writer per working tree, and what never to do to somebody else's uncommitted work), read-before-edit.md (read the real path first, then make the smallest change that works), and asking-the-user.md (decide the judgment calls, ask for what only the user knows).
Each of them carries a builtin: true line in its front matter, the same mark a skill that ships with the program carries, so you can tell a shipped default from something you or a refinement wrote. They are ordinary memory files otherwise: edit one, delete one, or delete the line to adopt it as your own. Seeding only ever writes a file that is absent — the directory keeps a .seeded list of the names it has already handed over, so a file you edited is never overwritten and a file you deleted is not written back by a later version. The startup note names exactly which files were seeded, and --bare seeds nothing at all, the way it loads nothing at all.
The project directory
The first session in a project whose projects/<slug>/ folder does not exist yet creates it: memory/ for what this repository has taught, tasks/ for its queue, subagents/ for its role documents. Two templates come with it — a memory/project.md paragraph naming what that file is for, and a subagents/README showing the shape of a role document, its files and lane header and its body. Nothing that exists is touched. revolve init does the same on demand and prints what it created, and agent_info reports the project directory and whether this session is the one that created it.
Standing goals
/goal <text> gives the session an objective that outlives the turn it was set in. It is stored beside the memory files and loaded into the system prompt of every turn as standing intent: something you asked for earlier, still in effect, needing no acknowledgement. Because it lives in the prompt rather than in the history, a compaction cannot lose it and a later run in the same directory picks it up again.
The model records what advanced with GoalProgress, at the moment it happens rather than at the end of the turn, and the last eight notes travel in the prompt with the goal. The same tool proposes that the objective is reached — a proposal, never a state change.
The goal keeps standing until you answer /goal done, because a goal that clears itself is a goal that lies about success: a limit reached, a run that finished, or a check that passed is not the same thing as the objective being met. /goal pause takes it out of the prompt without forgetting it, /goal resume brings it back, /goal clear drops it.
The status row under the input carries the goal in one line, and it says which state it is in: goal:, goal (paused):, or goal (met?): when the model has proposed it is done.
What a session does not carry back
Heartbeats, quality gates, an autonomous run, and the names a script bound live in the process holding the session. A session restarted comes back without them. The standing goal, stored beside the memory files, comes back with it.