Troubleshooting
Symptom-first fixes for a system whose hooks fail open - where silence, not an error, is what a broken install looks like.
On this page
- Start here, always
- No <seam-briefing> appears at session start
- Codex hooks are current, but doctor says trust unverified
- Codex MCP is stale or incompatible
- A briefing appears, but it is nearly empty or names the wrong project
- A call is rejected as ambiguous scope, session, or agent
- Recall returns junk, or misses what you just wrote
- A task is stuck in_progress and nobody is working it
- tasks_update says the task is already claimed - by my own session
- A seam flag did nothing
- A briefing setting in the YAML is ignored
- A tool is missing, or a console screen says it is switched off
- A console screen is not in the sidebar
- Two daemons, the wrong port, or code changes that never land
- seamlessd update refuses the release with a signature error
- The update check keeps failing
- The update notice will not go away
- An automatic update is waiting
- An automatic update failed
- memory_write says the name is held by a superseded memory
- A supersedes failed but the memory was written
Seamless has one property that makes troubleshooting unlike most software:
hooks fail open. A stopped daemon, a wrong key, a seam binary that moved -
none of these produce an error you or your agent will see. The handler returns
200 with empty context, seam hook reports to stderr and exits 0, and work
proceeds as if Seamless were not installed.
So the failure mode is silence. Nothing is broken-looking; you just quietly stop getting briefings. That is a deliberate trade - a memory system must never block an agent - but it means you cannot wait for an error. You have to go ask.
Start here, always
seamlessd doctor # server side: config, database, credentials, embedder, hooks
seam doctor # client side: reachable, key accepted, tool count matches
They answer different questions and neither subsumes the other.
seamlessd doctor |
seam doctor |
|
|---|---|---|
| Runs | Against config and the database directly - no daemon needed, except on a role: client install, where the tool count dials the server |
Against a running daemon over HTTP and MCP |
| Checks | binary, config, data_dir, mcp.api_key, llm, embedder, database, mcp_tools, hooks, gardener |
server (/healthz), mcp_tools (tools/list), projects |
| Fails on | Only a fail - warnings are informational |
Any failed check |
The hooks check exists only in seamlessd doctor. If your symptom is "no
briefing", seam doctor cannot tell you why - it will happily report a healthy
server that no hook is calling. Run both.
seamlessd doctor's embedder check makes a real embed call, which is the
one way to learn that recall has quietly gone keyword-only.
No <seam-briefing> appears at session start
What is happening. Something in the chain - hook installed, seam on disk,
daemon up, key valid - is broken, and every link in it fails silently by design.
Fix. Walk the chain in order:
seamlessd doctor→ read the client-specific definition line. Claude Code checks~/.claude/settings.jsonand then./.claude/settings.json; Codex names exactcurrent,stale, andmissingevents in its hooks file. A warning does not fail the run, so read it rather than trusting the exit code. Repair owned drift withseamlessd install-hooks.- Codex only: read
codex hook trust.trust unverifiedis honest, not a failed probe - open/hooks, inspect the current commands, and approve them. A recent activity timestamp is supporting evidence only; it cannot prove a newly changed definition is trusted. seam doctor→ is the daemon actually up and is the key accepted? An emptymcp.api_keymakes the daemon reject every MCP and hook request while looking perfectly healthy on/healthz.- Did the
seambinary move? Command hooks bake in an absolute path, andmake installpoints them at~/.local/bin/seam- a stable copy, not your working tree. If you moved the install prefix or wired the hooks up by hand against some other path, re-runmake install. - Check the hook's type if you hand-edited settings.json. Claude Code
silently ignores an
httphook forSessionStart- it must be acommandhook. Codex's Seamless profile uses command handlers for all five events.
Definition classification is stricter than “contains hook <event>.” A current
entry must match the desired event, paths, arguments, client discriminator,
timeout, and OS command shape. The installer can adopt a marker-free entry only
when it is unmistakably a documented Seamless URL or seam/seam.exe command;
foreign entries survive install and uninstall.
Codex hooks are current, but doctor says trust unverified
What is happening. Definition validity and trust are different facts. Codex does not expose a supported command that tells Seamless whether the current hook hash is trusted, and Seamless deliberately does not parse or pre-seed private trust state.
Fix. Open /hooks inside Codex. Review and approve the current Seamless
definitions. This warning remains honest even after approval; use a real session
briefing as the end-to-end check. codex hook activity can tell you when
SessionStart/UserPromptSubmit last reached the daemon, but it is not proof for a
definition that may have changed since then.
Codex MCP is stale or incompatible
What is happening. seamlessd doctor compares codex mcp get seamless --json with the exact enabled stdio bridge the installer wants, including the
absolute seam path, ordered mcp-proxy --config ... arguments, and target-file
existence. Mere registration is not enough.
Fix. Run seamlessd install-hooks --client codex. A disabled or drifted
owned stdio bridge is replaced in place and verified. A direct-HTTP or arbitrary
foreign entry under the reserved seamless name is not overwritten: either keep
it and use --mcp=false for future hook/skill refreshes, or explicitly run
codex mcp remove seamless before reinstalling the maintained proxy.
A briefing appears, but it is nearly empty or names the wrong project
What is happening. The hook fired and the daemon answered - this is not a plumbing problem. The cwd resolved to a project you did not expect, or to none.
Fix. The precedence chain resolves scope from the working directory via the repo map. Three outcomes:
- Wrong project - the cwd matched a mapping you forgot about. Check
/console/projects, and re-map withseamlessd map-repo --path <dir> --project <slug>. - A project named after your directory - an unmapped git repo auto-registers a project named after the repository root. It is real, it is just new and therefore empty.
- A
<slug>-2project after moving a repo on disk - the repo's old path still owned the slug, so registration minted a fresh project and the briefing lost the original's memories. Current daemons adopt the existing project automatically when the old path is gone (arepo.movedevent lands in the console feed); if a split already happened, point the path back withseamlessd map-repo --path <new-root> --project <slug>.seamlessd doctorlists mapped paths that no longer exist on disk, andseamlessd unmap-repo --staleremoves them. Once the leftover<slug>-2is empty,seamlessd retire-project --project <slug>-2marks it retired. - No project at all - the cwd is not inside a git repo, so nothing resolved and the session is global.
An empty briefing for a project that genuinely has no constraints and no memories is correct behavior, not a fault. The header counts what exists; if it says zero, the store is telling the truth.
A call is rejected as ambiguous scope, session, or agent
What is happening. This is the fail-closed rule doing its job. A call on a connection bound to no session has to infer its project or session from the agents that are live, and it refuses rather than guesses when that inference has no single answer. A durable write with no resolvable scope is rejected rather than landing in the global scope, because a global memory is seen by every agent in every repo forever - and that must never be what happens when the system is unsure.
From Claude Code or Codex, suspect the seam binary first. Their tool calls
are bound to the agent's own session
automatically, so they should rarely see these errors. A seam older than that
binding sends no agent-process identity, which leaves the connection unbound -
and the briefing's Seam session line then tells the agent to
bind once with session_start name=... instead of saying no session_start is
needed. Update Seamless on the agent's machine and start a new
session. An agent that has ended its own session is unbound too, by design: its
next bare call - a read as much as a write - is refused rather than borrowing
another agent's project or quietly falling back to the global scope.
Fix. The message tells you which case you are in:
| Message says | Cause | Fix |
|---|---|---|
| no bound or ambient session to infer the project from | Nothing bound, and no ambient session this caller may inherit | Bind with session_start (cwd, or project=<slug>), or pass project=<slug> on the write |
| the active ones belong to other agents | This connection names its agent process but owns no live session - an agent that ended its own session, or seam run by hand - and every live session is another agent's |
Pass project=<slug> (--project on the CLI); an agent that ended its own session can resume it with session_start name= the cc/... or cx/... on its briefing's Seam session line |
| active ambient sessions span multiple projects | You are unbound and other agents are live in several repos, so inheriting would bleed your write into someone else's project | Pass project=<slug>, or bind once with session_start (name= the cc/... or cx/... on your briefing's Seam session line, or project=<slug>) |
| ambiguous session or ambiguous agent | session_update, session_end, or a task claim or release named no session while several agents are live |
Pass session= with the cc/... or cx/... on your briefing's Seam session line, or bind once with session_start name=<that name> |
In the scope cases, project=<slug> may name a project that does not exist yet -
the write creates it. Do not answer this error with project=global because a new
slug felt riskier: an invented project is cheap and local, while global puts the
item in every project's briefing forever. The error text lists the slugs that do
exist, so you can match one instead of coining a near-duplicate.
If a call that worked all morning starts failing this way on a connection bound
with session_start, suspect a lost binding - see the daemon-restart section
below. project: global is always accepted; it is a token you pass on purpose.
Recall returns junk, or misses what you just wrote
What is happening. Three unrelated causes wear the same symptom.
Recall has degraded to keyword-only. If the embedding provider is unreachable,
rate-limited, or rejecting the key, recall degrades rather than errors - you
get worse ranking, not a failure, because a partial answer beats no answer during
a network incident. That is correct in the moment and terrible for three weeks.
Run seamlessd doctor and read the embedder line: it probes with a real call.
Note that provider anthropic has no embeddings API at all, so selecting it means
permanently lexical recall.
The memory is not written to be findable. The description is the retrieval
surface - the only text indexes show, and the only text the prompt-injection
matcher scores (name and description; never the body). A description like "notes
about the console" cannot be retrieved by anything. See Write memories that get
recalled.
You wrote it seconds ago. The prompt matcher's corpus is rebuilt on a 30-second
interval, and an expired lookup serves the stale corpus while the rebuild runs
behind the hook - so the hook never pays for a cold rebuild, and a brand-new
memory can miss the next prompt's injection by a little more than the interval
suggests. An explicit recall call does not use that corpus and sees the write
immediately.
Before searching at all: read what was already injected. The briefing is in context. Re-recalling it spends tokens to learn what the agent was already told.
A task is stuck in_progress and nobody is working it
What is happening. Its holder died, and you are between two clocks. An expired
lease makes a task stealable by id - but it does not re-queue it. The task
is still in_progress, and tasks_ready returns only open tasks, so nothing
surfaces it. Lease expiry is enforced lazily inside tasks_claim; there is no
sweeper watching leases.
What does re-queue it is the session reaper: it expires sessions idle past
gardener.session_idle_minutes (default 45) and releases their claims back to
open.
Fix.
- Confirm the diagnosis:
seam task list --status in_progress, or/console/tasks- a task's detail panel shows its holder and whether the lease is live or expired. - Check
gardener.enabled. The reaper runs inside the gardener's pass. With the gardener off, nothing reaps idle sessions and nothing ever returns a dead agent's claims - they stay stuck forever.seamlessd doctorwarns when the gardener is disabled. - Do not wait if you do not want to:
seam task release --force <id>, or the release lock button in the console. Both force-release any holder regardless of the lease. Neither is on the MCP surface - agents get the cooperative protocol, you get the override.
tasks_update says the task is already claimed - by my own session
What is happening. The connection lost its session_start binding. The task
is genuinely held by your session id; your connection no longer knows that, so
the holder check sees a stranger.
Fix. Re-run session_start with the same name to rebind. It resumes the
session rather than opening a second one.
The binding is keyed by the transport's Mcp-Session-Id, held in the daemon's
memory. Any daemon restart drops it - see below. A Claude Code or Codex agent's
automatic binding is not held there - it is matched from the agent process on
every request - so this bites only a connection bound with session_start.
A seam flag did nothing
This no longer happens. A trailing flag was once dropped in silence - seam task release 01K7ABCD --force took the normal holder-checked path and the
override never happened, with no error and no warning. Both halves of that are
gone:
seam task release --force 01K7ABCD # --force applies
seam task release 01K7ABCD --force # --force applies here too
Every seam command parses flags and positionals in any order. A typo'd flag
(--projct) is an error everywhere rather than being absorbed into the
positionals, and an unrecognized enum value (--status bogus) is rejected at
parse time rather than quietly widening the filter.
If a flag still looks ignored, it is not a flag-order problem. Check that you
are running the seam you think you are (seam doctor), and see the seam CLI
reference for what each command accepts.
A briefing setting in the YAML is ignored
What is happening. The briefing: block has a fourth precedence layer above
file and environment: a runtime override stored in the database, written by the
console's Settings → Briefing section. It wins over both, applies from the
next session start without a restart, and stays until reset.
Fix. Check the console before you check the YAML. This is the one place the config file is not the last word, and it exists so you can tune what agents get injected while they are running. See Configuration.
A tool is missing, or a console screen says it is switched off
What is happening. It belongs to an
optional feature, and optional features
ship off. Nothing is broken and nothing was deleted - the feature's data is
untouched, and every surface returns the moment it is switched back on. Tools
and whole screens belong to research (the Labs and Trials screens, the trials
search scope, and lab_open, trial_record, trial_query); the in-page
momentum surfaces (finish-line cards, the capture calendar, payoff moments,
maturity stages) belong to momentum; the Now screen's arcade (the day tape,
personal records, the hot-streak pulse, celebration moments) belongs to
gamification - the Now screen itself is core and always on.
Fix. Turn the feature on in the console under Settings → Features (or set
its features: key in YAML, or SEAMLESS_FEATURES_RESEARCH=1 /
SEAMLESS_FEATURES_MOMENTUM=1 / SEAMLESS_FEATURES_GAMIFICATION=1). The
console toggle stores an override that beats both the file and the
environment, so check it before you edit YAML.
If you already turned it on and the agent still cannot call the tool, you are looking at the propagation seam, not a failure:
| Surface | When the change lands |
|---|---|
| The console | Immediately. |
| A tool call | Immediately - and while a feature is off its tools are refused as unknown, which is what a client holding a stale list should see. |
| A client's tool list | The next time it lists tools, in practice its next session. Seamless declares listChanged: false and sends no tool-list notification, so a connected client keeps the list it was given. Start a new session rather than hunting for a refresh button. |
The seam-research skill in a client's skill home |
The next seamlessd install-hooks run. seamlessd doctor raises an info line while a skill for a disabled feature is still installed - it never deletes in a client's directory behind your back. |
One thing that is not a symptom of this: a gated tool call leaves no row in the Interactions feed. The filter rejects it before the middleware that records tool calls, so silence there is expected.
A console screen is not in the sidebar
What is happening. If a link still opens it - with a note that it is not in your sidebar - it is the console's experience level, not a switched-off feature. A fresh installation starts at Basic, which keeps the sidebar to the essentials. Levels only change what the console shows: your agents get the same tools and briefings at every level.
Fix. Choose a level in the console under Settings → Experience (the note's
Switch button does it in one click), or set console.level in YAML /
SEAMLESS_CONSOLE_LEVEL. A choice made in the console wins over both until you
reset it there.
Two daemons, the wrong port, or code changes that never land
What is happening. Seamless is one server instance per machine: one port
(8081), one data directory (~/.seamless). The dev and release install layouts
drive that same instance, so installing one replaces the other. (A role: client
install has neither a port nor a data directory, so it cannot collide this way -
see Share one daemon across a LAN.) Most confusion here is
someone accidentally interacting with a second copy, or with the same copy running
older code.
Fix.
- Compare versions. The same version string appears in
/healthz, the MCP handshake, and the startup log line.seam statusprints health and version. If it disagrees with what you just built, the daemon is running older code. - A rebuild does not restart anything.
make buildandmake checkrewritebin/seamlessd, but a running process keeps its in-memory image. The new binary takes effect on the next restart - which also means an accidental restart silently upgrades the running daemon to whatever is in your working tree. - Never
pkill -f "seamlessd serve". Running a throwaway daemon on a spare port is the right way to test in isolation, but that pattern matches the real service's command line too and kills it. Under launchd it comes back in about a second with a new pid and no data loss - but every agent's MCP connection drops, and bindings made withsession_startare lost mid-task, which resurfaces as the ambiguous-scope and self-claimed-task symptoms above (a Claude Code or Codex agent's automatic binding survives). Kill the throwaway by its own pid, or by its port. - Tools missing in some repos? That is not a daemon problem.
claude mcp adddefaults tolocalscope, tying the registration to the directory you ran it from. Register with--scope user. - Codex tools missing? Run
codex mcp get seamless --json, thenseamlessd doctor. The maintained registration is an enabled user-level stdio bridge; a disabled entry, stale binary/config path, or project override is a client-configuration problem even while/healthzis green.
seamlessd update refuses the release with a signature error
What is happening. In v0.6.0 Seamless moved from github.com/0spoon/seamless
to github.com/arctop/seamless, and every release since is signed by the new
repository's release workflow. A binary older than v0.5.4 trusts only the old
repository's signature, so its seamlessd update rejects every release after
the move. It stops with refusing to run ...: sigstore verification: ...,
and the error names github.com/arctop/seamless. Nothing is installed or
changed. v0.5.4 and later accept both signatures, so this happens at most once.
Fix. Re-run the installer one-liner once. It installs the latest release
with its own signature check and keeps your config and ~/.seamless:
curl -fsSL https://thereisnospoon.org/install | sh # macOS, Linux
irm https://thereisnospoon.org/install.ps1 | iex # Windows
After that, seamlessd update works again (seamlessd version shows what you
have). Installed with Homebrew from the old 0spoon/tap? That install does not
use seamlessd update; switch taps once as
Install & deploy describes.
The update check keeps failing
What is happening. The daemon's automatic update check
could not read GitHub's release list. seamlessd doctor's updates row and
Settings -> Updates show the error and how long it has lasted; after a week
the row turns into a warning. A failed check retries after 15 minutes, doubling
up to the check interval, and a GitHub rate limit (60 anonymous requests an hour
per address, shared by everything behind your router) is waited out.
Fix. Usually the network: the daemon reaches api.github.com directly
unless its own environment carries HTTPS_PROXY. A proxy exported in your shell
does not reach a daemon the service manager starts; see
The service & where things live. If the machine should
not talk to GitHub at all, set update.check: false - then nothing is sent and
nothing is reported. seamlessd update --check asks directly, so it shows the
same error with its cause.
The update notice will not go away
What is happening. A newer release is out that this install will not install
by itself, and the daemon says so in new sessions' briefings for a week after it
first saw it, in the console, and in seamlessd doctor. Either the install is
only ever told - Homebrew, a build from source, a client machine, or a layout the
daemon cannot vouch for (who updates itself) -
or its automatic updates are off, paused, or skip that release.
Fix. Update with the command the notice names - it depends on how Seamless
was installed (seamlessd update, brew upgrade --cask arctop/tap/seamless, or
git pull && make install); the table in
automatic update checks lists them. On an
install that updates itself, Settings -> Updates says why it skips the
release, and Update now installs it. To stop hearing about updates instead,
turn checks off in Settings -> Updates or set update.check: false.
An automatic update is waiting
What is happening. This install updates itself, a newer release is out, and
it has not been installed yet. Usually that is the design working:
automatic updates wait out a soak, then wait for a
quiet daemon. Settings -> Updates says what this one waits for on its
Next update row (and on the Home health strip's hover);
seamlessd update --check prints the release and its deadline on its target
and pending rows.
| Next update says | Why | What to do |
|---|---|---|
installs by itself once it is 24h old |
The release is younger than update.min_age. |
Wait, or use Update now. |
waiting for 2 live agent sessions to go idle, or at a lull in requests in 19h |
Agents are working. Past update.max_defer it takes the next lull instead. |
Nothing. |
past its 24h deadline; waiting for 90s without requests |
Something keeps calling the daemon. | Nothing: at 1.5 times max_defer it installs once no request is in flight. An open Interactions page refreshes every 15 seconds, so closing it lets the lull come sooner. |
waiting for 1 request in flight to finish |
A request is being served. | Nothing: at twice max_defer it installs anyway. |
an earlier attempt failed; trying again in 1h |
The backoff after a failed attempt. | Read Last attempt; Update now does not wait for the backoff. |
an updater is already running |
A seamlessd update holds the update lock. |
Let it finish. |
re-checking the release list in 12m before installing v0.7.3, or could not be re-checked |
Its copy of the release list is older than a minute, and it re-checks at most every 15 minutes. | Nothing; if the check keeps failing, see the update check keeps failing. |
live agent sessions could not be counted |
The database did not answer the count, which never counts as idle. | Run seamlessd doctor. |
the update state could not be saved |
~/.seamless/update/ cannot be written - a full disk, or permissions. |
Free space or fix the permissions; an update never starts without its record on disk. |
v0.7.3 carries no signed checksums bundle |
Automatic updates verify that bundle, and this release has none. | Update by hand with seamlessd update. |
No Next update row at all means nothing is pending: the release sits under Held back (you went back to an older release) or Skipped releases (an update to it failed), or the Mode row says automatic updates are off or paused. Automatic updates covers each.
An automatic update failed
What is happening. An update the daemon started - by itself, or through Update now - did not apply. For a week every console page says so in a banner ("The update to v0.7.3 rolled back."), and three places read the same record:
- Settings -> Updates -> Last attempt: the outcome, the updater's own error, and its log;
seamlessd doctor, on itsupdatesrow;seamlessd update --check, on itslast,error,logandbackuprows.
The log, ~/.seamless/update/logs/<attempt>.log, has every step and the
installer's full output; the newest 20 logs are kept. The outcome says how bad
it is:
- rolled back, failed or interrupted: Seamless still runs the release you had, and nothing is lost. Automatic updates skip a release that rolled back or failed verification, and retry the rest after a backoff (when an update fails). There is nothing to do unless it keeps happening; the error says why.
- not rolled back: the new release did not come up, and the release you had could not be confirmed either, so the daemon may be down. Automatic updates paused themselves, and the banner is red.
Fix, when it was not rolled back. The error spells out the steps. In order:
-
See whether the daemon runs:
seamlessd status. Ifseamlessd doctorwarns ontls trust, read the TLS cause below first - the release you had is probably running, and only its confirmation failed. -
Reinstall the release you had, pinned to it:
curl -fsSL https://thereisnospoon.org/install | SEAMLESS_VERSION=0.7.2 sh$env:SEAMLESS_VERSION='0.7.2'; irm https://thereisnospoon.org/install.ps1 | iex -
Only if the data has to go back to how it was before the update, restore the backup the error names.
seamlessd import --from <backup>merges into a data dir that already holds data, so a true restore needs an empty one: stop the daemon, move the data dir aside, import, start.seamlessd stop mv ~/.seamless ~/.seamless.broken seamlessd import --from ~/.seamless.broken/backups/pre-update-v0.7.2-20261009T141500Z.tar.gz seamlessd startseamlessd stop Move-Item $HOME\.seamless $HOME\.seamless.broken seamlessd import --from $HOME\.seamless.broken\backups\pre-update-v0.7.2-20261009T141500Z.tar.gz seamlessd startThe backup lives inside the data dir, so after the move its path starts with the new name. Anything written after the backup was taken stays in the moved directory.
-
Once the install is healthy, press Resume automatic updates in Settings -> Updates. The release that failed stays skipped.
Causes worth knowing:
- The updater refused to start it. The attempt failed while checking it may
install now (the
gatesstage), before anything changed. The daemon tries again after the backoff, and is refused the same way until you act. Last attempt, the console banner,seamlessd doctor'supdatesrow andseamlessd update --check'sfixrow name the cause and, while the daemon still runs the release the attempt started from, the fix:The cause The fix the installed seamlessd is not the version the daemon runs - a release was installed without restarting the service seamlessd restart, so the daemon runs the release on diskthe install no longer looks like one the installer manages seamlessd restart, so the service re-reads how it was installedthe updater could not make sure it runs outside the service, which the installer restarts update by hand from a terminal: seamlessd updateautomatic updates were turned off before it began nothing: that was your choice - A TLS daemon this machine cannot verify. The updater confirms an update
through this machine's own client - the system's roots plus
tls.ca_file, dialingserver_url- the one its hooks use. If that client cannot verify the daemon's certificate, no update can be confirmed, and neither can the rollback after it, so every attempt ends "not rolled back" and pauses automatic updates.seamlessd doctor'stls trustrow warns about this and names the repair; fix it before you resume. - The rollback drill is on. While
~/.seamless/update/test-fail-confirmexists, every update fails its confirmation on purpose and rolls back, andseamlessd doctorwarns onupdate drill. Delete the file. not enough free space for the backup. The attempt stopped before changing anything; the error says how much the backup needs. Free space on the data dir's disk; it is retried after the backoff.- The updater never started. Nothing changed, and it is retried after the
backoff. The daemon's own log says why, on its
update: could not start the updaterline; The updater on your OS has where that log is and what the updater needs. - applied with warnings. Not a failure: the new release serves, but the
installer reported a problem after it came up, usually while wiring the agent
clients. Run
seamlessd install-hooks.
memory_write says the name is held by a superseded memory
What is happening. A superseded or archived memory leaves every index but stays on disk as provenance - and it still owns its filename. The name is taken by something you cannot see in any index.
Fix. Pick a different name, or free the old one with memory_delete. Prefer a
different name: the old file is the record of what you used to believe, and
deleting it destroys the history that explains the current state. See Memory &
notes.
A supersedes failed but the memory was written
What is happening. This is a deliberate partial failure, reported rather than swallowed. The new memory's content is valid knowledge, so it is written and kept - but the supersession did not happen, which means the old memory is still active, live in briefings and recall alongside its replacement. That is exactly the contradictory-store state the lifecycle exists to prevent, so it comes back as a tool error rather than an error field inside a success payload.
Fix. Correct the supersedes target and re-run the write. Re-writing the same
name is a lossless in-place update, so retrying is safe.