Skip to content

Runtime Diagnostics ​

IMPORTANT

TL;DR: LoopTroop exposes three different diagnostic surfaces: a local runtime-stall report, persisted blocked-error diagnostics on ticket failures, and structured retry diagnostics on artifacts that needed correction or re-prompting. Use the surface that matches the failure mode instead of treating everything as a generic "the ticket broke" event.

This page covers the diagnostics that help explain slow local behavior, blocked ticket runs, and recoverable structured-output failures.

1. Choose the Right Diagnostic Surface ​

SurfaceWhen it appearsWhere to inspect itBest for
looptroop doctorAny time, before the first ticket or after anything goes wrongYour terminalWhether this machine can run LoopTroop at all, and how this copy was installed
Runtime stall reportYou run npm run diagnose:stall while the app is slow or behaving oddlytmp/diagnostics/runtime-stall-*.logSlow refreshes, missing tickets after reload, OpenCode reachability issues, disk / CPU / memory pressure
Blocked-error diagnosticsA phase ends in BLOCKED_ERRORTicket error view and persisted error occurrence dataProvider failures, timeouts, session errors, transport failures, model output truncation
Structured retry diagnosticsA structured-output phase rejects one or more model attempts before validating or finally failingArtifact processing notices and artifact detail viewsWhy a response was retried, what validation failed, and what excerpt caused the retry

Complete DEBUG history and bounded diagnostics ​

NOTE

Current behavior. Complete DEBUG/history reads use the full available native OpenCode file set. Provider-error diagnostics retain their bounded defaults: the ten newest candidate files and at most 5 MiB per file. Diagnostic reads remain best effort; complete metadata, read, and index failures are surfaced instead of being turned into an empty history.

The DEBUG/history path is an action-triggered complete read for Go to top, bead navigation, and export. Initial log views stay paginated and do not eagerly download the archive. Native history uses incremental index ranges for appends and retains four recent snapshots for cursor stability. A cold or unseen session still scans the needed prefix, and files removed upstream cannot be recovered. Native page rows are LIMIT-bounded, while lineage visibility checks grow with ancestry depth.

1a. looptroop doctor ​

The first thing to run, and the cheapest.

bash
looptroop doctor
looptroop doctor --json

It checks the machine rather than a ticket. In a normal CLI run that means a leading version update check plus machine checks named node, npm, git, gh, gh auth, config dir, install, schema, last start, project ignores, opencode cli, opencode, port, and daemon. Each failing check prints what to do about it. The floor is Node 24.18.0 or newer. The npm check reports the version it finds and does not hold it to a floor.

Versions are shown against the newest published one: v26.7.0 (latest v27.1.0) for LoopTroop, Node, npm and the OpenCode CLI. For OpenCode, Doctor checks the package for the installed major (opencode-ai for v1, @opencode/cli for v2), so it does not suggest a major-version change. Node works the same way: Doctor reads nodejs.org's release list and shows the newest release of the major line you run, so a 24.x Node is compared with the newest 24.x. When the latest lookup is unavailable, the report says latest unknown instead of stalling the rest of Doctor. The LoopTroop version is emphasized when it is behind, because it is the one this machine can act on directly. These lookups are cached for fifteen minutes, failures included, and never delay the local checks.

The opencode check reports the authenticated server protocol and version. If authentication fails, the detail says whether LoopTroop had no password to send or sent one the server rejected, and the remedy tells you to set OPENCODE_PASSWORD to that server's password (plus OPENCODE_SERVER_USERNAME for a v1 server whose user is not opencode), or to remove the OpenCode address you set so LoopTroop starts its own. When a running daemon's health check succeeds, Doctor includes the OpenCode base URL recorded by that daemon in the check detail, and says so when LoopTroop moved its OpenCode off a default address another server holds. Without a running daemon, it probes the configured base URL. If that is the default address and a server LoopTroop cannot use answers there, the check is a warning, not a failure: looptroop start leaves that server alone and starts its own OpenCode on the next free port, and names the port it would take. It fails when no opencode could be launched or no port is free. Only a refused connection counts as nothing being there: an address that accepts the connection and never answers fails the check, because it fails the start too, and one that answers with a 5xx error is reported as a server still starting up, which a start waits for.

