# Continue-Here Files

## Problem

When a swival session ends mid-task — by Ctrl+C, max turns, compaction failure, or REPL exit — all context is lost. The next session starts cold. HISTORY.md is an append-only log, not a structured resume plan. Memory (MEMORY.md) is for cross-project notes, not "pick up where I left off."

Terminal close / EOF is best-effort only — we can handle EOFError in the REPL, but a hard kill (SIGKILL, force-quit) won't run cleanup. That's acceptable.

## Goal

On abnormal session end, write a structured `.swival/continue.md` file. On the next session start, inject its contents into the system prompt and delete it. The agent picks up where it left off without the user re-explaining anything.

## Design

### File format

`.swival/continue.md` — plain markdown, human-readable, consumed on next session start.

```markdown
# Continue Here

## What was completed
- Read and analyzed auth.py (150 lines, JWT-based)
- Identified the bug: token refresh doesn't check expiry

## What remains
- Fix the expiry check in `refresh_token()` at auth.py:47
- Add test for expired token refresh
- Update docstring

## Decisions made
- Using `datetime.utcnow()` consistently (not `datetime.now()`)
- Keeping backward compat with existing token format

## Watch out for
- The `verify_token()` function has a try/except that silently swallows errors
- Tests use a mock clock, don't use `time.sleep()`

## Next step
Edit auth.py:47 to add `if token.exp < now: raise TokenExpired()`
```

The format is deliberately loose — it's written by the LLM in natural language, not parsed programmatically beyond reading the whole file.

### When to write

Six triggers, all writing the same file:

1. **Max turns exhausted** — `run_agent_loop` returns with `exhausted=True`.
2. **KeyboardInterrupt during agent loop** — caught in repl_loop (3 handlers) and `_run_main` single-shot path.
3. **Compaction failure** — right before raising `AgentError("context window exceeded even after compaction")`.
4. **REPL exit (EOFError/Ctrl+C at prompt)** — `repl_loop` line ~3334, currently just `break`s.
5. **Single-shot KeyboardInterrupt** — `main()` currently only catches `AgentError` (line ~2007). Add `KeyboardInterrupt` handling in `_run_main` around the `run_agent_loop` call (line ~2503).
6. **REPL initial question KeyboardInterrupt** — `_run_main` line ~2585 runs the initial `--repl "question"` via `run_agent_loop` before entering `repl_loop`. This call is not inside `repl_loop`'s interrupt handlers, so it needs its own `KeyboardInterrupt` wrapper.

NOT triggered on normal completion (agent gives a final answer with `exhausted=False`).

### How to write: deterministic-first, LLM optional

On interrupt paths (Ctrl+C, EOFError, compaction failure), **always write deterministically first, then optionally enhance with an LLM call**. The interrupt path must not hang waiting for a summary call.

Deterministic extraction (always runs, no network):
- Most recent substantive user message as "current task", via a `_find_last_user_task(messages)` helper. This scans backward for the last `role: user` message that isn't a known synthetic prompt. The skip list uses the canonical constants defined in `agent.py` — import them rather than duplicating strings. Define them as module-level constants in `agent.py` (e.g. `SYNTHETIC_USER_PREFIXES`) so both the intervention logic and `_find_last_user_task` reference the same source of truth. Also include the first real user message as "original task" if different, to give context in multi-turn sessions.
- Todo state: remaining items as "what remains"
- Snapshot history: prior investigation summaries (from `snapshot_state.history`)
- Thinking history: key reasoning steps (from `thinking_state.history`, last N entries)
- Last few tool calls and their results (from tail of messages)

