Skip to content

Troubleshooting

Polytoken keeps its own state in three directories. The defaults follow the XDG Base Directory specification and are the same on every platform, including macOS.

ContentsDefault locationRedirect with
Configuration file, skills, facets, subagents, hooks, themes$XDG_CONFIG_HOME/polytoken (default: ~/.config/polytoken)POLYTOKEN_CONFIG_PATH
Sessions, logs, provider login state$XDG_DATA_HOME/polytoken (default: ~/.local/share/polytoken)POLYTOKEN_DATA_PATH
Model catalogs discovered from dynamic providers$XDG_CACHE_HOME/polytoken (default: ~/.cache/polytoken)POLYTOKEN_CACHE_PATH

A redirect variable names the directory itself: Polytoken uses the value exactly as written and does not append polytoken. With POLYTOKEN_DATA_PATH=/tmp/pt, a new session lands in /tmp/pt/sessions-v1/<id>. An unset or empty variable falls back to the matching $XDG_* value, then to the default inside your home directory.

If POLYTOKEN_CACHE_PATH, XDG_CACHE_HOME, and HOME are all unset or empty, Polytoken cannot resolve the cache directory for dynamic provider catalogs. Polytoken then skips caching, logs a warning, and still discovers the catalogs live from the provider.

The .polytoken/ directory at the root of a project is a different thing from the global config directory. It holds project-scoped configuration only; see Checking your configuration.

The daemon’s log, the interface logs, and crash logs all land in $XDG_DATA_HOME/polytoken/logs/ (default: ~/.local/share/polytoken/logs/).

Terminal window
ls -lt ~/.local/share/polytoken/logs/ | head

The path in this example is the default location; substitute it when you have set $XDG_DATA_HOME.

Run logs carry a timestamp and a process ID, so each run gets its own file:

  • 2026-09-05T13-22-04Z-368502.log is the daemon’s log for the run with process ID 368502. The daemon writes structured JSON entries to this file. A daemon that runs in the background also redirects its stdout and stderr into the same file, so startup failures and panics appear alongside the JSON entries.
  • The conversation interface writes logs ending in -tui.log; the configurator writes logs ending in -configui.log.
  • A crash log ends in .crash.log and marks an involuntary exit. The file records the session ID, the exit reason, and, for panics, a backtrace. Normal exits write no crash log. When a panic or daemon attachment lease loss writes a crash log, the TUI announces the file path after restoring the terminal. The lease-loss detach message prints even when no crash log was written, without the path.

Two controls set verbosity, one per process. For the daemon’s log, set daemon.log_level; its values are error, warn, info (the default), debug, and trace. For the interface logs, set RUST_LOG to a standard tracing filter, for example RUST_LOG=debug; the default is info. Polytoken reports an error instead of launching when RUST_LOG holds an invalid filter.

Polytoken does not delete old log files. Runs accumulate until you remove the files yourself.

For non-interactive runs, polytoken exec --print-session-logs mirrors the session’s JSONL events and the run’s trace log to stderr while the run executes. That trace log, trace.jsonl, lives in the session directory rather than the logs directory. See CLI.

Each session owns a directory named by its session ID. Current sessions live under $XDG_DATA_HOME/polytoken/sessions-v1/, sessions created by older releases live under $XDG_DATA_HOME/polytoken/sessions/, and migration artifacts live under $XDG_DATA_HOME/polytoken/sessions-migration/. Three files answer most questions:

  • log.jsonl holds the session’s event log: every prompt, reply, tool call, and marker, in order. Polytoken replays this file to rebuild the conversation when you resume. See Sessions.
  • session.json holds the metadata the session picker reads, including the project directory, the title, and timestamps. It may also contain usage_tracking, which identifies sessions created by this version, and first_user_activity_at, the time of the session’s first user turn. Older sidecars omit both fields.
  • record.json is the ownership record the daemon writes while it owns the session; it records the daemon’s port and process ID. After an abnormal exit, a stale record.json can remain. While listing live sessions, polytoken sessions re-checks each recorded process and removes the records it confirms are stale. If Polytoken cannot verify a recorded process, it keeps the record and logs a warning. The record must stay in its own session’s directory: if you move or edit a record.json so its contents name a different session, commands that dispatch on the record (continue, terminate, reap) reject it with an error instead of trusting the misplaced payload. Restore the record to its own session directory to fix this.

The session directory also holds shell-job output, subagent transcripts, and the exec trace log for the session; todo records live in a todos/ subdirectory. When daemon.logging.persist_system_prompts is on, Polytoken also creates system-prompts/ and stores one 0600 text sidecar for each changed composed system prompt. Polytoken keeps the directory at mode 0700. The sidecars help you compare provider prompts after a run and do not participate in session replay or exports.