One server on that address is different: the OpenCode that a LoopTroop killed outright started, which outlives it with a password only that daemon knew. looptroop start will not run alongside it, so the check fails and says to run looptroop stop, which ends that server and clears the record. When its identity cannot be confirmed, the check says to end the process yourself first, or to delete the daemon record if that pid now belongs to something else; LoopTroop never signals a process it cannot identify. The check also fails for the other records a start refuses: an OpenCode that exited while processes it started still run in its process group, which the check names so you can end them; a daemon that exited before it finished shutting down, which stop cannot finish for it, so the remedy is to delete its record once nothing it started is still running; and a record of LoopTroop's own OpenCode with no pid. These run in mock mode too.

A LoopTroop that is alive but not answering is not treated as stopped. The daemon check fails, because a start refuses to run beside it, and says to run looptroop stop. The port check counts that daemon's port as its own. When the pid cannot be confirmed as LoopTroop, stop will not signal it either, so the remedy is to end it yourself, or to delete the record if it is something else. An OpenCode address you set on LoopTroop's own port and host fails the check, because the start stops on it. With the default address, the check is a warning that names the free port the start would move OpenCode to.

Three marks, and the detail line matters. ✓ is fine. ! is a warning. ✗ is a failing check. For tool probes, the message underneath tells you which kind of problem it is:

  • missing: not found on PATH; for OpenCode, not found in an OpenCode installation directory or on PATH
  • refused: the tool exists, but LoopTroop will not trust that directory or real path
  • timed out: <tool> --version or a similar probe did not answer within its deadline
  • degraded: the tool answered, but something around it is still wrong, such as gh auth not being signed in

A refused tool is not the same as a missing one. If the directory is genuinely operator-controlled, add that absolute directory to LOOPTROOP_TRUSTED_EXECUTABLE_DIRS; otherwise move or reinstall the tool into a location owned by you, root, or the Node runtime owner.

OpenCode follows the tool lookup order, including canonical directories absent from PATH; a refusal stops the search. Doctor prints Resolved executable: <path> beneath a successful version probe and in the JSON check's note. Failed launch, exit, or signal probes name the path and cause; timeout messages stay unchanged. Repair a broken installation or select another through LOOPTROOP_TRUSTED_EXECUTABLE_DIRS. The lookup order also explains differences between Doctor's version and your shell's. When that CLI blocks startup, the failing opencode check keeps the repair or refusal advice; installation advice is for a missing CLI.

Now, this refusal also applies when a Linux user namespace hides ownership behind its overflow UID. That value cannot prove that host root owns the tool. An explicitly trusted directory is required in that case; the Node executable having the same unverifiable owner does not grant trust.

NOTE

Current behavior. When a diagnostic prints an HTTP origin, IPv6 host literals are bracketed before the port, for example http://[::1]:3000. The same formatting is used by status, open, and setup, so the displayed origin is a usable URL.

git is required and gh is not: a missing git fails the run, while a missing gh only warns, because LoopTroop starts and plans tickets without it. A ticket needs it before coding starts: its pre-flight check verifies gh, its sign-in and access to the GitHub repository. A missing gh still prints ✗; the mark describes what is there, and the severity decides the exit code. The line under it says so: Optional to start LoopTroop, but a ticket needs it before coding starts.

Doctor's last line names the checks that stop LoopTroop from running, for example LoopTroop cannot run until this is fixed: opencode., so a missing optional tool is not mistaken for one of them. With nothing failing it prints This machine can run LoopTroop.

NOTE

doctor exits non-zero when any check fails. That is what makes it usable in a script, and it means a fresh machine with no OpenCode configured yet reports a failure by design rather than by fault.

--json emits JSON on stdout and nothing else, so it can be piped into a parser. The stable contract is:

  • top-level ok
  • optional top-level update
  • checks[], keyed by stable name

Script against checks[].name and structured fields such as checks[].install.channel and checks[].install.upgradeCommand; treat detail, label, note, remedy, and check ordering as human-oriented text. The ordinary CLI attempts to include a leading version check and an update summary, but release discovery can be unavailable without making the machine checks disappear.

update contains current/latest versions, update availability, install channel, ordered upgrade commands, and the latest release's version, name, URL, and publication date. The release body is left out here because it is prose, often several kilobytes long. GET /api/health/update returns it in full for the interface to render.

The install check ​

The install-method check reports which copy this is, independently of whether LoopTroop runs:

text
✓ install method  npm
  ↳ upgrade: npm install -g looptroop@latest

It names the channel this copy was installed from and the exact command that upgrades it. That matters because the commands are not interchangeable: running npm install -g looptroop@latest against a bun or pnpm installation does not upgrade it, it installs a second copy under npm's prefix. See Installation.

The channel is worked out from where the files landed, and recorded so the answer is stable. Some installers state it outright by leaving a marker file; the rest are inferred from the install path. The answer can legitimately be unknown: an archive unpacked by hand has no evidence to read. In that case the advice is generic rather than confidently wrong.

Doctor always shows the current and latest known versions. Release discovery uses the latest published stable GitHub release, with a 15-minute cache shared by the CLI and interface. When GitHub cannot be reached, the last known release is kept; with no cached answer, Doctor prints latest unknown. Other human-readable commands stay silent unless a newer version is known.

The interface says "Signed out" ​

This screen means the daemon refused this browser's session. It is not a broken install, and there are four reasons it appears, with different fixes.

You opened the origin, not the sign-in link. looptroop open prints Opened http://127.0.0.1:3000, which is the address without the single-use nonce that actually signs a browser in. The nonce is a live credential, and this line ends up in scrollback and screenshots. Opening that address by hand therefore always lands here. Use the tab looptroop open itself opens, or looptroop open --print-url to get a link you can paste.

No browser opened at all. Over SSH, in WSL, in a fresh virtual machine, or on a machine with nothing registered for http, there may be no browser for looptroop open to launch. Since 0.5.7, it waits for the browser to sign in and prints the sign-in link itself when none does. To ask for that link without an attempt:

bash
looptroop open --print-url

The URL it prints ends in #bootstrap=…. It signs one browser in and then expires, so treat it as a password: do not paste it into a bug report.

looptroop start prints the same link when it starts the daemon.

The daemon restarted. Session tokens live in the daemon's memory and are regenerated on every start, so looptroop restart, a crash, or an upgrade ends every browser session immediately. Sessions otherwise last 12 hours. Sign in again with looptroop open.

The page is on localhost rather than 127.0.0.1. Session cookies are host-only: one obtained at 127.0.0.1:3000 is never sent to localhost:3000, even though they are the same server on the same port. Signing in again does not help, because looptroop open signs you in at 127.0.0.1 and a localhost bookmark still has no cookie. Use the 127.0.0.1 address.

NOTE

Current behavior. A brief SSE disconnect does not by itself sign the browser out. The client asks an ordinary API route with a five-second deadline after a failed stream connection; only an HTTP 401 response marks the session signed out. An unreachable daemon leaves the session state unchanged while the stream reconnects.

2. Runtime Stall Report ​

Run the report while npm run dev is still running, ideally during the slowdown:

bash
npm run diagnose:stall

The command writes a timestamped local report under tmp/diagnostics/, for example:

text
tmp/diagnostics/runtime-stall-YYYYMMDD-HHMMSS.log

The script is read-only. It does not mutate tickets, repair databases, or modify attached projects.

2.1 Platform Support ​

The diagnostic script runs on Linux, WSL2, macOS, and Windows.

FeatureLinux/WSLmacOSWindows
Process /proc inspection✅--
Pressure-stall metrics✅--
Cgroup resource snapshot✅--
TCP stats✅ (ss)✅ (netstat)✅ (netstat)
FD limits✅✅-
Zombie process count✅✅-
vm_stat / top integration-✅-
Shell baselinebash / shbash / shPowerShell

Platform-specific sections that are unavailable simply show as unavailable or n/a; the report still runs.

2.2 What the Report Captures ​

The report combines several layers of evidence:

  • Environment and startup context: resolved ports, candidate/listener PIDs, backend env snapshot, watcher context, shell startup latency, and focused ticket path resolution.
  • Endpoint probes: frontend, backend health, startup status, projects, tickets, and OpenCode reachability.
  • Short repeated samples: repeated backend and ticket probes to confirm whether the app was actually stalled during capture.
  • Runtime trend window: by default a 3-minute trend that samples backend health, /api/tickets, watched-process CPU/RSS/I/O, Linux pressure deltas, app/project DB and log growth, and trend-wide whole-system read/write/RSS/CPU leaders.
  • Process activity: backend, frontend, and OpenCode memory snapshots, wait state, thread count, FD count, and I/O counters.
  • System resource state: load, memory, pressure-stall metrics, cgroup state, vmstat, disk stats, and top resource consumers.
  • Storage and project state: mount type, free space, inode usage, filesystem latency, SQLite / WAL / SHM sizes, project ticket/session state, and Git responsiveness.
  • Focused ticket runtime sizing: when --ticket-path is supplied, the report also tracks runtime log growth, largest runtime subdirectories, and large artifact files such as build outputs.
  • Advanced probes: event-loop lag, localhost DNS probe, TCP states, zombie process count, swap pressure, and a diagnostic heap snapshot.

