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.

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 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.
  • 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 write is rejected as ambiguous scope

What is happening. This is the fail-closed rule doing its job. 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.

Fix. The message tells you which of two cases you are in:

Message says Cause Fix
no bound or ambient session to infer the project from No session_start, and no ambient session for this cwd Call session_start with your cwd, or pass project=<slug>
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> explicitly

In both 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, 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 binding was lost. 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 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 injection form. 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.

Two daemons, the wrong port, or code changes that never land

What is happening. Seamless is one 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. 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 their session bindings are lost mid-task, which resurfaces as the ambiguous-scope and self-claimed-task symptoms above. 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.

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.