seamless docs

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

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:

  1. seamlessd doctor → read the client-specific definition line. Claude Code checks ~/.claude/settings.json and then ./.claude/settings.json; Codex names exact current, stale, and missing events in its hooks file. A warning does not fail the run, so read it rather than trusting the exit code. Repair owned drift with seamlessd install-hooks.
  2. Codex only: read codex hook trust. trust unverified is 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.
  3. seam doctor → is the daemon actually up and is the key accepted? An empty mcp.api_key makes the daemon reject every MCP and hook request while looking perfectly healthy on /healthz.
  4. Did the seam binary move? Command hooks bake in an absolute path, and make install points 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-run make install.
  5. Check the hook's type if you hand-edited settings.json. Claude Code silently ignores an http hook for SessionStart - it must be a command hook. 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 with seamlessd 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>-2 project 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 (a repo.moved event lands in the console feed); if a split already happened, point the path back with seamlessd map-repo --path <new-root> --project <slug>. seamlessd doctor lists mapped paths that no longer exist on disk, and seamlessd unmap-repo --stale removes them. Once the leftover <slug>-2 is empty, seamlessd retire-project --project <slug>-2 marks 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.

  1. 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.
  2. 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 doctor warns when the gardener is disabled.
  3. 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 status prints health and version. If it disagrees with what you just built, the daemon is running older code.
  • A rebuild does not restart anything. make build and make check rewrite bin/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 with session_start are 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 add defaults to local scope, tying the registration to the directory you ran it from. Register with --scope user.
  • Codex tools missing? Run codex mcp get seamless --json, then seamlessd 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 /healthz is 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 its updates row;
  • seamlessd update --check, on its last, error, log and backup rows.

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:

  1. See whether the daemon runs: seamlessd status. If seamlessd doctor warns on tls trust, read the TLS cause below first - the release you had is probably running, and only its confirmation failed.

  2. 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
    
  3. 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 start
    
    seamlessd stop
    Move-Item $HOME\.seamless $HOME\.seamless.broken
    seamlessd import --from $HOME\.seamless.broken\backups\pre-update-v0.7.2-20261009T141500Z.tar.gz
    seamlessd start
    

    The 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.

  4. 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 gates stage), 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's updates row and seamlessd update --check's fix row 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 disk
    the install no longer looks like one the installer manages seamlessd restart, so the service re-reads how it was installed
    the updater could not make sure it runs outside the service, which the installer restarts update by hand from a terminal: seamlessd update
    automatic 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, dialing server_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's tls trust row warns about this and names the repair; fix it before you resume.
  • The rollback drill is on. While ~/.seamless/update/test-fail-confirm exists, every update fails its confirmation on purpose and rolls back, and seamlessd doctor warns on update 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 updater line; 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.