Skip to content

Warp-Drive State Reference

The warp-drive state machine, exhaustively. State file shape, the state-machine CLI, and the full phase-transition table. Extracted from the warp-drive guide's internals section.

← docs home

Internals Reference

For contributors and power users.

State File

Location: <project>/.claude/.warp-drive-state.json

This is what state-machine.js init produces (with a few rows populated after the first transitions). Keep this in sync with the state object literal at the bottom of init() in scripts/warp-drive/state-machine.js.

json
{
  "version": 1,
  "session": {
    "started_at": "2026-05-08T15:56:59.401Z",
    "started_at_epoch": 1778255819402,
    "pid": 51506,
    "level": 3,
    "github_user": "paulirv",
    "initial_dirt": []
  },
  "phase": "coding",
  "requirement": "42",
  "branch": "feature/issue-42-title",
  "chunk": { "index": 1, "total": 5, "acs": [["AC-01","AC-02"],["AC-03"]] },
  "checklist": {
    "docs_updated": false,
    "tests_passed": false,
    "committed": false,
    "report_filed": false
  },
  "budgets": {
    "phase_started_at": "2026-05-08T15:57:53.487Z",
    "retry_count": 0,
    "coding_cycles": 0
  },
  "aborted": false,
  "edit_counts": { "src/file.ts": 3 },
  "doom_loop_events": [],
  "gates": {
    "code_complete": { "semantic": "diff_relevance_check" },
    "docs_updated":  { "semantic": "ac_count_check" }
  },
  "completed_chunks": [],
  "history": [
    { "phase": "prerequisites", "at": "..." },
    { "phase": "discovering", "at": "...", "from": "prerequisites", "event": "prerequisites_ok", "source": "agent" }
  ],
  "pre_commit_head": "<sha recorded at tests_passed — the committed precondition, #1835>",
  "last_bash_eval": { "last_tool_command": "...", "payload_shape": { "tool_response": ["stdout", "stderr"], "tool_output": "null" }, "at": "..." },
  "harness_defects": [],
  "metrics": {
    "commits": 0,
    "reports_filed": 0,
    "tests_run": 0,
    "chunks_completed": 0,
    "session_duration_minutes": 0
  },
  "token_usage": {
    "session_total": null,
    "chunk_snapshots": [],
    "current_chunk_started_at": null
  },
  "main_branch": "master",
  "session_branch": null,
  "merge_target": "master",
  "merge_strategy": "ff-only"
}