LLM enhancement (only on max-turns-exhausted, where we're not in a hurry):
- Call the LLM with the conversation excerpt + deterministic data
- If the call fails, the deterministic file is already written — no data loss

### Where the logic lives

New module: `swival/continue_here.py`

```python
CONTINUE_PATH = ".swival/continue.md"
MAX_CONTINUE_CHARS = 4000

def write_continue_file(
    base_dir: str,
    messages: list,
    *,
    todo_state: TodoState | None = None,
    snapshot_state: SnapshotState | None = None,
    thinking_state: ThinkingState | None = None,
    call_llm_fn=None,
    model_id: str | None = None,
    base_url: str | None = None,
    api_key: str | None = None,
    top_p: float | None = None,
    seed: int | None = None,
    provider: str | None = None,
) -> bool:
    """Write .swival/continue.md from current session state.

    Always writes a deterministic version first. If call_llm_fn is
    provided and succeeds, overwrites with the richer LLM version.
    Returns True if file was written.
    """

def _build_deterministic_continue(
    messages: list,
    todo_state: TodoState | None,
    snapshot_state: SnapshotState | None,
    thinking_state: ThinkingState | None,
) -> str:
    """Build continue-file content from structured state, no LLM needed."""

def load_continue_file(base_dir: str, *, delete: bool = True) -> str | None:
    """Read and optionally delete .swival/continue.md. Returns content or None."""

def format_continue_prompt(content: str) -> str:
    """Wrap continue-file content in <continue-here> tags for prompt injection."""
```

### Integration points

#### Writing (6 places)

**1. `run_agent_loop` — max turns exhausted (line ~3031)**

After the `while` loop exits, before `return last_text, True`. This is the only path where we attempt the LLM summary call (not time-pressured).

```python
write_continue_file(base_dir, messages,
    todo_state=todo_state, snapshot_state=snapshot_state,
    thinking_state=thinking_state,
    call_llm_fn=call_llm, model_id=model_id, ...)
return last_text, True
```

**2. `run_agent_loop` — compaction failure (line ~2823)**

Deterministic only (the LLM is already struggling with context).

```python
write_continue_file(base_dir, messages,
    todo_state=todo_state, snapshot_state=snapshot_state,
    thinking_state=thinking_state)
raise AgentError(...)
```

**3. `repl_loop` — KeyboardInterrupt during agent loop (lines ~3416, 3440, 3461)**

Deterministic only (user is interrupting, don't hang).

```python
except KeyboardInterrupt:
    fmt.warning("interrupted, question aborted.")
    write_continue_file(base_dir, messages,
        todo_state=todo_state, snapshot_state=snapshot_state,
        thinking_state=thinking_state)
    continue
```

**4. `repl_loop` — REPL exit (line ~3334)**

When user hits Ctrl+C or Ctrl+D at the prompt. Only write if the session has done actual work (not just the system prompt sitting there). Use `any(m for m in messages if m.get("role") != "system")` — this handles both the normal case (system prompt + user messages) and `--no-system-prompt` (no system message, user messages start at index 0).

```python
except (EOFError, KeyboardInterrupt):
    print(file=sys.stderr)
    if any(_msg_role(m) != "system" for m in messages):
        write_continue_file(base_dir, messages,
            todo_state=todo_state, snapshot_state=snapshot_state,
            thinking_state=thinking_state)
    break
```

**5. `_run_main` — single-shot KeyboardInterrupt**

Currently `main()` only catches `AgentError`. Wrap the `run_agent_loop` call in `_run_main` (line ~2503) with a `KeyboardInterrupt` handler:

```python
try:
    answer, exhausted = run_agent_loop(messages, tools, ...)
except KeyboardInterrupt:
    fmt.warning("interrupted.")
    write_continue_file(base_dir, messages,
        todo_state=todo_state, snapshot_state=snapshot_state,
        thinking_state=thinking_state)
    sys.exit(130)  # standard Ctrl+C exit code
```

**6. `_run_main` — REPL initial question KeyboardInterrupt (line ~2585)**

When invoked as `swival --repl "initial question"`, the initial question runs via `run_agent_loop` at line ~2585, *before* `repl_loop` is entered. This call site needs its own handler:

```python
try:
    answer, exhausted = run_agent_loop(messages, tools, **loop_kwargs)
except KeyboardInterrupt:
    fmt.warning("interrupted during initial question.")
    write_continue_file(base_dir, messages,
        todo_state=todo_state, snapshot_state=snapshot_state,
        thinking_state=thinking_state)
    # Fall through to repl_loop — user can /continue or start fresh
```

#### Reading (1 place, independent of system_prompt source)

**`build_system_prompt` — after all other content is assembled (line ~2244)**

The continue-file injection must happen **after the `if system_prompt:` / `else:` branch**, in the common tail where date/skills/commands are appended. This ensures it works regardless of whether the user passed `--system-prompt` or uses the default prompt.

```python
# After date/skills/commands, before return:
continue_content = load_continue_file(base_dir)
if continue_content:
    system_content += "\n\n" + format_continue_prompt(continue_content)
```

The file is deleted by `load_continue_file` (consumed on read). This is one-shot — subsequent sessions won't see stale continue data.

Add `no_continue` parameter to `build_system_prompt` to skip loading when disabled.

### LLM summary prompt (only used on max-turns path)

```
You are summarizing an interrupted AI coding session so it can be resumed.
The session was working on a task and was interrupted before completion.

Extract from the conversation:
1. What was completed (files read, edits made, tests run, findings)
2. What remains to be done
3. Key decisions that were made (so they aren't re-debated)
4. Anything tricky or surprising encountered
5. The exact next step to take when resuming

Be specific: include file paths, line numbers, function names, error messages.
Write in concise bullet points. No preamble.
```

### Config / CLI

Full plumbing, following the `no_history` / `no_memory` pattern:

**config.py:**
- Add `"no_continue": bool` to `CONFIG_KEYS` (line ~28)
- Add `"no_continue": False` to `_ARGPARSE_DEFAULTS` (line ~82)
- Add `"no_continue": "continue_here"` to `_INVERT_KEYS` in `config_to_session_kwargs` (line ~514)
- Add `"# no_continue = false"` to init-config template (line ~577)

**agent.py (CLI):**
- Add `--no-continue` flag to `build_parser()`, `action="store_true"`, default `_UNSET`
- Pass through to `build_system_prompt` and all `write_continue_file` call sites

**session.py:**
- Add `continue_here: bool = True` parameter to `Session.__init__`
- Pass to `build_system_prompt` (via `_setup`)
- Pass to `_make_per_run_state` / `_build_loop_kwargs` so `run_agent_loop` knows whether to write

**run_agent_loop signature:**
- Add `continue_here: bool = True` parameter
- Gates all `write_continue_file` calls

### REPL integration

- `/continue-status` — show whether a continue file exists and preview first 500 chars
- The existing `/continue` command (resume agent loop) is unrelated and stays as-is

### Library mode (`Session.run` / `Session.ask`)

Continue-file writing is **CLI-only**. `Session.run()` and `Session.ask()` do not catch `KeyboardInterrupt` — they let it propagate to the caller, who controls their own interrupt handling. Writing a continue file from inside library code would be surprising side-effect behavior.

Library users who want continue-file support can:
- Catch `KeyboardInterrupt` themselves and call `write_continue_file()` directly (it's a public API in `continue_here.py`)
- The `continue_here=True` Session param still controls whether `build_system_prompt` *reads* the file on startup — so a CLI-written continue file is picked up by a subsequent library session

The two internal write sites (`run_agent_loop` max-turns and compaction failure) *do* run in library mode. These are gated on `continue_here=True` and are non-interrupt paths, so writing is safe. Only the external interrupt handlers (Ctrl+C wrappers in `_run_main` and `repl_loop`) are CLI-only.

### Safeguards

- Path validation: same `is_relative_to(base_dir)` check used by todo.py and history
- Size cap: continue file capped at `MAX_CONTINUE_CHARS` (4000 chars)
- Staleness: if the continue file is older than 24 hours, warn but still load it
- No stacking: writing a new continue file overwrites any existing one
- The file is gitignored (already covered by `.swival/` in typical .gitignore)
- All write paths wrapped in try/except OSError — continue-file failure must never crash the agent

## Implementation order

1. `continue_here.py` — module with `_build_deterministic_continue`, `write_continue_file`, `load_continue_file`, `format_continue_prompt`
2. Wire writing into `run_agent_loop` (max turns + compaction failure)
3. Wire writing into `repl_loop` (3 KeyboardInterrupt handlers + EOFError exit)
4. Wire writing into `_run_main` (single-shot KeyboardInterrupt + REPL initial question interrupt)
5. Wire reading into `build_system_prompt` (common tail, works with custom prompts)
6. Config plumbing: `config.py` `CONFIG_KEYS`/`_ARGPARSE_DEFAULTS`/`_INVERT_KEYS`, `build_parser` flag, `Session` param, `run_agent_loop` param
7. `/continue-status` REPL command
8. Tests

## Testing

### Unit tests (`test_continue_here.py`)
- `_build_deterministic_continue` with messages + todo + snapshot + thinking state
- `_build_deterministic_continue` with only messages (other states None)
- `write_continue_file` with mocked `call_llm` — verify LLM version overwrites deterministic
- `write_continue_file` with `call_llm` that raises — verify deterministic file survives
- `load_continue_file` — returns content and deletes file
- `load_continue_file` with `delete=False` — returns content, file persists
- `load_continue_file` — returns None when file doesn't exist
- `format_continue_prompt` — wraps in `<continue-here>` tags
- Path validation (symlink escape)
- Size cap enforcement
- Staleness warning (mock file mtime > 24h)
- Overwrite behavior (existing file replaced)

### Integration tests
- `run_agent_loop` with `max_turns=1` produces a continue file
- `run_agent_loop` normal completion does NOT produce a continue file
- Compaction failure path produces a continue file
- `build_system_prompt` with existing continue file — content appears in prompt and file is deleted
- Calling `build_system_prompt` a second time after consumption — no continue content, file stays gone
- `build_system_prompt` with `--system-prompt` and existing continue file — content still appears
- `build_system_prompt` with `no_continue=True` — file not loaded
- `--no-continue` flag prevents both writing and reading

### REPL tests
- KeyboardInterrupt during agent loop writes continue file
- EOFError at prompt writes continue file (if non-system messages exist)
- EOFError at prompt with only system message does NOT write
- EOFError at prompt with `--no-system-prompt` and one user message DOES write

### Single-shot tests
- KeyboardInterrupt in `_run_main` single-shot path writes continue file and exits 130
- KeyboardInterrupt in `_run_main` REPL initial question writes continue file and falls through to REPL

### Library-mode tests
- `Session.run()` with max_turns=1 writes continue file (non-interrupt path in `run_agent_loop`)
- `Session.run()` does NOT write continue file on normal completion
- `Session.run()` does NOT catch KeyboardInterrupt (propagates to caller)
- `Session(continue_here=False).run()` suppresses continue file on max turns

### Deterministic content tests
- Multi-turn REPL: "current task" is the most recent user message, not the first
- Single-turn: "current task" and "original task" are the same, no duplication
- System intervention messages (nudges, guardrails) are not picked as "current task"