Polytoken stores full output for capped tool results in the session’s tool-results/ directory and sets each spill file to mode 0600.

Native context records and exported artifacts

Section titled “Native context records and exported artifacts”

A native compaction boundary stays in log.jsonl as a complete record. Polytoken records each conversion from a native context to portable text the same way. Each record is self-contained: Polytoken can resume the session, measure the context, and convert it without rebuilding provider state from files that can change. A crash before Polytoken writes the record leaves the previous context active. A crash after Polytoken writes the record lets Polytoken resume from it after a restart. Subagents whose models use native compaction keep the same kind of records in their own subagent-*.jsonl transcripts, under the same rules.

The conversation interface shows which compaction method ran, whether Polytoken fell back to text compaction, how full the context is, and timing details that contain no secrets, but never the provider’s continuation state. Feedback artifact exports redact native records, while the on-disk session log keeps everything Polytoken needs to resume. Treat the raw session directory as sensitive.

polytoken sessions lists live sessions and, as a side effect, removes registry entries it confirms are stale. Add --all to list every session directory on disk, including finished ones; the --all listing is read-only and skips the stale-entry cleanup. Legacy sessions that predate session.json appear in the --all listing but cannot be resumed.

When you resume a large legacy session, Polytoken shows live migration progress while it converts the session. Migration can take several minutes for a session with extensive history or many subagent logs. You can interrupt the resume safely. A later resume retries the conversion and converges on the migrated session.

A conflicting-session-tree error means Polytoken found both the legacy session directory and the migrated session directory for one session ID. Polytoken does not choose one tree automatically because the two trees may contain different history.

Polytoken keeps the original legacy tree in sessions-migration/<id>/legacy-backup while migration remains recoverable. When a recreated legacy directory contains only startup scaffolding, Polytoken moves it to sessions-migration/<id>/recreated-stub/ so a later retry can run without deleting the scaffolding. A legacy directory containing log.jsonl is never scaffolding.

To recover, stop any process that owns the session and resume the session again. Another resume converges automatically when the legacy directory lacks log.jsonl or migration artifacts remain: Polytoken archives the scaffolding stub and either publishes the staged history or restores the backup for another retry. When both the legacy log.jsonl and a valid migrated session directory contain history, no retry can resolve the conflict. Polytoken refuses to choose either tree and preserves both. Keep both trees unchanged and seek assistance.

Use polytoken sessions repair <session-id> when Polytoken warns that v1 history is corrupt and a session fails to load, resume, or hand off, but every line in log.jsonl remains valid JSON. This command repairs records whose indexes no longer follow their physical order. It does not change record content, and it saves the original log beside log.jsonl as log.jsonl.corrupt-<UTC-timestamp> before replacing the log.

Stop the daemon that owns the session before running the command. Polytoken refuses to repair a session with a live owner or an owner record that it cannot read or verify. Run:

Terminal window
polytoken sessions repair <session-id>

The command refuses a missing session, a legacy session, any unparseable or torn line, and timestamps that move backward. Those problems are not index-order corruption and need a different recovery path. Polytoken validates the repaired log before reporting success; if the log is already valid, the command leaves it unchanged.

With --sessions-dir <dir>, Polytoken accepts either the sessions root the daemon uses or the versioned <name>-v1 directory beside it, and refuses a copied or moved v1 directory whose sibling directories are missing. The refusal names both accepted locations. Repair requires the session storage lock. That lock lives beside the v1 data, so a relocated copy cannot be repaired in place.

Polytoken looks for a file named config.json, config.json5, config.toml, config.yaml, or config.yml in the global config directory and in .polytoken/ at the root of your project. A directory may hold exactly one of them; two produce a load error naming both files. Project values override global values, and a null in the project layer deletes a global key. Lists merge instead of replacing: the project layer appends its items to the global list, and an entry written as !item removes the matching item inherited from the global list.

Some keys belong to the global layer only; data, telemetry, updates, and daemon.discovery are the common ones, and the configuration reference notes which blocks carry the same restriction. A project layer that sets a global-only key fails the load. models and defaults follow the same rule when a global layer also exists.

Model-group definitions use a different precedence rule: user/global definitions replace project definitions, and project definitions replace shipped definitions. A member of a model group cannot be null. Omit modelgroups.<name> to remove that layer’s override and restore the lower-priority definition.

  • polytoken config validate checks the merged result. On success, the first line reads merged config valid. Add --user or --project to check one layer alone; the first line then reads user config valid: <path> or project config valid: <path> and names the directory Polytoken checked. The second line starts with active_session.default_facet (info): and reports the effective merged value when you check the merged result, or the value you configured in the selected layer. When the key is unset, the second line explains that startup falls back to execute.
  • polytoken config edit opens the project layer in your editor by default, or the user layer with --user, and validates before saving: the real file changes only after validation passes.
  • polytoken config ui opens the interactive configurator.

