cargo-hauler 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +108 -20
  3. package/artifact/agent-bundle.manifest.json +1 -1
  4. package/artifact/claude/.claude-plugin/marketplace.json +1 -1
  5. package/artifact/claude/.claude-plugin/plugin.json +1 -1
  6. package/artifact/claude/INSTALL.md +1 -1
  7. package/artifact/claude/bin/cargo-hauler-flight.mjs +5325 -4253
  8. package/artifact/claude/bin/cargo-hauler.mjs +5849 -4228
  9. package/artifact/claude/hooks/event-route-session-start.mjs +2 -2
  10. package/artifact/claude/hooks/event-route-stop.mjs +2 -2
  11. package/artifact/claude/hooks/event-route-tool-after.mjs +2 -2
  12. package/artifact/claude/hooks/event-route-tool-before.mjs +2 -2
  13. package/artifact/claude/hooks/hooks-flight.mjs +776 -287
  14. package/artifact/claude/mcp/mcp-hauler-2b8242f9-flight.mjs +7554 -6597
  15. package/artifact/claude/mcp/mcp-hauler-2b8242f9.mjs +6239 -5297
  16. package/artifact/claude/mcp-apps/dashboard.html +1 -1
  17. package/artifact/claude/scripts/hauler.mjs +2170 -572
  18. package/artifact/claude/skills/cargo-hauler/SKILL.md +32 -0
  19. package/artifact/codex/.codex-plugin/plugin.json +1 -1
  20. package/artifact/codex/INSTALL.md +1 -1
  21. package/artifact/codex/bin/cargo-hauler-flight.mjs +5325 -4253
  22. package/artifact/codex/bin/cargo-hauler.mjs +5849 -4228
  23. package/artifact/codex/hooks/event-route-session-start.mjs +2 -2
  24. package/artifact/codex/hooks/event-route-stop.mjs +2 -2
  25. package/artifact/codex/hooks/event-route-tool-after.mjs +2 -2
  26. package/artifact/codex/hooks/event-route-tool-before.mjs +2 -2
  27. package/artifact/codex/hooks/hooks-flight.mjs +776 -287
  28. package/artifact/codex/mcp/mcp-hauler-2b8242f9-flight.mjs +7554 -6597
  29. package/artifact/codex/mcp/mcp-hauler-2b8242f9.mjs +6239 -5297
  30. package/artifact/codex/mcp-apps/dashboard.html +1 -1
  31. package/artifact/codex/scripts/hauler.mjs +2170 -572
  32. package/artifact/codex/skills/cargo-hauler/SKILL.md +32 -0
  33. package/artifact/cursor/.cursor-plugin/plugin.json +1 -1
  34. package/artifact/cursor/INSTALL.md +1 -1
  35. package/artifact/cursor/bin/cargo-hauler-flight.mjs +5325 -4253
  36. package/artifact/cursor/bin/cargo-hauler.mjs +5849 -4228
  37. package/artifact/cursor/hooks/event-route-session-start.mjs +2 -2
  38. package/artifact/cursor/hooks/event-route-stop.mjs +2 -2
  39. package/artifact/cursor/hooks/event-route-tool-after.mjs +2 -2
  40. package/artifact/cursor/hooks/event-route-tool-before.mjs +2 -2
  41. package/artifact/cursor/hooks/hooks-flight.mjs +776 -287
  42. package/artifact/cursor/install.mjs +1 -1
  43. package/artifact/cursor/mcp/mcp-hauler-2b8242f9-flight.mjs +7554 -6597
  44. package/artifact/cursor/mcp/mcp-hauler-2b8242f9.mjs +6239 -5297
  45. package/artifact/cursor/mcp-apps/dashboard.html +1 -1
  46. package/artifact/cursor/scripts/hauler.mjs +2170 -572
  47. package/artifact/cursor/skills/cargo-hauler/SKILL.md +32 -0
  48. package/artifact/portable/INSTALL.md +1 -1
  49. package/artifact/portable/bin/cargo-hauler-flight.mjs +5325 -4253
  50. package/artifact/portable/bin/cargo-hauler.mjs +5849 -4228
  51. package/artifact/portable/install.mjs +1 -1
  52. package/artifact/portable/mcp/mcp-hauler-2b8242f9-flight.mjs +7537 -6580
  53. package/artifact/portable/mcp/mcp-hauler-2b8242f9.mjs +6235 -5293
  54. package/artifact/portable/mcp-apps/dashboard.html +1 -1
  55. package/artifact/portable/plugin.json +1 -1
  56. package/artifact/portable/scripts/hauler.mjs +2170 -572
  57. package/artifact/portable/skills/cargo-hauler/SKILL.md +32 -0
  58. package/dist/bin/cargo-hauler-flight.mjs +5325 -4253
  59. package/dist/bin/cargo-hauler.js +5849 -4228
  60. package/dist/bin/hauler.js +2170 -572
  61. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 4257982: Survive a CLI or plugin upgrade under a daemon left running from the older install, and add `hauler daemon restart`. The client schemas now default the fields older daemons never send (`outputPath`, `after`), so `hauler status`, `hauler result`, `hauler await`, and the `hauler_*` tools read a 0.4.1 daemon's replies instead of printing a Zod issue array; a reply that still cannot be read fails as `daemon is 0.4.1 (pid N, since 3h ago), this CLI is 0.4.4 — restart it with \`hauler daemon restart\`` with the first mismatch (`active[0].outputPath expected string, received undefined`) on a second line, and `hauler daemon status` says the same. The status report carries the daemon `version` (older daemons are asked with one `ping`), `hauler status --json` exposes it as `daemonVersion`, and every document header — plus the status summary — warns `daemon 0.4.2 ≠ cli 0.4.4 — restart it with \`hauler daemon restart\`` while the versions differ. `hauler daemon restart` sends the graceful stop, waits up to 5 s for the old pid to exit, starts a daemon from this install, and prints both pids and versions (`previousPid` in the JSON result; exit `1` when the old daemon did not exit, in which case nothing is started). Tickets in flight at a restart are not handed over: the new daemon marks them `killed` with `orphaned by daemon restart`, and `hauler result` / `hauler_result` explain that instead of showing a plain kill. The shell header ends with `state dir <path>` so a moved `CARGO_HAULER_STATE_DIR` is visible on every command. (#75)