2.3 Useful Flags ​

FlagDefaultUse it when
--timeout-ms <ms>4000Probes are timing out too aggressively and you want slow endpoints or shell checks to finish
--sample-ms <ms>1000CPU or I/O spikes are brief and you want a wider one-window process sample
--trend-ms <ms>180000You want a longer or shorter observation window; use 0 to disable the trend entirely
--trend-interval-ms <ms>1000You want finer or coarser trend granularity
--ticket-path <path>noneYou want runtime sizing focused on one ticket; pass a .ticket dir, its runtime dir, or the worktree root
--backend-port, --frontend-port, --opencode-urlauto-detect when possibleYou started the stack on non-default ports or against a non-default OpenCode server
--no-coloroffYou are piping the report or running in CI; NO_COLOR is also respected
--helpoffYou want the built-in usage summary of all flags without running a diagnostic pass

Examples:

bash
npm run diagnose:stall -- --timeout-ms 8000
npm run diagnose:stall -- --sample-ms 5000
npm run diagnose:stall -- --trend-ms 120000 --trend-interval-ms 1000
npm run diagnose:stall -- --ticket-path /path/to/worktree/.ticket
npm run diagnose:stall -- --backend-port 3001 --frontend-port 5175 --opencode-url http://127.0.0.1:4097

2.4 Reading the Report ​

The top-level sections map directly to the report banners:

  • 🔍 ENVIRONMENT & CONFIGURATION: resolved ports, detected PIDs, backend env vars, shell latency baseline, and focused ticket path resolution.
  • 🌐 NETWORK & ENDPOINT HEALTH: current HTTP probe results for frontend, backend, ticket/project routes, startup status, and OpenCode.
  • 🔁 REPEATED RUNTIME SAMPLES: repeated backend/ticket probes plus the longer Runtime Observation Trend output.
  • ⚙️ APPLICATION PROCESS ACTIVITY: backend/frontend/OpenCode candidate processes, memory snapshots, open files, and per-process CPU / I/O samples.
  • 💻 SYSTEM RESOURCES: pressure, memory, uptime, and whole-system top CPU / RSS / read / write consumers.
  • 💾 STORAGE, MOUNTS & FILESYSTEM: mount details, disk and inode usage, filesystem latency, and optional focused ticket-runtime sizing.
  • 🗄️ DATABASE & PROJECT STATE: app DB pathing, project DB/WAL state, recent ticket/session state, and execution-log tailing.
  • 🔀 GIT RESPONSIVENESS: git status, Trace2 perf output, branch resolution, and other responsiveness checks for attached repos.
  • 🧬 ADVANCED DIAGNOSTICS: event-loop lag, DNS, TCP state counts, zombie counts, swap pressure, and diagnostic heap output.

For intermittent issues, save at least one report from a healthy moment and one from a slow moment. The diff between the two is usually more useful than either report alone.

3. Blocked-Error Diagnostics ​

When a phase fails hard enough to enter BLOCKED_ERROR, LoopTroop persists a normalized diagnostic payload alongside the error occurrence. The workspace summary immediately names the failed phase, shows a bounded first line of the captured error, and explains the available recovery actions. The ticket error view keeps the complete sanitized error message visible beneath one failed-phase heading. A distinct underlying cause also appears when it adds information. Technical details is collapsed by default beneath recovery actions. Every occurrence includes its ticket and occurrence identity, failed phase, exact timestamp, complete sanitized error, and the last five matching Debug-view records, even when no model or provider diagnostics were captured. Missing evidence is stated explicitly. The captured model, exact OpenCode session, provider metadata, codes, request timing, and metrics add further context when available. Initial interview-batch failures that block the ticket retain the model and session used for the failed response. The surrounding phase log has its own collapsible section. Diagnostics are normalized by shared/errorDiagnostics.ts and typically assembled by server/opencode/blockedErrorDiagnostics.ts.

