1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
//! Per-conversation budget enforcement (ARCH §6 "Budgets (v0.7)").
//!
//! `workflow.yaml` declares `budgets: {max_total_tokens, max_wall_seconds,
//! max_depth}` (all optional; omitted → unbounded). The harness checks
//! them at every model-call boundary, *before* invoking the adapter
//! (`crate::prompt::dispatch::run_exchange`). Spend, wall, and depth are
//! all derived from disk at check time by [`derive`] — no running counter
//! is stored (PRINCIPLES "Single source of truth").
//!
//! **One live whole-tree check — no inheritance.** A budget is a
//! per-conversation-tree ceiling, and `steps/` is one shared tree at the
//! conv-repo root, written live by every conversation (root and every
//! subagent) and never merged (ARCH §2.2/§2.3/§2.6). So any driver — root
//! or subagent — derives the *whole tree's* live spend against the root
//! id ([`root_of`]) and checks it against the single frozen `workflow.yaml`
//! limit. Nothing is handed down at dispatch: the child reads the same
//! total the parent would, so there is no snapshot to freeze and no
//! parent-minus-child to double-count. Tokens and wall derive over the
//! whole tree; `max_depth` is positional and derives from the driver's own
//! branch name. (An optional per-subtree cap — a future `--token-cap`-style
//! knob checked against a subtree's own spend — is not built here.)
//!
//! **Exhaustion is an ordinary terminal state.** On exhaustion the
//! harness ceases the branch's step loop and writes
//! `refs/lernie/budget-exhausted/<branch>` ([`mark_exhausted`]) — the
//! same git-native marking pattern as the §2.6-step-6 conflicted ref.
//! No new event type, no `response.json` marker — a ref plus a stop,
//! which deposits a result message into the parent's inbox like any
//! other terminal event (ARCH §6, §2.11).
//!
//! **`max_depth` and the root (flagged, ARCH §6).** §6 does not spell out
//! the depth boundary. This module reads `max_depth` as the deepest
//! *allowed* dispatch depth: a conversation is exhausted iff
//! `depth(branch) > max_depth`. The root is depth 0, so it is never
//! depth-exhausted for any non-negative `max_depth`; a subagent
//! `max_depth + 1` levels below the root exhausts on its first model
//! call. See `docs/ARCHITECTURE.md` §6.
use crateBudgets;
use crateGitRunner;
use Path;
/// Git-native marker ref for an exhausted conversation
/// (`refs/lernie/budget-exhausted/<branch>`, ARCH §6 — mirrors the
/// §2.6-step-6 conflicted ref). The single home of the prefix; the
/// terminal budget-exhausted state it marks is surfaced as a result
/// deposit into the parent's inbox (§2.11) like any other.
pub const BUDGET_EXHAUSTED_REF_PREFIX: &str = "refs/lernie/budget-exhausted/";
/// Which declared limit a conversation crossed.
/// The crossed limit and the derived actual that crossed it. Carried for
/// the operator-facing diagnostic only — the terminal state is the ref,
/// not this value (ARCH §6 "an ordinary terminal state").
/// Evaluate the single frozen `budgets` against spend/wall/depth derived
/// live from disk. Tokens and wall are whole-tree consumables — derived
/// over [`root_of`]`(branch)` (the branch plus its entire descent, ARCH
/// §6) and exhausted at `actual >= limit`, so the driver stops *before* it
/// overspends. Depth is positional — derived from `branch` itself and
/// exhausted at `actual > limit` (`max_depth` is the deepest allowed
/// depth; the root at depth 0 is never depth-exhausted). Returns the first
/// crossed axis, or `None` when every declared limit still has headroom;
/// an unbounded axis (`None` limit) never triggers.
/// The root conversation id of a branch: its first two hyphen-delimited
/// tokens (`<ts>-<short>`, ARCH §2.2). Every dispatch appends
/// `-<ts>-<short>` (hyphenated descent), so the root is the prefix before
/// the second hyphen. Whole-tree spend/wall derive against this, since
/// [`derive`] sums a branch plus its entire descent — the root's descent
/// *is* the whole tree. A bare root id (at most one hyphen) is its own root.
/// Write the budget-exhausted marker ref for the agent id `branch` at
/// its tip (`git update-ref refs/lernie/budget-exhausted/<agent-id>
/// HEAD`), run inside `worktree` — whose checked-out branch *is*
/// `agents/<agent-id>` (§2.3), so `HEAD` is the tip with no ref-name
/// round trip. State lives in git, not a sidecar file (PRINCIPLES SSOT)
/// — the same pattern as the §2.6 conflicted ref, which is likewise
/// keyed by agent id.