Fields appear roughly in this order in the file. Notes on the less-obvious ones:

  • session.started_at_epoch and session.github_user are populated at init; the latter comes from gh api user -q .login (empty string if gh is unauthenticated, which silently disables @mention notifications).
  • session.initial_dirt (#1756) is the working-tree dirt the run inherited, snapshotted at init via git status --porcelain -uall (null when git could not be read). The segmentation checkpoint classifies against it so an operator's uncommitted changes are refused with their own message rather than reported as the loop's unfinished work — see Session segmentation.
  • aborted: false flips to true when the engine routes through the aborted phase. Cleanup messaging keys off this flag.
  • gates is seeded with the two semantic gates (diff_relevance_check, ac_count_check) plus any verification_gates from _workflow config. The targets arrays from config land alongside semantic on the matching event keys.
  • metrics.session_duration_minutes is updated when the engine writes the state — useful when reporting from warp status.
  • main_branch / session_branch / merge_target / merge_strategy are populated from ~/.claude/scripts/branch-detect.sh at init and consulted during merging.
  • After budget_exceeded, budgets gains exceeded_reasons, exceeded_types, exceeded_at, and exceeded_from_phase to record the trip.
  • budgets.budget_auto_continues counts the bounded L4 auto-continues taken (see below); budget_extensions counts all continues (human or auto).
  • checkpoint and session.segments[] appear only once a long run has segmented (see Session segmentation below). checkpoint records the last clean boundary (at, segment_index, tokens_at, chunks_at, reason); session.segments[] is the append-only audit trail of segments ended so far.
  • session.completed_issues (#992) accumulates the issue number of each work item as it finishes (recorded on entry to awaiting_continue). Together with session.kickoff.issue it defines how far an issue-scoped run may still reach: the kickoff scope minus the completed list is the remaining authorized work, computed by kickoffScope() (scripts/warp-drive/kickoff.js) and surfaced as scope in the status output.

Bounded budget_exceeded auto-continue (Level 4, #873)

budget_exceeded is normally a mandatory human checkpoint at every level. Under automation Level 4 it may become a bounded auto-continue — the one breaker A4 relaxes, and never silently:

  • Opt-in. Off unless _workflow.max_budget_auto_continues > 0. At 0 (the default) — and at L2/L3 always — the breaker is unchanged.
  • Bounded. Under L4 the loop may auto-continue at most max_budget_auto_continues times for soft breakers (no-progress, cycle, phase-time, chunk, qa). Each auto-continue increments budgets.budget_auto_continues; once it reaches the cap, the breaker halts for a human.
  • Hard ceiling always halts. The cost/token/dollar ceiling (cost_budget_exceeded) is never auto-continued past, even under L4 with budget left.
  • Reviewable. Every auto-continue is logged and recorded as a decision issue.

The decision is a pure function (scripts/warp-drive/budget-policy.js), consulted at runtime via the budget-decision CLI action (exit 0 = may auto-continue, 3 = halt). The counter advances only on transition <root> budget_continue --data '{"auto":true}'; a human-approved budget_continue does not count against the cap.

Session segmentation (long runs, #870)

A long L3/L4 run degrades in quality as one Claude context accumulates hours of history. Session segmentation lets the loop checkpoint at a clean per-requirement boundary, end the session, and resume in a fresh context — reusing the existing substrate (the persisted state file + the warp-drive-inject.sh resume hook + a cdfork-style fresh-process spawn). There is no new resume engine; the only new pieces are the threshold trigger and the clean-boundary checkpoint.

  • Default-on, opt-out. segment_after_tokens defaults to 120000 (#959 — keeping segments below the ~150k high-cost context regime); segment_after_chunks defaults to 0. The default is resolved in exactly one place (the state-machine config layer, WORKFLOW_DEFAULTS), and an explicit _workflow value always wins — an explicit segment_after_tokens: 0 (with segment_after_chunks unset/0) fully disables segmentation.

  • Boundary. Segmentation is only evaluated at awaiting_continue (a requirement is done and merged onto the integration branch, git is clean). The checkpoint action refuses to run at any other phase, or over a working tree that carries real changes — see the classification below for what "real" means.

  • Clean means clean of work (#1756). The checkpoint classifies each dirty path rather than counting them, because a --flightplan run's provision overlay rewrites the fenced cdprov-managed block in the tracked .gitignore for the entire window in which segmentation can fire:

    ClassificationOutcomeRefusal kind
    The overlay's own ignore-block editTolerated — bookkeeping, not work
    Changes that pre-date the sessionRefused: commit or stash them outside the runpreexisting
    Anything elseRefused: the requirement is not committed yetinflight

    Overlay tolerance is narrow by design and requires both conditions: an overlay is actually applied (.claude/.provision-overlay.json exists — the same signal cdprov overlay status reports), and the working .gitignore is byte-identical to HEAD once the fenced block is stripped from both sides. An edit anywhere outside the fence is never excused, and neither is a managed-block change with no overlay active — so a run that uses no overlay behaves exactly as it did before.

    session.initial_dirt — a git status --porcelain -uall path snapshot taken at init — is the baseline separating dirt the run inherited from dirt it created. When both are present the refusal reports inflight (the condition the run itself owns) and lists the pre-existing paths under also_preexisting. A session with no baseline reads every path as in-flight, the stricter reading. Note that a path dirty at init, later committed, then dirtied again still classifies as pre-existing — the baseline is a path set, not a content hash.

  • Per-segment trigger, not cumulative. The threshold measures tokens/chunks accrued since the last checkpoint (tokens_at / chunks_at anchors), computed from the monotonic token_usage.chunk_snapshots sum. Because the state file persists across a segment, a cumulative measure would re-trip the threshold immediately in the fresh session and loop forever — the per-segment delta resets the budget at each checkpoint.

  • Level floor. Only Level 3+ auto-segments (where awaiting_continue already auto-continues). At L2 the boundary asks a human, so there is nothing to auto-segment.

  • Seam-aware (#960). work_selected records the item's seam metadata to state.work_meta ({issue, cap, area} — parent cap from Part of #NN, area: label slug; best-effort, nulls on failure). At awaiting_continue the shared segment context compares it against the next queued item's metadata (seamBetween): if a known-on-both-sides cap or area differs, the lower segment_at_seam_tokens bar (default 40000) governs instead of segment_after_tokens. Missing metadata never seams; 0 disables the seam trigger; the full segmentation opt-out keeps it inert. Seam trips return kind: "seam" and a segment at seam: … reason, which also becomes the checkpoint log line.

  • State is preserved, not deleted. A checkpoint writes state.checkpoint + appends session.segments[] and leaves the phase at awaiting_continue — the fresh session resumes from the same state file via the inject hook, losing no state. (The state file is only deleted when the whole run reaches completed.)

  • Scope-bounded (#992). The decision's moreWork input is computed within the run's kickoff scope: an issue-scoped run (session.kickoff.issue) measures its issue list minus session.completed_issues (deterministic, no queue peek), so a scoped-and-done run never segments — and the awaiting_continue caller routes moreWork: false to continue_no instead of continuing or widening into the approved queue.

The decision is a pure function (scripts/warp-drive/segmentation.js), consulted via the segment-decision CLI action (exit 0 = segment, 3 = keep going in-process). The checkpoint action records the boundary. warp status surfaces segmentation (the thresholds, the per-segment usage so far, the checkpoint lineage, and session.segments[]).

State Machine CLI

bash
node ~/.claude/scripts/warp-drive/state-machine.js <action> <project-root> [args]
ActionPurpose
initCreate new session: init <root> --level 2 --issue 42 --pid $PPID
statusShow current state (exit 2 if no session)
declaredList the declared automatic behaviours with, per entry, whether the source hook is present in $BOB_HOME/hooks and wired in $BOB_HOME/settings.json (--json; exit 3 when a promise cannot be kept)
budget-decisionL4 bounded-auto-continue verdict for the current budget_exceeded halt (exit 0 = may auto-continue, 3 = halt) — see above
segment-decisionShould the run segment at the current awaiting_continue boundary? (exit 0 = segment, 3 = keep going) — see Session segmentation
checkpointRecord a segmentation checkpoint (clean boundary; keeps state, doesn't change phase): checkpoint <root> [--data '{"reason":"..."}']
decisions-digestPrint the session-end decisions digest (markdown) aggregating state.decisions[] for the session summary — one-place review/reversal, cross-links the tool-level audit log (#877)
transitionMove phases: transition <root> <event> [--data '{"key":"val"}']
injectGet phase context for SessionStart hook
gateCheck if an operation is allowed: gate <root> git_commit
verifyRun verification gates: verify <root> code_complete
abortShorthand for transition <root> abort
resetDelete state file immediately

Declared automatic behaviours (#1834)

The automatic behaviours BoB promises — hook auto-advances, gates, detectors — are declared once, in scripts/warp-drive/declared-behaviours.js, each with a stable fingerprint of the form <kind>:<source>:<phase>→<event> (gates omit the event). A fingerprint names the declared behaviour, never a diagnosed cause, so later tooling can key compensation events and bug de-dup on it without re-diagnosing (#1833).

FingerprintHookPromise
hook:warp-drive-commit-detector:committing→committedPostToolUse (Bash) warp-drive-commit-detector.shA successful git commit during committing advances to reporting — no manual committed transition
hook:warp-drive-docs-detector:updating_docs→docs_updatedPostToolUse (Edit|Write|Bash) warp-drive-docs-detector.shDuring updating_docs, a successful gh issue edit <req> --body… (the AC tick) or a docs/ edit advances to testing (#1852)
gate:warp-drive-gate:pre-commitPreToolUse (Bash) warp-drive-gate.shA real git commit (argv-position detection, #858) is blocked outside committing

The registry is the source the state machine renders from: the advances text for the two hook-fired phases (shown in warp status / warp viz tooltips) is generated from the registry entry, so the promise a human reads and the fingerprint tooling keys on cannot drift apart. tests/test-declared-behaviours.js (make test-declared-behaviours) asserts every fingerprint names an existing hook file and a valid (phase, event) pair in the transition table, and that every hook that calls state-machine.js transition has an entry.

bash
node ~/.claude/scripts/warp-drive/state-machine.js declared "$(pwd)" [--json]

present = the hook file exists in $BOB_HOME/hooks; wired = $BOB_HOME/settings.json references it (null when that file is unreadable — unknown is not the same as unwired). Exit 3 when any declared behaviour is missing or unwired: a promise the runtime cannot keep.

Compensation detection (#1835)

The state machine — not the model — notices when an agent compensates for a declared automatic behaviour that did not happen. A hand-fired committed "because the hook didn't" used to be indistinguishable from the hook working; that is how #1830 was normalised into memory and loop notes for two months.

Source attribution. Every hook that calls state-machine.js transition exports WARP_HOOK_SOURCE=<hook-name> first. transition records source: "hook:<name>" or source: "agent" on the history[] entry and in its JSON output (warp status --log shows it).

What is recorded. When an event that a kind: hook registry entry promises arrives with source: "agent" in that entry's phase, transition appends to harness_defects[] in the state file:

json
{
  "fingerprint": "hook:warp-drive-commit-detector:committing→committed",
  "phase": "committing", "event": "committed", "at": "2026-08-21T05:19:02Z",
  "session_id": "…",
  "last_tool_command": "git commit -q -F -",
  "payload_shape": { "tool_response": ["interrupted", "isImage", "noOutputExpected", "stderr", "stdout"], "tool_output": "null" },
  "payload_file": ".claude/harness-defects/hook_warp-drive-commit-detector_committing_committed-2026-08-21T05-19-02-719Z.json"
}

and returns "compensation": {"fingerprint"} plus a COMPENSATION RECORDED system message so the agent knows it is compensating. last_tool_command / payload_shape come from last_bash_eval, which warp-drive-commit-detector.sh writes on every evaluated Bash call during committing (before any gate, so a failed commit is captured); payload_shape holds only the top-level keys (or JSON type) of tool_response / tool_output, never values. The entries feed defect filing (#1836); they are never deleted by the loop.

Live payload (#1839). Shape alone cannot reproduce a dead hook, so the detector hooks (warp-drive-commit-detector.sh in committing, warp-drive-docs-detector.sh in updating_docs) also keep the full redacted payload of the last call they evaluated at .claude/.last-hook-payload.json — every string truncated to 400 chars, token-shaped values (ghp_…, sk-…, xox…, AKIA…) replaced with <redacted>, stamped _captured_by / _captured_at; the same redaction is applied to last_tool_command. When transition records a compensation it copies that file to .claude/harness-defects/<fingerprint>-<timestamp>.json (fingerprint characters outside [A-Za-z0-9_.-] become _) and sets payload_file to the project-relative path; null when nothing was captured or the capture is not valid JSON. harness-defect-report.js reads payload_file and appends a ## Payload fenced JSON block (bounded at 6000 chars, the file path named) to the created issue body and to each recurrence comment, so the filed bug carries what the hook actually saw. Both paths are derived session state, gitignored, and reaped with the other .claude/ residue. A captured payload that exposes a new harness shape is also the raw material for a fixture — scripts/capture-hook-fixture.js record writes the same redacted form into tests/fixtures/hooks/.

Precondition rules. A compensation may never run the state ahead of reality, so an agent-sourced declared event is accepted only if the fact it claims is observable:

EventAccepted only ifOtherwise
committedgit rev-parse HEAD differs from pre_commit_head (recorded at tests_passed; falls back to the last commit_hashes[] sha)exit 1, error: "Compensation precondition unmet", precondition names HEAD as unmoved, phase unchanged
docs_updatedthe current issue body (gh issue view <req> --json body) contains at least one - [x]exit 1, precondition says no checked AC, phase unchanged

Both guards fail open when the fact cannot be verified (not a git repo; gh unreachable) — blocking the loop on a forge outage would be worse than a missed refusal. Hook-sourced events are never guarded or recorded: the hook observed the fact directly.

What this means for an agent. The committing and updating_docs phase instructions no longer say "fire it yourself if the hook didn't". If the phase is still committing after git log -1 shows the commit, the hook is broken; the right move is to record that (the manual fire does it for you) and carry on — not to add a "known quirk" to memory.

Outward filing of harness defects (#1836)

harness_defects[] entries would be just another accommodation if they stayed on the state file. scripts/warp-drive/harness-defect-report.js <project-root> [--dry-run] [--json] is the channel that crosses into the maintainer's tracker. The state machine runs it on entry to chunk_complete and session_ending (after the state is written; best-effort — a failure is reported as harness_defect_filing: {result: failed} in the transition output and never blocks; _workflow.harness_defect_filing: false opts out).

Per unreported fingerprint it either bumps the existing open harness-defect issue — found by gh issue list --label harness-defect --search "<fingerprint>" and confirmed by the body marker <!-- harness-defect: <fingerprint> --> — updating its Recurrences: N · first seen · last seen line and commenting (project, session id, date, compensation one-liner), or creates one in the /report-bug structure with labels bug, harness-defect, p3-medium. A second occurrence anywhere in the fleet lands on the same issue; it never opens a duplicate.

Routing keys on the fingerprint's <source>: a BoB hook/script → the bigbrain tracker (resolved from the BOB_SOURCE checkout's origin remote — never hard-coded; BOB_BIGBRAIN_REPO overrides); project-owned configuration (dev-json*, dev-health*, project-hook*, reap*, checks*) → the project repo (from its origin); unknown → both, cross-linked.

Workaround provenance (#1837). Once a filed defect has an issue number, the report also records the compensation as a loop note tagged defect: {repo, number} (kind gotcha, summary <fingerprint> did not fire — agent fired <event> by hand in <phase>), so the project's next run sees it as [workaround for open <repo>#<n>] rather than a permanent rule. init runs loop-notes.js retire on entry to prerequisites and surfaces loop_notes_retire: {text, retired, fixed[], remaining} in its response (the retired N workaround note(s) (fixed: #a, #b) line is also prepended to the phase instruction); a project with no tagged notes makes no forge call. A queued create (forge degraded, number unknown) records no note — an untagged compensation is exactly what the tagging exists to prevent. Closure lookups share scripts/lib/defect-status.js (see the loop-memory reference).

Visibility and escalation (#1838). scripts/warp-drive/harness-defect-summary.js is the read side of the same two sources. state [<root>] summarises the state file's reported and unreported entries as {open, recurrences, fingerprints[], issues[], reported, unreported} — the cdb line harness-defects: N open (M recurrences) and the fleet snapshot's per-project harness_defects field read it. issues / report list the open harness-defect issues on bigbrain and the project repo (recurrences and first/last seen from the Recurrences: line, affected projects from the body plus Recurred on comments) — report prints the /what-next --report block and nothing at all when nothing is open; an unreachable forge is stated, never read as zero. escalate bumps any bigbrain issue at/over _workflow.harness_defect_escalate_after (default 3; 0 disables) to p2-high with a comment carrying the <!-- harness-defect-escalated --> marker, skipping issues already at p1/p2 or already marked (idempotent); harness-defect-report.js runs it after each filing pass and surfaces the result as escalation. cdb prints the state line under each project (and under cdb --check <project>) only when the count is non-zero; scripts/fleet/audit.js snapshot records harness_defects: {open, recurrences, unreported, fingerprints[], issues[]} per present project from the state file alone (a snapshot never calls the forge), summary.harness_defects rolls it up per machine (null when no project records any), and audit.js view renders a "Harness defects" table — per-machine rows plus a FLEET row that de-dupes fingerprints across machines — in both the human table and --json. make test-harness-defect-summary (state, issues, report, escalate, the cdb line) and make test-fleet-audit (snapshot field, roll-up, view) cover these against fixture state files and issue payloads.

Every gh call goes through the bin/gh shim: when the forge is degraded the create/edit/comment is spooled to the #1750 write-ahead queue (the entry is stamped reported.queued: true with issue: null so it is not re-filed) and replays on recovery. Reported entries carry reported: {repo, issue, at[, queued, also]}; --dry-run prints the plan (reads only); re-running is idempotent. make test-harness-defect-report covers create, bump, routing, dry-run, idempotence and the degraded-forge spool against the real shim.

Transition Table

Every phase lists its valid events and where they lead. This matches the TRANSITIONS object in scripts/warp-drive/state-machine.js.

PhaseEventNext Phase
idlestartprerequisites
idleabortaborted
prerequisitesprerequisites_okdiscovering
prerequisitesabortaborted
discoveringwork_selectedplanning
discoveringno_worksession_ending
discoveringabortaborted
planningplan_readychunking
planningwork_skippeddiscovering
planningabortaborted
chunkingchunks_definedcoding
chunkingabortaborted
codingcode_completeupdating_docs
codingabortaborted
updating_docsdocs_updatedtesting
updating_docsabortaborted
testingtests_passedcommitting
testingtests_failedcoding
testingabortaborted
committingcommittedreporting
committingcommit_with_doc_gatedoc_drift_check
committingabortaborted
doc_drift_checkdrift_cleanreporting
doc_drift_checkdrift_blockedcoding
doc_drift_checkabortaborted
reportingreport_filedchunk_complete
reportingabortaborted
chunk_completenext_chunkcoding
chunk_completerequirement_donerequirement_complete
chunk_completeabortaborted
requirement_completemerge_readymerging
requirement_completeabortaborted
mergingmergedawaiting_continue
mergingmerge_failedmerging
mergingpush_failedmerging
mergingabortaborted
awaiting_continuecontinue_yesdiscovering
awaiting_continuecontinue_nosession_ending
awaiting_continueabortaborted
session_endingsession_endedcompleted
session_endingabortaborted
budget_exceededbudget_continuethe interrupted phase (budgets.exceeded_from_phase, #1183); falls back to coding
budget_exceededbudget_abortaborted
budget_exceededabortaborted
abortedabort_resolvedcompleted
abortedabort_cleanup_failedcompleted
abortedrestartidle

Skipping blocked items (work_skipped, #1270). An item discovered at planning to have zero agent-side work remaining (every open AC is human-only, a blocked label, or an open-todo gate) is skipped state-only: work_skipped records {issue, reason, at} to state.skipped_issues, clears the selection, and returns to discovering — no commit, no prerequisites re-run. Skipped issues are excluded from re-discovery for the remainder of the run (segment-context scope math, the discovery cache's emitted queue, and the resumed-segment context injection all honour the list) and are surfaced by warp status (a Skipped: line) and the session summary ("Skipped (blocked)" section). The exit is deliberately planning-only: blockage is discovered when the issue is first read; once coding has begun, uncommitted edits may exist, so mid-coding blockage stays on the error-escalation/todo path.

Re-validation sub-mode (revalidating, #1008). Planning has a display-only sub-mode with no new transition edges: when work_selected crosses a cap/area seam (same seamBetween signal as #960 — both sides known and different; the first selection of a run and missing metadata never arm it), the state machine sets state.revalidation.pending with the from/to boundary, the transition's planning instruction gains a RE-VALIDATION (#1008) addendum, and displayPhase reports revalidating until the pass is recorded. The pass itself is scripts/warp-drive/revalidate.js: gate <issue> compares HEAD against the issue's <!-- approved-at: sha --> marker (exit 0 = fresh, near no-op; exit 3 = moved/unknown → lightweight AC re-check, drift classified by the #878 reversibility engine), and record appends the findings to state.revalidation.entries plus a phase-history line (event: "revalidation", rendered by warp log) and clears the pending flag. The flag is re-armed or cleared on every selection, so an unresolved pass can never leak onto the next item. Gated by _workflow.revalidate_on_area_entry (default on); scope-safe under #992 (re-validates only the already-selected item). See the how-to section.

Budget-trigger reasons. When an enforced budget trips, the engine routes the current phase into budget_exceeded and records the reason on state.budgets.exceeded_reasons. The reasons are:

ReasonSourceDefault ThresholdConfig Key
phase_timeoutPhase elapsed too long30 minmax_phase_minutes (+ phase_timeout_enforcement)
retry_exceededPer-chunk retry counter exceeded5 retriesmax_retries_per_chunk
coding_cycles_exceededCode/test cycles for this chunk exceeded3 cyclesmax_coding_cycles
no_progressConsecutive identical failed attempts — same failure signature AND no new diff (stagnation, not raw attempts)3 attemptsmax_no_progress
total_chunks_exceededChunks completed in this session exceeded hard limit. Each human-approved budget_continue extends the cap by 10 (budgets.max_total_chunks_extension, #1149). Gates starting new chunk work only — wrap-up transitions that land/close in-flight work (merge_ready, qa_passed/qa_skipped, merged/merge_failed/push_failed, promoted/promote_skipped/promote_failed, continue_no) are exempt (#1183)20 chunksmax_total_chunks
session_timeoutTotal session duration exceeded hard limit480 minmax_session_minutes

phase_timeout is only enforced (rather than a soft warning) when phase_timeout_enforcement is block or abort — see Configuration. The other five are always enforced.