Pre-flight, workspace setup, final-test, and bead failures preserve the actual cause in the ticket and error-occurrence message; stable diagnostic codes remain separate. Readiness failures name every failed doctor check and its reason. OpenCode health and model discovery can pass while the actual capability request fails, for example because the model provider's login expired. Capability-probe failures preserve the reported model, provider, session, operation, and cause when available. Authentication failures while checking OpenCode health, listing models, or creating the probe session are attributed to the OpenCode server; they do not suggest refreshing the model provider's login. Coding errors put the final stopping reason first, followed by earlier attempt failures from newest to oldest. Error history shows each occurrence's phase and resolution without borrowing the current bead's counters or notes.

Workspace initialization failures retain the actual error and available nested cause stacks, with the operation initializing the ticket workspace, runtime/system diagnostics, and their INIT_* codes. A Git authentication failure stays a workspace initialization failure; AI-provider login advice requires evidence of an AI-provider authentication failure. Initialization error messages are sanitized before entering ticket events, logs, stored error text, or API responses.

Known OpenCode-server failures keep server attribution. A model-provider rejection received while dispatching a prompt keeps its provider identity, HTTP status, error type, and response preview; arriving through OpenCode does not make it a server-login failure. Provider-login guidance requires actual provider diagnostic evidence or a provider-auth code. An expired token reported by a test, setup command, or another service keeps that phase's explanation. When several readiness checks fail, the summary identifies the combined readiness failure and the original error keeps each named reason. It does not add a second failed-check checklist.

If a prompt error reports only Unauthorized or Forbidden and provides no evidence identifying the server or provider, the error keeps the captured status, cause, and other metadata with neutral guidance and Retry. It does not infer a provider identity, suggest refreshing a particular login, or offer Continue.

After a known OpenCode-server login or permission rejection (HTTP 401 or 403), an existing preserved session can offer Continue once server login or permissions are repaired and the usual exact-session ownership and availability checks pass. Billing, quota, invalid-request, and policy denials do not qualify for this server-login exception. Failed session creation without an existing session stays Retry-only. Model-provider authentication failures keep their existing Retry behavior.

When available, Technical details also includes the HTTP status, provider error type, retryability, response finish reason, and input/output/reasoning token counts, including cache reads and writes. The provider response preview removes terminal control sequences before redacting recognized credential fields and common credential formats, then applies the OpenCode extractor's 280-character limit. The shared persistence normalizer has an outer ceiling of 1,000 characters. OpenCode's structured model errors retain their reported type, status, message, and response body, so recovery decisions use those fields even when the sentence alone does not identify authentication or a temporary provider failure. If a provider 401 includes explicit billing or payment-method evidence, the guidance addresses that cause without suggesting a login refresh. The reported type, status, and response preview remain available, and recovery stays Retry-only. The existing provider 402 Continue rule is unchanged. Long whitespace or escape sequences in malformed errors no longer cause repeated scanning during URL redaction or headline formatting, keeping error processing responsive. Redaction covers recognized credential keys, including camelCase assignments, secret and signing keys, SSH keys, passphrases, JWTs, and bearer values. It handles assignments split across lines in formatted JSON, labeled authorization values, Basic and Bearer credentials, and cookie header secrets. Cookie redaction preserves unambiguous neighboring log fields, such as session, model, status, and message, and keeps expanded JSON valid. A real cookie header remains fully redacted when its contents cannot be safely separated from other fields. Common failure prose, structured formatting, and repeated lines remain readable, and the preview supports keyboard scrolling. Background refreshes of the same failure preserve the open section and keyboard focus. Selecting another ticket or error occurrence closes Technical details again. Initial interview parsing failures that block the ticket retain available finish and token/cache metrics; if stopping the session also fails, the cleanup error keeps the original failure as its cause.

