# Troubleshooting

> Retry and network failures, credential refresh hangs, SIGTERM cleanup, bash output truncation, and session event settlement issues.

- Repository: earendil-works/pi
- GitHub: https://github.com/earendil-works/pi
- Human docs: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1
- Complete Markdown: https://grok-wiki.com/public/docs/earendil-works-pi-7860a70e44d1/llms-full.txt

## Source Files

- `packages/coding-agent/test/suite/regressions/3317-network-connection-lost-retry.test.ts`
- `packages/coding-agent/test/suite/regressions/6019-explicit-provider-retry-message.test.ts`
- `packages/coding-agent/test/suite/regressions/7027-credential-refresh-hang.test.ts`
- `packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts`
- `packages/coding-agent/test/suite/regressions/5303-bash-output-truncation.test.ts`
- `packages/coding-agent/src/core/auth-guidance.ts`

---

---
title: "Troubleshooting"
description: "Retry and network failures, credential refresh hangs, SIGTERM cleanup, bash output truncation, and session event settlement issues."
---

Transient provider failures, post-login catalog refresh, process signal teardown, and bash child-process I/O are handled inside `packages/coding-agent` with explicit retry settings, session events, and regression-backed cleanup contracts. This page records the failure modes those paths cover and the signals that confirm recovery.

## Auto-retry for transient provider errors

When session settings enable retry, a failed assistant turn with a recognized transient error is retried instead of ending the prompt as a hard failure. Recovery is observable as session events and a second model call that can produce normal assistant text.

### Retry settings shape

Harnesses enable auto-retry with:

```ts
settings: {
  retry: {
    enabled: true,
    maxRetries: 3,
    baseDelayMs: 1,
  },
}
```

| Key | Type | Role in recovery |
| --- | --- | --- |
| `retry.enabled` | `boolean` | Turns auto-retry on for the session |
| `retry.maxRetries` | `number` | Upper bound on retry attempts (tests use `3`) |
| `retry.baseDelayMs` | `number` | Base delay between attempts (tests use `1` ms) |

Without `retry.enabled: true`, the regression cases that expect a second model call do not apply.

### Errors that retry

| Pattern | Example / form | Issue coverage |
| --- | --- | --- |
| Network drop | Exact message `Network connection lost.` with `stopReason: "error"` | #3317 |
| OpenAI explicit retry guidance | Long prose that tells the client to retry and include a request ID | #6019 |
| Bedrock explicit retry guidance | JSON body: `{"message":"The system encountered an unexpected error during processing. Try your request again."}` | #6019 |

OpenAI message used in regression coverage:

```text
An error occurred while processing your request. You can retry your request, or contact us through our help center at help.openai.com if the error persists. Please include the request ID req_******** in your message.
```

### Session events for retry settlement

Successful auto-retry emits a start event, then an end event after the recovered turn:

| Event type | Fields asserted in tests | Meaning |
| --- | --- | --- |
| `auto_retry_start` | `errorMessage` | Retry cycle started for the failing error text |
| `auto_retry_end` | `success: true` | Retry cycle settled successfully |

On success, the faux model call count is `2` (initial failure + recovered response), and assistant text includes the recovered message (for example `recovered after reconnect` or `recovered`).

```mermaid
stateDiagram-v2
  [*] --> Prompt
  Prompt --> ErrorTurn: stopReason error
  ErrorTurn --> AutoRetryStart: retry.enabled
  AutoRetryStart --> RecoveredTurn: second model call
  RecoveredTurn --> AutoRetryEndSuccess: success true
  AutoRetryEndSuccess --> [*]
```

<Note>
If `auto_retry_start` fires but `auto_retry_end` never arrives with `success: true`, treat the turn as unsettled: the first error matched retry guidance, but recovery did not complete within the observed event stream.
</Note>

### Quick checks

1. Confirm `retry.enabled` is true for the session under test or embed.
2. Confirm the provider error text matches a known transient pattern (exact network string or provider-supplied “try again” guidance).
3. Expect `auto_retry_start` with that `errorMessage`, then `auto_retry_end` with `success: true`, and a second model invocation.

---

## Credential refresh hang after login

Login must not stall behind a slow or infinite model-catalog network refresh. Issues **#7027** and **#7113** cover two related contracts: concurrent `ModelRuntime` login during a stalled refresh, and interactive post-login refresh with a hard timeout.

