@monotykamary/pi-ledger 0.2.0 → 0.4.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.
@@ -63,6 +63,7 @@ import {
63
63
  type SettingItem,
64
64
  type TUI,
65
65
  } from '@earendil-works/pi-tui';
66
+ import { installNestedAgentTelemetryHarvester } from './nested-agent-telemetry.js';
66
67
 
67
68
  // ─── Constants ──────────────────────────────────────────────────────────────
68
69
 
@@ -72,6 +73,19 @@ const TPS_TELEMETRY_EVENT = 'tps:telemetry';
72
73
  /** Custom entry type written by @monotykamary/pi-tps into the session JSONL. */
73
74
  const TPS_CUSTOM_TYPE = 'tps';
74
75
 
76
+ /** Custom events emitted by @monotykamary/pi-retry around its retry loop:
77
+ * 'started' (with a retryId) when the loop spins up, 'completed' when it
78
+ * exits on success, 'cancelled' when it exits on abort/session-change.
79
+ * pi-ledger subscribes so the engagement wizard does not pop mid-retry —
80
+ * agent_settled can fire during a pi-retry backoff sleep (before pi-retry has
81
+ * re-prompted), and popping then would bill the backoff as human time
82
+ * (violating scale-to-zero: a slow/queued provider is a retry, not billable).
83
+ * The prompt is deferred until the retry settles. Mirrors localterm's
84
+ * agent-notify handshake. */
85
+ const PI_RETRY_STARTED_EVENT = 'pi-retry:started';
86
+ const PI_RETRY_COMPLETED_EVENT = 'pi-retry:completed';
87
+ const PI_RETRY_CANCELLED_EVENT = 'pi-retry:cancelled';
88
+
75
89
  /** Minimum gap between streaming updates to count as an inference stall (ms). */
76
90
  const STALL_THRESHOLD_MS = 500;
77
91
 
@@ -847,6 +861,17 @@ function reveal(text: string): string {
847
861
  return ` data-reveal="${esc(text)}">`;
848
862
  }
849
863
 
