import type { LastModelRequest } from '#src/core/debugCapture.js'; import type { ApprovalDecisionCapture } from '#src/core/shell/approvalCapture.js'; import type { GthFinishReasonObservation, GthTerminationReason } from '#src/core/terminationReason.js'; import { type GthOutstandingWork } from '#src/core/outstandingWork.js'; /** * GS2-46/GS2-47 — `/debug-dump`: a live-session diagnostic archive. GS2-46 shipped it raw; GS2-47 * adds a shared secret-redaction pass (`redactSecrets.ts`) that is ON BY DEFAULT and * applied to EVERY artifact before it hits disk. The caller opts out via `redact: false`, in which * case the archive is raw and the caller surfaces the loud "may contain secrets" warning; when * redaction is on the caller shows a softened "secrets redacted; review before sharing" note. */ /** Input to {@link writeDebugDump}. */ export interface WriteDebugDumpInput { /** The full transcript (all turns, tool calls + results). Opaque — serialized as JSON. */ transcript: unknown; /** The resolved effective config (the live `GthConfig`). Opaque — serialized as JSON. */ config: unknown; /** Model display name, already resolved by the caller. */ modelDisplayName?: string; /** * GS2-47 — apply the shared secret-redaction pass to every artifact before writing. Defaults to * ON: omitted or any value other than the literal `false` redacts (read-site `!== false`, matching * the config default so an opt-out has to be explicit). `false` writes a RAW archive (the caller * is then responsible for the loud "may contain secrets" warning). */ redact?: boolean; /** * Working directory for git-state collection. Defaults to `process.cwd()`; overridable for * tests so the "not a git repo" / "inside a git repo" paths are both exercisable without * depending on where the test runner happens to execute. */ cwd?: string; /** * GS2-56 — the always-on snapshot of the last model request (the composed system prompt, tool * defs with schema, model params, tool-choice, and the AS-SENT post-summarization messages), * threaded straight from `agent.lastModelRequest` by the caller (both the TUI and the readline * `--no-tui` surface). ADDITIVE & optional: when present, `writeDebugDump` writes * `model-request.json` (the extras) and `model-messages.json` (the as-sent messages, distinct * from the conversation-view `transcript.json`), both routed through the same redaction pass as * every other artifact. Absent (no model call yet / a surface with no agent handle) ⇒ those two * files are simply omitted. */ modelRequest?: LastModelRequest; /** * [[TUI-C27]] — the approvals gate's own record of every gated decision this session made: * **what the rater was SHOWN and what it ANSWERED**, on the approving branch as much as the * rejecting one, plus which stage of the gate decided. * * Threaded straight from `runner.getApprovalCaptures()` by the caller, exactly as * {@link modelRequest} is threaded from the agent. ADDITIVE & optional: present and non-empty ⇒ * `approvals.json`; absent or empty ⇒ the file is simply omitted (a session that gated nothing * has nothing to say about it). * * It goes through the same `renderStructured` pass as `transcript.json` and * `model-messages.json` — the [[GS2-47]]/[[GS2-54]] literal + pattern redaction over the secret * values {@link collectSecretValues} harvested from env and config. That is deliberate reuse * rather than a policy of its own: a record carries the command, the rating, and — through the * alignment check's `user` role ([[EXT-127]], fed from the retained provenance window) — the * user's own recent messages verbatim, so it is at least as sensitive as the transcript, and a * second redaction policy for one artifact is how two policies come to disagree. * * [[EXT-109]] — redaction is not the control for what a user asked to be FORGOTTEN, because that * matches no secret shape. `GthAgentRunner.clearConversation` empties the log on `/clear`, so a * dump taken after one carries no record from before it. */ approvals?: readonly ApprovalDecisionCapture[]; /** * [[EXT-159]] — why the last turn ended, and what the provider said about each message in it. * * **This is the artifact's answer to the question it exists to be asked.** Measured on two real * dumps of a stopped run: the terminating error appeared only in `transcript.json`, nothing in * `debug-log.txt` said what happened, and no `finish_reason` was recorded anywhere at all — so * the file a maintainer opens first could not say which of a dozen unrelated causes it was. * * ALWAYS written when the caller threads it, including when `reason` is `null`. An absent reason * means a termination site nobody classified, which is a defect signal this archive exists to * carry; omitting the file for it would make the one case worth reporting the one case that * leaves no trace. */ termination?: DebugDumpTermination; } /** [[EXT-159]] — the termination section of a dump: the reason, and the provider's own tokens. */ export interface DebugDumpTermination { /** * The typed reason the last turn ended, or `null` when no site classified it. * * `null` is written as `null`, never replaced by a placeholder category: the taxonomy's contract * is that absence means a site we missed, and a filled-in default would silence exactly the * detector that contract exists to provide. */ reason: GthTerminationReason | null; /** * The provider's stop/finish reason per observed model message — with `token: null` on any * message that carried none. * * Three states, and they are all different: a token says what the provider said; a `null` token * says the provider said nothing about a message we DID see; and an empty list says no finished * model message was observed at all. */ finishReasons: readonly GthFinishReasonObservation[]; /** * [[EXT-158]] §(3) — the checklist work the last turn left outstanding, or `null` when it left * none (or there was no checklist, which is ordinary). * * **Recorded as automated, in the field name and in the value.** The one reliable marker for * finding this failure in a transcript is a human typing *"continue"*, so whoever reads this dump * must be able to see at a glance that the counts here are the runtime's own observation and not * something a person or the model wrote. {@link outstandingWorkSource} says so in words, because * a field name is a convention and a sentence is not. */ outstandingWork: GthOutstandingWork | null; /** * What produced {@link outstandingWork}, in words a reader cannot mistake for anything else. * Always present when the section is, whether or not there was anything outstanding. */ outstandingWorkSource: string; } /** The read surface {@link readTermination} needs — satisfied by `GthAgentRunner`. */ export interface TerminationReadable { getTerminationReason(): GthTerminationReason | null; getFinishReasonObservations(): readonly GthFinishReasonObservation[]; /** * [[EXT-158]] — optional, so a runner predating it (an embedder's own, a test double) still * yields a termination section rather than none at all. The dump then records `null`, which is * the same answer as "nothing was outstanding" — an acceptable conflation here, because unlike * the reason above this fact carries no defect signal in its absence. */ getOutstandingWork?(): GthOutstandingWork | null; } /** * [[EXT-159]] — build the dump's termination section from a runner, or `undefined` if it cannot be * read at all. * * **Three outcomes, and collapsing any two of them loses the point.** A reason is the answer. A * `null` reason is the answer that no site classified the ending — a defect signal, and the reading * a maintainer most needs, so it is written rather than omitted. A failed read is neither: it says * this session could not report on itself (an older runner, an embedder's own), and omitting the * section is how that stays distinguishable from "nobody classified it". * * Shared by the two `/debug-dump` writers rather than written twice, so the surfaces cannot come to * disagree about what an absence means. */ export declare function readTermination(runner: TerminationReadable): DebugDumpTermination | undefined; export interface WriteDebugDumpResult { /** The absolute path to the archive directory just written. */ archiveDir: string; } /** * The filesystem-safe directory-name segment for one dump: the ISO timestamp with `:` and `.` * (illegal on Windows, noisy everywhere) replaced by `-`. This is the ONLY path component * debugDump generates — and the only one it is responsible for sanitizing. The parent it is joined * under (the global `~/.gsloth` dir) is supplied by the environment and may legitimately carry a * drive-letter colon on Windows (`C:\…`), which is not ours to strip. Exported so the invariant * "the generated segment is colon-free" is testable on any platform without asserting anything * about the (platform-dependent) parent path (GS2-50). */ export declare function debugDumpDirName(date?: Date): string; /** * Write one timestamped `/debug-dump` archive under the GLOBAL `~/.gsloth/debug-dumps//` * (via `ensureGlobalGslothDir()` — mirrors how `resolveHistoryDbPath()` builds its path under the * same dir — NOT the per-project cwd-relative helper of the same name elsewhere in this codebase). * Contains: the full transcript, the resolved config, env/version info, the in-memory debugLog * ring buffer, and (best-effort) git repo state. * * GS2-47 — unless `input.redact === false`, the shared secret-redaction pass * (`redactSecrets.ts`) is applied to EVERY artifact before it is written: the literal * values of secret-named env vars + inline config secrets are substituted everywhere, a tight set of * provider-key/auth-header patterns is masked, and the config's sensitive fields are masked in place. * On opt-out the archive is raw and the caller surfaces the loud "may contain secrets" warning. */ export declare function writeDebugDump(input: WriteDebugDumpInput): WriteDebugDumpResult; /** Input to {@link writeCrashSnapshot}. */ export interface WriteCrashSnapshotInput { /** The thrown error / rejection reason. Need not be an `Error`. */ error: unknown; /** Where the failure came from — e.g. `'uncaughtException'` / `'unhandledRejection'`. */ origin: string; /** The effective resolved config, if the crashing process had one registered. Opaque. */ config?: unknown; /** Model display name, if known. */ modelDisplayName?: string; /** The in-flight turn's transcript tail, if a session registered one. Opaque. */ transcriptTail?: unknown; } export interface WriteCrashSnapshotResult { /** The absolute path to the crash directory just written. */ crashDir: string; /** The absolute path to the `crash.json` file inside it. */ crashFile: string; } /** * GS2-48 — write a MINIMAL, ALWAYS-REDACTED crash snapshot to * `~/.gsloth/debug-dumps/crash-/crash.json` and return its paths. * * This is the unattended sibling of {@link writeDebugDump}: it fires from the process-level crash * handler (`crashHandler.ts`) with no human present, so — unlike `/debug-dump` — it * exposes NO `redact` opt-out. Redaction (GS2-47, hard-dep) is MANDATORY and applied to the whole * snapshot. It reuses this module's location convention (`ensureGlobalGslothDir()` + `debug-dumps/`) * and rendering helpers, but deliberately stays minimal and crash-safe: * - a SINGLE `crash.json` (not the multi-file interactive archive); * - it does NOT shell out for git state (a subprocess is unsafe/slow while the process is dying); * - it keeps only the last `CRASH_DEBUG_LOG_TAIL_LINES` debugLog lines and the transcript TAIL. * * Redaction shape (conscious choice): the entire normalized snapshot is passed through * {@link redactValue} in one pass. That is stricter than {@link writeDebugDump}, which uses * {@link redactText} (literal+pattern only) for its non-config artifacts to avoid field-masking a * legitimately `token`/`secret`-named field in tool output. For an unattended crash file the * over-masking is the SAFE direction and buys uniform circular-ref / depth / function handling in * one place, so it is intended, not an oversight. The config subtree is stripped of live instances * first (as `writeDebugDump` does) so a live LLM client's internals never serialize. * * Callers wrap this in their own try/catch (see `crashHandler.ts`); it does its best * to be fail-safe internally (`redactValue`/`safeStringify` never throw), but the ONE unavoidably * throwing operation is the `mkdirSync`/`writeFileSync` to disk — an unwritable `~/.gsloth` is what * the handler's degraded path is for. */ export declare function writeCrashSnapshot(input: WriteCrashSnapshotInput): WriteCrashSnapshotResult;