@marshal/pi-turn-stats 0.1.7 → 0.1.8

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.
@@ -164,6 +164,25 @@ function fmtCost(c: number): string {
164
164
  return `$${c.toFixed(4)}`;
165
165
  }
166
166
 
167
+ /**
168
+ * After session replacement (e.g. /new) or reload, pi invalidates the old
169
+ * extension runner, but a run's `finally` can still deliver agent_settled to
170
+ * it; any ctx/`pi` access on the stale runner throws. Treat those as
171
+ * "no UI / no session" and skip side effects instead of erroring.
172
+ */
173
+ function isStaleCtxError(err: unknown): boolean {
174
+ return err instanceof Error && err.message.includes("extension ctx is stale");
175
+ }
176
+
177
+ /** Run a side effect against ctx/`pi`; silently skip when the ctx is stale. */
178
+ function withStaleGuard(fn: () => void): void {
179
+ try {
180
+ fn();
181
+ } catch (err) {
182
+ if (!isStaleCtxError(err)) throw err;
183
+ }
184
+ }
185
+
167
186
  export default function (pi: ExtensionAPI) {
168
187
  // ---- session cumulative stats ----
169
188
  const sessionTotals = {
@@ -248,8 +267,10 @@ export default function (pi: ExtensionAPI) {
248
267
 
249
268
  // ===== Session start: init status bar =====
250
269
  pi.on("session_start", (_event, ctx) => {
251
- if (!ctx.hasUI) return;
252
- ctx.ui.setStatus("turn-stats", ctx.ui.theme.fg("dim", t("statusWaiting")));
270
+ withStaleGuard(() => {
271
+ if (!ctx.hasUI) return;
272
+ ctx.ui.setStatus("turn-stats", ctx.ui.theme.fg("dim", t("statusWaiting")));
273
+ });
253
274
  });
254
275
 
255
276
  // ===== User submits: start timer =====
@@ -258,10 +279,12 @@ export default function (pi: ExtensionAPI) {
258
279
  startTime = Date.now();
259
280
  turnCount = 0;
260
281
  accum = emptyAccum();
261
- lastModel = ctx.model?.id ?? "unknown";
262
- if (ctx.hasUI) {
263
- ctx.ui.setStatus("turn-stats", ctx.ui.theme.fg("dim", t("statusRunning")));
264
- }
282
+ withStaleGuard(() => {
283
+ lastModel = ctx.model?.id ?? "unknown";
284
+ if (ctx.hasUI) {
285
+ ctx.ui.setStatus("turn-stats", ctx.ui.theme.fg("dim", t("statusRunning")));
286
+ }
287
+ });
265
288
  });
266
289
 
267
290
  // ===== Each LLM turn ends: accumulate usage =====
@@ -297,27 +320,31 @@ export default function (pi: ExtensionAPI) {
297
320
  sessionTotals.durationMs += durMs;
298
321
 
299
322
  if (SHOW_CARD && turnCount > 0) {
300
- pi.appendEntry<TurnStatsData>(ENTRY_TYPE, {
301
- kind: "exchange",
302
- startTime,
303
- endTime,
304
- turns: turnCount,
305
- exchanges: 1,
306
- ...accum,
307
- tokensPerSec: genTps,
308
- model: lastModel,
323
+ withStaleGuard(() => {
324
+ pi.appendEntry<TurnStatsData>(ENTRY_TYPE, {
325
+ kind: "exchange",
326
+ startTime,
327
+ endTime,
328
+ turns: turnCount,
329
+ exchanges: 1,
330
+ ...accum,
331
+ tokensPerSec: genTps,
332
+ model: lastModel,
333
+ });
309
334
  });
310
335
  }
311
336
 
312
- if (ctx.hasUI) {
313
- ctx.ui.setStatus(
314
- "turn-stats",
315
- ctx.ui.theme.fg(
316
- "dim",
317
- t("statusDone", fmtDuration(durMs), fmtThroughput(genTps), fmtTokens(accum.totalTokens), fmtCost(accum.cost)),
318
- ),
319
- );
320
- }
337
+ withStaleGuard(() => {
338
+ if (ctx.hasUI) {
339
+ ctx.ui.setStatus(
340
+ "turn-stats",
341
+ ctx.ui.theme.fg(
342
+ "dim",
343
+ t("statusDone", fmtDuration(durMs), fmtThroughput(genTps), fmtTokens(accum.totalTokens), fmtCost(accum.cost)),
344
+ ),
345
+ );
346
+ }
347
+ });
321
348
  });
322
349
 
323
350
  // ===== /turnstats: append session cumulative stats card =====
@@ -326,20 +353,22 @@ export default function (pi: ExtensionAPI) {
326
353
  handler: async () => {
327
354
  const sessDurMs = sessionTotals.durationMs;
328
355
  const sessGenTps = calcOutputPerSec(sessionTotals.output, sessDurMs);
329
- pi.appendEntry<TurnStatsData>(ENTRY_TYPE, {
330
- kind: "session",
331
- startTime: sessDurMs > 0 ? Date.now() - sessDurMs : Date.now(),
332
- endTime: Date.now(),
333
- turns: sessionTotals.exchanges,
334
- exchanges: sessionTotals.exchanges,
335
- input: sessionTotals.input,
336
- output: sessionTotals.output,
337
- cacheRead: sessionTotals.cacheRead,
338
- cacheWrite: sessionTotals.cacheWrite,
339
- totalTokens: sessionTotals.totalTokens,
340
- cost: sessionTotals.cost,
341
- tokensPerSec: sessGenTps,
342
- model: t("sessionModel"),
356
+ withStaleGuard(() => {
357
+ pi.appendEntry<TurnStatsData>(ENTRY_TYPE, {
358
+ kind: "session",
359
+ startTime: sessDurMs > 0 ? Date.now() - sessDurMs : Date.now(),
360
+ endTime: Date.now(),
361
+ turns: sessionTotals.exchanges,
362
+ exchanges: sessionTotals.exchanges,
363
+ input: sessionTotals.input,
364
+ output: sessionTotals.output,
365
+ cacheRead: sessionTotals.cacheRead,
366
+ cacheWrite: sessionTotals.cacheWrite,
367
+ totalTokens: sessionTotals.totalTokens,
368
+ cost: sessionTotals.cost,
369
+ tokensPerSec: sessGenTps,
370
+ model: t("sessionModel"),
371
+ });
343
372
  });
344
373
  },
345
374
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marshal/pi-turn-stats",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "pi extension: per-exchange duration, token & cost stats for conversations (stream card + status bar + /turnstats command)",
5
5
  "keywords": [
6
6
  "pi-package"