The five debug records appear newest first, and each record starts collapsed. Expand a record to inspect its complete multiline message and structured data after credential redaction. Each original JSONL line counts as one record, even when its displayed data spans many lines. Repeated session errors remain separate records even if they share the same display identifier. LoopTroop matches the selected error occurrence's failed phase and time window, so later retries cannot replace the evidence, including retries that reused the same OpenCode session. If only a reconstructed error is available, the view says that it matched records by phase and time and labels a reconstructed failure time as approximate. If a phase or usable end time is missing, the view says that matching records are unavailable. The server applies known bounds before selecting the five records, so newer activity cannot hide earlier evidence. OpenCode native records use either time or timestamp for their recorded time. Missing metadata, absent records, and log-read failures are explicit; a failed read offers Retry loading debug records. Technical details does not page through older records. Use the separate phase log's Debug view for the complete history.

LoopTroop discovers native session ownership within the selected ticket and phase scope before filtering record times. An earlier ownership record can link a native error inside the failure window without appearing as evidence itself. Attempt and bead filters still apply, and foreign sessions and out-of-window records remain excluded.

OpenCode communication failures name the operation that failed, such as sending a prompt or reading session messages. Aborts and timeouts retain their existing handling. Other failures while reading an unsuccessful HTTP response body keep the known status, any established origin, and the original read cause when available. An unreadable 404 does not prove an existing session is gone, so its ownership remains preserved for later verification. An unidentified prompt rejection keeps neutral guidance and Retry. LoopTroop keeps the original error cause through session-creation retries and nested AI retry errors, and shows the reported connection code or cause when available. If OpenCode or the runtime reports only fetch failed, the message says that the underlying cause was not reported. This leaves a connection failure distinct from a model rejecting a request or an expired deadline. Technical details keeps the operation, connection code, and bounded sanitized cause message alongside the model and session. Terminal session failures retain their known operation, and non-connection codes are not shown as Connection code. URL credentials, query strings, and fragments are removed before diagnostic text is saved or shown.

Use this surface when the ticket already blocked and you want the reason, not the whole-machine health picture.

Startup artifact recovery distinguishes ordinary orphan content from an in-progress fallback. An orphan YAML temp without its matching proof, or a torn whole-file JSONL temp, is warned about and left unpromoted; append logs alone may receive bounded trailing-line repair. RECOVERY_BLOCKED is raised only when an in-progress fallback's .recovery ownership or completeness cannot be verified, before rebuilding projections, hydrating ticket actors, or starting execution timers. The affected files and diagnostic remain available at that blocking point. This process-level startup failure occurs before ticket actors exist, so it does not create a BLOCKED_ERROR ticket occurrence or expose ticket Retry, Continue, or Cancel actions.

Coding and integration recovery safeguards ​

NOTE

Current behavior. The conditional step-cap and Git-hook recovery safeguards in this subsection describe the current implementation.

When CODING reports an OpenCode step-cap restore conflict, LoopTroop keeps the edited root opencode.json and .ticket/opencode-steps-restore.json available for review. It refuses a destructive reset only when the current bytes conflict with valid marker evidence. Ordinary capped retries continue normally. A later bead may continue without a fresh cap when no reset is needed, and the valid marker keeps the root config out of bead and final staging. If the sidecar is missing after a restart, ownership cannot be proved, so LoopTroop does not guess.

During INTEGRATING_CHANGES, protected explicit hook validation can refuse reentry when an interrupted restore cannot safely account for unknown untracked additions. The persisted marker binds the worktree and Git directory as well as the index/worktree trees and initial untracked set. Invalid or escaped markers fail before recovery writes. Unknown additions remain intact until their attribution is resolved. Normal integration still follows the selected hook policy; this guard does not claim that unrelated hook commands are harmless or fully transactional.

Approval-save and draft diagnostics ​

NOTE

Current behavior. The approval-save baseline checks, retained drafts, and best-effort leaving flush described in this subsection are current.

Approval panes retain the content hash loaded with a dirty interview or PRD draft. A missing baseline is reported as HTTP 428; a stale baseline is a typed HTTP 409 conflict. The server checks that precondition for both raw and structured saves before changing the authoritative artifact. A competing post-approval writer can also receive 409 before restart or invalidation, so the right recovery is to reload or retry the current draft rather than assume both saves were accepted.

If a pane leaves or the selected ticket changes, its UI-state flush is best-effort. A failed keepalive/beacon leaves the newest draft visibly unsaved/error for the existing retry path. A completed GET may remember the remote revision while retaining that unconfirmed local payload; it must not be read as proof that the browser unload delivered the save.

Configuration, project, and prompt form diagnostics ​

NOTE

Current behavior. Form dirty-state and preview handling described in this subsection are current client behavior.

