@monotykamary/pi-ledger 0.2.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,406 @@
1
+ <div align="center">
2
+
3
+ # ๐Ÿงพ pi-ledger
4
+
5
+ **Billing engine for the serverless agency โ€” a [pi](https://github.com/earendil-works/pi-coding-agent) extension that meters agentic dev work like cloud compute and invoices it like a timesheet.**
6
+
7
+ _Per-invocation ยท duration-based ยท scale-to-zero idle. A pomodoro human-time wizard, and an invoice-grade receipt._
8
+
9
+ [![pi extension](https://img.shields.io/badge/pi-extension-blueviolet)](https://github.com/earendil-works/pi-coding-agent)
10
+ [![license](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
11
+
12
+ </div>
13
+
14
+ ---
15
+
16
+ pi-ledger is the billing engine for the **serverless agency** โ€” a dev shop that
17
+ runs on on-demand agents and bills the way serverless compute is billed:
18
+ **per-invocation, duration-based, scale-to-zero idle**.
19
+
20
+ | Serverless compute | pi-ledger |
21
+ | ---------------------- | ------------------------------------------------------------------ |
22
+ | On-demand function | The agent โ€” each turn is an invocation |
23
+ | Execution duration | Per-turn agent time (generation normalized to ref TPS + tool time) |
24
+ | Scale-to-zero idle | Human idle costs nothing by default |
25
+ | Provisioned capacity | Opt-in pomodoro extensions (billed human oversight) |
26
+ | Usage report / invoice | `/ledger-receipt` โ€” an invoice-grade HTML receipt |
27
+
28
+ The agent is the on-demand function; each turn is an invocation billed by
29
+
30
+ duration, with stalls excluded (a slow or queued provider is a retry, not
31
+ billable time). Human oversight โ€” review, steering, the next prompt โ€” is
32
+ metered separately, like managed capacity: you **provision** billable human
33
+ time via opt-in pomodoro extensions (rolling credit), and idle/steering bill
34
+ against it. Even the time spent writing the first prompt is metered: an initial
35
+ window opens on your **first keystroke** and commits when you send the prompt
36
+ (it produces agent work), billed against your credit. So is steering: a steer
37
+ or queued followUp you compose while the agent runs is metered by its typing
38
+ bursts โ€” billed when the message is actually **delivered** to the agent (the
39
+ agent outcome), not at submit โ€” under the same cap. Reverting a queued message
40
+ and re-steering it bills the composition once at the re-steer's delivery; one
41
+ you dequeue and never re-send bills nothing. Idle costs nothing by default: an idle window opens only when you
42
+ **engage** it (first keystroke or extension) after `agent_end`, and bills only
43
+ when your next submit produces agent work (`agent_start`) โ€” so pure idle, or
44
+ idle you walk away from, bills nothing. A wizard pops at `agent_settled` (inline,
45
+ pi-core settings style) and on `/resume` to offer pomodoro-style blocks when no
46
+ rolling credit remains โ€” a styled TUI component in the terminal, or a `select`
47
+ dialog in a GUI (the vscode-pi extension runs pi in RPC mode, where the custom
48
+ component can't render). Extensions are **rolling credit** โ€” provisioned pomodoro blocks
49
+ survive across agent turns (like provisioned capacity) and are themselves an
50
+ engagement signal, so the wizard stays silent while credit remains and only
51
+ re-pops when it's exhausted. For headless/GUI sessions where a prompt can't
52
+ render (or a hands-off "bill my review time" policy), enable **auto-extend** to
53
+ provision a block silently instead of prompting โ€” it bills only idle a later
54
+ submit commits, capped at the block, so walking away never over-bills.
55
+ `/ledger-receipt` then emits the invoice โ€” the cloud-provider usage report, for your own work.
56
+
57
+ > **Standalone, but pi-tps-aware.** pi-ledger works on its own โ€” it measures
58
+ > agent time itself when [`@monotykamary/pi-tps`](https://github.com/monotykamary/pi-tps)
59
+ > isn't installed. When pi-tps **is** present, it emits the `tps:telemetry` event
60
+ > after every turn and pi-ledger consumes its refined generation/stall numbers
61
+ > (and adds tool-execution time of its own). pi-tps writes `tps` markers to the
62
+ > session JSONL; pi-ledger keeps its own event log in a per-session sidecar.
63
+ > Installing pi-tps is purely an upgrade in fidelity.
64
+
65
+ ## Quick start
66
+
67
+ ```bash
68
+ pi install github:inloopstudio-team/pi-ledger
69
+ # optional โ€” better stall detection & per-turn fidelity:
70
+ pi install npm:@monotykamary/pi-tps
71
+ ```
72
+
73
+ Then in pi: `/ledger-settings` to set your rates, work a session, and
74
+ `/ledger-receipt` for the receipt.
75
+
76
+ ### Demo shortcut: receipt from an existing pi-tps session
77
+
78
+ Didn't run a full pi-ledger session? `/ledger-receipt` also works on a session
79
+ that only has pi-tps markers (e.g. resume an older pi-tps session, set rates
80
+ with `/ledger-settings`, then `/ledger-receipt`). With no live ledger data it
81
+ converts the `tps` entries into the receipt โ€” lower fidelity (no tool time;
82
+ no human time โ€” markers carry no credit/commit info, so only agent time is
83
+ billed) but enough to demo the output.
84
+
85
+ ## Commands
86
+
87
+ | Command | What it does |
88
+ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
89
+ | `/ledger` | Show running totals: agent/human hours, costs, total. |
90
+ | `/ledger-settings` | Configure billing (rates, pomodoro, project, author, currency, auto-wizard, auto-extend). TUI: a searchable settings list; GUI: a `select`โ†’`input` flow. |
91
+ | `/ledger-extend [m]` | Open the human-time wizard to extend the window by `m` minutes (default: pomodoro length); confirm or stop in the dialog (TUI component or GUI `select`). Engages a window if none is open. |
92
+ | `/ledger-receipt` | Export a self-contained HTML receipt for the session and open it. |
93
+
94
+ ## How time is measured
95
+
96
+ | Phase | Bracket | Billed as |
97
+ | --------- | ------------------------------------------------------------------------ | ---------- |
98
+ | **Agent** | `agent_start` โ†’ `agent_end` (sum of turns) | Agent time |
99
+ | **Human** | idle after `agent_end` (engaged โ†’ committed at `agent_start`) | Human time |
100
+ | **Human** | first-prompt composition (first keystroke โ†’ first `agent_start`) | Human time |
101
+ | **Human** | steer/followUp composed during a run (typing bursts, billed at delivery) | Human time |
102
+
103
+ > A provider-error turn (`stopReason` `error`) opens **no** human window โ€” its
104
+ > retry/queue backoff isn't human idle and isn't billed (see below).
105
+
106
+ **Agent time** is the billable agent work per turn โ€” generation normalized to a
107
+ reference TPS plus real tool-execution time โ€” summed across turns and priced at
108
+ the agent rate:
109
+
110
+ ```
111
+ agent_ms_per_turn = (output_tokens / reference_tps ร— 1000) + tool_ms
112
+ agent_hours = ฮฃ agent_ms_per_turn / 3_600_000
113
+ agent_cost = agent_hours ร— agent_rate_per_hour
114
+ ```
115
+
116
+ (`ร— 1000` carries token-seconds to ms; `3_600_000` ms = 1 hour.)
117
+
118
+ - **Generation** is billed by **output tokens at a reference TPS** (frontier-model average, default 75), so model speed can't change the bill โ€” a fast model and a slow one producing the same output tokens bill the same, and the faster model no longer punishes the contractor. `referenceTps` is configurable in `/ledger-settings`.
119
+ - **Tool execution** is the agent doing the work (running bash, reading files, โ€ฆ) โ€” billable, measured as the union wall-clock of tool calls within the turn (parallel tools don't double-count). It isn't token-bound, so it's billed as real time.
120
+ - **Stalls** (mid-stream inference pauses) **drop out automatically** โ€” a stall produces no tokens, so token-normalized billing never counts it (the abuse vector a slow/queued provider could inflate). The real wall-clock `generationMs`/`stallMs` are still recorded on the event for audit.
121
+ - **Source** is either `tps` (high-fidelity, from pi-tps's event) or `fallback`
122
+ (self-measured). Exactly one segment is written per turn regardless of
123
+ extension load order โ€” a `fallback` may be corrected by a later `tps` entry
124
+ for the same turn, and rehydration keeps the last per turn (no double-count).
125
+
126
+ **Human time** is the idle window between when the agent hands control back
127
+ (`agent_end`) and when the user takes it again (`agent_start`), capped by the
128
+ rolling extension credit you've provisioned. The engagement prompt (and the
129
+ window it opens) is armed at `agent_settled` โ€” when the run is fully settled
130
+ and no auto-retry, compaction, or queued follow-up will continue. A turn that
131
+ auto-continues (a **provider error** a retry extension sleeps with backoff then
132
+ re-prompts, or an overflow pi-core compacts and retries, or a queued follow-up)
133
+ never reaches `agent_settled`, so no window opens and the backoff/compaction
134
+ wait is never billed as human time (scale-to-zero: a slow/queued provider is a
135
+ retry, not billable). The window reopens at the next `agent_settled`. The **first prompt** is
136
+ special โ€” nothing precedes it โ€” so an **initial window** opens on your
137
+ **first keystroke** (not at `session_start`) and closes at the first
138
+ `agent_start`, metering the time you spend composing (or reviewing a resumed
139
+ session before your next prompt) under the same cap:
140
+
141
+ ```
142
+ billed_human = min(engaged_idle, granted_budget) # only when committed at agent_start
143
+ granted_budget = remaining_extension_credit
144
+ ```
145
+
146
+ **Steering while the agent runs** is also human time, and โ€” like idle โ€” it's
147
+ **commit-gated on an agent outcome**. A thin input-editor wrapper stages every
148
+ keystroke during a run; when you submit a steer or queued followUp (the `input`
149
+ event's `streamingBehavior`), the composition becomes **pending** (queued to
150
+ the agent) and is **billed at delivery** โ€” the `message_start` user message
151
+ that means the queued composition reached the agent โ€” as the sum of its
152
+ **typing bursts** (consecutive keystrokes within a gap threshold), not the
153
+ wall-clock from the first keystroke. A single key, or keys spread minutes
154
+ apart, bills nothing; only sustained typing that's actually delivered to the
155
+ agent bills, under the same credit cap as any window. Billing at delivery
156
+ closes the revert/re-steer abuse: reverting a queued message back to the
157
+ editor (alt+up, "restore queued messages") carries its composition forward to
158
+ the next submit, so reverting then re-steering bills the original typing once
159
+ at the re-steer's delivery โ€” never twice, never free. A composition you dequeue
160
+ and never re-send never reaches the agent, so it bills nothing (no agent
161
+ outcome) and is abandoned at `session_shutdown`. Typing never submitted never
162
+ reached the agent either, so it's discarded โ€” it bills nothing and can't
163
+ inflate the post-turn idle window.
164
+
165
+ **Idle time is engagement-gated and commit-gated.** Between turns an idle
166
+ window opens only when you **engage** โ€” the first keystroke you type, or the
167
+ first extension (the wizard's extend / `/ledger-extend`, which both grant
168
+ capacity and count as engagement). It bills **wall-clock from that onset**
169
+ (capturing thinking, not just keystrokes), capped at your rolling credit โ€” but
170
+ only when your next submit produces agent work (`agent_start` commits it). Pure
171
+ idle (no keystroke, no extension) opens no window and bills nothing; idle you
172
+ walk away from (no submit) is abandoned at `session_shutdown` and bills 0.
173
+ Idle with no output is wasted time.
174
+
175
+ - Engaged idle bills against your rolling credit
176
+ only. No engagement โ†’ no window โ†’ no bill; no credit โ†’ bills 0 even when
177
+ committed (the wizard prompts you to extend first).
178
+ - The **initial window** opens on your **first keystroke** (not at
179
+ `session_start`) and commits at the first `agent_start`, metering first-prompt
180
+ composition under the same credit cap. Review time _before_ the
181
+ first keystroke has no signal and bills nothing โ€” so on `/resume` the wizard
182
+ pops to let you extend (engaging) and bill that review.
183
+ - Extensions are **rolling credit**: `remaining_extension_credit` is the
184
+ provisioned pomodoro balance carried across agent turns. All billed idle and
185
+ steering time consumes it; the remainder rolls forward to the next idle
186
+ window (like provisioned capacity).
187
+ - **At `agent_settled`**, a **wizard** pops inline (the same pi-core settings style
188
+ as `/ledger-settings`, so the status bar stays visible) **only when no
189
+ rolling credit remains** โ€” to prompt engagement (an extension both engages and
190
+ grants capacity). In the TUI it renders a custom component; in a GUI (RPC) it
191
+ falls back to a `select` dialog; with **auto-extend** on, it skips the prompt
192
+ and provisions a block silently. `agent_settled` fires once the run is fully settled (no
193
+ auto-retry, compaction, or queued follow-up left), so the wizard never pops
194
+ mid-retry or mid-continuation. With credit, it stays silent and arms to fire
195
+ when the engaged window's credit is exhausted; the exhaustion pop offers the
196
+ next extension (the `extend + extend + extend` chain).
197
+ - `/ledger-extend [m]` opens the wizard manually โ€” with or without an open
198
+ window (no window โ†’ extend engages one) โ€” offering to extend by `m` minutes;
199
+ confirm in the dialog, or stop.
200
+ - The status bar and receipt total the **entire session up to now** โ€” they
201
+ include the in-progress engaged window's idle (capped at its remaining
202
+ credit) and, for a pi-tps-only session, no human time (markers carry no
203
+ credit/commit info). Unlike pi-tps (per-turn), this is the full session so far.
204
+
205
+ Because billing is `min(engaged_idle, budget)` and only commits on an agent
206
+ action, the time you spend _deciding_ in the wizard is unbilled if you decline
207
+ โ€” and unused extension credit isn't forfeited when you re-engage the agent
208
+ after a short idle; it rolls into the next window. But idle you never commit
209
+ (walk away, dismiss, quit) bills nothing.
210
+
211
+ ## Design principles
212
+
213
+ pi-ledger bills **forward progress, not process** โ€” four choices shape the
214
+ whole engine:
215
+
216
+ - **Bill the outcome, not the time spent.** Idle bills only when a submit
217
+ produces agent work; steering bills only typing actually delivered to the
218
+ agent (billed at delivery, not submit).
219
+ Thinking that led nowhere (you dismissed, walked away, or the agent did
220
+ nothing) costs nothing. We charge for collaboration that moved the session,
221
+ not for minutes the human spent.
222
+ - **Detect engagement instead of demanding a button.** A window opens on your
223
+ first keystroke or extension โ€” engagement is _observed_, not self-reported โ€”
224
+ yet a single stray key bills nothing; only a real typing burst counts. You
225
+ don't start a timer, but you do have to actually be there.
226
+ - **You set the budget; the engine spends it.** Billable idle is capped at the
227
+ pomodoro credit you provision, and it drains as you use it โ€” no billable idle
228
+ accrues beyond what you authorized, and the leftover rolls forward so a short
229
+ productive idle isn't forfeited.
230
+ - **Noise and stalls drop out.** Generation is token-normalized (model speed
231
+ can't move the bill), stalls emit no tokens so they're never billed, and
232
+ unsubmitted typing never reached the agent so it's discarded. The receipt
233
+ tracks value delivered, not wall-clock spent.
234
+
235
+ The shape is a deliberate hybrid: Toggl's _you-decide-the-budget_ control with
236
+ the convenience of automatic tracking, gated on a real outcome โ€” so it never
237
+ bills the process of working, only the work that landed.
238
+
239
+ ## Settings
240
+
241
+ `/ledger-settings` opens a pi-core-style bordered, searchable list. Rate and
242
+ text fields open an inline input on `Enter`; currency and the auto-wizard
243
+ toggle cycle through presets. Settings persist to the per-session sidecar (see [Data model](#data-model))
244
+ and rehydrate on resume and `/tree` navigation.
245
+
246
+ | Setting | Default | Notes |
247
+ | ---------------- | -------- | ----------------------------------------------------------- |
248
+ | Agent rate | `60` | $/hour billed for agent work |
249
+ | Human rate | `60` | $/hour billed for human work |
250
+ | Pomodoro minutes | `20` | Minutes added per extension |
251
+ | Reference TPS | `75` | Output tokens/sec to normalize generation to (frontier avg) |
252
+ | Project | _(cwd)_ | Shown on the receipt; falls back to the cwd name |
253
+ | Author | _(user)_ | Shown on the receipt; falls back to your OS user |
254
+ | Currency | `USD` | Symbol for amounts |
255
+ | Auto-wizard | `on` | Auto-popup at `agent_settled` (no credit) and on `/resume` |
256
+
257
+ ## Receipt / invoice
258
+
259
+ `/ledger-receipt` writes a self-contained HTML file to
260
+ `~/.cache/pi-ledger/receipt-<session>-<timestamp>.html` and opens it.
261
+
262
+ - **White background, white receipt** card with a hairline border and a whisper shadow.
263
+ - **Geist Mono** throughout.
264
+ - Values **stream in autoregressively** โ€” each field types out character-by-character
265
+ like an LLM token stream, with a blinking cursor tracking the active field.
266
+ - **A grouped invoice**, not a flat receipt. Two groups โ€” **Agent** and
267
+ **Human** โ€” each at its hourly rate, with itemized sub-lines that roll up to
268
+ the group subtotal and corroborate the pricing (every sub-line is its hours
269
+ at the group rate, summing to the group total):
270
+ - **Agent** โ†’ _Compute_ (generation, token-normalized) + _Tool execution_
271
+ (wall-clock) + _Stalls_ ($0, not billed), then a **Subtotal**.
272
+ - **Human** โ†’ _Review / think_ (committed idle) + _Steering_ + _Queuing_
273
+ (followUp) + _Idle abandoned_ ($0, not billed), then a **Subtotal**.
274
+ - A **Total** sums the two subtotals, followed by a footer with the
275
+ **provisioned capacity** (extensions granted ยท used ยท remaining) and the
276
+ **session span** vs. billed hours.
277
+ - The `$0` lines are the audit story made visible: time the extension captured
278
+ but the commit pattern excluded (walked away โ†’ no submit โ†’ no bill; stalls).
279
+
280
+ The HTML is fully self-contained (inline CSS + JS, Geist Mono via Google Fonts)
281
+ and prints cleanly to PDF (`โŒ˜P`) โ€” the cursor hides for print.
282
+
283
+ ## Data model
284
+
285
+ pi-ledger keeps a **per-session sidecar event log** at
286
+ `~/.cache/pi-ledger/sessions/<sessionId>.jsonl` โ€” the source of truth, outside
287
+ the session JSONL so it survives **compaction** (which discards old custom
288
+ entries) and accumulates across **all branches** of the session. Events:
289
+
290
+ - `settings` โ€” a settings snapshot (last one wins on replay).
291
+ - `agent` โ€” one per turn: `{ id, turnIndex, agentMs, generationMs, stallMs, toolMs, tokens, model, source, supersedes?, timestamp }`. `agentMs` is the billable time (generation normalized to the reference TPS + tool time); `generationMs`/`stallMs` are the real wall-clock (audit). A `'tps'` turn may `supersede` an earlier `'fallback'` for the same turn (load-order race) so it isn't double-counted.
292
+ - `human-open` โ€” on **engagement** (the first keystroke you type after a turn, or the first extension โ€” both open the window), and re-recorded on each wizard extend: `{ openedAt, engagedVia, grantedBudgetMs, extensions, extensionBudgetMs, timestamp }`. `openedAt` is the engagement onset; `engagedVia` is `"keystroke"` or `"extension"` (audit); `grantedBudgetMs` is the window's cap = `extensionBudgetMs` (the rolling credit carried into the window). No `human-open` is written at `session_start`, `agent_end`, or `agent_settled` โ€” the window is engagement-gated.
293
+ - `human-close` โ€” on the next `agent_start` (**committed** = your submit produced agent work) **or on `session_shutdown`** (**abandoned** = you left without submitting): `{ openedAt, closedAt, billedMs, idleMs, keystrokes, committed, grantedBudgetMs, extensions, extensionBudgetMs, timestamp }`. Committed bills `min([onset, agent_start], credit)`; abandoned bills 0 (idle with no output is wasted). `keystrokes` is the composition-density count while the window was open (after held-key collapse; idle bills wall-clock, so it's analytics, not a billing input). `committed` defaults to `true` on legacy events. Its `extensionBudgetMs` is the rolling credit remaining after this window's consumption (carried forward). Legacy events lacking `extensionBudgetMs` are backfilled on replay.
294
+ - `steer` โ€” a steer/followUp composed while the agent ran, billed at **delivery** (the `message_start` user message = the agent outcome), not at submit: `{ startedAt, submittedAt, durationMs, billedMs, keystrokes, behavior, grantedBudgetMs, extensionBudgetMs, timestamp }`. `submittedAt` is the (re-)submit time; `timestamp` is the delivery/commit time; `billedMs` is `min` of the typing-burst sum and `credit` (not the wall-clock span โ€” `durationMs` is the span, kept for audit); `keystrokes` is the staged count; `behavior` is `"steer"` (mid-stream interrupt) or `"followUp"` (queued). Billed as human time, consuming rolling credit (same rule as an idle window). A pending composition (submitted but not yet delivered) is in-memory only โ€” never persisted; one dequeued and not re-sent, or interrupted by reload/shutdown, is abandoned (bills 0; no agent outcome).
295
+
296
+ On `session_start` (fresh load/reload), pi-ledger replays the sidecar to rebuild
297
+ totals, settings, and the rolling extension credit. An unclosed window from a
298
+ prior session was never committed by an agent action, so it's **abandoned** (a
299
+ `human-close` with `committed: false`, billed 0) rather than restored โ€” it
300
+ isn't carried forward. No window is opened at `session_start`; the next one
301
+ opens on engagement. `/tree` branching stays in the same session, so the live
302
+ in-memory totals are kept as-is (not re-read โ€” never reset to $0). Totals are
303
+ **global across branches**; idle you commit is retained across exit/re-enter,
304
+ idle you abandon is not.
305
+
306
+ ## Architecture
307
+
308
+ ```
309
+ session_start โ†’ rehydrate from sidecar (rebuild totals, settings,
310
+ rolling credit; ABANDON any unclosed prior window โ€”
311
+ uncommitted idle bills 0, not restored). Open NO
312
+ window (engagement-gated). On /resume (or /reload) pop
313
+ the wizard to prompt engagement (bill review via
314
+ extend); startup/new stay silent. Wrap the input editor
315
+ (TUI) to stage keystrokes โ€” during a run for a
316
+ steer/followUp burst, and between turns the FIRST
317
+ keystroke engages an idle window at its onset.
318
+ turn_start โ†’ reset per-turn tool + fallback accumulators
319
+ tool_execution_start โ†’ tool depth counter (union timing)
320
+ tool_execution_end โ†’ (parallel tools don't double-count)
321
+ message_start/update/end โ†’ fallback generation + stall gate (self-sufficient)
322
+ tps:telemetry (pi-tps)โ†’ record 'tps' agent segment = (output tokens / ref TPS) + tool
323
+ โ”” corrects a 'fallback' already written this turn
324
+
325
+ turn_end (no pi-tps) โ†’ record 'fallback' agent segment from own measurement
326
+ agent_end โ†’ discard any uncommitted in-run typing (a steer never
327
+ submitted never reached the agent โ†’ bills 0). Open NO
328
+ window here (engagement-gated); the engagement prompt
329
+ is armed at agent_settled (below), not here โ€” agent_end
330
+ fires per run, and Pi may still auto-retry, auto-compact
331
+ and retry, or continue with a queued follow-up.
332
+ agent_settled โ†’ the run is fully settled (no retry/compaction/follow-up
333
+ left). Open NO window here either (engagement-gated):
334
+ โ”” no credit left โ†’ pop the wizard (engagement prompt)
335
+ โ”” extend โ†’ engage + grant a pomodoro, arm at boundary
336
+ โ”” dismiss/ignore โ†’ no engagement, no window, no bill
337
+ โ”” rolling credit > 0 โ†’ stay silent (arm at engagement)
338
+ โ”” also re-offers the prompt after a retry storm exhausts
339
+ (the run has settled and the human must take over)
340
+ engage (idle) โ†’ first keystroke OR first extension opens the idle
341
+ window at onset (rolling credit); an extension
342
+ also grants capacity. Arms the wizard for exhaustion.
343
+ agent_start โ†’ COMMIT the engaged window: billed =
344
+ min([onset, agent_start], credit), committed;
345
+ consume credit = billed (rolls the rest).
346
+ No engagement โ†’ no window โ†’ bills nothing.
347
+ input (steer/followUp)โ†’ stage the composition as PENDING (queued to the agent):
348
+ snapshot the typing bursts (a prior dequeue's
349
+ dequeuedBuffer prepended), clear staging โ€” NOT billed
350
+ yet. Interactive sources only; pass-through (never
351
+ transform the input). A no-typing submit with no prior
352
+ dequeue stages nothing. Held keys (auto-repeat)
353
+ collapse, so they can't fake a burst; idle keystrokes
354
+ are counted (composition density).
355
+ dequeue (alt+up) โ†’ a queued composition reverts to the editor (the
356
+ app.message.dequeue action, observed via the editor
357
+ wrapper): merge all pending compositions into
358
+ dequeuedBuffer, carried forward to the next submit
359
+ (the re-steer/re-queue). Not abandoned โ€” re-steering
360
+ bills it once at delivery.
361
+ message_start (user) โ†’ a queued steer/followUp DELIVERED to the agent (the
362
+ agent outcome): commit the front pending composition as
363
+ a `steer` event โ€” billed = the typing-burst sum (not
364
+ wall-clock), capped at credit; consume credit. The
365
+ initial/normal prompt stages no pending (no
366
+ streamingBehavior), so it's a no-op for them.
367
+ session_shutdown โ†’ ABANDON any open window (committed: false, billed 0 โ€”
368
+ idle with no submit is wasted; nothing retained) AND
369
+ any pending/dequeued steer composition (never delivered
370
+ โ†’ no agent outcome โ†’ bills 0).
371
+ ```
372
+
373
+ Agent timing prefers pi-tps's `tps:telemetry` (`generationMs`, `stallMs`,
374
+ `tokens.output`); when pi-tps is absent, pi-ledger measures generation + a basic
375
+ stall gap gate itself at `turn_end`. Either way generation is billed by output
376
+ tokens at the reference TPS (speed-invariant); the real generation/stall ms are
377
+ recorded for audit. Tool-execution time is always measured locally, billed as
378
+ real time, and paired with the turn. The wizard is driven entirely by the extension (the agent is
379
+ unaware): it auto-pops at `agent_settled` only when no rolling pomodoro credit
380
+ remains (and on `/resume`, to prompt engagement for review), and is disarmed on
381
+ the next `agent_start` or `session_shutdown`. No window opens at `session_start`,
382
+ `agent_end`, or `agent_settled` โ€” an idle window opens only on engagement (first keystroke or
383
+ extension) and bills only when committed by a submitted prompt at `agent_start`;
384
+ abandoned idle (shutdown without a submit) bills 0. All billed idle and
385
+ steering time consumes the rolling extension credit; the
386
+ remainder carries forward to the next idle window. State is **stateless**: everything is
387
+ rebuilt from the per-session sidecar on `session_start` (fresh load/reload);
388
+ `/tree` keeps the live in-memory totals (branching stays in the same session,
389
+ so the status never resets to $0). A `'tps'`
390
+ agent event `supersedes` the `'fallback'` it replaces, so the same turn isn't
391
+ double-counted. The status and receipt compute the whole session up to the
392
+ current moment, including the in-progress open human window, from the sidecar
393
+ โ€” so they survive compaction and branching.
394
+
395
+ ## Testing
396
+
397
+ ```bash
398
+ pnpm install
399
+ pnpm test # vitest run (129 tests)
400
+ pnpm run typecheck # tsc --noEmit
401
+ pnpm run lint:dead # knip
402
+ ```
403
+
404
+ ## License
405
+
406
+ MIT