864
+ /** Pull the 'retryId' out of a @monotykamary/pi-retry event payload, or
865
+ * 'undefined' if it is absent/malformed. Used to match a 'completed'/
866
+ * 'cancelled' event back to its 'started' so pi-ledger only acts on its own
867
+ * retry. Mirrors localterm's retry-event-id util. Pure.
868
+ * @internal Exported for testing only. */
869
+ export function retryEventId(event: unknown): number | undefined {
870
+ if (typeof event !== 'object' || event === null || !('retryId' in event)) return undefined;
871
+ const id = (event as { retryId?: unknown }).retryId;
872
+ return typeof id === 'number' ? id : undefined;
873
+ }
874
+
850
875
  /**
851
876
  * Build a self-contained HTML receipt. White-on-white, Geist Mono, with
852
877
  * values that stream in autoregressively (char-by-char) on load.
@@ -1090,6 +1115,8 @@ function fmtNumber(n: number): string {
1090
1115
  // ─── Extension ──────────────────────────────────────────────────────────────
1091
1116
 
1092
1117
  export default function ledgerExtension(pi: ExtensionAPI) {
1118
+ installNestedAgentTelemetryHarvester(pi);
1119
+
1093
1120
  let settings: LedgerSettings = { ...DEFAULT_SETTINGS };
1094
1121
 
1095
1122
  const totals: Totals = {
@@ -1204,6 +1231,25 @@ export default function ledgerExtension(pi: ExtensionAPI) {
1204
1231
 
1205
1232
  let wizardTimer: ReturnType<typeof setTimeout> | null = null;
1206
1233
 
1234
+ // pi-retry awareness: when @monotykamary/pi-retry is installed it emits
1235
+ // pi-retry:started/completed/cancelled around its (possibly multi-attempt)
1236
+ // retry loop. agent_settled can fire during a retry's backoff sleep —
1237
+ // before pi-retry has decided whether to re-prompt — so without gating the
1238
+ // wizard would pop mid-retry (billing the backoff as human time, violating
1239
+ // scale-to-zero). We capture the active retryId and DEFER the settled
1240
+ // prompt until the retry settles: pop only on 'completed', never on
1241
+ // 'cancelled', and never while a retry is in flight. Mirrors localterm's
1242
+ // agent-notify handshake.
1243
+ let retryActiveId: number | undefined;
1244
+ // The run settled (agent_settled fired, no rolling credit) but a retry was
1245
+ // in flight, so the engagement prompt is pending until the retry completes.
1246
+ // Cleared on pop, on agent_start (a new run supersedes the stale settled
1247
+ // state), on retry cancelled, and at session boundaries.
1248
+ let pendingSettledWizard: { ctx: ExtensionContext } | null = null;
1249
+ // Unsubscribers for the pi-retry event capture (a session-scoped resource);
1250
+ // torn down at session_shutdown. The factory re-binds on the next session.
1251
+ let retryUnsubs: Array<() => void> = [];
1252
+
1207
1253
  // Latest ctx (event-bus listeners for tps:telemetry don't receive one).
1208
1254
  let lastCtx: ExtensionContext | null = null;
1209
1255
 
@@ -1903,6 +1949,11 @@ export default function ledgerExtension(pi: ExtensionAPI) {
1903
1949
  // rule as an unclosed idle window: uncommitted, so not restored.
1904
1950
  pendingSteers = [];
1905
1951
  dequeuedBuffer = null;
1952
+ // Reset pi-retry capture state — a fresh session has no in-flight retry and
1953
+ // no deferred prompt (an unclosed prior session's retry could otherwise
1954
+ // leak across the session boundary on a per-process extension instance).
1955
+ retryActiveId = undefined;
1956
+ pendingSettledWizard = null;
1906
1957
  // Wrap the input editor so keystrokes stage for billing: during a run they
1907
1958
  // feed a steer/followUp burst (committed on submit via `input`); between
1908
1959
  // turns the FIRST keystroke engages an idle window at its onset. Extends
@@ -1944,6 +1995,20 @@ export default function ledgerExtension(pi: ExtensionAPI) {
1944
1995
  // at shutdown was never committed, so it's abandoned and bills 0 (idle
1945
1996
  // with no output is wasted). Persisted as a close for replay cleanliness.
1946
1997
  closeHumanWindow(lastCtx, false);
1998
+ // Tear down the pi-retry event capture (a session-scoped resource): the
1999
+ // factory re-binds subscriptions on the next session, so unbind here to
2000
+ // avoid leaking listeners across the session boundary. Reset the capture
2001
+ // state too — no in-flight retry survives a session teardown.
2002
+ for (const unsub of retryUnsubs) {
2003
+ try {
2004
+ unsub();
2005
+ } catch {
2006
+ // best-effort — ignore a stale bus
2007
+ }
2008
+ }
2009
+ retryUnsubs = [];
2010
+ retryActiveId = undefined;
2011
+ pendingSettledWizard = null;
1947
2012
  });
1948
2013
 
1949
2014
  // ── Agent timing (tool execution) ─────────────────────────────────────
@@ -2044,6 +2109,41 @@ export default function ledgerExtension(pi: ExtensionAPI) {
2044
2109
  // extension load order — a 'fallback' may be corrected by a later 'tps'
2045
2110
  // entry for the same turnIndex (rehydrate keeps the last per turnIndex).
2046
2111
 
2112
+ // ── pi-retry capture ─────────────────────────────────────────────────
2113
+ // @monotykamary/pi-retry emits started/completed/cancelled around its
2114
+ // (possibly multi-attempt) retry loop. agent_settled can land during a
2115
+ // retry's backoff sleep — before pi-retry has re-prompted — so without this
2116
+ // capture the engagement wizard would pop mid-retry, billing the backoff as
2117
+ // human time (violating scale-to-zero: a slow/queued provider is a retry,
2118
+ // not billable). Defer the settled prompt until the retry settles: pop on
2119
+ // 'completed' (if still no credit), never on 'cancelled', never while one is
2120
+ // in flight. Unsubscribed at session_shutdown (a session-scoped resource);
2121
+ // the factory re-binds on the next session. Mirrors localterm's agent-notify.
2122
+ retryUnsubs.push(
2123
+ pi.events.on(PI_RETRY_STARTED_EVENT, (event: unknown) => {
2124
+ retryActiveId = retryEventId(event);
2125
+ })
2126
+ );
2127
+ retryUnsubs.push(
2128
+ pi.events.on(PI_RETRY_COMPLETED_EVENT, (event: unknown) => {
2129
+ if (retryEventId(event) !== retryActiveId) return;
2130
+ retryActiveId = undefined;
2131
+ const pending = pendingSettledWizard;
2132
+ if (!pending) return;
2133
+ pendingSettledWizard = null;
2134
+ // Credit may have changed since settle (e.g. a mid-backoff /ledger-extend);
2135
+ // with none, pop now that the retry has settled. With credit, stay quiet.
2136
+ if (extensionBudgetMs <= 0) armWizardNow(pending.ctx);
2137
+ })
2138
+ );
2139
+ retryUnsubs.push(
2140
+ pi.events.on(PI_RETRY_CANCELLED_EVENT, (event: unknown) => {
2141
+ if (retryEventId(event) !== retryActiveId) return;
2142
+ retryActiveId = undefined;
2143
+ pendingSettledWizard = null; // abort/session-change → no prompt
2144
+ })
2145
+ );
2146
+
2047
2147
  pi.events.on(TPS_TELEMETRY_EVENT, (payload: unknown) => {
2048
2148
  const t = payload as TpsTelemetry | null;
2049
2149
  if (!t || !t.timing || !t.tokens || !t.model) return;
@@ -2159,6 +2259,11 @@ export default function ledgerExtension(pi: ExtensionAPI) {
2159
2259
  // If the human never engaged (no keystroke, no extension), there's no
2160
2260
  // window to close and the turn handoff bills nothing.
2161
2261
  closeHumanWindow(ctx, true);
2262
+ // A new run supersedes any deferred settled prompt (a retry turn, a queued
2263
+ // follow-up, or a fresh prompt all re-settle later — agent_settled
2264
+ // re-evaluates then). Keeps a pi-retry that completes after a retry
2265
+ // turn's agent_end from popping on a stale 'settled' until that run settles.
2266
+ pendingSettledWizard = null;
2162
2267
  });
2163
2268
 
2164
2269
  pi.on('agent_end', (_event, ctx) => {
@@ -2208,7 +2313,22 @@ export default function ledgerExtension(pi: ExtensionAPI) {
2208
2313
  // after a retry storm exhausts — the last errored agent_end no longer
2209
2314
  // suppresses it, since the run has now settled and the human must take
2210
2315
  // over.
2211
- if (extensionBudgetMs <= 0) armWizardNow(ctx);
2316
+ if (extensionBudgetMs <= 0) {
2317
+ // @monotykamary/pi-retry (if installed) drives its own retry loop with
2318
+ // backoff sleeps outside processEvents; pi-core's settlement detection
2319
+ // can fire this agent_settled during that backoff — before pi-retry has
2320
+ // decided whether to re-prompt. Popping then would bill the backoff as
2321
+ // human time (violating scale-to-zero: a slow/queued provider is a
2322
+ // retry, not billable). Defer: if a pi-retry is in flight, stage the
2323
+ // prompt and pop when the retry settles (on pi-retry:completed); a
2324
+ // cancelled retry never pops. With no pi-retry (or none active), pop now
2325
+ // as before.
2326
+ if (retryActiveId !== undefined) {
2327
+ pendingSettledWizard = { ctx };
2328
+ } else {
2329
+ armWizardNow(ctx);
2330
+ }
2331
+ }
2212
2332
  });
2213
2333
 
2214
2334
  // The `input` event fires for every interactive submit that isn't a slash