Form close warnings compare actual current values with the saved or initial snapshot, including custom model/profile controls; typing and then restoring a value clears the warning. Hydration or a background refetch does not replace a draft already being edited. Model catalog loading and errors are announced, folder-check failures remain distinct from a genuine non-Git directory and can be retried, and prompt preview errors belong only to the current prompt and draft. A stale preview response is ignored rather than displayed as current.

Remote-stop uncertainty and ownership recovery ​

NOTE

Current behavior. Confirmed-stop handling, marker replay, and the two-storage restart limit in this subsection describe the current implementation.

An abort request that returns false, throws, or cannot be verified is not proof that OpenCode stopped. The ticket stays retryable, and the session ownership remains visible so Retry or a later reconciliation can try again. Project SQLite ownership normally has a ticket-contained fallback marker at .ticket/runtime/opencode-pending-sessions.json, which startup can replay even when the database row is missing. If both SQLite and marker storage are unavailable, only the current process guard remains. A restart cannot claim recovery in that case.

3.1 OpenCode Provider Error Enrichment ​

OpenCode sometimes streams only Provider returned error even though its local log contains the exact provider failure. LoopTroop best-effort correlates those generic stream errors with recent OpenCode log files by session.id and replaces the generic summary with a sanitized provider summary when a match exists. The diagnostic reader considers the ten newest candidate files and at most 5 MiB from each by default; complete DEBUG/history reads use a separate uncapped path.

The enrichment keeps only compact diagnostic fields such as HTTP status, retryability, provider/model identity, request model, provider error type/title/message, and a short response-body preview. It does not persist prompt bodies, raw request payloads, headers, cookies, authorization values, or URL query strings.

  • Managed local OpenCode: LoopTroop checks the default local OpenCode log directory.
  • External or nonstandard OpenCode: set LOOPTROOP_OPENCODE_LOG_DIR to the log directory.
  • No matching log found: the ticket keeps the generic provider error and adds a troubleshooting hint instead of inventing details.

3.2 Diagnostic Classification ​

Each blocked-error diagnostic has a normalized kind and source.

Kind (BlockedErrorDiagnosticKind)

KindMeaning
model_output_truncatedOpenCode reported a finish reason such as length, so the model response was cut off
opencode_providerThe provider returned an API-style failure such as auth, quota, rate limit, or invalid request
opencode_sessionSession creation, reconnect, or session-level lifecycle failure
timeoutA prompt or execution deadline was exceeded
transportConnection reset, DNS, socket, or other transport failure
runtimeInternal LoopTroop runtime or orchestration failure
unknownThe failure could not be classified

Source (BlockedErrorDiagnosticSource)

SourceMeaning
opencodeOriginated from the OpenCode integration layer
providerOriginated from the underlying model provider
systemOriginated from a LoopTroop system-level source
runtimeOriginated from a runtime execution error

In the main OpenCode blocked-error builder, provider-like failures are currently emitted with source provider; the other generated OpenCode failure kinds use source opencode.

3.3 Sensitive Data Redaction ​

Before persistence, normalizeBlockedErrorDiagnostics() sanitizes string fields:

  • API keys, bearer tokens, passwords, and similar secrets are replaced with [redacted].
  • Query strings are stripped from error text so token-like values in URLs are not persisted.
  • The redacted payload still keeps enough structure to debug the issue: status codes, provider/model identity, finish reason, and token counts survive when present.

Debug events from every workflow phase redact recognized credentials before formatting, shortening, live broadcasts, or saved log writes. This covers coding, council work, and readiness probes while they are running. Redaction uses a log copy, preserving model, session, process, timing, and numeric token-count metadata and the existing broadcast, persistence, and mirroring choices. Readiness probe event copies are also sanitized before other log views are derived from them. The completed doctor report remains sanitized before check logs and artifact storage. Debug-classified diagnostic rows use the same protection even when emitted through the normal phase logger. Debug copies also remove URL credentials, query strings, and fragments under the existing diagnostic rules. Ordinary AI, normal, and command transcripts keep their existing logging behavior.

3.4 Persisted Fields ​

The normalized blocked-error payload may include:

FieldMeaning
summaryRequired short explanation shown in the UI when it differs from the primary ticket error
errorNameCaptured error type, such as TypeError, when available
stackTraceSanitized error stack and available nested cause stacks, bounded to 12,000 characters
modelIdLoopTroop/OpenCode model identifier used for the failed run
sessionIdOpenCode session involved in the failure
operationOpenCode operation that failed
failedChecksCompact summaries of every failed readiness check: stable name, category, and reported reason; detailed evidence stays in debug records and the pre-flight report
transportCodeReported connection error code, when available
causeMessageBounded sanitized underlying error message, when available
providerIdProvider identifier such as openai
providerModelIdProvider-native model identifier when it differs from the requested model
requestModelExact request model recorded by provider diagnostics
statusCodeHTTP status code when available
isRetryableWhether provider diagnostics marked the failure as retryable
providerErrorTypeProvider error type/classification
providerErrorTitleProvider error title or headline
providerErrorMessageRedacted provider error message
responseBodyPreviewShort redacted preview of the provider response body
finishReasonOpenCode finish reason for truncation-style failures
inputTokens / outputTokens / reasoningTokensToken counts reported by OpenCode
cacheReadTokens / cacheWriteTokensToken-cache counts when OpenCode exposes them

The blocked-error panel shows available diagnostics in Technical details, including error type and stack, the bounded provider response preview, and cache token counts. Failed readiness steps remain in the recorded error and debug evidence. Each failure line keeps its step, error-type prefix, and HTTP status in regular text; only the actionable cause is bold. Generic missing-detail messages remain in regular text. The section starts collapsed so the actual failure and recovery actions remain easy to find. The log supplies the last five matching debug records. Evidence entries requested by occurrence or time window are redacted before leaving the backend; ordinary log pages and exports retain their existing content and opaque cursors. Native records' added rawLine field is sanitized in the common reader for every consumer. Existing source files are not rewritten. New phase debug events are sanitized before broadcast or log storage. Doctor reports are sanitized before they become debug records or report artifacts. Nested diagnostic data stops at 100 levels with an explicit omission marker. Debug evidence has no preview-length limit.

Coverage phase logs separately label preparation and each audit or revision. Each coverage check has its own number, and response attempts within that check have a separate counter. Session messages say whether LoopTroop created a new session or reconnected to an existing one. A session-ready message does not confirm that a prompt was sent or accepted; request preparation and waiting are separate steps. A reconnection keeps the current check number. PRD and beads logs also name the candidate version and distinguish the coverage audit from a revision to resolve its reported gaps. See Coverage Control for the counters and limits.

AI gap fixes requested from PRD or blueprint approval use a separate manual coverage fix N label, with revision and audit response attempts. Their coverage check numbers do not show an automatic-limit denominator. Reconnecting keeps that fix number, too. Interview coverage timeouts retain their captured model, session, operation, and diagnostic codes in the error occurrence so existing continuation checks can use them.

4. Structured Retry Diagnostics ​

Structured retry diagnostics explain recoverable structured-output failures. They are normalized by shared/structuredRetryDiagnostics.ts, merged into structured-output metadata by server/structuredOutput/metadata.ts, and rendered in artifact notices / viewers as Retry Attempts.

Use this surface when a phase kept going after rejecting one or more malformed outputs, or when a final failed artifact needs to show exactly why previous attempts were rejected.

4.1 What Gets Stored ​

Each retry entry captures one rejected attempt:

FieldMeaning
attempt1-based retry attempt number
validationErrorWhy the parser or validator rejected that attempt
failureClassOptional coarse classification such as validation_error, output_truncated, empty_response, provider_error, connection_reset, session_protocol_error, or transport_error
targetOptional target field or schema area that failed
line / columnOptional location for line-oriented parse failures
excerptBest-effort trimmed excerpt from the rejected output

Malformed retry entries are dropped during normalization, and duplicate retry entries are collapsed so the UI does not show the same failure repeatedly.

4.2 Where It Shows Up ​

Structured retry diagnostics currently surface in artifact-oriented UI rather than the blocked-error panel:

  • artifact processing notices
  • expanded artifact viewers
  • per-owner aggregate vote views when retry metadata exists on contributing artifacts

These diagnostics complement blocked-error diagnostics rather than replacing them:

  • Blocked-error diagnostics answer "why did the ticket stop?"
  • Structured retry diagnostics answer "why was this earlier model output rejected before we recovered or finally gave up?"

LoopTroop documentation for the current runtime.