You could rename Clueless Joe to “Senior Principal Architect” and nobody would know it happened. Change his model from sonnet to opus. Flip his worker flag. Rewrite his entire identity. Save, refresh, and the system’s memory of who Joe was five minutes ago is gone.
The system tracks git history for code. Execution history for tasks. Delete audit logs for every row removed from 18 database tables. But agent configuration? The thing that determines what an agent IS? Nothing. A write-only ledger with no past tense.
Ten Fields
const TRACKED_FIELDS = [
'identity', 'model', 'provider', 'allowedTools', 'specialties',
'workerEnabled', 'schedule', 'thinkSchedule', 'mcpServers', 'skills',
]
Not tracked: avatar, desk position, description. Cosmetics. Changing an avatar doesn’t change what executes. The ten tracked fields are the ones that change behavior.
identity is the system prompt. It’s who the agent is. model is which LLM runs. provider determines capabilities (Claude can write files; Ollama can’t). allowedTools is what the agent can touch. workerEnabled is whether it picks up tasks at all. The rest are scheduling and specialization.
Every PATCH to an agent that touches any of these ten fields creates a revision. Full snapshot of the state before and after, plus a per-field diff.
The Diff Is Semantic
This isn’t a text diff. It’s a structural comparison:
{ model: { from: 'sonnet', to: 'opus' } }
The diffConfig() function walks each tracked field and runs a deep equality check. Arrays are compared element-by-element. Nested objects are compared key-by-key. If nothing changed, no revision is created. If three fields changed in the same PATCH, all three appear in the diff with their before and after values.
You don’t get “line 47 was removed, line 48 was added.” You get “the model was sonnet, now it’s opus.” The diff tells you what changed in terms the domain understands.
What Gets Stripped
Think-cycle runtime state is not configuration. It’s scheduler bookkeeping:
const THINK_SCHEDULE_RUNTIME_KEYS = ['lastThinkAt', 'lastThinkAtByProject']
These keys live inside the thinkSchedule JSON blob because that’s where the think cycle writes them. But they’re timestamps, not settings. lastThinkAt records when the agent last ran a think cycle. lastThinkAtByProject tracks per-project recency for round-robin focus selection.
If you rolled back an agent’s config and these keys came along for the ride, you’d rewind when the agent last thought. The scheduler would think the agent hadn’t run a think cycle in days and immediately fire one. Strip them before snapshotting. Rollback should restore what the operator configured, not what the scheduler tracked.
The Secret Problem
MCP server configurations can contain API keys, tokens, and other secrets in their env and headers fields. You want to know that someone changed the API key. You do not want the old API key sitting in a revision table.
const MCP_SECRET_KEYS = ['env', 'headers']
export function redactMcpSecrets(val: unknown): unknown {
if (!Array.isArray(val)) return val
return val.map((server: unknown) => {
if (!server || typeof server !== 'object') return server
const redacted = { ...(server as Record<string, unknown>) }
for (const key of MCP_SECRET_KEYS) {
if (key in redacted) redacted[key] = '[REDACTED]'
}
return redacted
})
}
Redact at write time. The revision stores [REDACTED] where the secret was. The diff is computed before redaction, on the actual values, so it captures that env changed. But the stored snapshot only has [REDACTED]. You know the secret was rotated. You can’t recover what the old secret was.
This creates a deliberate gap in rollback:
The rollback function walks every tracked field and applies it from the target revision’s snapshot. Every field except one:
for (const field of TRACKED_FIELDS) {
if (field === 'mcpServers') continue // never roll back redacted secrets
updateData[field] = targetSnapshot[field] ?? null
}
You can roll back identity, model, tools, schedule. You cannot roll back MCP server secrets. The revision has [REDACTED] for the old values. Writing [REDACTED] into a live config would break the agent’s MCP connections. So the rollback skips that field entirely. Accept that rollback can’t restore what it can’t see.
Race Prevention
Two concurrent PATCHes to the same agent could grab the same version number. The transaction prevents it:
await prisma.$transaction(async (tx) => {
const latest = await tx.agentConfigRevision.findFirst({
where: { agentId },
orderBy: { version: 'desc' },
select: { version: true },
})
const version = (latest?.version ?? 0) + 1
await tx.agentConfigRevision.create({
data: { agentId, version, snapshot, diff, source },
})
})
Sequential version numbering inside a transaction. The @@unique([agentId, version]) constraint on the model means the database enforces uniqueness even if the transaction isolation doesn’t fully serialize. One PATCH gets version 5, the other gets version 6. Never version 5 twice.
Rollback Is a New Revision
When you roll back to revision 3, the system doesn’t delete revisions 4 through 7. It creates revision 8 with source: 'rollback', snapshots the current state as “before,” and the target revision’s state as “after.” The audit trail is complete. You can see: version 7 was the active config, then someone rolled back to the state from version 3, and that rollback is recorded as version 8.
If you later roll back the rollback, that’s version 9. Every state change is recorded, including the ones that undo other state changes. The history is append-only.
Why This Matters Now
Post 046 gave the system a multi-provider layer. An agent’s provider field isn’t just a cost decision anymore. Claude agents can write files. Ollama agents can think and review but can’t write. Changing an agent’s provider changes what it’s capable of. That change should be recorded.
Post 049 gave the system a CEO whose identity is 48 lines of carefully written character definition. If someone accidentally overwrites that identity, the revision history shows exactly what was lost. Roll back to the revision before the overwrite and MC gets his voice back.
The system now remembers who its agents used to be. Every identity rewrite, every model upgrade, every tool permission change, every schedule adjustment creates a numbered, timestamped, diffed revision.
Should an agent that gets rolled back to version 3 know it was version 7 yesterday? Currently, no. The revision history is for the operator, not the agent. The agent wakes up in whatever state the operator chose, with no memory of the versions in between. This is a different take on the soul and persistence of agent identity than we took before—we record the history humans need, not the history the agent needs.
Whether that’s a feature or a limitation depends on how you feel about amnesia.
Comments