### Login is not blocked by a stalled catalog refresh

`ModelRuntime` can run a network `refresh({ allowNetwork: true, providers: [...] })` while `login` for the same provider still completes.

| Operation | Behavior under stall |
| --- | --- |
| `refreshModels({ allowNetwork: true })` hanging | Does not prevent `runtime.login(providerId, "api_key", …)` from resolving |
| Login result | Resolves to the stored credential (e.g. `{ type: "api_key", key: "secret" }`) |
| Available models | Snapshot still includes models from the non-network registration path |
| Credentials | `AuthStorage` retains the written API key for the provider |
| Stalled refresh completion | Can still resolve later with `{ aborted: false }` without having blocked login |

```ts
// Pattern under test: offline register, start hanging network refresh, then login
await runtime.refresh({ allowNetwork: false, providers: [provider.id] });
const stalledRefresh = runtime.refresh({ allowNetwork: true, providers: [provider.id] });
// login resolves while refreshModels is still awaiting forever
await runtime.login(provider.id, "api_key", { prompt, notify });
```

### Interactive login finishes before bounded background refresh

After interactive API-key auth, `InteractiveMode.completeProviderAuthentication` kicks a scoped catalog refresh with an `AbortSignal`:

```ts
runtime.refresh({
  providers: [providerId],
  signal: /* AbortSignal */,
});
```

| Timing | UI behavior |
| --- | --- |
| Immediately after login completes | No timeout warning yet |
| After **15_000** ms if refresh has not finished | Warning via `showWarning` |

Exact warning string:

```text
Saved API key for Stalled Login, but its model catalog refresh timed out; using cached models.
```

(The provider display name is the authenticated provider’s name; the test uses `"Stalled Login"`.)

<Warning>
If login appears to hang, distinguish “credential write stuck” from “catalog refresh still running.” Interactive mode is expected to finish authentication first and only warn later if the background catalog refresh times out.
</Warning>

### Symptom matrix

| Symptom | Likely cause | Expected recovery |
| --- | --- | --- |
| `/login` never returns while models refresh | Older hang pattern (login gated on network catalog) | Login must complete independently of stalled `refreshModels` |
| Login succeeds but models look stale | Post-login refresh aborted after 15s | Warning about timeout; cached models remain in use |
| Credentials missing after “successful” UI | Auth write failed (not covered as hang) | Re-check `AuthStorage` / re-run login |

---

## SIGTERM and signal-exit cleanup