String values in the file can reference an environment variable with ${VAR} or $VAR, the common way to supply an API key. A referenced variable that is not set fails the load, and polytoken config validate names it. Write ${VAR:-default} to use the default text when the variable is unset or empty, and write $$ for a literal $. Command substitution, $(command), is a separate global-layer feature that stays off until you set config_command_substitution; see the configuration reference.

polytoken doctor runs the same startup checks the daemon runs and prints each result as a checklist line. The checks cover the startup surface: configuration, dynamic models, model groups, providers and models, permissions, shell behavior, hooks, skills, facets, subagents, tools, and MCP servers. The model-groups check names skipped candidates when an authored route is degraded, and reports the published-route count when every route is healthy. The command exits non-zero when a check fails; MCP connection failures and partial model groups appear as warnings. It starts no session and writes no session record. The dynamic-models check can refresh the cached provider catalogs, the same cache a session launch refreshes.

When a model-group warning appears, review its group and skipped leaves. Daemon logs also emit a WARN for each degraded or withheld group during startup and a successful reload. An authored group whose every leaf is confirmed unavailable prevents config loading, and the load error names each unavailable leaf. When every leaf is skipped because a dynamic provider’s catalog is still pending, Polytoken withholds the group with a warning instead. Check provider configuration, model names, and dynamic provider availability before trying the session again.

A “model not found” error does not always mean the model name is wrong. Some providers, Google AI Studio among them, return this same error when your API key or project is not allowed to use a model that exists. When the provider explains the refusal in its own response, Polytoken shows that message with the error, so read it before editing your configuration. If the message names your key or project, the fix is on the provider’s console side: check the model’s availability for your account tier, or switch to a model your credentials can serve.

Polytoken reads these variables when it starts.

VariableEffect
POLYTOKEN_CONFIG_PATHUses this directory as the config root, without appending polytoken.
POLYTOKEN_DATA_PATHUses this directory as the data root, without appending polytoken.
POLYTOKEN_CACHE_PATHUses this directory as the cache root, without appending polytoken.
XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_CACHE_HOMEStandard XDG locations, consulted when the matching POLYTOKEN_*_PATH variable is unset.
RUST_LOGVerbosity of the interface log files; the default is info.
EDITORSelects the editor for polytoken config edit; the default is vi. For the interface’s drop-to-editor command, a configured tui.editor key wins, then VISUAL, then EDITOR; the default is vim.
POLYTOKEN_SKIP_UPDATE_CHECKSet to any value to skip the launch-time update check. Polytoken sets this itself when it re-launches after an update.
POLYTOKEN_DISABLE_FEEDBACKSet to 1 to disable manual feedback, including /feedback and Ctrl+Shift+F.
POLYTOKEN_FEEDBACK_DSNUses a custom destination for feedback reports instead of the default service.
POLYTOKEN_TUI_COLOR_MODEForces truecolor or 256 output when terminal color detection is wrong. See Installation.
POLYTOKEN_TUI_DISABLE_KEYBOARD_ENHANCEMENTSet to 1 to skip requesting the terminal’s extended keyboard protocol.
POLYTOKEN_DAEMON_CREDENTIAL_FILEEnvironment form of the daemon’s --credential-file flag. See Daemon Authentication.
POLYTOKEN_OUTPOST_URLDefault URL for Outpost management commands that accept --outpost-url.
POLYTOKEN_OUTPOST_TOKENToken for Outpost commands and polytoken connect when --outpost-token is omitted.
OTEL_*OpenTelemetry exporter settings, when trace export is enabled. See Telemetry.

Bash hook handlers receive their own set of POLYTOKEN_* variables describing the session; see Hooks.

Raise daemon.log_level to debug, reproduce the problem, and read the newest daemon log.

For a durable record, turn on daemon.logging.provider_autopsy. Each provider failure then produces a redacted report under autopsies/ inside the session directory, without the request or response contents. The same directory also holds diagnostics for tool calls that failed to parse, and those contain the raw malformed input, so treat autopsies/ as sensitive. Polytoken prunes older reports automatically.

Polytoken also writes a small summary line to the daemon log when each provider request starts and finishes. Those lines carry counts and sizes only, never request contents, and they are always on at the default log level. For a detailed size and structure breakdown of every provider request, including successful ones, turn on daemon.logging.provider_request_diagnostics in your configuration file, or use the Provider request diagnostics toggle in the configurator’s Daemon pane. The key is off by default because the breakdown adds preparation work to each request. Provider autopsy works independently of this key: autopsy keeps gathering the detail failure reports need even when the diagnostics key is off.

