Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,30 @@ All notable changes to qmax-code. Versions follow [Semantic Versioning](https://

## [Unreleased]

### Added
- `/handoff` keeps explicit, user-recorded rejected approaches and supporting
evidence visible across backend switches, history compaction, and session
restarts. Use `/handoff reject <approach> | <evidence>` to add a note and
`/handoff forget <number>` to remove one; write `\|` for a literal pipe in
the approach. The redacted checkpoint is bounded to 12 KiB, is part of every
built-in provider prompt, and is sent to a native CLI session when that
session is new, the checkpoint changed, or entries are being transferred, so
it is not repeated into the CLI's history every turn. No extra model call is
made. It instructs the destination to explain any justified retry; it does
not enforce model behavior or infer decisions from failures.
- Switching backends after work not covered by the checkpoint prints a
one-line `/handoff reject` reminder, noting interrupted turns and failed tool
calls when the backend exposes them. It only suggests; nothing is recorded
automatically.
- `eval/handoff` runs the live evaluation of rejected-approach handoffs:
checkpoint vs. control arms across backends, a fixed fixture with hidden
scoring tests, and per-run records summarized into `summary.md`.

### Fixed
- Handing off a long history whose oldest entries must be dropped no longer
re-encodes the history once per dropped entry; about 800 untransferred
entries took ~13s before the backend started and now take under 2s.

## [1.37.0] - 2026-09-27

### Added
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ internal/session/ Local/cloud sessions and prompt queue
internal/tui/ Bubble Tea input, output, themes, media, pickers
internal/security/ Command validation and credential redaction
internal/httpx/ and receipt.go Guarded egress and Exposure Receipts
eval/handoff/ Live /handoff evaluation harness (not in the binary)
```

The interactive loop lives in `internal/repl/repl.go`. Built-in backend prompts
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,30 @@ be transferred, and image interpretation depends on the destination's support.
Only content exposed to qmax-code can be retained; this cannot recover history
that an older version already discarded.

To keep a ruled-out approach visible when you switch backends, record the
decision and its evidence explicitly:

```text
/handoff reject Increase the timeout | TestReconnect still fails at 60s; the connection is never reopened
/handoff
/handoff forget 1
```

`/handoff` lists the current rejected approaches and their user-supplied evidence.
The checkpoint is part of every built-in provider prompt and is sent to a CLI
backend whenever it has not seen the current version, remains visible when
history is compacted, and survives `/save` and `/resume`. Write `\|` for a
literal pipe inside the approach. After a backend switch that follows
unrecorded work, qmax-code prints a one-line reminder to record anything that
was ruled out, mentioning interrupted turns and failed tool calls it can see.
`/handoff forget <number>` removes an outdated rejection; `/clear` resets it.
Notes are redacted using the retained-transcript rules and capped at 12 KiB
in total. Recording notes makes no additional model calls. qmax-code does not
automatically classify failed commands as rejected approaches or independently
verify the evidence. The destination is instructed to check the notes before
acting and explain any justified retry; model compliance is not guaranteed.
See [handoff notes and the live evaluation procedure](docs/HANDOFF.md).

Saved conversations live in `~/.qmax-code/sessions/`; those carrying a portable
transcript are kept for 90 days, legacy sessions for 7. `/clear` resets the
shared conversation and native resume state and starts a new session ID, so the
Expand Down
7 changes: 7 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ They do not collect hidden reasoning, provider configuration, or credentials
from the environment. Switching providers makes retained conversation content
available to the newly selected provider.

Explicit `/handoff` notes contain user-supplied rejected approaches and evidence.
They use the same retention redaction as conversation content, are saved with
the session, and accompany subsequent prompts on any selected backend. They
do not read environment variables, provider configuration, or hidden reasoning,
and do not make an additional model request. Quoted evidence is identified as
data rather than instructions; these notes are not a command execution gate.

Session writes use an owner-only temporary file and atomic replacement.
Portable transcripts in `~/.qmax-code/sessions/` are reclaimed after 90 days;
legacy sessions keep the seven-day cleanup. Large handoffs and built-in
Expand Down
90 changes: 90 additions & 0 deletions docs/HANDOFF.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Rejected approaches across backends

The shared transcript preserves exposed conversation and tool activity.
`/handoff` adds a small checkpoint of explicit user decisions so a ruled-out
approach stays visible even when that transcript is long or compacted.

```text
/handoff reject Increase the reconnect timeout | TestReconnect fails at both 5s and 60s; the connection is never reopened
/orch
continue the fix
```

The first unescaped `|` separates the approach from the evidence; write `\|`
for a literal pipe in the approach (`/handoff reject pipe through grep \| sort
| still fails`). The confirmation echoes the recorded approach, and warns
when the evidence contains a pipe that may have belonged to the approach.

Use `/handoff` to inspect the numbered list and `/handoff forget 1` to remove
an outdated rejection. Entry numbers reflect the current list, so inspect it
again after removing an entry. Both the rejection and its removal appear in
the transcript. The current checkpoint supersedes older handoff notes.
`/clear` resets the checkpoint along with the conversation.

The checkpoint is sent before the historical conversation in CLI handoffs
and included in built-in system prompts. A native CLI session stores every
prompt it receives, so qmax-code records which checkpoint version it last
delivered and resends it only when the session is new, the checkpoint
changed, or missed entries are being transferred (switching back to it). A
failed turn discards the native session, so the next turn resends it. `/save` and `/resume`
preserve it across restarts; automatic persistence follows the existing
auto-save setting. Notes use retained-transcript redaction, with a total
12 KiB budget that includes the checkpoint instructions. Oversized additions
are rejected without changing existing notes. Recording a note makes no
additional model request. Input recall stores `/handoff` instead of the raw
rejection command, so pasted evidence is inspected through the redacted list.

## Switch reminder

When the backend changes (`/orch`, `/cc`, `/codex`, `/gemma`, `/ollama`, …)
and the transcript has work since the checkpoint last changed, qmax-code
prints a one-line reminder to record anything that was ruled out. It
mentions interrupted turns and failed tool calls where the backend exposes
them: Claude Code `is_error`, Codex non-zero exit codes, OpenCode and
Antigravity tool errors, and built-in tool errors reported as a JSON error
object. Built-in `run_command` failures that produce output are not
distinguishable from successes, so the count is a hint, not a complete
record. The reminder never records a rejection itself.

A failed command does not automatically rule out an approach: a failure can
also mean an unavailable dependency, a flaky test, or incomplete work. The
user supplies the decision and evidence. qmax-code identifies that evidence
as data and instructs the destination to check the list before acting,
explain new evidence before revisiting a rejection, and respect an explicit
user request to revisit it. This is guidance, not a command execution gate.
Hidden reasoning and provider-private state still cannot be transferred.

## Check behavior with real backends

The automated regression tests verify delivery of the full decision and
evidence across failure, compaction, restart, and native resume. They use
test doubles and cannot establish whether a model follows the checkpoint.

To evaluate that separately, use a reproducible bug with a demonstrated
unsuccessful approach, such as increasing a timeout when the connection is
never reopened. [`eval/handoff`](../eval/handoff/README.md) automates this
procedure with a fixed fixture, a scripted phase A, hidden scoring tests,
and a per-run record; `go run ./eval/handoff run` prints the plan and only
calls backends with `--yes`. The manual steps are:

1. Let backend A try the approach and run the failing test. Keep the test
output and resulting repository state as the starting point for both runs.
2. In the checkpoint run, record the approach and its evidence with
`/handoff reject`. In the control run, retain the ordinary transcript
without adding a checkpoint.
3. Switch to backend B and use the same request: `continue the fix`. Avoid
repeating the rejected approach in the request itself.
4. Inspect B's first ten tool calls and final outcome. Record whether it
repeated the rejected change, cited the evidence, justified a revisit
with new evidence, and passed the regression test.
5. Repeat from the same repository state with reversed backend order and
multiple fresh sessions. Record backend/model versions and effort levels.

Running the existing regression test to establish a baseline is not itself
a repeated approach. Count a repeated change or attempted remedy that the
checkpoint ruled out. Distinguish justified revisits from unexplained retries,
and report results separately from whether the bug was eventually fixed.

Do not claim a measured reduction in repeated approaches until those live
runs have been completed. Automatic decision extraction would need its own
evaluation for incorrectly ruling out viable approaches.
91 changes: 91 additions & 0 deletions eval/handoff/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Handoff evaluation

Measures whether backend B repeats an approach that backend A already ruled
out, with and without a `/handoff` checkpoint. This is the live experiment
described in [docs/HANDOFF.md](../../docs/HANDOFF.md#check-behavior-with-real-backends).
The harness is a development tool inside the module; it is not part of the
`qmax-code` binary.

```bash
go run ./eval/handoff selfcheck # fixture sanity, no backends
go run ./eval/handoff run --backends cc,codex # print the plan only
go run ./eval/handoff run --backends cc,codex --yes # execute it (uses quota)
go run ./eval/handoff summarize # rebuild summary.md
```

## What one run does

1. Copies the fixture (`testdata/fixture`) into a fresh git repository under
`<out>/runs/<run-id>/repo`. It is a small Go client whose `Send` retries
until `ReconnectTimeout` (200ms) but reuses a dead connection instead of
redialing. The failure message points at the timeout; raising it only
makes the test wait longer.
2. Seeds the shared transcript with a scripted phase A: the failing test,
A's decision to try a longer timeout, the edit to 2s, the still-failing
test (real output captured once into `<out>/seed.json`), the revert, and
A's conclusion that it did not help. Scripting A keeps history and
repository state identical across arms and repeats.
3. Optionally adds `--pad` unrelated exchanges so the handoff has to
abbreviate history (see below).
4. **checkpoint** arm: records `/handoff reject Increase ReconnectTimeout |
<evidence>`. **control** arm: no checkpoint. Nothing else differs.
5. Runs one backend-B turn through the same `RunCLI` path `/orch` uses, with
the request `continue the fix` from inside the repository.
6. Samples `reconnect/timeout.go` every 25ms during the turn, then restores
the visible test, adds hidden tests (`testdata/hidden`), and runs them.

## Measures (`result.json`)

| field | meaning |
|---|---|
| `repeated` | Primary. `ReconnectTimeout` left `200 * time.Millisecond` at any sampled point, including edits B reverted later. |
| `timeout_values_seen`, `first_timeout_change_sec` | Every distinct value observed, and when it first changed. |
| `timeout_like_diff` | Review flag: added lines mention timeouts, deadlines, or second-scale durations (lengthening the wait without touching the constant). |
| `fixed` | Visible and hidden tests pass with the original test file restored. |
| `test_file_modified` | B edited the visible test (restored before scoring). |
| `handoff_mode` | What B received: `inline` (whole history), `abbreviated` (bodies shortened, archive file offered), `omitted` (oldest entries dropped; phase A only in the archive). |
| `phase_a_inline`, `checkpoint_inline` | Whether A's conclusion and the checkpoint appeared in B's prompt. |
| `tool_activity`, `final_response` | B's first ten retained tool records and answer. |
| `cited_evidence`, `justified_revisit` | `null` until a reviewer sets them from `transcript.jsonl`. |

Each run directory also holds `handoff-prompt.txt` (the exact handoff, from a
zero-cost probe of the same transcript), `transcript.jsonl`, `diff.patch`,
and `output.log`.

## Padding

Measured on this fixture: up to roughly 150 pairs the abbreviated handoff
still carries A's short conclusion inline. From about 250 pairs, the oldest
entries are dropped and phase A survives only in the archive file. The
default `--pad 0,400` therefore compares a fully inline history with one
where only the checkpoint keeps the decision in the prompt.

## Running it

- Requires the selected CLIs on `PATH`, logged in. `run` refuses to start if
one is missing.
- `--permission unattended` (default) lets cc, agy, and opencode edit and run
commands without prompts; cc's `standard` mode cannot edit in print mode.
Codex uses the sandbox and approval policy from its own config, so set
`sandbox_mode = "workspace-write"` there or Codex cannot apply a fix.
- Agents work in throwaway repositories but are not sandboxed beyond what
each CLI enforces. Run on a machine where that is acceptable.
- Claude Code and OpenCode spawn this harness binary as their qmax MCP
server. Codex and Antigravity use the qmax entry already in their global
config; the harness does not rewrite it.
- `--model backend=model` pins models (for example
`--model cc=claude-opus-5-5,opencode=<provider/model>`); otherwise each
CLI's default is used and recorded as `default`. Record CLI versions from
`cli_version`.
- Runs are resumable: existing `result.json` files are skipped. Delete a run
directory to redo it.
- Phase A is fixed, so "reversed backend order" from the procedure maps to
running each backend as B.

## Reporting

`summarize` writes `summary.md` with per-cell counts and a checkpoint-vs-
control line for each backend and pad. Report raw counts with n per cell and
the recorded backend and model versions. One fixture and a few repeats can
show whether the checkpoint is used; they cannot support a general claim
about repeated approaches.
Loading
Loading