Interactive process exit interacts with `proper-lockfile` → `signal-exit`. That library can re-send `SIGTERM`/`SIGHUP` when it sees no other process listeners during the same signal dispatch. `InteractiveMode` therefore must keep its own signal handlers registered until async terminal cleanup finishes (issue **#5724**).

### Signal-triggered shutdown order

When `shutdown({ fromSignal: true })` runs:

| Phase | Call | Handlers |
| --- | --- | --- |
| 1 | `runtimeHost.dispose()` starts | Signal handlers still registered (`unregisterSignalHandlers` not called yet) |
| 2 | After `dispose` resolves | `ui.terminal.drainInput(ms)` then `stop()` |
| — | While dispose is pending | Order is only `["dispose"]`; unregister has not run |

```mermaid
sequenceDiagram
  participant SIG as Process signal
  participant IM as InteractiveMode.shutdown
  participant RH as runtimeHost.dispose
  participant UI as terminal.drainInput
  participant Stop as stop

  SIG->>IM: fromSignal true
  IM->>RH: dispose() async
  Note over IM: signal handlers still registered
  RH-->>IM: resolved
  IM->>UI: drainInput
  IM->>Stop: stop()
```

<Warning>
Unregistering signal handlers before `runtimeHost.dispose()` completes can let `signal-exit` re-fire `SIGTERM`/`SIGHUP` and interrupt cleanup. Keep handlers installed for the full async dispose window on signal-triggered shutdown.
</Warning>

### Failure mode

| Failure | Observable effect |
| --- | --- |
| Handlers removed too early during SIGTERM path | Cleanup interrupted; process may exit before dispose/drain complete |
| Correct path | `dispose` → `drainInput` → `stop` after dispose resolves; unregister not called mid-dispose |

---

## Bash output truncation after process exit

`waitForChildProcess` waits for child exit and remaining stdio. A historical bug armed a **fixed 100 ms** timer on `exit` and destroyed streams when it fired. If a short-lived detached descendant held stdout open, `close` never fired; any writes more than 100 ms after `exit` were dropped. That showed up as truncated bash tool output (for example mid lint-staged / listr2 output after `git commit`), which models often read as a hang (issue **#5303**).

### Current contract

| Situation | Behavior |
| --- | --- |
| Child emits `exit`, stdout still open, more data keeps arriving | Grace timer **re-arms on each chunk**; wait stays open while the pipe is active |
| Child emits `exit`, stdout held open but quiet | Wait resolves after one full grace interval (~100 ms) with no further data |
| Data every 50 ms for several ticks after exit | All chunks (including late ones) remain readable before resolve |

```text
exit(0)
  │
  ├─ write chunk  → re-arm grace (100ms)
  ├─ write chunk  → re-arm grace
  ├─ …quiet…
  └─ grace elapses → waitForChildProcess resolves (exit code)
```

### Symptom vs fix

| Before fix | After fix |
| --- | --- |
| Fixed 100 ms from `exit` → destroy streams | Grace restarts on every stdout/stderr chunk |
| Late hook output truncated mid-stream | Active writers keep being read |
| Model sees partial hook output / “hang” | Full post-exit burst captured until idle grace |

If bash results still look truncated with a quiet held-open pipe, that is expected after the idle grace: the wait intentionally releases when no further data arrives.

---

## Auth and model availability messages

When credentials or model selection are missing, user-facing strings from `auth-guidance` point operators at `/login` and local provider/model docs under the docs path from `getDocsPath()`.

| Helper | Message shape |
| --- | --- |
| `formatNoModelsAvailableMessage()` | `No models available.` + login help |
| `formatNoModelSelectedMessage()` | `No model selected.` + login help + “Then use /model to select a model.” |
| `formatNoApiKeyFoundMessage(provider)` | `No API key found for <provider or "the selected model">.` + login help |

Login help body:

```text
Use /login to log into a provider via OAuth or API key. See:
  <docsPath>/providers.md
  <docsPath>/models.md
```

`<ParamField body="provider" type="string">`
When the provider id is the sentinel `"unknown"`, `formatNoApiKeyFoundMessage` displays **the selected model** instead of that id.
</ParamField>

### Recovery steps

<Steps>
  <Step title="Authenticate">
    Run `/login` (OAuth or API key) for the provider that owns the selected model.
  </Step>
  <Step title="Select a model">
    If the message is “No model selected,” use `/model` after credentials exist.
  </Step>
  <Step title="If login succeeded but catalog timed out">
    Expect the 15s warning about timed-out catalog refresh and cached models; retry provider/model refresh later or check network access for `refreshModels`.
  </Step>
</Steps>

---

## Issue index

| Issue | Surface | Contract |
| --- | --- | --- |
| #3317 | Session auto-retry | Retries `Network connection lost.` when `retry.enabled` |
| #5303 | `waitForChildProcess` / bash | Re-arm 100 ms grace on post-exit I/O chunks |
| #5724 | `InteractiveMode.shutdown` | Keep signal handlers until signal-path dispose finishes |
| #6019 | Session auto-retry | Retries OpenAI and Bedrock explicit “try again” error text |
| #7027 / #7113 | `ModelRuntime` + interactive auth | Login not blocked by stalled refresh; 15s post-login refresh timeout warning |

---

## Related pages

<CardGroup>
  <Card title="Authentication" href="/authentication">
    API keys, OAuth login, credential storage, and refresh hang failure modes.
  </Card>
  <Card title="Providers and models" href="/providers-and-models">
    Provider registration, catalog refresh, and provider-retry message behavior.
  </Card>
  <Card title="Settings" href="/settings">
    Session settings load/reload, including `retry` merge and related options.
  </Card>
  <Card title="Session runtime" href="/session-runtime">
    Runtime services, lifecycle events, and non-TUI embedding of sessions.
  </Card>
  <Card title="Agent sessions" href="/agent-sessions">
    Prompt loop, concurrent behavior, and turn ownership around recovery.
  </Card>
  <Card title="Tools and allowlists" href="/tools">
    Default bash tool surface and how tool output reaches the model.
  </Card>
</CardGroup>
