feat(orchestration): always-on resume across the agent graph
A scan that crashes or is stopped can now be resumed by re-invoking
``strix`` with the same ``--run-name``. Resume is implicit — presence
of ``{run_dir}/bus.json`` triggers it. To force a fresh start, delete
the run dir.
What survives a process restart with the same scan_id:
* Root agent's LLM history — already worked (root SDK SQLiteSession).
* Every non-terminal subagent's LLM history — new. ``create_agent``
now opens SQLiteSession(session_id=child_id,
db_path={run_dir}/sessions/{child_id}.db) per child and passes it
to ``run_with_continuation``.
* Bus topology — new. ``AgentMessageBus`` gains snapshot/restore/
_maybe_snapshot async methods plus a ``metadata`` field that holds
per-agent {task, skills, is_whitebox, scan_mode, diff_scope}.
``register``, ``finalize``, ``park``, and ``mark_llm_failed`` each
call ``_maybe_snapshot`` to atomically persist the bus to
{run_dir}/bus.json (tempfile + Path.replace).
* Vulnerability reports — new. ``ScanArtifactWriter._write_
vulnerabilities`` now also writes ``vulnerabilities.json``
(atomic). ``Tracer.hydrate_from_run_dir`` reads it on resume so
new vuln-NNNN ids don't collide with prior on-disk files.
What does not survive: the sandbox container itself (fresh per
process), so ``/workspace/scratch`` and Caido state are lost.
``/workspace/sources`` re-mounts from the host so source code is
unchanged.
``orchestration/scan.py:run_strix_scan`` does the actual resume:
1. Resolve run_dir up front; if bus.json exists it's a resume.
2. Acquire {run_dir}/.lock (fcntl.flock) so a second strix process
can't run concurrently on the same scan_id.
3. ``bus.set_snapshot_path(...)``, ``tracer.hydrate_from_run_dir()``.
4. On resume: load + bus.restore, find root_id from snapshot (the
agent with parent_of[id] is None), spawn the sandbox, skip the
root's bus.register (already in snapshot).
5. ``_respawn_subagents`` walks every agent with status in
running/waiting/llm_failed: reopens its SQLiteSession, rebuilds
the child agent via the captured factory, builds run config /
context, asyncio.create_task the run with initial_input=[] so
the SDK replays from session. Per-child failure (missing/corrupt
DB, factory raises) finalizes that child as crashed and continues.
6. Open root SQLiteSession at the same path, run the root with
initial_input=[] on resume (or the formatted root task on a
fresh run), and let SDK replay drive the next turn.
7. ``finally``: close every per-agent session, take a final
snapshot, tear down sandbox, release the lock.
HARNESS_WIKI.md updated with the new run-dir layout (sessions/,
bus.json, vulnerabilities.json, .lock) and the resume contract.
Net: +500 LoC across 7 files. No new deps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+26
-4
@@ -530,13 +530,35 @@ Async run loop. Rich panels for vuln-found events, live stats panel updated ever
|
||||
|
||||
### 9.6 Run Directory Layout (`strix_runs/<run_name>/`)
|
||||
|
||||
Created and managed by `telemetry/tracer.py`. Contents:
|
||||
Created and managed by `telemetry/tracer.py` + `orchestration/scan.py`. Contents:
|
||||
- `events.jsonl` — every span/event in append-only JSONL (thread-safe writes).
|
||||
- `vulnerabilities/vuln_{id}.json` — one file per finding, sorted by severity, dedup-checked.
|
||||
- `penetration_test_report.md` — final markdown report (executive summary + methodology + technical analysis + recommendations).
|
||||
- `vulnerabilities/vuln_{id}.md` — one file per finding, sorted by severity, dedup-checked.
|
||||
- `vulnerabilities.csv` — index of every finding for spreadsheet consumption.
|
||||
- `vulnerabilities.json` — machine-readable mirror, used by `Tracer.hydrate_from_run_dir` to repopulate vuln state on resume so id allocation doesn't collide.
|
||||
- `penetration_test_report.md` — final markdown report.
|
||||
- `session.db` — SDK `SQLiteSession` for the **root** agent's conversation history.
|
||||
- `sessions/{child_id}.db` — per-subagent `SQLiteSession`, one file per spawned child.
|
||||
- `bus.json` — atomic snapshot of the orchestration bus (topology, statuses, inboxes, per-agent stats, per-agent metadata `{task, skills, is_whitebox, scan_mode, diff_scope}`). Written after every `bus.register` / `finalize` / `park` / `mark_llm_failed`, plus a final dump at scan teardown.
|
||||
- `.lock` — advisory `flock`-style file lock; prevents two `strix` processes from running on the same `scan_id` concurrently.
|
||||
- `strix.log` — per-scan log file (DEBUG to file, ERROR to stderr).
|
||||
- `<target_subdir>/` — local source clones, per-target.
|
||||
|
||||
There is **no execution checkpointing** — if the process crashes mid-run, the agent restarts from scratch on retry. Resumability is limited to the interactive-mode wait/resume on inbound messages.
|
||||
### 9.7 Resume
|
||||
|
||||
Resume is **always on**: presence of `bus.json` triggers it. Fresh runs simply have no `bus.json` to begin with. To force a fresh start with the same `--run-name`, delete the run dir.
|
||||
|
||||
What survives a process restart with the same `scan_id`:
|
||||
- Root agent's full LLM conversation (replayed by SDK from `session.db`).
|
||||
- Every non-terminal subagent's full LLM conversation (replayed from `sessions/{child_id}.db`).
|
||||
- Bus topology: `parent_of`, `statuses`, `names`, `stats_live`, `stats_completed`, pending `inboxes`, `metadata` (task + skills per agent).
|
||||
- Vulnerability reports (hydrated from `vulnerabilities.json`).
|
||||
- Run log (appended to existing `strix.log`).
|
||||
|
||||
What does **not** survive:
|
||||
- The sandbox container itself — fresh container per process. Files agents wrote under `/workspace/scratch/` or scanner outputs to `/workspace/.strix-source-aware/` are lost. `/workspace/sources` re-mounts from the host so source code is unchanged.
|
||||
- Caido proxy state (request log, scopes, replay sessions).
|
||||
|
||||
On resume, every subagent in `running` / `waiting` / `llm_failed` status is respawned via `_respawn_subagents` with `initial_input=[]`; the SDK's session replay drives them from where they stopped. Terminal-status agents (`completed` / `crashed` / `stopped`) are left alone but their stats remain in `stats_completed` for the TUI footer. Per-child failure (corrupt session DB, missing skill module) finalizes that child as `crashed` and continues with the rest.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user