@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 +21 -0
- package/README.md +406 -0
- package/extensions/pi-ledger/__tests__/helpers.ts +326 -0
- package/extensions/pi-ledger/__tests__/ledger.test.ts +2528 -0
- package/extensions/pi-ledger/index.ts +2698 -0
- package/package.json +70 -0
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
|
+
[](https://github.com/earendil-works/pi-coding-agent)
|
|
10
|
+
[](./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
|