The /feedback command can attach the current TUI log tail and screenshots to a report. Ctrl+Shift+F opens the same form. See Crash Reporting and Feedback.

When you set a goal, the Goal Driver keeps the work moving by sending continuation turns on its own. If a provider repeatedly returns empty responses, Polytoken pauses that automatic continuation and shows a reminder instead of asking the same model to continue. Check the provider or select another model before trying again. With a model group, Polytoken tries the next candidate after repeated empty responses; see Model groups and failover.

Use /goal resume to continue the goal with a fresh allowance of automatic continuations. The same command works when the goal pauses at its continuation limit.

The assistant stalls in extended reasoning

Section titled “The assistant stalls in extended reasoning”

Some models reason for a long time before they produce an answer. When a response keeps producing reasoning without any visible text or tool call, Polytoken interrupts the stream, asks the model to answer or act, and retries the response. These interruptions happen silently in the conversation view. Polytoken records each reminder in session history and replays it to the model on the retry.

The retry sequence is scoped to a stretch without progress. When the model resumes writing text that actually says something, or calls a tool, Polytoken discards the accumulated retries and starts fresh, so earlier interruptions do not weigh on later productive work. Blank text does not count: a response that emits only whitespace between reasoning bursts never resets the count, so the retry sequence keeps climbing until the response produces real output or the turn ends. A prompt you send after an interruption also starts fresh.

The retries are also bounded for your whole turn. When the same turn keeps falling back into extended reasoning, Polytoken counts every interruption and ends the turn after a bounded number of them. From the second interruption onward you also see a warning card that reports how many times reasoning stalled in this turn, so a repeating stall is visible without opening the session log.

An interruption does not prove the model was repeating itself. Long, legitimate reasoning can trigger the interruption too. When retries still produce no answer or tool call, or when the turn reaches its interruption limit, Polytoken ends the turn with a visible note that says the response was stuck in a reasoning loop. Try sending the request again, or switch to a model that reasons less.

Polytoken watches every response stream on the wall clock, for the main conversation and for subagents. If a stream stops making progress (for example after your computer sleeps and wakes), Polytoken reconnects it through the same bounded retry ladder it uses for ordinary stream interruptions. The retry salvages the response up to the last complete block and repeats the request; repeated consecutive stalls end the turn with a visible error.

In the main conversation, time spent running a tool and time spent in a provider rate-limit wait never count toward that ladder, so a long tool batch or a legitimate backoff does not trigger a reconnect. Subagents get the same protection during a provider rate-limit wait, so no stall marker appears while the backoff runs. A long subagent tool batch can still show the marker briefly.

While a response is in flight, the status card names what the model is waiting for, including how long since the provider last sent anything, so a dead stream is visible before recovery fires.

When you cancel a turn yourself and the turn had not produced any visible output yet, Polytoken appends a short muted note that says you canceled the turn before it produced any output, so the transcript never ends silently on your prompt.

Checking language servers, themes, and definitions

Section titled “Checking language servers, themes, and definitions”
  • polytoken lsp check lists every language server Polytoken knows and whether its binary resolves on this machine. Naming a server also runs its --version probe and reports the outcome as an informational note; the exit code reflects PATH resolution only. See Language Servers.
  • polytoken validate skill <target>, polytoken validate facet <target>, and polytoken validate subagent <target> parse and validate one definition file, by name or by path, using the same logic the daemon runs at startup.
  • polytoken theme validate <path> checks one theme file against the token catalog. With no path, the command checks the built-in default theme, and --all checks every discovered theme. See Themes.
  • polytoken vfs ls and polytoken vfs cat list and print the resources Polytoken ships, such as the default theme. See The Polytoken VFS.

When a language server misbehaves (missing diagnostics, wrong edits, a hang), you can record the raw protocol exchange between Polytoken and that server in a private per-session file:

  1. Set daemon.lsp.protocol_trace.mode to verbose in your global config file (~/.config/polytoken/config.yaml is the default location), and restrict capture with the allowlist:

    daemon:
    lsp:
    protocol_trace:
    mode: verbose
    servers: ["typescript-language-server"]
  2. Restart the session. Capture is scoped to session startup: a running session never starts or stops capturing from a config change.

  3. Reproduce the problem, then inspect the trace file. Polytoken stores it next to the session’s other diagnostic files under $XDG_DATA_HOME/polytoken (default ~/.local/share/polytoken), named __lsp_<server>_<root>.protocol.jsonl, with owner-only file permissions.

  4. Turn the key back to off and restart the session when you are done.

The trace can contain document text and file contents, so it is never sent anywhere automatically. mode: messages records one line per protocol message without content; verbose records full message content. Share only an excerpt you have reviewed. This key exists only in configuration files: it does not appear in the interactive configurator, and a project config that sets it is rejected at load.