# DOX — packages/extension/src

Files in this directory. One row per source file.

| File | Purpose |
|------|---------|
| `abort-latch.ts` | Pure class `AbortLatch`. Keeps user abort latched so provider backoff (5–60s) outliving 2s persistent-abort… → see `abort-latch.ts.AGENTS.md` |
| `agent-settled.ts` | Bridge `agent_settled` normalization (pure). `nativeAgentSettledSupported(piVersion)` → pi ≥ 0.80.4 emits… → see `agent-settled.ts.AGENTS.md` |
| `artifact-roots.ts` | Artifact-root allowlist for Fix B bridge image inlining. `resolveArtifactRoots({homedir,env,realpathSync})` →… → see `artifact-roots.ts.AGENTS.md` |
| `ask-user-attachments.ts` | Persist image attachments for ask_user input responses. Exports `attachmentDirForSession`,… → see `ask-user-attachments.ts.AGENTS.md` |
| `ask-user-tool.ts` | Register `ask_user` pi tool at `session_start` (avoids static-name conflict). Exports `registerAskUserTool`. → see `ask-user-tool.ts.AGENTS.md` |
| `auto-session-namer.ts` | Automatic session topic-naming (bridge-side). Pure helpers `shouldSkipByPrefilter`, `parseTitle`,… → see `auto-session-namer.ts.AGENTS.md` |
| `autostart-guard.ts` | Worktree auto-start refusal + durable log. Exports `isWorktreeCliPath`, `shouldRefuseWorktreeAutoStart`, `appendAutoStartLog`. See change: fix-worktree-server-autostart-leak. |
| `autostart-lock.ts` | Single-flight auto-start lock at `~/.pi/dashboard/autostart-<port>.lock`. Exports `acquireAutoStartLock`, `releaseAutoStartLock`, `recordChildPid`, pure `isLockStale`. See change: fix-worktree-server-autostart-leak. |
| `bridge-context.ts` | Shared mutable bridge state + pure predicates. Exports `BridgeContext`, `DASHBOARD_NATIVE_COMMANDS`,… → see `bridge-context.ts.AGENTS.md` |
| `bridge-default-model-gate.ts` | Pure predicate `shouldApplyDefaultModel({reason, entryCount, hasModelRegistry, hasDefaultModel, hasExplicitModel})` + pure `hasExplicitModelArg(argv)` (exact-token `--model` match on pi's own argv). Explicit `--model` dominates — the default never overrides the spawner's resolved choice. → see `bridge-default-model-gate.ts.AGENTS.md` |
| `bridge-ticket-client.ts` | Mints the credential a REMOTE bridge needs to open a gateway connection (§6 made TCP bridge auth mandatory). `DEVICE_TOKEN_ENV` (`PI_DASHBOARD_TOKEN`), `readDeviceToken(env)`, `mintBridgeTicket({httpBase, token, fetchImpl})` → single-use bridge-scoped ticket, `withTicket(url, ticket)`. The durable paired-device bearer NEVER reaches the gateway — only the 15s ticket does. Every failure names a cause (`no-token`/`unreachable`/`refused`/`malformed`) and the bridge refuses to dial rather than attempt an unauthenticated upgrade. Wired via `ConnectionManager.prepareConnect`, which re-mints on EVERY attempt because a ticket is single-use. See change: add-pi-gateway-transport-identity. |
| `bridge.ts` | Main bridge extension entry (default export). Connects to dashboard server, forwards pi events via… → see `bridge.ts.AGENTS.md` `applyDefaultModel()` success branch also applies `config.defaultThinkingLevel` via `pi.setThinkingLevel(level)` (synchronous, before the model-update push) when non-empty; empty defers to pi's own resolution. Single site covers both the gate call and the pending-provider retry. See change: add-default-thinking-level. Carries the auto-namer's `PersistedNamerState` across reload via `prev.namerState` (VALUES, never the namer object — its closures would hold a stale connection/ctx), adopts the server's `auto_name_state_restore` STOP fields only (provenance deliberately untouched), and no longer gates the naming call site on the toggle so `disabled` is reportable. See change: fix-auto-naming-reasoning-model. pi does NOT dispatch `entry_appended` to extensions, and idle-path sendMessage emits internally only — so `wrapCustomPersistenceForCtx(ctx)` (session_start) wraps `sessionManager.appendCustomMessageEntry`/`appendCustomEntry` (the persistence chokepoint, same pattern as wrapAppendMessageForCtx) forwarding `message_end` role=custom + `custom_entry`; the enriched loop SKIPS role=custom message_start/end (no double-forward of the agent-loop path). See change: render-inline-reasoning-and-custom-entries. |
| `command-handler.ts` | Command routing: `!`/`!!` bash, `/compact`, slash commands. → see `command-handler.ts.AGENTS.md` Exports `validateImages` (MIME allow-list via `imageBlockMime`/`imageBlockData`, returns dropped reasons for reporting) + `buildUserMessageContent` (content assembly, NO send options — the bridge drain reuses both without the `deliverAs` that breaks it); `onFollowupSent(text, images?)` carries attachments into the bridge buffer. See change: fix-bridge-followup-image-drop. `request_models` is dispatched OUTSIDE the serialized inbound lane (`connection.ts` `IMMEDIATE_TYPES`), so its awaited catalogue refresh cannot head-of-line-block a later `send_prompt`. See change: fix-optimistic-prompt-stuck-sending. `request_models` keeps the `reportRefresh()` result and attaches per-provider failures as `models_list.refreshErrors` (omitted when none); last-known catalogue still served. See change: upgrade-model-selector-primitives. |
| `connect-target.ts` | `parseConnectTarget()` / `describeConnectTarget()` — parses the overloaded `/dashboard connect <target>` argument into default/socket/port/url/instance. Shape-only: no fs, no network, so a mistyped path stays a PATH rather than becoming a bogus "no such instance". Resolution is a separate step (task 9.5). See change: add-pi-gateway-transport-identity. |
| `connection.ts` | WebSocket connection manager with exponential backoff reconnect, message buffering while disconnected,… → see `connection.ts.AGENTS.md` `IMMEDIATE_TYPES` also carries `request_models`: serialized, a hung catalogue refresh head-of-line-blocks the pump and later `send_prompt`s never dispatch. See change: fix-optimistic-prompt-stuck-sending. Migration gate: `retargetTo`/`noteRegistered`/reversal+cooldown replace `updateUrl`. See change: fix-bridge-mdns-migration-hijack. |
| `custom-entry-forward.ts` | Pure mappers for the bridge's custom-content forwarding: `toCustomEntryForward(entry)` (null for non-custom/empty/`flow-event` customType) and `toCustomMessageForward({customType,content,display,details,entryId})` (null for `display === false` exact, or flow-event). Driven by `wrapCustomPersistenceForCtx` in bridge.ts. Payload types carry optional `groupId` — server-stamped, never set extension-side. See changes: render-inline-reasoning-and-custom-entries, add-custom-event-group-filters. |
| `dashboard-context-injector.ts` | Registers `before_agent_start` handler. Splice-replaces trailing `Current working directory:` line of system… → see `dashboard-context-injector.ts.AGENTS.md` |
| `dashboard-default-adapter.ts` | Built-in last-resort `PromptAdapter` (priority `9999`). Exports `DashboardDefaultAdapter`. → see `dashboard-default-adapter.ts.AGENTS.md` |
| `dev-build.ts` | Dev build-on-reload helper. Exports `runDevBuild`, `DevBuildOptions`. → see `dev-build.ts.AGENTS.md` |
| `empty-actionable-guard-config.ts` | Resolve empty-actionable guard config from env. Exports `resolveGuardConfig(env)` → `{mode,retryCap}`. → see `empty-actionable-guard-config.ts.AGENTS.md` |
| `empty-actionable-guard.ts` | Bounded continue-or-surface decision for empty-actionable turns. → see `empty-actionable-guard.ts.AGENTS.md` |
| `endpoint-resolution.ts` | Pure D3 precedence ladder + D4 stickiness decisions; the only place the bridge chooses an endpoint. `isLoopbackEndpoint` classifies the localhost-preference invariant. → see `endpoint-resolution.ts.AGENTS.md` |
| `instance-verification.ts` | "Is this the dashboard I meant?" — the question no local credential answers (socket mode + local token are per-HOME, so every same-HOME instance passes them). `decideAdoption({expected,observed})` pure fail-closed decision (silence ≠ verification; no expectation ⇒ adopt, for records predating `instanceId`), `healthUrlForInstance(httpPort)`, `verifyInstanceIdentity({healthUrl,expectedInstanceId,fetchImpl?,timeoutMs?})`, `probeEndpointReachability({healthUrl,fetchImpl?,timeoutMs?})` (reachability-only `{ok:true}` probe for the migration admission gate). Returns `conflict` ALONGSIDE `adopt`: only an endpoint that ANSWERED and was not the expected instance (or could not name itself) is a conflict; unreachable/non-OK is `unverified`. `bridge.ts` disconnects terminally on a conflict only — `disconnect()` sets `intentionalClose`, and `POST /api/restart` makes `/api/health` unreachable on every rebuild while the gateway socket stays healthy. Used by `bridge.ts` after connect on a `rendezvous-record` endpoint, and to stamp the id every re-target is judged against. See change: add-pi-gateway-transport-identity (D8, D14, tasks 3.4/3.8). `probeEndpointReachability` wired as the migration gate's `verify`. See change: fix-bridge-mdns-migration-hijack. |
| `event-forwarder.ts` | Map pi event objects to `event_forward` protocol messages. Exports `mapEventToProtocol`. → see `event-forwarder.ts.AGENTS.md` |
| `flow-event-wiring.ts` | Register pi-flows + pi-subagents event listeners on `pi.events`. → see `flow-event-wiring.ts.AGENTS.md` |
| `followup-buffer.ts` | Bridge-owned follow-up buffer, extracted from `bridge.ts` so admission is testable + the byte ceiling injectable. Exports `FollowUpEntry` (`{text, images?}`), `FOLLOWUP_QUEUE_CAP` (20), `FOLLOWUP_BUFFER_MAX_BYTES` (32 MiB), `entryBytes`, `createFollowupBuffer({cap?, maxBytes?})`. Two independent bounds (depth + aggregate bytes); total RECOMPUTED from live entries per admission, never accumulated. Refusal not eviction; entries refused whole, images never stripped. Sizes via `imageBlockData` (a direct `.data` read sizes a nested block at zero). Holds no transport — returns decisions; `bridge.ts` owns emission. See change: fix-bridge-followup-image-drop. |
| `git-link-builder.ts` | Parse SSH/HTTPS remote URLs into branch + PR links. Exports `parseRemoteUrl`, `detectPlatform`,… → see `git-link-builder.ts.AGENTS.md` |
| `commit-draft.ts` | Pure AI-draft fallback ladder (no pi-SDK coupling). Exports `draftCommitMessage(deps)` → `{message, source}`,… → see `commit-draft.ts.AGENTS.md` |
| `commit-draft-agent.ts` | pi-SDK-coupled half of AI-draft. Exports `buildSessionContextText(ctx, maxChars)` (compacts… → see `commit-draft-agent.ts.AGENTS.md` |
| `git-poll.ts` | Exports `runGitPollTick(deps)` + `GitPollDeps` interface. Pure git + name/model poll-tick body. → see `git-poll.ts.AGENTS.md` |
| `hasui-flip.ts` | Flip `ctx.hasUI` to `true` after bridge patches `ctx.ui.*`. Exports `flipHasUI`. → see `hasui-flip.ts.AGENTS.md` |
| `local-token-header.ts` | Bridge side of D6: `readLocalToken(env?)` (`~/.pi/dashboard/local/token`, trimmed; empty ⇒ undefined) + `localTokenHeaders(endpoint, env?)` → `X-Pi-Local-Token` ONLY on a loopback TCP dial (undefined over `ws+unix:` and never to a remote endpoint). Wired as `ConnectionManager.headers` in `bridge.ts`. See change: add-pi-gateway-transport-identity (task 5.3). |
| `markdown-image-inliner.ts` | Bridge helper rewriting assistant `![alt](path)` → `![alt](pi-asset:<hash>)` (SHA-256/16, MIME allowlist, 5… → see `markdown-image-inliner.ts.AGENTS.md` |
| `model-refresh.ts` | Shared `ModelRegistry.refresh()` handling: `reportRefresh(pending,label)` surfaces abort/per-provider errors and bounds the wait by `REFRESH_TIMEOUT_MS` (10s) so a hung refresh can never block its caller; late rejections swallowed. See change: fix-optimistic-prompt-stuck-sending. |
| `model-tracker.ts` | Diff-and-send trackers for model / session name / git info / pi version / cwd-missing. → see `model-tracker.ts.AGENTS.md` |
| `multiselect-decode.ts` | Pure helper decoding `PromptResponse` into `string[] | undefined`. → see `multiselect-decode.ts.AGENTS.md` |
| `multiselect-list.ts` | TUI multi-select component implementing pi-tui `ComponentLike`. Exports `MultiSelectList`, `ComponentLike`. → see `multiselect-list.ts.AGENTS.md` |
| `multiselect-polyfill.ts` | Polyfill `ctx.ui.multiselect`. Exports `polyfillMultiselect`, `PolyfillCtx`. → see `multiselect-polyfill.ts.AGENTS.md` |
| `notify-proxy.ts` | `createNotifyProxy({sessionId, send, originalNotify, newId?})` — builds the `ctx.ui.notify` replacement bridge.ts installs. Calls pi's original notify, then sends `{type:"notify", sessionId, notifyId, message, level?}`; never PromptBus, never `prompt_request`, no `promptId`/`component`/`placement`. Level normalized via `normalizeNotifyLevel`; omitted when the caller passes none. `newId` injectable for tests. See change: split-notify-from-prompt-request. |
| `openspec-cli-shim.ts` | Provision bare `openspec` in-session: shim pinned CLI onto `process.env.PATH` at bridge init (fail-soft). Exports `provisionOpenspecCli`,… → see `openspec-cli-shim.ts.AGENTS.md` |
| `pi-env.d.ts` | Ambient fallback declarations for current `@earendil-works/pi-*` hosts plus legacy Mario aliases; `ExtensionAPI` mirrors custom `sendMessage(...,{triggerTurn,deliverAs})` used by hidden dashboard Retry. See change: fix-retry-error-lifecycle. |
| `pi-retry-settings.ts` | READ-ONLY reader for pi's own retry policy. Exports `readPiRetrySettings({home?,cwd?,readFile?,fileExists?})`, `PiRetrySettings`, `PI_RETRY_DEFAULTS` (`maxRetries:3`, `baseDelayMs:2000`). Merges global `~/.pi/agent/settings.json` then project `<cwd>/.pi/settings.json` (project wins). Present-but-unparseable file → `baseDelayMs:0` → surface renders elapsed-only instead of a fabricated countdown. Never throws, never writes. Feeds `RetryTracker`'s `delayMs`/`maxAttempts` display math. See change: retry-forever-with-stop-control. |
| `process-metrics.ts` | Lightweight process metrics collector for bridge heartbeats. → see `process-metrics.ts.AGENTS.md` |
| `process-scanner.ts` | Detect child processes of a pi session. Exports `getOwnPgid`, `captureChildPgids`, `scanTrackedProcesses`,… → see `process-scanner.ts.AGENTS.md` |
| `project-trust.ts` | `project_trust` auto-decision (pure gate + defensive cwd read). → see `project-trust.ts.AGENTS.md` |
| `prompt-bus.ts` | Prompt dispatch bus — first-response-wins adapter routing + cross-adapter dismissal. → see `prompt-bus.ts.AGENTS.md` |
| `prompt-expander.ts` | Expand prompt templates from disk for dashboard slash commands (`pi.sendUserMessage` skips expansion). → see `prompt-expander.ts.AGENTS.md` |
| `provider-register.ts` | Register custom LLM providers + auto-discovered models from `~/.pi/agent/providers.json`. → see `provider-register.ts.AGENTS.md` |
| `session-move.ts` | `createMoveCoordinator()` + `MOVE_TIMEOUT` (30s) — owns TWO connections so a move is a handover, not a gap (`updateUrl()` tears the origin down first). Send ownership is explicit state, never inferred from socket liveness: exactly one owner at every instant (task 9.3c). Commit is the single swap instant; refusal/identity-mismatch/timeout all keep the origin serving. See change: add-pi-gateway-transport-identity (9.3b). |
| `subagent-forward-sites.ts` | The two subagent forward paths that call `sendEventForward` directly, extracted so strip PLACEMENT is testable. Exports `flushBufferedSubagentFrames` (strips drained frames), `serveSubagentResync` (sends the retained snapshot UNSTRIPPED, echoes `__resyncRequestId`, returns undefined on not-ready/unknown/evicted → `resyncNoop`). See change: reduce-subagent-details-payload. |
| `subagent-frame-buffer.ts` | Pure class `SubagentFrameBuffer` + `SUBAGENT_CHANNELS` set. Makes running-subagent timeline reconcilable. → see `subagent-frame-buffer.ts.AGENTS.md` |
| `subagent-frame-strip.ts` | Drops `details.entries` from `queued`/`running` subagent frames on the forward path. Exports `stripSubagentEntries`, `stripForForward`, `NON_TERMINAL_STATUSES`. ALLOWLIST not `!terminal` (`"stopped"` must survive); CLONES because the buffer retains frames by reference. Rollback flag `PI_DASHBOARD_SUBAGENT_STRIP=0`. See change: reduce-subagent-details-payload. |
| `subagent-tick-throttle.ts` | Bridge-side rate limiter for subagent `Agent` tool `tool_execution_update` ticks (WIRE cost; the parent collapse change bounded only STORED ticks). Exports class `SubagentTickThrottle` (leading edge + trailing timer + latest-wins coalescing, keyed per `toolCallId`; 60 s self-rearming idle-TTL sweep `DEFAULT_TICK_IDLE_TTL_MS`; fire-time gating re-checks `sessionReady`/`isActive`/`sessionId` and resolves the LIVE connection at call time; terminal DISCARD never flush; stats `tickForwarded`/`tickCoalesced`/`tickDiscardedAtTerminal`/`tickDroppedNotReady`) + `isSubagentTick` allowlist predicate (`toolName === "Agent"` AND string `partialResult.details.agentId` — mirrors the client hydration key; an over-match would rate-limit unrelated streaming tools). Leading edge past a window cancels a due-but-unfired trailing timer + counts its held pending as coalesced (production I/O race). Wired in `bridge.ts` at the `tool_execution_update` forward site; window from `loadConfig().subagentTickThrottleMs` (0 = OFF, byte-identical rollback). See change: reduce-bridge-tick-bandwidth. |
| `retry-tracker.ts` | Pure helper class `RetryTracker` synthesizes `auto_retry_start` / `auto_retry_end` by OBSERVING pi's own… → see `retry-tracker.ts.AGENTS.md` |
| `role-manager.ts` | Manages session model roles. Registers six `roles:*` handlers… → see `role-manager.ts.AGENTS.md` Sole WRITER of the role slice; the pure schema helpers (`DEFAULT_ROLE_NAMES`, `effectiveRoleNames`, `overlayRoles`, types) + the normalizer now live in shared `role-schema.ts` and are imported back — `loadRoleConfig` keeps the file read but delegates normalization to `parseRoleConfig`; re-exports the shared names for existing importers. See change: add-roles-read-api. |
| `role-model-tools.ts` | Agent-facing tools registered via `pi.registerTool` (capability agent-role-model-tools). → see `role-model-tools.ts.AGENTS.md` |
| `remote-registration-gate.ts` | Pre-register D8 gate for REMOTE endpoints: `isRemoteEndpoint`, `httpBaseUrlFor`, `gateRemoteRegistration` (local → skip; no pin at all → allow as `unpinned-legacy`; any pin exists → challenge or refuse, never pin on sight). Wired in `bridge.ts` before `connection.connect()`. See change: add-pi-gateway-transport-identity (tasks 7.2/7.3). |
| `server-pin-store.ts` | `~/.pi/dashboard/pinned-servers.json` (0600) — server identities pinned at pairing time, keyed by FINGERPRINT so a moved dashboard verifies without re-pairing. Exports `serverPinsPath`, `loadServerPins`, `recordServerPin`, `resolvePinForEndpoint` (exact address, else the sole pin), `notePinEndpoint`. See change: add-pi-gateway-transport-identity (task 7.1). |
| `server-auto-start.ts` | Auto-start orchestration: discover dashboard via mDNS → health-check fallback → spawn server process. → see `server-auto-start.ts.AGENTS.md` |
| `server-launcher.ts` | Spawns dashboard server as detached process via shared `launchDashboardServer`. → see `server-launcher.ts.AGENTS.md` |
| `server-probe.ts` | TCP port probe. Exports `isPortOpen(port)` — 1s timeout localhost connect, resolves `true` on connect else `false`. Detects running dashboard server. |
| `session-sync.ts` | Session register/replay/switch lifecycle. Exports `sendStateSync`, `replaySessionEntries`,… → see `session-sync.ts.AGENTS.md` |
| `slash-dispatch.ts` | Extension slash-command dispatch (routing-step 9). Exports `tryDispatchExtensionCommand`, `FeedbackSink`,… → see `slash-dispatch.ts.AGENTS.md` |
| `source-detector.ts` | Detects session source env. Exports `detectSessionSource(hasUI?, sessionFile?)` → `SessionSource`. → see `source-detector.ts.AGENTS.md` |
| `tool-result-image-inliner.ts` | Bridge Fix B tool-result image inliner. On `tool_execution_end` scans result text for absolute image paths… → see `tool-result-image-inliner.ts.AGENTS.md` |
| `transcript-backfill.ts` | `readTranscriptChunk(file, cursor, {maxBytes})` + `makeCursor()` — bounded resumable `.jsonl` reads for D12's lazy backfill. Cursor carries a length+hash witness for the last line, so a rewritten prefix or truncation RESTARTS instead of resuming into misaligned bytes; a partial trailing line is never emitted nor counted complete (#X18); oversized lines overshoot the budget rather than stall. See change: add-pi-gateway-transport-identity (tasks 11.5/11.6). |
| `transcript-request-guard.ts` | `decideTranscriptRequest()` — pure, fs-free. Sessions are addressed by id ONLY: any path-bearing field refuses with `path-on-the-wire`, checked BEFORE the `foreign-session` check so two refusals cannot be differenced into an existence oracle. See change: add-pi-gateway-transport-identity (tasks 11.3/11.4). |
| `transport-diagnostics.ts` | `createTransportDiagnostics()` — bounded buffer (32, oldest dropped) that turns the endpoint decision + every re-target refusal into `bridge_diagnostic` messages once a sessionId exists, so 10.1/10.2 survive the default `capturePiOutput:false`. Throwing `send` never strands the rest. See change: add-pi-gateway-transport-identity (tasks 10.1/10.2/10.5). |
| `tui-prompt-adapter.ts` | Production PromptBus adapter for Pi's TUI. Exports `createTuiPromptAdapter`, `TuiPromptUi`. Presents select/input/confirm/editor via captured unpatched UI; forwards confirm `metadata.message`; no multiselect arm. → see `tui-prompt-adapter.ts.AGENTS.md` |
| `turn-actionability.ts` | Pure provider-agnostic classifier. Exports `classifyTurnActionability(turn)` →… → see `turn-actionability.ts.AGENTS.md` |
| `ui-modules.ts` | Extension UI system bridge side. Exports `refreshUiModules`, `subscribeUiInvalidate`, `handleUiManagement`,… → see `ui-modules.ts.AGENTS.md` |
| `ui-stale-guard.ts` | `runUiSafely(fn)` — runs a `ctx.ui` thunk and absorbs the throw when the extension ctx has been invalidated by a session replacement/reload; returns the value or `undefined`. pi >= 0.84 makes EVERY `ctx.ui` getter throw after `AgentSession.dispose()` → `ExtensionRunner.invalidate()`, and staleness is NOT observable (no `isActive`/`isStale` predicate), so attempt-and-swallow is the only guard. Used by `bridge.ts` at the 3 `ctx.ui` sites reachable from `autoStartServer`'s late continuation (`notify`, spinner mount, `stopSpinner`) — without it that throw is an unhandled rejection inside a floating promise and KILLS the pi process mid-session. Wrap ONLY `ctx.ui` access; auto-start's own errors must still reach its `.catch()`. See change: fix-bridge-stale-ctx-crash. |
| `vcs-info.ts` | Gathers git branch/remote/PR/worktree info via shared platform git helpers. → see `vcs-info.ts.AGENTS.md` |
| `visibility-intent.ts` | Resolves `session_register` visibility fields. Exports `resolveVisibilityIntent`,… → see `visibility-intent.ts.AGENTS.md` |