8
+
9
+ ## 0.4.5
10
+
11
+ ### Patch Changes
12
+
13
+ - 62d4e16: Add `--after <ticket>[,<ticket>…]` to `hauler exec` and `hauler request` (and `after: string[]` on `hauler_request`) to declare explicit ticket dependencies: the request stays `queued` — skipped by lane admission and batch folding — until every named ticket has settled, fails with `prerequisite cc-N failed`/`killed` (exit code `null`) if one of them does, resolves at once when they already finished, and is rejected as a `bad-intent` error naming an unknown ticket. Prerequisites may live in another lane; the ledger stores them (`after` on every request record) and `hauler status`/`hauler result`/await heartbeats show `waits for cc-N (running 2m/~5m)` while a dependent is held. The `exec --bg` and `request` acknowledgements now name the tickets ahead in the lane (`queued behind cc-3281 (~13m)`, `ahead` on the ack and `queue` on the request result) so a cost-ordered reorder is visible, and `hauler request` reports the daemon's rejection reason instead of a generic submit failure. (#45)
14
+ - 1135585: The daemon rejects a path-shaped program that is not cargo (`bad-intent`) instead of running it and recording the path as the subcommand; the dashboard's by-command table shows legacy rows by basename, and the all-time latency tile reads "latency added by attaching" when riders waited longer than their solo estimate instead of showing a negative "saved" value. Dashboard screenshots refreshed.
15
+ - d86d8b6: The shared fifo jobserver is armed only when the host `make` can speak it (GNU make 4.4+, or no make at all); on older makes the daemon falls back to per-run `CARGO_BUILD_JOBS` grants so `-sys` crates whose build scripts run `make` (jemalloc, openssl, …) build again. `CARGO_HAULER_JOBSERVER=fifo|off|auto` overrides the detection (#76).
16
+ - b13db35: Detect stalled tickets and end orphaned ones. The daemon samples every running ticket's process-tree CPU time (Linux `/proc`, macOS `ps`) and flags a run `stalled` once it is past `CARGO_HAULER_STALL_ESTIMATE_FACTOR` (3) times its estimate with no CPU and no output for `CARGO_HAULER_STALL_IDLE_MS` (10 min). `hauler status` / `hauler_status`, the dashboard, and `hauler await` heartbeats show `stalled` with the idle duration; `hauler result` / `hauler_result` answer `ticket looks stalled (no CPU for Nm) — hauler kill cc-N`. A stalled ticket whose submitting connection has disconnected is killed automatically through the `hauler kill` path with the error `stalled: no CPU for Nm after owner disconnected; killed automatically`; `CARGO_HAULER_STALL_AUTO_KILL=0` only flags. Status records gain optional `stall` and `orphaned` fields. (#46)
17
+ - cfb6967: Keep every ticket's whole output on disk and make it readable after the fact. The daemon writes a leader run's combined stdout+stderr, as emitted (rendered diagnostics for demultiplexed runs, ANSI as captured), to `<state dir>/tickets/<ticket>.log`, bounded by the new `CARGO_HAULER_TICKET_LOG_MAX_BYTES` (default 64 MiB, then one truncation line; `0` disables). The ledger gains `output_path`, exposed as `outputPath` on every request record (attached followers carry their leader's path); the startup retention pass removes the logs of pruned rows and any log without a row. `hauler result <ticket>` now shows `Full output: <path> (size)` and `--json` carries `request.outputPath`; `hauler result <ticket> --full` and `hauler_result { ticket, full: true }` render the whole log as the document body (the last ~768 KiB when it does not fit, with the path for the rest). When a synchronous command is auto-backgrounded and the caller's stdout is not a terminal, the exit-75 notice adds that the redirect receives no output and to read it with `hauler result cc-N --full`. (#68)
18
+
3
19
  ## 0.4.4
4
20
 
5
21
  ### Patch Changes
package/README.md CHANGED
@@ -41,11 +41,14 @@ src/
41
41
  Every rendered route — MCP tool, CLI command, rendered script — composes
42
42
  through one layout, the way a page framework's `layout.tsx` wraps every page:
43
43
 
44
- - **Header:** `<DaemonBadge>` prints what the request-start probe proved:
45
- `cargo-hauler · daemon running (pid 4021) · 2/5 permits +1 riding, 1 queued
46
- · 2 lanes busy · up since 3h ago`, or `daemon stopped · no socket; it starts
47
- on demand…`, or `daemon unresponsive · did not accept a connection within
48
- 750ms (machine saturated)…`.
44
+ - **Header:** `<DaemonBadge>` prints what the request-start probe proved and
45
+ which state directory it is: `cargo-hauler · daemon running (pid 4021) ·
46
+ 2/5 permits +1 riding, 1 queued · 2 lanes busy · up since 3h ago · state dir
47
+ /fast/cache/cargo-hauler`, or `daemon stopped · no socket; it starts on
48
+ demand…`, or `daemon unresponsive · did not accept a connection within
49
+ 750ms (machine saturated)…`. When the daemon is another build than the CLI
50
+ or MCP server rendering the document, a second line says so: `cargo-hauler
51
+ · daemon 0.4.2 ≠ cli 0.4.4 — restart it with \`hauler daemon restart\``.
49
52
  - **Body:** the route's own document, unchanged. The route keeps its
50
53
  `<Agent.Result value>`; the runtime merges it into the shell so
51
54
  `structuredContent` and `--json` are exactly what the route declared.
@@ -66,7 +69,7 @@ and a `health` value from one bounded `status` probe:
66
69
 
67
70
  | `health.state` | meaning |
68
71
  | --- | --- |
69
- | `running` | `pid`, `startedAtMs`, `latencyMs`, `running` (permit holders), `riding` (attached), `queued`, `busyLanes`, `maxConcurrent` |
72
+ | `running` | `pid`, `startedAtMs`, `latencyMs`, `running` (permit holders), `riding` (attached), `queued`, `busyLanes`, `maxConcurrent`, and `version` when the daemon states one (on the status report from 0.4.5, by one extra `ping` for older daemons) |
70
73
  | `stopped` | `socket-missing` (starts on demand) or `connection-refused` (stale socket) |
71
74
  | `unresponsive` | `accept-timeout` (never accepted), `answer-timeout` (accepted, no `status-result`), or `connection-closed` within the probe budget (750 ms for the accept and for the answer); ledger reads still work |
72
75
  | `unreachable` | `open-failed` with the errno (`EACCES`, `EMFILE`, …): the socket is present but could not be opened, which is not evidence the daemon is down |
@@ -90,6 +93,7 @@ assertion share one derivation.
90
93
  | `<AdmissionState>` | permits in use, load, memory clamp, sharing savings; calls out a paused admission gate |
91
94
  | `<KacheStats>` | kache coverage and freshness, slowest crates by profile, or an honest "not detected" |
92
95
  | `<LogTail>` | the captured output tail, labelled live while the run is in progress |
96
+ | `<FullOutput>` | where the ticket's whole output log lives and how large it is; under `full`, the log itself in code-block chunks |
93
97
  | `<BuildDiagnostics>` | an index of cargo `error[E…]`/`warning:` blocks (level / code / message / location) followed by every captured block verbatim |
94
98
  | `<DashboardLink>` | where the MCP App lives and how to open it elsewhere |
95
99
  | `<TicketGuidance>` | what to do next, one component per ticket status |
@@ -135,7 +139,7 @@ the same filter as its `session` field). Results carry
135
139
  | `tool:hauler/hauler_log` · `cli:log` | recent requests | `LogStream` → `LogDocument` |
136
140
  | `tool:hauler/hauler_last` · `cli:last` | most recent request | `LastDocument` |
137
141
  | `tool:hauler/hauler_await` · `cli:await` | long-poll a ticket (≤ 2 h) | `AwaitStream` → `AwaitDocument` |
138
- | `tool:hauler/hauler_result` · `cli:result` | one ticket, live tail while running | `ResultDocument` |
142
+ | `tool:hauler/hauler_result` · `cli:result` | one ticket, live tail while running; `full` renders the whole on-disk output log | `ResultDocument` (`<FullOutput>`) |
139
143
  | `tool:hauler/hauler_kill` · `cli:kill` | stop a queued or running ticket | `KillDocument` |
140
144
  | `tool:hauler/hauler_request` · `cli:request` | submit a background request | `RequestDocument` |
141
145
  | `cli:daemon` | `run` / `start` / `stop` / `status` | plain JSON, exit code from the result |
@@ -216,15 +220,15 @@ executable beside it (`dist/bin/cargo-hauler.js` in the package,
216
220
 
217
221
  | Command | Behavior |
218
222
  | --- | --- |
219
- | `hauler exec [--session ID] [--host HOST] [--cwd DIR] [--bg] -- <cargo …>` | Submit Cargo through the daemon and stream output; hooks rewrite commands to this form. A relative `--cwd` is resolved against the caller's directory. Exits with cargo's code; `130`/`143` after a SIGINT/SIGTERM (the ticket is killed first); `75` when auto-backgrounded. |
223
+ | `hauler exec [--session ID] [--host HOST] [--cwd DIR] [--bg] [--after TICKET[,TICKET…]] -- <cargo …>` | Submit Cargo through the daemon and stream output; hooks rewrite commands to this form. A relative `--cwd` is resolved against the caller's directory. `--after` (repeatable or comma-separated) keeps the request queued until every named ticket has finished; it fails with `prerequisite cc-N <status>` if one of them fails or is killed, and an unknown ticket is rejected as a bad intent. Exits with cargo's code; `130`/`143` after a SIGINT/SIGTERM (the ticket is killed first); `75` when auto-backgrounded. |
220
224
  | `hauler status [--limit N] [--cwd DIR] [--session ID] [--lane KEY] [--ticket ID …] [--status S …] [--command-contains TEXT]` | Queue, active runs, lanes, admission, kache, optionally filtered. |
221
225
  | `hauler log [--limit N]` | Recent requests from the ledger. |
222
226
  | `hauler last` | The most recent request. |
223
227
  | `hauler await <ticket> [--max-wait-ms N]` | Long-poll until the ticket finishes or the wait expires (default 30 s, ceiling 55 s per call — the rendered-route budget; call again to keep waiting). |
224
- | `hauler result <ticket>` | A stored ticket; running tickets include a live output tail. |
228
+ | `hauler result <ticket> [--full]` | A stored ticket; running tickets include a live output tail. The document names the full on-disk output log (`Full output: <path> (size)`) and `--json` carries it as `request.outputPath`; `--full` prints that whole log instead of the tail (the last ~768 KiB when it does not fit, with the path for the rest). |
225
229
  | `hauler kill <ticket>` | Stop a ticket: drop it from the queue or SIGTERM (then SIGKILL) its cargo process group, freeing the lane. Riders return to their lane or fail with it. |
226
- | `hauler request [--session ID] [--host HOST] [--cwd DIR] -- <cargo …>` | Submit a background request and return its ticket. |
227
- | `hauler daemon <run\|start\|stop\|status>` | Manage the daemon lifecycle. |
230
+ | `hauler request [--session ID] [--host HOST] [--cwd DIR] [--after TICKET …] -- <cargo …>` | Submit a background request and return its ticket, with where it landed in its lane (`queued behind cc-3281 (~13m)`, `waiting for cc-3281`, or `attached to cc-3281`). `--after` works as for `exec`. |
231
+ | `hauler daemon <run\|start\|stop\|status\|restart>` | Manage the daemon lifecycle. `restart` sends the graceful stop, waits up to 5 s for the old pid to exit, then starts a daemon from this install and prints both (`restarted: pid 741314 (0.4.1) → pid 742001 (0.4.4)`); a daemon that has not exited by then is reported, not killed, and nothing is started (exit `1`). Tickets in flight at the restart are not handed over: the new daemon marks them `killed` with the error `orphaned by daemon restart`. |
228
232
  | `hauler install-shim [--dir DIR] [--real-cargo PATH] [--force]` | Install the optional PATH shim. |
229
233
 
230
234
  The `hauler` MCP server projects the same operations as `hauler_status`,
@@ -330,6 +334,26 @@ callers, dependency-unblocking work, and recently edited packages receive a
330
334
  lower scheduling score. Waiting time lowers the score further so broad work
331
335
  eventually runs.
332
336
 
337
+ Admission within a lane is therefore cost-ordered, not first-in-first-out: a
338
+ queued `cargo test -p foo` (a cheap estimate) normally starts before a queued
339
+ `cargo build --workspace` submitted a minute earlier, even when the test
340
+ spawns a binary that build produces. The acknowledgement makes the order
341
+ visible — `ticket cc-3289 queued behind cc-3281 (1 ahead, wait ~780s)` on
342
+ `exec`, `cc-3289 submitted, queued behind cc-3281 (~13m)` on `request` — and
343
+ `--after` makes the dependency explicit: `hauler exec --after cc-3281 -- cargo
344
+ test …` (or `hauler request --after`, or `after: ["cc-3281"]` on
345
+ `hauler_request`) stays `queued` and is skipped by admission and batch folding
346
+ until every named ticket has settled. Prerequisites may live in any lane; a
347
+ prerequisite in the same lane also scores higher while a ticket waits on it,
348
+ like a dependency-closure leaf. A prerequisite
349
+ that ends `failed` or `killed` settles the dependent `failed` with
350
+ `prerequisite cc-N failed` and no exit code, without spawning cargo; its riders
351
+ follow the normal attachment rules. A prerequisite that already finished
352
+ resolves immediately; an unknown ticket is rejected as a bad intent. A blocked
353
+ ticket never attaches to a run already in flight (that run started before the
354
+ prerequisite finished), and `hauler status`, `hauler result`, and await
355
+ heartbeats show `waits for cc-N (running 2m/~5m)` while it is held.
356
+
333
357
  Admission is separate from lane scheduling. It observes one-minute load per
334
358
  core and, on Linux, CPU PSI `some avg10`, then applies the configured thresholds
335
359
  and the global permit cap. Load and CPU pressure never defer below
@@ -363,18 +387,31 @@ own `-j` flag or `CARGO_BUILD_JOBS` always wins over both.
363
387
  | Lane isolation | A workspace-root and target-directory pair is serialized independently from other lanes. |
364
388
  | Admission | Per-core load, Linux CPU PSI, Linux memory PSI and `MemAvailable`, macOS VM pressure, configured thresholds, and the global permit cap control new starts. |
365
389
  | Parallelism | One daemon-owned jobserver FIFO shared by every spawned Cargo; a per-run `CARGO_BUILD_JOBS` grant only when the FIFO could not be armed. |
366
- | Scheduling | EWMA estimates, optional kache priors, fan-out, dependency topology, recent edits, and request age determine lane order. |
367
- | Persistence | Tickets, output tails, timings, outcomes, and savings are stored in SQLite. |
390
+ | Scheduling | EWMA estimates, optional kache priors, fan-out, dependency topology, recent edits, and request age determine lane order; `--after cc-N` holds a request until the named tickets settle. |
391
+ | Persistence | Tickets, output tails, timings, outcomes, and savings are stored in SQLite; every leader run's whole combined output is kept on disk as `<state dir>/tickets/<ticket>.log`. |
368
392
  | Caller output and status | Output streams to attached callers; late callers receive buffered replay. After 30 seconds without output, the client emits a progress heartbeat every 15 seconds with lane queue position, the lane-head ticket, and an aggregate wait ETA. |
369
- | Wait escalation | A queued request waiting longer than the larger of twice its own estimate and ten minutes is flagged as delayed; running jobs silent for more than five minutes show a quiet-duration hint. Nothing is killed automatically. |
393
+ | Wait escalation | A queued request waiting longer than the larger of twice its own estimate and ten minutes is flagged as delayed; running jobs silent for more than five minutes show a quiet-duration hint. A running job past three times its estimate whose process tree has burned no CPU and printed nothing for ten minutes is flagged `stalled`; only a stalled job whose submitting connection is gone is killed automatically. |
370
394
  | Daemon status | `running`, `stopped`, or `unresponsive`: a socket that exists but does not answer within its budget is reported as unresponsive, never as stopped. |
371
395
 
372
396
  ### Tickets and long-running requests
373
397
 
398
+ ![cargo-hauler dashboard detail drawer streaming a running ticket's live output](docs/media/dashboard-live-output.png)
399
+
374
400
  Every request has a durable ticket (`cc-<n>`). Its status, exit code, output
375
401
  tail, estimate, and timestamps are stored in SQLite and can be read from later
376
- sessions. `hauler exec --bg -- cargo …` and `hauler_request` return the ticket
377
- immediately. A synchronous request also switches to background mode when a
402
+ sessions. The ledger keeps only a bounded tail (16 KiB); the run's whole
403
+ combined stdout+stderr goes to `<state dir>/tickets/<ticket>.log` as the daemon
404
+ emits it — for a demultiplexed `check`/`build`/`clippy` that is the rendered
405
+ diagnostics stream, not cargo's JSON — up to `CARGO_HAULER_TICKET_LOG_MAX_BYTES`
406
+ (64 MiB by default; the file then ends with one truncation line). A request
407
+ that attached to an in-flight run shares its leader's log, and the row records
408
+ that path. `hauler result cc-N` names the file and its size, `--json` carries
409
+ it as `request.outputPath`, and `hauler result cc-N --full` (or `hauler_result`
410
+ with `full: true`) renders the log itself — so a red `cargo test` is triaged
411
+ from the ticket's own `failures:` list and panic sections instead of a second
412
+ run. The startup retention pass that prunes old ledger rows removes their logs
413
+ too, along with any log whose row is gone. `hauler exec --bg -- cargo …` and
414
+ `hauler_request` return the ticket immediately. A synchronous request also switches to background mode when a
378
415
  *measured* estimate — EWMA history or kache priors, never the cold-start
379
416
  default — exceeds the host's shell-tool cap (nine minutes for Claude, ten for
380
417
  Codex, fourteen for Cursor; the PATH shim uses `CARGO_HAULER_HOST` when it is
@@ -383,7 +420,9 @@ whole wait: the work queued ahead in the lane plus the job's own runtime,
383
420
  which the queued line reports as `wait ~Ns, run ~Ns`. That conversion exits
384
421
  `75` (`EX_TEMPFAIL`) with the ticket on stderr, so `cargo build && …` chains
385
422
  and scripts cannot mistake "submitted" for "built"; explicit `--bg` keeps exit
386
- `0`. Failed runs feed the estimate history too, so a broken build is not
423
+ `0`. When the caller's stdout is not a terminal (`cargo test > out.log`), the
424
+ notice adds that the redirect receives no output and to read it with
425
+ `hauler result cc-N --full`. Failed runs feed the estimate history too, so a broken build is not
387
426
  re-estimated cold on every retry.
388
427
 
389
428
  A foreground `hauler exec` that receives SIGINT or SIGTERM (Ctrl-C, or a
@@ -396,6 +435,45 @@ start cargo at all. If the connection drops after the ticket was accepted, the
396
435
  client prints `connection to daemon lost; ticket cc-N continues — hauler
397
436
  result cc-N` and exits `1`; the daemon finishes the ticket on its own.
398
437
 
438
+ A deadlocked test binary holds its lane for ever at 0% CPU with nothing on
439
+ stdout, and neither the estimate overrun nor the output silence alone can
440
+ tell it from a slow build. The daemon therefore samples the CPU time of every
441
+ running ticket's process tree every 30 seconds (`/proc` on Linux, `ps` on
442
+ macOS; other platforms do not detect stalls). A ticket is flagged `stalled`
443
+ when its elapsed time exceeds `CARGO_HAULER_STALL_ESTIMATE_FACTOR` (3) times
444
+ its estimate, the tree's CPU time has not changed for
445
+ `CARGO_HAULER_STALL_IDLE_MS` (ten minutes), and it printed nothing in that
446
+ window. `hauler status`, `hauler_status`, and the dashboard show `stalled`
447
+ with the idle duration; `hauler result` and `hauler_result` answer `ticket
448
+ looks stalled (no CPU for Nm) — hauler kill cc-N`; `hauler await` heartbeats
449
+ say the same. Riders of a stalled leader report the leader's stall and its
450
+ ticket, since killing a rider only detaches it. A stalled ticket whose
451
+ submitting connection is still open is only flagged. When that connection has
452
+ disconnected (a dead agent shell, a killed hook), the ticket is marked
453
+ orphaned, and once it is also stalled the daemon kills it through the normal
454
+ `hauler kill` path — riders settle or requeue as for any kill — with the
455
+ error `stalled: no CPU for Nm after owner disconnected; killed automatically`.
456
+ `CARGO_HAULER_STALL_AUTO_KILL=0` keeps the flag and never kills. Background
457
+ tickets (`--bg`, `hauler_request`) have no streaming connection and are only
458
+ ever flagged.
459
+
460
+ Tickets do not survive a daemon restart. `hauler daemon restart` (or `stop`
461
+ then `start`) ends every queued or running ticket: the cargo processes die
462
+ with the daemon, and the new daemon's first ledger pass marks each of them
463
+ `killed` with the error `orphaned by daemon restart`, so `hauler result cc-N`
464
+ answers `cc-N killed — orphaned by daemon restart: the daemon stopped while it
465
+ was in flight and does not hand runs over; resubmit if the work is still
466
+ needed` rather than looking like a failure of the command itself. Restart when
467
+ the CLI or plugin was upgraded under a running daemon: every document then
468
+ carries `daemon 0.4.2 ≠ cli 0.4.4 — restart it with \`hauler daemon restart\``,
469
+ and a reply the newer client still cannot read is reported as `daemon is 0.4.2
470
+ (pid N, since 3h ago), this CLI is 0.4.4 — restart it with \`hauler daemon
471
+ restart\`` with the first schema mismatch on a second line — never as a raw
472
+ validation dump. The client schemas default the fields older daemons never
473
+ send (`outputPath`, `after`), so a plain version difference alone does not
474
+ break `status`, `result`, or `await`; finish or `hauler kill` what is in
475
+ flight before restarting if the work matters.
476
+
399
477
  The `tool/after` route checks the session's background tickets — `--bg`,
400
478
  `hauler_request`, and synchronous requests the client converted to a ticket
401
479
  — and, on the first tool call after one finishes, adds its result to the
@@ -469,10 +547,11 @@ is reported as unavailable and never rejects a request.
469
547
 
470
548
  | Variable | Default | Meaning |
471
549
  | --- | --- | --- |
472
- | `CARGO_HAULER_STATE_DIR` | Per-user cache directory | Unix socket or Windows named pipe source, SQLite ledger, daemon log, pid lock, `hook-state.json`, and `hook-events.jsonl`. No legacy alias. |
550
+ | `CARGO_HAULER_STATE_DIR` | Per-user cache directory | Unix socket or Windows named pipe source, SQLite ledger, daemon log, pid lock, `hook-state.json`, `hook-events.jsonl`, and the per-ticket output logs under `tickets/`. No legacy alias. |
473
551
  | `CARGO_HAULER_CARGO_BIN` | `$CARGO_HOME/bin/cargo` | Cargo binary for daemon-started work; bare `cargo` is the last fallback. Never resolved through `PATH`. Read from the daemon's own environment (export it where the daemon starts, or before `hauler daemon start`); clients do not forward it. |
474
552
  | `CARGO_HAULER_MAX_CONCURRENT` | `5` | Global admission permits for Cargo processes across all lanes; an integer >= 1. |
475
553
  | `CARGO_HAULER_JOBS_GRANT` | `max(4, cores / max concurrent)` | `CARGO_BUILD_JOBS` added to each Cargo process only while the shared jobserver FIFO is not armed; an armed daemon injects `MAKEFLAGS` instead and leaves `CARGO_BUILD_JOBS` unset. `0` disables injection. |
554
+ | `CARGO_HAULER_JOBSERVER` | `auto` | Machine-wide fifo jobserver for daemon-spawned cargo: `auto` arms it only when the host `make` is 4.4+ (or absent) because older makes reject `--jobserver-auth=fifo:` in build scripts; `fifo` forces it on, `off` disables it (per-run `CARGO_BUILD_JOBS` grants apply instead). |
476
555
  | `CARGO_HAULER_LOAD_THRESHOLD` | Disabled | Per-core one-minute load threshold for deferring new admissions. |
477
556
  | `CARGO_HAULER_LOAD_MIN` | `2` | Active Cargo processes below which load, CPU PSI, and soft memory pressure do not defer admission. |
478
557
  | `CARGO_HAULER_CPU_PRESSURE_THRESHOLD` | `75` | Linux CPU PSI `some avg10` percentage for deferring new admissions; `0` or `off` disables. |
@@ -487,9 +566,13 @@ is reported as unavailable and never rejects a request.
487
566
  | `CARGO_HAULER_BATCH` | Enabled | `0`, `false`, `off`, or `no` disables the batch composer. |
488
567
  | `CARGO_HAULER_BATCH_WINDOW_MS` | `150` | Delay applied to a batchable lane head so nearby requests can fold; `0` disables. |
489
568
  | `CARGO_HAULER_KILL_GRACE_MS` | `8000` | Time between SIGTERM and SIGKILL when the daemon stops a Cargo process. |
569
+ | `CARGO_HAULER_STALL_ESTIMATE_FACTOR` | `3` | A running ticket becomes a stall candidate once its elapsed time exceeds this multiple of its estimate. |
570
+ | `CARGO_HAULER_STALL_IDLE_MS` | `600000` | Window with no process-tree CPU time and no output after which a stall candidate is flagged `stalled`; `0` or `off` disables stall detection. |
571
+ | `CARGO_HAULER_STALL_AUTO_KILL` | Enabled | Kill a stalled ticket automatically once the connection that submitted it has disconnected. `0`, `false`, `off`, or `no` only flags it. |
490
572
  | `CARGO_HAULER_STOP_WAIT_MS` | `30000` | Maximum wait for one stop-hook invocation; values above the 7200000 ms await ceiling are clamped. |
491
573
  | `CARGO_HAULER_LEDGER_RETENTION_DAYS` | `30` | Finished ledger rows older than this many days are deleted when the daemon starts; `0` disables the age limit. |
492
- | `CARGO_HAULER_LEDGER_MAX_ROWS` | `50000` | Total ledger rows beyond which the oldest finished rows are deleted when the daemon starts; `0` disables the row cap. |
574
+ | `CARGO_HAULER_LEDGER_MAX_ROWS` | `50000` | Total ledger rows beyond which the oldest finished rows are deleted when the daemon starts; `0` disables the row cap. Pruned rows take their `tickets/<ticket>.log` files with them. |
575
+ | `CARGO_HAULER_TICKET_LOG_MAX_BYTES` | `67108864` (64 MiB) | Bytes of a leader run's combined output written to `<state dir>/tickets/<ticket>.log` before the log stops with one truncation line; `0` writes no ticket logs (`hauler result` then has only the tail). |
493
576
  | `CARGO_HAULER_LOG_LEVEL` | `Info` | Daemon log level. |
494
577
  | `CARGO_HAULER_HOST`, `CARGO_HAULER_SESSION` | Unset | Default `--host` and `--session` attribution for `hauler exec`; the PATH shim also borrows `CARGO_HAULER_HOST`'s shell cap for auto-background. |
495
578
 
@@ -519,7 +602,12 @@ unset, the daemon reads kache's configured local store from
519
602
  `hauler_await` fail loudly when the daemon is unreachable instead of
520
603
  reporting a ticket as not found; `hauler_status`, `hauler_log`, and
521
604
  `hauler_last` read the ledger with the daemon marked `stopped` or
522
- `unresponsive`.
605
+ `unresponsive`. A daemon whose reply the client cannot read fails as a
606
+ version difference naming both versions and `hauler daemon restart`.
607
+ - The state directory is not migrated between installs. Every rendered
608
+ document names the one in use (`state dir …` in the header; `stateRoot` in
609
+ `--json`), so a `CARGO_HAULER_STATE_DIR` change is visible on the next
610
+ command rather than discovered from an empty ledger.
523
611
  - Test sharing uses identity attachment or batch folding, never coverage.
524
612
  Folded `test` and `nextest` requests receive the composite output and exit
525
613
  code, so a failure may come from another package in the batch.