@narumitw/pi-analytics 0.49.6 → 0.49.7

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/dist/index.ts ADDED
@@ -0,0 +1,1811 @@
1
+ // @generated by scripts/build-runtime.mjs; do not edit.
2
+ // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
+
4
+ // src/analytics.ts
5
+ import { randomUUID as randomUUID3 } from "node:crypto";
6
+ import path3 from "node:path";
7
+ import {
8
+ getAgentDir
9
+ } from "@earendil-works/pi-coding-agent";
10
+
11
+ // src/collector.ts
12
+ import { randomUUID } from "node:crypto";
13
+
14
+ // src/errors.ts
15
+ function classifyProviderError(message) {
16
+ const value = message?.toLowerCase() ?? "";
17
+ if (/\b(enotfound|eai_again|dns|getaddrinfo)\b/u.test(value)) return "dns";
18
+ if (/\b(etimedout|timeout|timed out)\b/u.test(value)) return "timeout";
19
+ if (/\b(econnrefused|connection refused)\b/u.test(value)) return "connection_refused";
20
+ if (/\b(econnreset|connection reset|socket hang up)\b/u.test(value)) {
21
+ return "connection_reset";
22
+ }
23
+ if (/\b(tls|ssl|certificate|cert_|handshake)\b/u.test(value)) return "tls";
24
+ if (/\b(fetch failed|network|socket|connection|transport)\b/u.test(value)) {
25
+ return "network_other";
26
+ }
27
+ return "provider_other";
28
+ }
29
+
30
+ // src/collector.ts
31
+ var ResponseCollector = class {
32
+ active;
33
+ hasActiveRun() {
34
+ return this.active !== void 0;
35
+ }
36
+ getActiveRunId() {
37
+ return this.active?.id;
38
+ }
39
+ begin(input) {
40
+ const interrupted = this.active ? this.finalize(input.now, "interrupted") : void 0;
41
+ this.active = {
42
+ id: input.id,
43
+ startedAtMs: input.now,
44
+ triggerSource: input.triggerSource,
45
+ initialModel: input.model,
46
+ attemptCount: 0,
47
+ generations: [],
48
+ generationIds: /* @__PURE__ */ new Set(),
49
+ tools: [],
50
+ toolIds: /* @__PURE__ */ new Set(),
51
+ skills: [],
52
+ skillIndexes: /* @__PURE__ */ new Map(),
53
+ providerErrors: []
54
+ };
55
+ return interrupted;
56
+ }
57
+ beginAttempt() {
58
+ if (this.active) this.active.attemptCount += 1;
59
+ }
60
+ beginGeneration(input) {
61
+ const active = this.active;
62
+ if (!active || active.generationIds.has(input.id)) return;
63
+ active.generationIds.add(input.id);
64
+ active.generations.push({
65
+ id: input.id,
66
+ ordinal: active.generations.length,
67
+ provider: input.model?.provider,
68
+ model: input.model?.model,
69
+ thinkingLevel: input.model?.thinkingLevel,
70
+ startedAtMs: input.now,
71
+ outcome: "pending",
72
+ responses: []
73
+ });
74
+ }
75
+ recordProviderResponse(input) {
76
+ const generation = this.latestGeneration();
77
+ if (generation?.outcome !== "pending") return;
78
+ generation.responses.push({
79
+ ordinal: generation.responses.length,
80
+ occurredAtMs: input.now,
81
+ status: input.status
82
+ });
83
+ }
84
+ finishGeneration(input) {
85
+ const active = this.active;
86
+ const generation = this.latestGeneration();
87
+ if (!active || !generation || generation.outcome !== "pending") return;
88
+ generation.finishedAtMs = input.now;
89
+ generation.durationMs = elapsed(generation.startedAtMs, input.now);
90
+ generation.stopReason = input.stopReason;
91
+ generation.outcome = generationOutcome(input.stopReason);
92
+ if (generation.outcome === "error") {
93
+ active.providerErrors.push({
94
+ id: randomUUID(),
95
+ generationId: generation.id,
96
+ occurredAtMs: input.now,
97
+ provider: generation.provider,
98
+ model: generation.model,
99
+ category: classifyProviderError(input.errorMessage),
100
+ recovered: false,
101
+ terminal: true
102
+ });
103
+ }
104
+ }
105
+ beginTool(input) {
106
+ const active = this.active;
107
+ if (!active || active.toolIds.has(input.id)) return;
108
+ active.toolIds.add(input.id);
109
+ active.tools.push({
110
+ id: input.id,
111
+ ordinal: active.tools.length,
112
+ name: input.name,
113
+ provider: input.model?.provider,
114
+ model: input.model?.model,
115
+ startedAtMs: input.now,
116
+ isError: false,
117
+ completionState: "running"
118
+ });
119
+ }
120
+ finishTool(input) {
121
+ const tool = this.active?.tools.find(({ id }) => id === input.id);
122
+ if (tool?.completionState !== "running") return;
123
+ tool.finishedAtMs = input.now;
124
+ tool.durationMs = elapsed(tool.startedAtMs, input.now);
125
+ tool.isError = input.isError;
126
+ tool.completionState = "finished";
127
+ }
128
+ activateSkill(input) {
129
+ const active = this.active;
130
+ if (!active) return;
131
+ const existingIndex = active.skillIndexes.get(input.name);
132
+ if (existingIndex !== void 0) {
133
+ const existing = active.skills[existingIndex];
134
+ if (existing && existing.initiatedBy === "model" && input.initiatedBy === "user") {
135
+ existing.initiatedBy = "user";
136
+ existing.occurredAtMs = input.now;
137
+ existing.provider = input.model?.provider;
138
+ existing.model = input.model?.model;
139
+ }
140
+ return;
141
+ }
142
+ active.skillIndexes.set(input.name, active.skills.length);
143
+ active.skills.push({
144
+ id: randomUUID(),
145
+ name: input.name,
146
+ initiatedBy: input.initiatedBy,
147
+ occurredAtMs: input.now,
148
+ provider: input.model?.provider,
149
+ model: input.model?.model
150
+ });
151
+ }
152
+ settle(now) {
153
+ return this.finalize(now);
154
+ }
155
+ interrupt(now) {
156
+ return this.finalize(now, "interrupted");
157
+ }
158
+ latestGeneration() {
159
+ return this.active?.generations.at(-1);
160
+ }
161
+ finalize(now, forcedOutcome) {
162
+ const active = this.active;
163
+ if (!active) return void 0;
164
+ this.active = void 0;
165
+ for (const generation of active.generations) {
166
+ if (generation.outcome !== "pending") continue;
167
+ generation.outcome = "interrupted";
168
+ generation.finishedAtMs = now;
169
+ generation.durationMs = elapsed(generation.startedAtMs, now);
170
+ }
171
+ for (const tool of active.tools) {
172
+ if (tool.completionState !== "running") continue;
173
+ tool.completionState = "interrupted";
174
+ tool.finishedAtMs = now;
175
+ tool.durationMs = elapsed(tool.startedAtMs, now);
176
+ }
177
+ const successfulGenerationIndexes = new Set(
178
+ active.generations.map((generation, index) => ({ generation, index })).filter(({ generation }) => isSuccessfulGeneration(generation)).map(({ index }) => index)
179
+ );
180
+ let recoveredHttpErrors = 0;
181
+ let httpErrors = 0;
182
+ for (const [generationIndex, generation] of active.generations.entries()) {
183
+ const hasLaterSuccess = [...successfulGenerationIndexes].some(
184
+ (index) => index > generationIndex
185
+ );
186
+ for (const [responseIndex, response] of generation.responses.entries()) {
187
+ if (response.status < 400) continue;
188
+ httpErrors += 1;
189
+ const laterSuccessInGeneration = generation.responses.slice(responseIndex + 1).some(({ status }) => status >= 200 && status < 400);
190
+ if (laterSuccessInGeneration || hasLaterSuccess) recoveredHttpErrors += 1;
191
+ }
192
+ }
193
+ for (const error of active.providerErrors) {
194
+ const generationIndex = active.generations.findIndex(({ id }) => id === error.generationId);
195
+ error.recovered = [...successfulGenerationIndexes].some((index) => index > generationIndex);
196
+ error.terminal = !error.recovered;
197
+ }
198
+ const recoveredGenerationErrors = active.providerErrors.filter(
199
+ ({ recovered }) => recovered
200
+ ).length;
201
+ const providerErrorCount = httpErrors + active.providerErrors.length;
202
+ const recoveredErrorCount = recoveredHttpErrors + recoveredGenerationErrors;
203
+ const outcome = forcedOutcome ?? deriveOutcome(active.generations, providerErrorCount);
204
+ return {
205
+ id: active.id,
206
+ startedAtMs: active.startedAtMs,
207
+ finishedAtMs: now,
208
+ durationMs: elapsed(active.startedAtMs, now),
209
+ triggerSource: active.triggerSource,
210
+ initialProvider: active.initialModel?.provider,
211
+ initialModel: active.initialModel?.model,
212
+ outcome,
213
+ attemptCount: active.attemptCount,
214
+ generations: active.generations,
215
+ tools: active.tools,
216
+ skills: active.skills,
217
+ providerErrors: active.providerErrors,
218
+ toolErrorCount: active.tools.filter(({ isError }) => isError).length,
219
+ providerErrorCount,
220
+ recoveredErrorCount
221
+ };
222
+ }
223
+ };
224
+ function elapsed(start, end) {
225
+ return Math.max(0, end - start);
226
+ }
227
+ function generationOutcome(stopReason) {
228
+ switch (stopReason) {
229
+ case "stop":
230
+ return "stop";
231
+ case "toolUse":
232
+ return "tool_use";
233
+ case "error":
234
+ return "error";
235
+ case "aborted":
236
+ return "aborted";
237
+ case "length":
238
+ return "length";
239
+ default:
240
+ return "interrupted";
241
+ }
242
+ }
243
+ function isSuccessfulGeneration(generation) {
244
+ return generation.outcome === "stop" || generation.outcome === "tool_use";
245
+ }
246
+ function deriveOutcome(generations, providerErrors) {
247
+ const last = generations.at(-1);
248
+ if (!last) return providerErrors > 0 ? "error" : "success";
249
+ switch (last.outcome) {
250
+ case "stop":
251
+ case "tool_use":
252
+ return providerErrors > 0 ? "recovered_success" : "success";
253
+ case "error":
254
+ return "error";
255
+ case "aborted":
256
+ return "aborted";
257
+ case "length":
258
+ return "length";
259
+ default:
260
+ return "interrupted";
261
+ }
262
+ }
263
+
264
+ // src/menu.ts
265
+ import { stripVTControlCharacters } from "node:util";
266
+
267
+ // src/storage/queries.ts
268
+ var DAY_MS = 24 * 60 * 60 * 1e3;
269
+ var ERROR_CATEGORIES = [
270
+ "dns",
271
+ "timeout",
272
+ "connection_refused",
273
+ "connection_reset",
274
+ "tls",
275
+ "network_other",
276
+ "provider_other"
277
+ ];
278
+ function resolveTimeRange(id, now = Date.now()) {
279
+ let fromMs = 0;
280
+ if (id === "today") {
281
+ const date = new Date(now);
282
+ fromMs = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
283
+ } else if (id === "7d") fromMs = now - 7 * DAY_MS;
284
+ else if (id === "30d") fromMs = now - 30 * DAY_MS;
285
+ return { id, fromMs, toMs: now + 1 };
286
+ }
287
+ async function querySnapshot(runs, range, signal) {
288
+ const generationCounts = [];
289
+ const seenRunIds = /* @__PURE__ */ new Set();
290
+ const skills = /* @__PURE__ */ new Map();
291
+ const tools = /* @__PURE__ */ new Map();
292
+ const categories = Object.fromEntries(
293
+ ERROR_CATEGORIES.map((category) => [category, 0])
294
+ );
295
+ let toolErrors = 0;
296
+ let providerErrors = 0;
297
+ let recoveredErrors = 0;
298
+ let http429 = 0;
299
+ let http5xx = 0;
300
+ let terminal = 0;
301
+ for await (const run of runs) {
302
+ throwIfAborted(signal);
303
+ if (seenRunIds.has(run.id)) continue;
304
+ seenRunIds.add(run.id);
305
+ if (run.startedAtMs < range.fromMs || run.startedAtMs >= range.toMs) continue;
306
+ generationCounts.push(run.generations.length);
307
+ toolErrors += run.toolErrorCount;
308
+ providerErrors += run.providerErrorCount;
309
+ recoveredErrors += run.recoveredErrorCount;
310
+ for (const skill of run.skills) {
311
+ const item = skills.get(skill.name) ?? {
312
+ name: skill.name,
313
+ count: 0,
314
+ modelInitiated: 0,
315
+ userInitiated: 0,
316
+ lastOccurredAtMs: 0,
317
+ models: []
318
+ };
319
+ item.count += 1;
320
+ if (skill.initiatedBy === "user") item.userInitiated += 1;
321
+ else item.modelInitiated += 1;
322
+ item.lastOccurredAtMs = Math.max(item.lastOccurredAtMs, skill.occurredAtMs);
323
+ mergeModelCount(item.models, {
324
+ provider: skill.provider,
325
+ model: skill.model,
326
+ count: 1
327
+ });
328
+ skills.set(skill.name, item);
329
+ }
330
+ for (const tool of run.tools) {
331
+ const item = tools.get(tool.name) ?? {
332
+ name: tool.name,
333
+ count: 0,
334
+ errors: 0,
335
+ averageDurationMs: 0,
336
+ totalDurationMs: 0,
337
+ lastOccurredAtMs: 0,
338
+ models: []
339
+ };
340
+ item.count += 1;
341
+ item.errors += tool.isError ? 1 : 0;
342
+ item.totalDurationMs += tool.durationMs ?? 0;
343
+ item.averageDurationMs = item.totalDurationMs / item.count;
344
+ item.lastOccurredAtMs = Math.max(item.lastOccurredAtMs, tool.startedAtMs);
345
+ mergeModelCount(item.models, {
346
+ provider: tool.provider,
347
+ model: tool.model,
348
+ count: 1
349
+ });
350
+ tools.set(tool.name, item);
351
+ }
352
+ for (const error of run.providerErrors) {
353
+ categories[error.category] += 1;
354
+ terminal += error.terminal ? 1 : 0;
355
+ }
356
+ for (const generation of run.generations) {
357
+ for (const response of generation.responses) {
358
+ if (response.status === 429) http429 += 1;
359
+ if (response.status >= 500 && response.status < 600) http5xx += 1;
360
+ }
361
+ }
362
+ }
363
+ const responses = responseStatistics(generationCounts);
364
+ return {
365
+ overview: {
366
+ responseCycles: responses.count,
367
+ llmCalls: responses.llmCalls,
368
+ callsPerResponse: responses.average,
369
+ p95CallsPerResponse: responses.p95,
370
+ toolCalls: sum([...tools.values()].map(({ count }) => count)),
371
+ toolErrors,
372
+ skillActivations: sum([...skills.values()].map(({ count }) => count)),
373
+ providerErrors,
374
+ recoveredErrors
375
+ },
376
+ skills: [...skills.values()].map((item) => ({ ...item, models: sortModels(item.models) })).sort((left, right) => right.count - left.count || left.name.localeCompare(right.name)),
377
+ tools: [...tools.values()].map(({ totalDurationMs: _, ...item }) => ({ ...item, models: sortModels(item.models) })).sort((left, right) => right.count - left.count || left.name.localeCompare(right.name)),
378
+ reliability: {
379
+ http429,
380
+ http5xx,
381
+ recovered: recoveredErrors,
382
+ terminal,
383
+ categories
384
+ },
385
+ responses
386
+ };
387
+ }
388
+ function responseStatistics(generationCounts) {
389
+ const sorted = [...generationCounts].sort((left, right) => left - right);
390
+ const count = sorted.length;
391
+ const llmCalls = sum(sorted);
392
+ const nearestRank = (percentile) => count === 0 ? 0 : sorted[Math.max(0, Math.ceil(percentile * count) - 1)] ?? 0;
393
+ const median = count === 0 ? 0 : count % 2 === 1 ? sorted[Math.floor(count / 2)] ?? 0 : ((sorted[count / 2 - 1] ?? 0) + (sorted[count / 2] ?? 0)) / 2;
394
+ return {
395
+ count,
396
+ llmCalls,
397
+ average: count > 0 ? llmCalls / count : 0,
398
+ median,
399
+ p95: nearestRank(0.95),
400
+ maximum: sorted.at(-1) ?? 0,
401
+ distribution: {
402
+ one: sorted.filter((value) => value === 1).length,
403
+ twoToThree: sorted.filter((value) => value >= 2 && value <= 3).length,
404
+ fourToSix: sorted.filter((value) => value >= 4 && value <= 6).length,
405
+ sevenPlus: sorted.filter((value) => value >= 7).length
406
+ }
407
+ };
408
+ }
409
+ function mergeModelCount(models, next) {
410
+ const existing = models.find(
411
+ ({ provider, model }) => provider === next.provider && model === next.model
412
+ );
413
+ if (existing) existing.count += next.count;
414
+ else models.push(next);
415
+ }
416
+ function sortModels(models) {
417
+ return models.sort(
418
+ (left, right) => right.count - left.count || `${left.provider ?? ""}/${left.model ?? ""}`.localeCompare(
419
+ `${right.provider ?? ""}/${right.model ?? ""}`
420
+ )
421
+ );
422
+ }
423
+ function throwIfAborted(signal) {
424
+ if (signal?.aborted)
425
+ throw signal.reason ?? new DOMException("Analytics query aborted", "AbortError");
426
+ }
427
+ function sum(values) {
428
+ return values.reduce((total, value) => total + value, 0);
429
+ }
430
+
431
+ // src/menu.ts
432
+ var RANGE_LABELS = {
433
+ today: "Today",
434
+ "7d": "Last 7 days",
435
+ "30d": "Last 30 days",
436
+ all: "All time"
437
+ };
438
+ function createAnalyticsMenu(source, now = Date.now, options) {
439
+ let rangeId = "7d";
440
+ let cachedState;
441
+ const loadState = async (signal) => {
442
+ if (cachedState?.rangeId === rangeId) return cachedState;
443
+ const range = resolveTimeRange(rangeId, now());
444
+ const loaded = { rangeId, range, path: source.path, result: await source.load(range, signal) };
445
+ if (!signal.aborted && rangeId === loaded.rangeId) cachedState = loaded;
446
+ return loaded;
447
+ };
448
+ const getState = ({ signal }) => loadState(signal);
449
+ const menu = {
450
+ start: "main",
451
+ screens: {
452
+ main: ({ state }) => ({
453
+ kind: "actions",
454
+ title: `Analytics \xB7 ${RANGE_LABELS[state.rangeId]}`,
455
+ lines: overviewLines(state.result),
456
+ items: [
457
+ { id: "range", label: "Change time range", to: "range" },
458
+ { id: "skills", label: "Skills", to: "skills" },
459
+ { id: "tools", label: "Tools", to: "tools" },
460
+ { id: "reliability", label: "Provider reliability", to: "reliability" },
461
+ { id: "responses", label: "Response cycles", to: "responses" },
462
+ { id: "privacy", label: "Data & privacy", to: "privacy" },
463
+ { id: "close", label: "Close", close: true }
464
+ ],
465
+ hint: "close"
466
+ }),
467
+ range: ({ state }) => ({
468
+ kind: "choice",
469
+ title: "Analytics time range",
470
+ items: Object.keys(RANGE_LABELS).map((id) => ({
471
+ id,
472
+ label: RANGE_LABELS[id]
473
+ })),
474
+ action: "setRange",
475
+ currentItemId: state.rangeId,
476
+ initialItemId: state.rangeId,
477
+ hint: "back"
478
+ }),
479
+ skills: ({ state }) => skillsScreen(state.result),
480
+ tools: ({ state }) => toolsScreen(state.result),
481
+ reliability: ({ state }) => ({
482
+ kind: "detail",
483
+ title: `Provider reliability \xB7 ${RANGE_LABELS[state.rangeId]}`,
484
+ lines: reliabilityLines(state.result),
485
+ hint: "back"
486
+ }),
487
+ responses: ({ state }) => ({
488
+ kind: "detail",
489
+ title: `Response cycles \xB7 ${RANGE_LABELS[state.rangeId]}`,
490
+ lines: responseLines(state.result),
491
+ hint: "back"
492
+ }),
493
+ privacy: ({ state }) => ({
494
+ kind: "actions",
495
+ title: "Analytics data & privacy",
496
+ lines: privacyLines(state),
497
+ items: [
498
+ {
499
+ id: "clear",
500
+ label: "Clear analytics data\u2026",
501
+ action: "clearData",
502
+ disabled: state.result.kind !== "ready"
503
+ }
504
+ ],
505
+ hint: "back"
506
+ })
507
+ },
508
+ actions: {
509
+ setRange: async ({ itemId, signal }) => {
510
+ if (!isRangeId(itemId)) return { kind: "rejected", error: new Error("Unknown range") };
511
+ rangeId = itemId;
512
+ cachedState = void 0;
513
+ await loadState(signal);
514
+ return signal.aborted ? { kind: "close" } : { kind: "to", screen: "main" };
515
+ },
516
+ clearData: async ({ ctx, state, signal }) => {
517
+ if (state.result.kind !== "ready") return { kind: "stay" };
518
+ if (!options) throw new Error("Analytics confirmation is unavailable");
519
+ const count = state.result.snapshot.overview.responseCycles;
520
+ const confirmation = await options.runConfirmation(ctx, {
521
+ title: "Delete analytics data?",
522
+ message: `This will clear all local analytics history from:
523
+
524
+ ${safeDisplayText(state.path)}
525
+
526
+ The selected range currently shows ${count} response cycles. Other running Pi processes may add new records afterward.`,
527
+ confirmLabel: "Delete data",
528
+ cancelLabel: "Keep data",
529
+ signal,
530
+ isCurrent: options.isCurrent,
531
+ // Keep the dashboard's existing domain-level error route as the only notifier.
532
+ onError: () => void 0
533
+ });
534
+ if (signal.aborted || !options.isCurrent()) return { kind: "close" };
535
+ if (confirmation.kind === "closed") {
536
+ return confirmation.reason === "close" ? { kind: "close" } : { kind: "stay" };
537
+ }
538
+ if (confirmation.kind === "stale") return { kind: "close" };
539
+ if (confirmation.kind === "unsupported") {
540
+ throw new Error(`Analytics confirmation is unavailable in ${confirmation.mode} mode`);
541
+ }
542
+ if (confirmation.kind === "error") throw confirmation.error;
543
+ const result = await source.clearAll(signal);
544
+ cachedState = void 0;
545
+ const isCurrent = options.isCurrent();
546
+ if (isCurrent) {
547
+ try {
548
+ ctx.ui.notify("Cleared local analytics data.", "info");
549
+ if (result.cleanupIncomplete) {
550
+ ctx.ui.notify(
551
+ "Some obsolete analytics files are still in use. Stop other Pi processes and clear again to remove them.",
552
+ "warning"
553
+ );
554
+ }
555
+ } catch {
556
+ }
557
+ }
558
+ return signal.aborted || !isCurrent ? { kind: "close" } : { kind: "to", screen: "main" };
559
+ }
560
+ }
561
+ };
562
+ return {
563
+ menu,
564
+ getState,
565
+ preload: (signal) => loadState(signal),
566
+ get rangeId() {
567
+ return rangeId;
568
+ }
569
+ };
570
+ }
571
+ async function showAnalyticsMenu(ctx, source, options) {
572
+ const { runConfirmation, runMenu, runTask } = await import("@narumitw/pi-tui-kit");
573
+ if (options.signal.aborted || !options.isCurrent()) return;
574
+ const controller = createAnalyticsMenu(source, Date.now, {
575
+ runConfirmation,
576
+ isCurrent: options.isCurrent
577
+ });
578
+ const loading = await runTask(ctx, {
579
+ label: "Loading local analytics\u2026",
580
+ signal: options.signal,
581
+ isCurrent: options.isCurrent,
582
+ task: ({ signal }) => controller.preload(signal),
583
+ onError: () => void 0
584
+ });
585
+ if (loading.kind !== "completed") {
586
+ if (loading.kind === "error") {
587
+ ctx.ui.notify(
588
+ "Analytics failed: The local analytics query could not be completed. Existing data was not changed.",
589
+ "error"
590
+ );
591
+ }
592
+ return;
593
+ }
594
+ await runMenu(ctx, controller.menu, {
595
+ getState: controller.getState,
596
+ signal: options.signal,
597
+ isCurrent: options.isCurrent,
598
+ onError: (_ctx, error) => {
599
+ ctx.ui.notify(`Analytics failed: ${safeErrorMessage(error)}`, "error");
600
+ }
601
+ });
602
+ }
603
+ function overviewLines(result) {
604
+ if (result.kind === "unavailable") {
605
+ return [result.message, "", "No analytics are being collected."];
606
+ }
607
+ const stats = result.snapshot.overview;
608
+ if (stats.responseCycles === 0) {
609
+ return [
610
+ "No analytics yet.",
611
+ "Collection is active. Complete one Pi response cycle, then open /analytics again.",
612
+ "",
613
+ "Includes settled response cycles only."
614
+ ];
615
+ }
616
+ return [
617
+ metric("Response cycles", stats.responseCycles),
618
+ metric("LLM calls", stats.llmCalls),
619
+ metric(
620
+ "Calls per response",
621
+ `${formatDecimal(stats.callsPerResponse)} \xB7 P95 ${stats.p95CallsPerResponse}`
622
+ ),
623
+ metric("Tool calls", stats.toolCalls),
624
+ metric("Tool errors", stats.toolErrors),
625
+ metric("Skill activations", stats.skillActivations),
626
+ metric("Provider errors", stats.providerErrors),
627
+ metric("Recovered errors", stats.recoveredErrors),
628
+ "",
629
+ "Includes settled response cycles only."
630
+ ];
631
+ }
632
+ function skillsScreen(result) {
633
+ if (result.kind === "unavailable") {
634
+ return {
635
+ kind: "browse",
636
+ title: "Skills",
637
+ lines: [result.message],
638
+ items: [],
639
+ hint: "back"
640
+ };
641
+ }
642
+ return {
643
+ kind: "browse",
644
+ title: "Skills",
645
+ lines: result.snapshot.skills.length === 0 ? ["No skill activations detected in this time range."] : void 0,
646
+ items: result.snapshot.skills.map(skillItem),
647
+ viewportSize: "adaptive",
648
+ hint: "back"
649
+ };
650
+ }
651
+ function skillItem(skill) {
652
+ return {
653
+ id: skill.name,
654
+ label: safeDisplayText(skill.name),
655
+ statusText: `${skill.count} \xB7 ${skill.modelInitiated} model / ${skill.userInitiated} user`,
656
+ searchText: skill.models.map(modelLabel).join(" "),
657
+ details: [
658
+ metric("Activations", skill.count),
659
+ metric("Model initiated", skill.modelInitiated),
660
+ metric("User initiated", skill.userInitiated),
661
+ "",
662
+ "By model",
663
+ ...skill.models.map((model) => `${modelLabel(model)}: ${model.count}`),
664
+ "",
665
+ `Last detected: ${formatTimestamp(skill.lastOccurredAtMs)}`
666
+ ]
667
+ };
668
+ }
669
+ function toolsScreen(result) {
670
+ if (result.kind === "unavailable") {
671
+ return {
672
+ kind: "browse",
673
+ title: "Tools",
674
+ lines: [result.message],
675
+ items: [],
676
+ hint: "back"
677
+ };
678
+ }
679
+ return {
680
+ kind: "browse",
681
+ title: "Tools",
682
+ lines: result.snapshot.tools.length === 0 ? ["No tool calls detected in this time range."] : void 0,
683
+ items: result.snapshot.tools.map(toolItem),
684
+ viewportSize: "adaptive",
685
+ hint: "back"
686
+ };
687
+ }
688
+ function toolItem(tool) {
689
+ return {
690
+ id: tool.name,
691
+ label: safeDisplayText(tool.name),
692
+ statusText: `${tool.count} \xB7 ${tool.errors} errors`,
693
+ searchText: tool.models.map(modelLabel).join(" "),
694
+ details: [
695
+ metric("Calls", tool.count),
696
+ metric("Errors", tool.errors),
697
+ `Average duration: ${formatDecimal(tool.averageDurationMs)} ms`,
698
+ "",
699
+ "By model",
700
+ ...tool.models.map((model) => `${modelLabel(model)}: ${model.count}`),
701
+ "",
702
+ `Last detected: ${formatTimestamp(tool.lastOccurredAtMs)}`
703
+ ]
704
+ };
705
+ }
706
+ function reliabilityLines(result) {
707
+ if (result.kind === "unavailable") return [result.message];
708
+ const value = result.snapshot.reliability;
709
+ return [
710
+ "Observed provider errors only; provider-internal failures may be invisible.",
711
+ "",
712
+ metric("HTTP 429", value.http429),
713
+ metric("HTTP 5xx", value.http5xx),
714
+ metric("DNS", value.categories.dns),
715
+ metric("Connection timeout", value.categories.timeout),
716
+ metric("Connection refused", value.categories.connection_refused),
717
+ metric("Connection reset", value.categories.connection_reset),
718
+ metric("TLS", value.categories.tls),
719
+ metric("Other network", value.categories.network_other),
720
+ metric("Other provider", value.categories.provider_other),
721
+ "",
722
+ metric("Recovered", value.recovered),
723
+ metric("Terminal failures", value.terminal)
724
+ ];
725
+ }
726
+ function responseLines(result) {
727
+ if (result.kind === "unavailable") return [result.message];
728
+ const value = result.snapshot.responses;
729
+ return [
730
+ metric("Cycles", value.count),
731
+ metric("LLM calls", value.llmCalls),
732
+ metric("Average", formatDecimal(value.average)),
733
+ metric("Median", formatDecimal(value.median)),
734
+ metric("P95", value.p95),
735
+ metric("Maximum", value.maximum),
736
+ "",
737
+ "Calls per response",
738
+ metric("1 call", value.distribution.one),
739
+ metric("2\u20133 calls", value.distribution.twoToThree),
740
+ metric("4\u20136 calls", value.distribution.fourToSix),
741
+ metric("7+ calls", value.distribution.sevenPlus)
742
+ ];
743
+ }
744
+ function privacyLines(state) {
745
+ return [
746
+ "Local analytics files:",
747
+ safeDisplayText(state.path),
748
+ "",
749
+ "Stored: timestamps, extension-generated record IDs, model/provider IDs, thinking level, tool and skill names, durations, counts, HTTP statuses, and classified errors.",
750
+ "Not stored: prompts, responses, thinking, tool arguments/results, raw errors, headers, cwd/file paths, session identity, or credentials.",
751
+ "",
752
+ "No database server, cloud connection, or other remote telemetry is used.",
753
+ "Analytics are non-critical derived metadata; a failed local write may be dropped."
754
+ ];
755
+ }
756
+ function metric(label, value) {
757
+ return `${label.padEnd(24)} ${value}`;
758
+ }
759
+ function formatDecimal(value) {
760
+ return Number.isInteger(value) ? String(value) : value.toFixed(2).replace(/0+$/u, "").replace(/\.$/u, "");
761
+ }
762
+ function formatTimestamp(value) {
763
+ return new Date(value).toLocaleString();
764
+ }
765
+ function modelLabel(model) {
766
+ if (!model.provider && !model.model) return "unknown";
767
+ return safeDisplayText(`${model.provider ?? "unknown"}/${model.model ?? "unknown"}`);
768
+ }
769
+ function safeDisplayText(value) {
770
+ return Array.from(stripVTControlCharacters(String(value)), (character) => {
771
+ const codePoint = character.codePointAt(0) ?? 0;
772
+ return codePoint <= 31 || codePoint >= 127 && codePoint <= 159 ? " " : character;
773
+ }).join("");
774
+ }
775
+ function isRangeId(value) {
776
+ return value === "today" || value === "7d" || value === "30d" || value === "all";
777
+ }
778
+ function safeErrorMessage(_error) {
779
+ return "The local analytics query could not be completed. Try again; existing data was not changed.";
780
+ }
781
+
782
+ // src/skills.ts
783
+ import { realpath } from "node:fs/promises";
784
+ import path from "node:path";
785
+ function explicitSkillName(text) {
786
+ return text.match(/^\/skill:([a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)(?:\s|$)/u)?.[1];
787
+ }
788
+ var SkillTracker = class {
789
+ constructor(cwd, canonicalize = realpath) {
790
+ this.cwd = cwd;
791
+ this.canonicalize = canonicalize;
792
+ }
793
+ cwd;
794
+ canonicalize;
795
+ pending;
796
+ skillByPath = /* @__PURE__ */ new Map();
797
+ availableNames = /* @__PURE__ */ new Set();
798
+ observeInput(text, source, now) {
799
+ if (source === "extension") return;
800
+ const name = explicitSkillName(text);
801
+ this.pending = name ? { name, observedAtMs: now, source } : void 0;
802
+ }
803
+ consumeExplicitSkill() {
804
+ const pending = this.pending;
805
+ this.pending = void 0;
806
+ return pending;
807
+ }
808
+ clearPending() {
809
+ this.pending = void 0;
810
+ }
811
+ hasAvailableSkill(name) {
812
+ return this.availableNames.has(name);
813
+ }
814
+ async setAvailableSkills(skills) {
815
+ this.skillByPath.clear();
816
+ this.availableNames.clear();
817
+ const seenNames = /* @__PURE__ */ new Set();
818
+ for (const skill of skills) {
819
+ if (seenNames.has(skill.name)) continue;
820
+ seenNames.add(skill.name);
821
+ this.availableNames.add(skill.name);
822
+ const canonical = await this.canonicalize(skill.filePath).catch(
823
+ () => path.resolve(this.cwd, skill.filePath)
824
+ );
825
+ this.skillByPath.set(canonical, skill.name);
826
+ }
827
+ }
828
+ async matchSuccessfulRead(input) {
829
+ if (input.toolName !== "read" || input.isError || !isRecord(input.input)) return void 0;
830
+ const rawPath = input.input.path;
831
+ if (typeof rawPath !== "string" || rawPath.length === 0) return void 0;
832
+ const normalized = rawPath.startsWith("@") ? rawPath.slice(1) : rawPath;
833
+ const absolute = path.resolve(this.cwd, normalized);
834
+ const canonical = await this.canonicalize(absolute).catch(() => absolute);
835
+ return this.skillByPath.get(canonical);
836
+ }
837
+ };
838
+ function isRecord(value) {
839
+ return typeof value === "object" && value !== null && !Array.isArray(value);
840
+ }
841
+
842
+ // src/storage/files.ts
843
+ import { randomUUID as randomUUID2 } from "node:crypto";
844
+ import { createReadStream } from "node:fs";
845
+ import {
846
+ chmod,
847
+ lstat,
848
+ mkdir,
849
+ open,
850
+ readdir,
851
+ readFile,
852
+ rename,
853
+ rm,
854
+ rmdir,
855
+ unlink,
856
+ writeFile
857
+ } from "node:fs/promises";
858
+ import path2 from "node:path";
859
+
860
+ // src/storage/format.ts
861
+ var MAX_STORED_RUN_BYTES = 1024 * 1024;
862
+ var MAX_STRING_LENGTH = 4096;
863
+ var MAX_NESTED_RECORDS = 2e4;
864
+ var TRIGGER_SOURCES = ["interactive", "rpc", "extension", "unknown"];
865
+ var RUN_OUTCOMES = [
866
+ "success",
867
+ "recovered_success",
868
+ "error",
869
+ "aborted",
870
+ "length",
871
+ "interrupted"
872
+ ];
873
+ var GENERATION_OUTCOMES = [
874
+ "pending",
875
+ "stop",
876
+ "tool_use",
877
+ "error",
878
+ "aborted",
879
+ "length",
880
+ "interrupted"
881
+ ];
882
+ var ERROR_CATEGORIES2 = [
883
+ "dns",
884
+ "timeout",
885
+ "connection_refused",
886
+ "connection_reset",
887
+ "tls",
888
+ "network_other",
889
+ "provider_other"
890
+ ];
891
+ var AnalyticsStorageFormatError = class extends Error {
892
+ constructor(message, options = {}) {
893
+ super(message, options);
894
+ this.name = "AnalyticsStorageFormatError";
895
+ }
896
+ };
897
+ function encodeStoredRun(run) {
898
+ const encoded = `${JSON.stringify({
899
+ formatVersion: 1,
900
+ run: parseRun(run, { remaining: MAX_NESTED_RECORDS })
901
+ })}
902
+ `;
903
+ if (Buffer.byteLength(encoded) > MAX_STORED_RUN_BYTES) {
904
+ throw new AnalyticsStorageFormatError("Analytics record is too large to store safely.");
905
+ }
906
+ return encoded;
907
+ }
908
+ function decodeStoredRun(line) {
909
+ if (Buffer.byteLength(line) > MAX_STORED_RUN_BYTES) {
910
+ throw new AnalyticsStorageFormatError("Analytics record is too large to read safely.");
911
+ }
912
+ let value;
913
+ try {
914
+ value = JSON.parse(line);
915
+ } catch (error) {
916
+ throw new AnalyticsStorageFormatError("Analytics record contains invalid JSON.", {
917
+ cause: error
918
+ });
919
+ }
920
+ const envelope = asRecord(value, "analytics record");
921
+ if (envelope.formatVersion !== 1) {
922
+ throw new AnalyticsStorageFormatError("Analytics record uses an unsupported format version.");
923
+ }
924
+ return parseRun(envelope.run, { remaining: MAX_NESTED_RECORDS });
925
+ }
926
+ function parseRun(value, budget) {
927
+ const run = asRecord(value, "run");
928
+ return {
929
+ id: requiredString(run.id, "run.id"),
930
+ startedAtMs: timestampValue(run.startedAtMs, "run.startedAtMs"),
931
+ finishedAtMs: timestampValue(run.finishedAtMs, "run.finishedAtMs"),
932
+ durationMs: durationValue(run.durationMs, "run.durationMs"),
933
+ triggerSource: enumValue(run.triggerSource, TRIGGER_SOURCES, "run.triggerSource"),
934
+ ...optionalProperty(
935
+ "initialProvider",
936
+ optionalString(run.initialProvider, "run.initialProvider")
937
+ ),
938
+ ...optionalProperty("initialModel", optionalString(run.initialModel, "run.initialModel")),
939
+ outcome: enumValue(run.outcome, RUN_OUTCOMES, "run.outcome"),
940
+ attemptCount: boundedCount(run.attemptCount, "run.attemptCount"),
941
+ generations: boundedArray(run.generations, "run.generations", budget).map(
942
+ (item, index) => parseGeneration(item, index, budget)
943
+ ),
944
+ tools: boundedArray(run.tools, "run.tools", budget).map(parseTool),
945
+ skills: boundedArray(run.skills, "run.skills", budget).map(parseSkill),
946
+ providerErrors: boundedArray(run.providerErrors, "run.providerErrors", budget).map(
947
+ parseProviderError
948
+ ),
949
+ toolErrorCount: boundedCount(run.toolErrorCount, "run.toolErrorCount"),
950
+ providerErrorCount: boundedCount(run.providerErrorCount, "run.providerErrorCount"),
951
+ recoveredErrorCount: boundedCount(run.recoveredErrorCount, "run.recoveredErrorCount")
952
+ };
953
+ }
954
+ function parseGeneration(value, index, budget) {
955
+ const item = asRecord(value, `run.generations[${index}]`);
956
+ const prefix = `run.generations[${index}]`;
957
+ return {
958
+ id: requiredString(item.id, `${prefix}.id`),
959
+ ordinal: boundedCount(item.ordinal, `${prefix}.ordinal`),
960
+ ...optionalProperty("provider", optionalString(item.provider, `${prefix}.provider`)),
961
+ ...optionalProperty("model", optionalString(item.model, `${prefix}.model`)),
962
+ ...optionalProperty(
963
+ "thinkingLevel",
964
+ optionalString(item.thinkingLevel, `${prefix}.thinkingLevel`)
965
+ ),
966
+ startedAtMs: timestampValue(item.startedAtMs, `${prefix}.startedAtMs`),
967
+ ...optionalProperty(
968
+ "finishedAtMs",
969
+ optionalTimestamp(item.finishedAtMs, `${prefix}.finishedAtMs`)
970
+ ),
971
+ ...optionalProperty("durationMs", optionalDuration(item.durationMs, `${prefix}.durationMs`)),
972
+ ...optionalProperty("stopReason", optionalString(item.stopReason, `${prefix}.stopReason`)),
973
+ outcome: enumValue(item.outcome, GENERATION_OUTCOMES, `${prefix}.outcome`),
974
+ responses: boundedArray(item.responses, `${prefix}.responses`, budget).map(
975
+ parseProviderResponse
976
+ )
977
+ };
978
+ }
979
+ function parseProviderResponse(value, index) {
980
+ const item = asRecord(value, `provider response ${index}`);
981
+ return {
982
+ ordinal: boundedCount(item.ordinal, "providerResponse.ordinal"),
983
+ occurredAtMs: timestampValue(item.occurredAtMs, "providerResponse.occurredAtMs"),
984
+ status: boundedInteger(item.status, "providerResponse.status", 999)
985
+ };
986
+ }
987
+ function parseTool(value, index) {
988
+ const item = asRecord(value, `run.tools[${index}]`);
989
+ const prefix = `run.tools[${index}]`;
990
+ const ordinal = boundedCount(item.ordinal, `${prefix}.ordinal`);
991
+ return {
992
+ id: `tool-${ordinal}`,
993
+ ordinal,
994
+ name: requiredString(item.name, `${prefix}.name`),
995
+ ...optionalProperty("provider", optionalString(item.provider, `${prefix}.provider`)),
996
+ ...optionalProperty("model", optionalString(item.model, `${prefix}.model`)),
997
+ startedAtMs: timestampValue(item.startedAtMs, `${prefix}.startedAtMs`),
998
+ ...optionalProperty(
999
+ "finishedAtMs",
1000
+ optionalTimestamp(item.finishedAtMs, `${prefix}.finishedAtMs`)
1001
+ ),
1002
+ ...optionalProperty("durationMs", optionalDuration(item.durationMs, `${prefix}.durationMs`)),
1003
+ isError: booleanValue(item.isError, `${prefix}.isError`),
1004
+ completionState: enumValue(
1005
+ item.completionState,
1006
+ ["running", "finished", "interrupted"],
1007
+ `${prefix}.completionState`
1008
+ )
1009
+ };
1010
+ }
1011
+ function parseSkill(value, index) {
1012
+ const item = asRecord(value, `run.skills[${index}]`);
1013
+ const prefix = `run.skills[${index}]`;
1014
+ return {
1015
+ id: requiredString(item.id, `${prefix}.id`),
1016
+ name: requiredString(item.name, `${prefix}.name`),
1017
+ initiatedBy: enumValue(item.initiatedBy, ["user", "model"], `${prefix}.initiatedBy`),
1018
+ occurredAtMs: timestampValue(item.occurredAtMs, `${prefix}.occurredAtMs`),
1019
+ ...optionalProperty("provider", optionalString(item.provider, `${prefix}.provider`)),
1020
+ ...optionalProperty("model", optionalString(item.model, `${prefix}.model`))
1021
+ };
1022
+ }
1023
+ function parseProviderError(value, index) {
1024
+ const item = asRecord(value, `run.providerErrors[${index}]`);
1025
+ const prefix = `run.providerErrors[${index}]`;
1026
+ return {
1027
+ id: requiredString(item.id, `${prefix}.id`),
1028
+ ...optionalProperty(
1029
+ "generationId",
1030
+ optionalString(item.generationId, `${prefix}.generationId`)
1031
+ ),
1032
+ occurredAtMs: timestampValue(item.occurredAtMs, `${prefix}.occurredAtMs`),
1033
+ ...optionalProperty("provider", optionalString(item.provider, `${prefix}.provider`)),
1034
+ ...optionalProperty("model", optionalString(item.model, `${prefix}.model`)),
1035
+ category: enumValue(
1036
+ item.category,
1037
+ ERROR_CATEGORIES2,
1038
+ `${prefix}.category`
1039
+ ),
1040
+ recovered: booleanValue(item.recovered, `${prefix}.recovered`),
1041
+ terminal: booleanValue(item.terminal, `${prefix}.terminal`)
1042
+ };
1043
+ }
1044
+ function asRecord(value, name) {
1045
+ if (typeof value !== "object" || value === null || Array.isArray(value)) invalid(name);
1046
+ return value;
1047
+ }
1048
+ function boundedArray(value, name, budget) {
1049
+ if (!Array.isArray(value)) invalid(name);
1050
+ budget.remaining -= value.length;
1051
+ if (budget.remaining < 0) {
1052
+ throw new AnalyticsStorageFormatError("Analytics record is too large to process safely.");
1053
+ }
1054
+ return value;
1055
+ }
1056
+ function requiredString(value, name) {
1057
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_STRING_LENGTH) {
1058
+ invalid(name);
1059
+ }
1060
+ return value;
1061
+ }
1062
+ function optionalString(value, name) {
1063
+ return value === void 0 ? void 0 : requiredString(value, name);
1064
+ }
1065
+ function boundedInteger(value, name, maximum) {
1066
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > maximum) {
1067
+ invalid(name);
1068
+ }
1069
+ return value;
1070
+ }
1071
+ function timestampValue(value, name) {
1072
+ return boundedInteger(value, name, Number.MAX_SAFE_INTEGER);
1073
+ }
1074
+ function optionalTimestamp(value, name) {
1075
+ return value === void 0 ? void 0 : timestampValue(value, name);
1076
+ }
1077
+ function durationValue(value, name) {
1078
+ return boundedInteger(value, name, Math.floor(Number.MAX_SAFE_INTEGER / MAX_NESTED_RECORDS));
1079
+ }
1080
+ function optionalDuration(value, name) {
1081
+ return value === void 0 ? void 0 : durationValue(value, name);
1082
+ }
1083
+ function boundedCount(value, name) {
1084
+ return boundedInteger(value, name, MAX_NESTED_RECORDS);
1085
+ }
1086
+ function booleanValue(value, name) {
1087
+ if (typeof value !== "boolean") invalid(name);
1088
+ return value;
1089
+ }
1090
+ function enumValue(value, values, name) {
1091
+ if (typeof value !== "string" || !values.includes(value)) invalid(name);
1092
+ return value;
1093
+ }
1094
+ function optionalProperty(key, value) {
1095
+ return value === void 0 ? {} : { [key]: value };
1096
+ }
1097
+ function invalid(name) {
1098
+ throw new AnalyticsStorageFormatError(`Analytics record has an invalid ${name}.`);
1099
+ }
1100
+
1101
+ // src/storage/files.ts
1102
+ var GENERATION_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
1103
+ var DEFAULT_WRITE_TIMEOUT_MS = 5e3;
1104
+ var YIELD_EVERY_RECORDS = 100;
1105
+ var AnalyticsGenerationChangedError = class extends Error {
1106
+ constructor() {
1107
+ super("The active analytics generation changed during the read.");
1108
+ this.name = "AnalyticsGenerationChangedError";
1109
+ }
1110
+ };
1111
+ var AnalyticsRunFiles = class {
1112
+ constructor(path4, options = {}) {
1113
+ this.path = path4;
1114
+ this.createId = options.createId ?? randomUUID2;
1115
+ this.writeTimeoutMs = options.writeTimeoutMs ?? DEFAULT_WRITE_TIMEOUT_MS;
1116
+ this.beforeAppend = options.beforeAppend;
1117
+ this.beforeCleanupEntry = options.beforeCleanupEntry;
1118
+ this.beforeReadFile = options.beforeReadFile;
1119
+ this.writerId = validCreatedId(this.createId());
1120
+ }
1121
+ path;
1122
+ createId;
1123
+ writeTimeoutMs;
1124
+ beforeAppend;
1125
+ beforeCleanupEntry;
1126
+ beforeReadFile;
1127
+ writerId;
1128
+ mutationTail = Promise.resolve();
1129
+ lifecycle = new AbortController();
1130
+ closed = false;
1131
+ append(run, signal) {
1132
+ if (this.closed) return Promise.reject(new Error("Analytics storage is closed."));
1133
+ const frame = encodeStoredRun(run);
1134
+ return this.enqueueMutation(
1135
+ () => withDeadline(
1136
+ signal,
1137
+ this.lifecycle.signal,
1138
+ this.writeTimeoutMs,
1139
+ (operationSignal) => this.appendFrame(frame, operationSignal)
1140
+ )
1141
+ );
1142
+ }
1143
+ async *read(signal) {
1144
+ throwIfAborted2(signal);
1145
+ const generation = await this.readOrCreateGeneration(signal);
1146
+ const directory = this.generationPath(generation);
1147
+ let entries;
1148
+ try {
1149
+ entries = await readdir(directory, { withFileTypes: true });
1150
+ } catch (error) {
1151
+ if (isNodeError(error) && error.code === "ENOENT") {
1152
+ await this.assertGenerationUnchanged(generation, signal);
1153
+ throw new AnalyticsGenerationChangedError();
1154
+ }
1155
+ throw error;
1156
+ }
1157
+ let count = 0;
1158
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
1159
+ throwIfAborted2(signal);
1160
+ if (!entry.name.endsWith(".jsonl")) continue;
1161
+ const filePath = path2.join(directory, entry.name);
1162
+ try {
1163
+ await this.beforeReadFile?.(signal);
1164
+ throwIfAborted2(signal);
1165
+ await assertPrivateRegularFile(filePath);
1166
+ for await (const run of readFrames(filePath, signal)) {
1167
+ yield run;
1168
+ count += 1;
1169
+ if (count % YIELD_EVERY_RECORDS === 0) await yieldToEventLoop(signal);
1170
+ }
1171
+ } catch (error) {
1172
+ if (isNodeError(error) && error.code === "ENOENT") {
1173
+ await this.assertGenerationUnchanged(generation, signal);
1174
+ throw new AnalyticsGenerationChangedError();
1175
+ }
1176
+ throw error;
1177
+ }
1178
+ }
1179
+ await this.assertGenerationUnchanged(generation, signal);
1180
+ }
1181
+ clear(signal) {
1182
+ if (this.closed) return Promise.reject(new Error("Analytics storage is closed."));
1183
+ let result = { cleanupIncomplete: false };
1184
+ return this.enqueueMutation(
1185
+ () => withLinkedSignals(signal, this.lifecycle.signal, async (operationSignal) => {
1186
+ throwIfAborted2(operationSignal);
1187
+ await this.readOrCreateGeneration(operationSignal);
1188
+ const next = validCreatedId(this.createId());
1189
+ await this.publishGeneration(next);
1190
+ this.writerId = validCreatedId(this.createId());
1191
+ const current = await readGenerationMarker(path2.join(this.path, "current"));
1192
+ await ensurePrivateDirectory(this.generationPath(current));
1193
+ if (!await this.cleanupObsoleteGenerations(operationSignal)) {
1194
+ result = { cleanupIncomplete: true };
1195
+ }
1196
+ })
1197
+ ).then(() => result);
1198
+ }
1199
+ async close() {
1200
+ if (this.closed) return;
1201
+ this.closed = true;
1202
+ this.lifecycle.abort(new DOMException("Analytics storage closed", "AbortError"));
1203
+ await this.mutationTail.catch(() => void 0);
1204
+ }
1205
+ enqueueMutation(operation) {
1206
+ const result = this.mutationTail.then(operation);
1207
+ this.mutationTail = result.catch(() => void 0);
1208
+ return result;
1209
+ }
1210
+ async appendFrame(frame, signal) {
1211
+ throwIfAborted2(signal);
1212
+ let obsoleteDirectory;
1213
+ for (let attempt = 0; attempt < 3; attempt += 1) {
1214
+ const generation = await this.readOrCreateGeneration(signal);
1215
+ const directory = this.generationPath(generation);
1216
+ await ensurePrivateDirectory(directory);
1217
+ const filePath = path2.join(directory, `${this.writerId}.jsonl`);
1218
+ await assertOptionalPrivateRegularFile(filePath);
1219
+ throwIfAborted2(signal);
1220
+ await this.beforeAppend?.(generation, signal);
1221
+ throwIfAborted2(signal);
1222
+ try {
1223
+ await writeFile(filePath, frame, {
1224
+ encoding: "utf8",
1225
+ flag: "a",
1226
+ mode: 384,
1227
+ signal
1228
+ });
1229
+ if (process.platform !== "win32") await chmod(filePath, 384);
1230
+ const current = await readGenerationMarker(path2.join(this.path, "current"), signal);
1231
+ if (current === generation) {
1232
+ if (obsoleteDirectory) {
1233
+ await cleanupGeneration(obsoleteDirectory, signal).catch(() => void 0);
1234
+ }
1235
+ return;
1236
+ }
1237
+ obsoleteDirectory = directory;
1238
+ } catch (error) {
1239
+ this.writerId = validCreatedId(this.createId());
1240
+ throwIfAborted2(signal);
1241
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
1242
+ }
1243
+ }
1244
+ throw new AnalyticsGenerationChangedError();
1245
+ }
1246
+ async readOrCreateGeneration(signal) {
1247
+ throwIfAborted2(signal);
1248
+ await ensurePrivateDirectory(this.path);
1249
+ await ensurePrivateDirectory(path2.join(this.path, "generations"));
1250
+ const markerPath = path2.join(this.path, "current");
1251
+ for (let attempt = 0; attempt < 3; attempt += 1) {
1252
+ let generation;
1253
+ try {
1254
+ generation = await readGenerationMarker(markerPath, signal);
1255
+ } catch (error) {
1256
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
1257
+ generation = validCreatedId(this.createId());
1258
+ throwIfAborted2(signal);
1259
+ try {
1260
+ await createPrivateFile(markerPath, `${generation}
1261
+ `);
1262
+ } catch (createError) {
1263
+ if (!isNodeError(createError) || createError.code !== "EEXIST") throw createError;
1264
+ generation = await readGenerationMarker(markerPath, signal);
1265
+ }
1266
+ }
1267
+ await ensurePrivateDirectory(this.generationPath(generation));
1268
+ const current = await readGenerationMarker(markerPath, signal);
1269
+ if (current === generation) return generation;
1270
+ }
1271
+ throw new AnalyticsGenerationChangedError();
1272
+ }
1273
+ async publishGeneration(generation) {
1274
+ const markerPath = path2.join(this.path, "current");
1275
+ const temporaryPath = path2.join(this.path, `.current.${validCreatedId(this.createId())}.tmp`);
1276
+ await createPrivateFile(temporaryPath, `${generation}
1277
+ `);
1278
+ try {
1279
+ await rename(temporaryPath, markerPath);
1280
+ if (process.platform !== "win32") await chmod(markerPath, 384);
1281
+ } catch (error) {
1282
+ await rm(temporaryPath, { force: true }).catch(() => void 0);
1283
+ throw error;
1284
+ }
1285
+ }
1286
+ async cleanupObsoleteGenerations(signal) {
1287
+ let complete = true;
1288
+ const root = path2.join(this.path, "generations");
1289
+ for (const entry of await readdir(root, { withFileTypes: true })) {
1290
+ const active2 = await readGenerationMarker(path2.join(this.path, "current"));
1291
+ if (entry.name === active2) continue;
1292
+ if (!entry.isDirectory() || !GENERATION_PATTERN.test(entry.name)) {
1293
+ complete = false;
1294
+ continue;
1295
+ }
1296
+ try {
1297
+ await cleanupGeneration(path2.join(root, entry.name), signal, this.beforeCleanupEntry);
1298
+ } catch {
1299
+ complete = false;
1300
+ if (signal?.aborted) break;
1301
+ }
1302
+ }
1303
+ const active = await readGenerationMarker(path2.join(this.path, "current"));
1304
+ const remaining = await readdir(root, { withFileTypes: true });
1305
+ return complete && remaining.every((entry) => entry.isDirectory() && entry.name === active);
1306
+ }
1307
+ async assertGenerationUnchanged(generation, signal) {
1308
+ const current = await readGenerationMarker(path2.join(this.path, "current"), signal);
1309
+ if (current !== generation) throw new AnalyticsGenerationChangedError();
1310
+ }
1311
+ generationPath(generation) {
1312
+ return path2.join(this.path, "generations", generation);
1313
+ }
1314
+ };
1315
+ async function* readFrames(filePath, signal) {
1316
+ let pending = "";
1317
+ const stream = createReadStream(filePath, { encoding: "utf8", signal });
1318
+ for await (const chunk of stream) {
1319
+ throwIfAborted2(signal);
1320
+ pending += String(chunk);
1321
+ while (true) {
1322
+ const newline = pending.indexOf("\n");
1323
+ if (newline < 0) break;
1324
+ const line = pending.slice(0, newline);
1325
+ pending = pending.slice(newline + 1);
1326
+ if (!line) continue;
1327
+ yield decodeStoredRun(line);
1328
+ }
1329
+ if (Buffer.byteLength(pending) > MAX_STORED_RUN_BYTES) {
1330
+ throw new AnalyticsStorageFormatError("Analytics record is too large to read safely.");
1331
+ }
1332
+ }
1333
+ }
1334
+ async function readGenerationMarker(markerPath, signal) {
1335
+ await assertPrivateRegularFile(markerPath);
1336
+ throwIfAborted2(signal);
1337
+ const value = (await readFile(markerPath, { encoding: "utf8", signal })).trim();
1338
+ if (!GENERATION_PATTERN.test(value)) {
1339
+ throw new AnalyticsStorageFormatError("Analytics generation marker is invalid.");
1340
+ }
1341
+ return value;
1342
+ }
1343
+ async function ensurePrivateDirectory(directoryPath) {
1344
+ try {
1345
+ const metadata = await lstat(directoryPath);
1346
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
1347
+ throw new Error("Analytics storage paths must be regular directories, not links.");
1348
+ }
1349
+ } catch (error) {
1350
+ if (!isNodeError(error) || error.code !== "ENOENT") throw error;
1351
+ await mkdir(directoryPath, { recursive: true, mode: 448 });
1352
+ const metadata = await lstat(directoryPath);
1353
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
1354
+ throw new Error("Analytics storage paths must be regular directories, not links.");
1355
+ }
1356
+ }
1357
+ if (process.platform !== "win32") await chmod(directoryPath, 448);
1358
+ }
1359
+ async function assertOptionalPrivateRegularFile(filePath) {
1360
+ try {
1361
+ await assertPrivateRegularFile(filePath);
1362
+ } catch (error) {
1363
+ if (isNodeError(error) && error.code === "ENOENT") return;
1364
+ throw error;
1365
+ }
1366
+ }
1367
+ async function assertPrivateRegularFile(filePath) {
1368
+ const metadata = await lstat(filePath);
1369
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
1370
+ throw new Error("Analytics storage files must be regular files, not links.");
1371
+ }
1372
+ if (process.platform !== "win32") await chmod(filePath, 384);
1373
+ }
1374
+ async function createPrivateFile(filePath, content) {
1375
+ const handle = await open(filePath, "wx", 384);
1376
+ try {
1377
+ await handle.writeFile(content, "utf8");
1378
+ await handle.sync();
1379
+ } finally {
1380
+ await handle.close();
1381
+ }
1382
+ }
1383
+ async function cleanupGeneration(directoryPath, signal, beforeEntry) {
1384
+ throwIfAborted2(signal);
1385
+ let entries;
1386
+ try {
1387
+ entries = await readdir(directoryPath, { withFileTypes: true });
1388
+ } catch (error) {
1389
+ if (isNodeError(error) && error.code === "ENOENT") return;
1390
+ throw error;
1391
+ }
1392
+ for (const entry of entries) {
1393
+ throwIfAborted2(signal);
1394
+ await beforeEntry?.(signal);
1395
+ throwIfAborted2(signal);
1396
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
1397
+ throw new Error("Analytics generation contains an unexpected storage entry.");
1398
+ }
1399
+ await unlink(path2.join(directoryPath, entry.name));
1400
+ }
1401
+ throwIfAborted2(signal);
1402
+ await rmdir(directoryPath);
1403
+ }
1404
+ async function withLinkedSignals(callerSignal, lifecycleSignal, operation) {
1405
+ throwIfAborted2(callerSignal);
1406
+ throwIfAborted2(lifecycleSignal);
1407
+ const controller = new AbortController();
1408
+ const abort = (signal) => controller.abort(
1409
+ signal.reason ?? new DOMException("Analytics operation aborted", "AbortError")
1410
+ );
1411
+ const callerAbort = () => callerSignal && abort(callerSignal);
1412
+ const lifecycleAbort = () => abort(lifecycleSignal);
1413
+ callerSignal?.addEventListener("abort", callerAbort, { once: true });
1414
+ lifecycleSignal.addEventListener("abort", lifecycleAbort, { once: true });
1415
+ try {
1416
+ return await operation(controller.signal);
1417
+ } finally {
1418
+ callerSignal?.removeEventListener("abort", callerAbort);
1419
+ lifecycleSignal.removeEventListener("abort", lifecycleAbort);
1420
+ }
1421
+ }
1422
+ async function withDeadline(callerSignal, lifecycleSignal, timeoutMs, operation) {
1423
+ throwIfAborted2(callerSignal);
1424
+ throwIfAborted2(lifecycleSignal);
1425
+ const controller = new AbortController();
1426
+ const abort = (signal) => controller.abort(
1427
+ signal.reason ?? new DOMException("Analytics operation aborted", "AbortError")
1428
+ );
1429
+ const callerAbort = () => callerSignal && abort(callerSignal);
1430
+ const lifecycleAbort = () => abort(lifecycleSignal);
1431
+ callerSignal?.addEventListener("abort", callerAbort, { once: true });
1432
+ lifecycleSignal.addEventListener("abort", lifecycleAbort, { once: true });
1433
+ const timer = setTimeout(
1434
+ () => controller.abort(new DOMException("Analytics write timed out", "TimeoutError")),
1435
+ Math.max(1, timeoutMs)
1436
+ );
1437
+ try {
1438
+ return await operation(controller.signal);
1439
+ } catch (error) {
1440
+ if (controller.signal.aborted && error instanceof Error && error.name === "AbortError" && error.cause === controller.signal.reason) {
1441
+ throw controller.signal.reason;
1442
+ }
1443
+ throw error;
1444
+ } finally {
1445
+ clearTimeout(timer);
1446
+ callerSignal?.removeEventListener("abort", callerAbort);
1447
+ lifecycleSignal.removeEventListener("abort", lifecycleAbort);
1448
+ }
1449
+ }
1450
+ async function yieldToEventLoop(signal) {
1451
+ await new Promise((resolve) => setImmediate(resolve));
1452
+ throwIfAborted2(signal);
1453
+ }
1454
+ function validCreatedId(value) {
1455
+ if (!GENERATION_PATTERN.test(value)) throw new Error("Analytics storage received an invalid ID.");
1456
+ return value;
1457
+ }
1458
+ function throwIfAborted2(signal) {
1459
+ if (signal?.aborted) {
1460
+ throw signal.reason ?? new DOMException("Analytics operation aborted", "AbortError");
1461
+ }
1462
+ }
1463
+ function isNodeError(error) {
1464
+ return error instanceof Error && "code" in error;
1465
+ }
1466
+
1467
+ // src/storage/store.ts
1468
+ var AnalyticsStore = class {
1469
+ files;
1470
+ constructor(rootPath, dependencies = {}) {
1471
+ this.files = dependencies.files ?? new AnalyticsRunFiles(rootPath, {
1472
+ createId: dependencies.createId,
1473
+ writeTimeoutMs: dependencies.writeTimeoutMs
1474
+ });
1475
+ }
1476
+ get path() {
1477
+ return this.files.path;
1478
+ }
1479
+ recordRun(run, signal) {
1480
+ return this.files.append(run, signal);
1481
+ }
1482
+ async getSnapshot(range, signal) {
1483
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1484
+ try {
1485
+ return await querySnapshot(this.files.read(signal), range, signal);
1486
+ } catch (error) {
1487
+ if (!(error instanceof AnalyticsGenerationChangedError) || attempt > 0) throw error;
1488
+ }
1489
+ }
1490
+ throw new AnalyticsGenerationChangedError();
1491
+ }
1492
+ clearAll(signal) {
1493
+ return this.files.clear(signal);
1494
+ }
1495
+ close() {
1496
+ return this.files.close();
1497
+ }
1498
+ };
1499
+
1500
+ // src/analytics.ts
1501
+ var EXPERIMENTAL_WARNING = "pi-analytics is experimental; its metrics and dashboard may change.";
1502
+ var STORAGE_DIRECTORY = "pi-analytics";
1503
+ function createAnalyticsExtension(dependencies = {}) {
1504
+ const deps = {
1505
+ createStore: dependencies.createStore ?? ((rootPath) => new AnalyticsStore(rootPath)),
1506
+ createSkillTracker: dependencies.createSkillTracker ?? ((cwd) => new SkillTracker(cwd)),
1507
+ getAgentDir: dependencies.getAgentDir ?? getAgentDir,
1508
+ now: dependencies.now ?? Date.now,
1509
+ createId: dependencies.createId ?? randomUUID3
1510
+ };
1511
+ return function analyticsExtension(pi) {
1512
+ let sessionGeneration = 0;
1513
+ let sessionController = new AbortController();
1514
+ let collector = new ResponseCollector();
1515
+ let skillTracker;
1516
+ let store;
1517
+ let storageFailure;
1518
+ const retiredCloseTasks = /* @__PURE__ */ new Set();
1519
+ let writeFailureActive = false;
1520
+ let pendingTriggerSource = "unknown";
1521
+ let pendingAttemptWithoutRun = false;
1522
+ pi.registerCommand("analytics", {
1523
+ description: "Open local Pi usage analytics",
1524
+ handler: async (args, ctx) => {
1525
+ if (args.trim()) {
1526
+ if (!ctx.hasUI || ctx.mode !== "tui" && ctx.mode !== "rpc") {
1527
+ throw new Error("/analytics does not accept arguments.");
1528
+ }
1529
+ ctx.ui.notify("/analytics does not accept arguments.", "warning");
1530
+ return;
1531
+ }
1532
+ if (!ctx.hasUI || ctx.mode !== "tui" && ctx.mode !== "rpc") {
1533
+ throw new Error("/analytics requires Pi TUI or RPC mode.");
1534
+ }
1535
+ const generation = sessionGeneration;
1536
+ const owner = sessionController;
1537
+ const source = menuSource(generation, owner.signal);
1538
+ await showAnalyticsMenu(ctx, source, {
1539
+ signal: owner.signal,
1540
+ isCurrent: () => generation === sessionGeneration && !owner.signal.aborted
1541
+ });
1542
+ }
1543
+ });
1544
+ pi.on("session_start", (_event, ctx) => {
1545
+ ++sessionGeneration;
1546
+ if (ctx.hasUI) ctx.ui.notify(EXPERIMENTAL_WARNING, "warning");
1547
+ const previousStore = store;
1548
+ sessionController.abort(new DOMException("Analytics session replaced", "AbortError"));
1549
+ if (previousStore) retire(previousStore);
1550
+ sessionController = new AbortController();
1551
+ collector = new ResponseCollector();
1552
+ skillTracker = deps.createSkillTracker(ctx.cwd);
1553
+ store = void 0;
1554
+ storageFailure = void 0;
1555
+ writeFailureActive = false;
1556
+ pendingTriggerSource = "unknown";
1557
+ pendingAttemptWithoutRun = false;
1558
+ const storageRoot = path3.join(deps.getAgentDir(), STORAGE_DIRECTORY);
1559
+ try {
1560
+ store = deps.createStore(storageRoot);
1561
+ } catch {
1562
+ storageFailure = unavailableMessage();
1563
+ safeNotify(ctx, storageFailure, "warning");
1564
+ }
1565
+ });
1566
+ pi.on("input", (event, ctx) => {
1567
+ const now = deps.now();
1568
+ const tracker = skillTracker;
1569
+ tracker?.observeInput(event.text, event.source, now);
1570
+ if (event.source !== "extension") pendingTriggerSource = event.source;
1571
+ if (!tracker || !collector.hasActiveRun()) return;
1572
+ const explicit = tracker.consumeExplicitSkill();
1573
+ if (!explicit || !tracker.hasAvailableSkill(explicit.name)) return;
1574
+ collector.activateSkill({
1575
+ name: explicit.name,
1576
+ initiatedBy: "user",
1577
+ now: explicit.observedAtMs,
1578
+ model: modelIdentity(ctx, pi)
1579
+ });
1580
+ });
1581
+ pi.on("before_agent_start", async (event, ctx) => {
1582
+ const generation = sessionGeneration;
1583
+ const tracker = skillTracker;
1584
+ const activeCollector = collector;
1585
+ if (!tracker) return;
1586
+ const skills = availableSkills(pi, event.systemPromptOptions.skills ?? []);
1587
+ await tracker.setAvailableSkills(skills);
1588
+ if (generation !== sessionGeneration || tracker !== skillTracker || activeCollector !== collector) {
1589
+ return;
1590
+ }
1591
+ const explicit = tracker.consumeExplicitSkill();
1592
+ const interrupted = activeCollector.begin({
1593
+ id: deps.createId(),
1594
+ now: deps.now(),
1595
+ triggerSource: explicit?.source ?? pendingTriggerSource,
1596
+ model: modelIdentity(ctx, pi)
1597
+ });
1598
+ pendingTriggerSource = "unknown";
1599
+ if (interrupted) {
1600
+ await persistRun(interrupted, ctx, generation, sessionController.signal);
1601
+ if (generation !== sessionGeneration || tracker !== skillTracker || activeCollector !== collector) {
1602
+ return;
1603
+ }
1604
+ }
1605
+ if (explicit && skills.some(({ name }) => name === explicit.name)) {
1606
+ activeCollector.activateSkill({
1607
+ name: explicit.name,
1608
+ initiatedBy: "user",
1609
+ now: explicit.observedAtMs,
1610
+ model: modelIdentity(ctx, pi)
1611
+ });
1612
+ }
1613
+ });
1614
+ pi.on("agent_start", () => {
1615
+ if (collector.hasActiveRun()) collector.beginAttempt();
1616
+ else pendingAttemptWithoutRun = true;
1617
+ });
1618
+ pi.on("turn_start", (_event, ctx) => ensureRun(ctx, "extension"));
1619
+ pi.on("before_provider_request", (_event, ctx) => {
1620
+ ensureRun(ctx, "extension");
1621
+ collector.beginGeneration({
1622
+ id: deps.createId(),
1623
+ now: deps.now(),
1624
+ model: modelIdentity(ctx, pi)
1625
+ });
1626
+ });
1627
+ pi.on("after_provider_response", (event) => {
1628
+ collector.recordProviderResponse({ status: event.status, now: deps.now() });
1629
+ });
1630
+ pi.on("message_end", (event) => {
1631
+ if (event.message.role !== "assistant") return;
1632
+ collector.finishGeneration({
1633
+ now: deps.now(),
1634
+ stopReason: event.message.stopReason,
1635
+ errorMessage: event.message.errorMessage
1636
+ });
1637
+ });
1638
+ pi.on("tool_execution_start", (event, ctx) => {
1639
+ ensureRun(ctx, "extension");
1640
+ collector.beginTool({
1641
+ id: event.toolCallId,
1642
+ name: event.toolName,
1643
+ now: deps.now(),
1644
+ model: modelIdentity(ctx, pi)
1645
+ });
1646
+ });
1647
+ pi.on("tool_result", async (event, ctx) => {
1648
+ if (event.toolName === "read" && !isBuiltinReadTool(pi)) return;
1649
+ const generation = sessionGeneration;
1650
+ const tracker = skillTracker;
1651
+ const activeCollector = collector;
1652
+ const runId = activeCollector.getActiveRunId();
1653
+ if (!tracker || !runId) return;
1654
+ const name = await tracker.matchSuccessfulRead({
1655
+ toolName: event.toolName,
1656
+ input: event.input,
1657
+ isError: event.isError
1658
+ });
1659
+ if (!name || generation !== sessionGeneration || tracker !== skillTracker || activeCollector !== collector || activeCollector.getActiveRunId() !== runId) {
1660
+ return;
1661
+ }
1662
+ activeCollector.activateSkill({
1663
+ name,
1664
+ initiatedBy: "model",
1665
+ now: deps.now(),
1666
+ model: modelIdentity(ctx, pi)
1667
+ });
1668
+ });
1669
+ pi.on("tool_execution_end", (event) => {
1670
+ collector.finishTool({ id: event.toolCallId, now: deps.now(), isError: event.isError });
1671
+ });
1672
+ pi.on("agent_settled", async (_event, ctx) => {
1673
+ const generation = sessionGeneration;
1674
+ const owner = sessionController;
1675
+ const run = collector.settle(deps.now());
1676
+ pendingAttemptWithoutRun = false;
1677
+ pendingTriggerSource = "unknown";
1678
+ skillTracker?.clearPending();
1679
+ if (run) await persistRun(run, ctx, generation, owner.signal);
1680
+ });
1681
+ pi.on("session_shutdown", async (_event, ctx) => {
1682
+ const activeStore = store;
1683
+ ++sessionGeneration;
1684
+ sessionController.abort(new DOMException("Analytics session shut down", "AbortError"));
1685
+ skillTracker?.clearPending();
1686
+ skillTracker = void 0;
1687
+ store = void 0;
1688
+ collector.interrupt(deps.now());
1689
+ const closing = activeStore ? [closeResult(activeStore), ...retiredCloseTasks] : [...retiredCloseTasks];
1690
+ const results = await Promise.all(closing);
1691
+ if (results.some((closed) => !closed)) {
1692
+ safeNotify(ctx, "Analytics storage shutdown was incomplete.", "warning");
1693
+ }
1694
+ });
1695
+ function retire(retiredStore) {
1696
+ const task = closeResult(retiredStore).finally(() => retiredCloseTasks.delete(task));
1697
+ retiredCloseTasks.add(task);
1698
+ }
1699
+ async function closeResult(activeStore) {
1700
+ try {
1701
+ await activeStore.close();
1702
+ return true;
1703
+ } catch {
1704
+ return false;
1705
+ }
1706
+ }
1707
+ function ensureRun(ctx, triggerSource) {
1708
+ if (collector.hasActiveRun()) return;
1709
+ collector.begin({
1710
+ id: deps.createId(),
1711
+ now: deps.now(),
1712
+ triggerSource,
1713
+ model: modelIdentity(ctx, pi)
1714
+ });
1715
+ if (pendingAttemptWithoutRun) {
1716
+ pendingAttemptWithoutRun = false;
1717
+ collector.beginAttempt();
1718
+ }
1719
+ }
1720
+ async function persistRun(run, ctx, generation, signal) {
1721
+ const activeStore = store;
1722
+ if (!activeStore || signal.aborted) return;
1723
+ try {
1724
+ await activeStore.recordRun(run, signal);
1725
+ if (generation !== sessionGeneration || activeStore !== store || signal.aborted) return;
1726
+ if (writeFailureActive) {
1727
+ writeFailureActive = false;
1728
+ safeNotify(ctx, "Local analytics storage recovered.", "info");
1729
+ }
1730
+ } catch {
1731
+ if (generation !== sessionGeneration || activeStore !== store || signal.aborted || writeFailureActive) {
1732
+ return;
1733
+ }
1734
+ writeFailureActive = true;
1735
+ safeNotify(
1736
+ ctx,
1737
+ "Analytics could not save this response cycle; its metrics were dropped.",
1738
+ "warning"
1739
+ );
1740
+ }
1741
+ }
1742
+ function menuSource(generation, signal) {
1743
+ return {
1744
+ path: store?.path ?? path3.join(deps.getAgentDir(), STORAGE_DIRECTORY),
1745
+ async load(range, actionSignal) {
1746
+ assertCurrent(generation, signal);
1747
+ const activeStore = store;
1748
+ if (!activeStore) {
1749
+ return { kind: "unavailable", message: storageFailure ?? unavailableMessage() };
1750
+ }
1751
+ const snapshot = await activeStore.getSnapshot(range, actionSignal);
1752
+ assertCurrent(generation, signal);
1753
+ return { kind: "ready", snapshot };
1754
+ },
1755
+ async clearAll(actionSignal) {
1756
+ assertCurrent(generation, signal);
1757
+ const activeStore = store;
1758
+ if (!activeStore) return { cleanupIncomplete: false };
1759
+ return activeStore.clearAll(actionSignal);
1760
+ }
1761
+ };
1762
+ }
1763
+ function assertCurrent(generation, signal) {
1764
+ if (generation !== sessionGeneration || signal.aborted) {
1765
+ throw new DOMException("Analytics interaction replaced", "AbortError");
1766
+ }
1767
+ }
1768
+ };
1769
+ }
1770
+ function modelIdentity(ctx, pi) {
1771
+ if (!ctx.model) return void 0;
1772
+ return {
1773
+ provider: ctx.model.provider,
1774
+ model: ctx.model.id,
1775
+ thinkingLevel: pi.getThinkingLevel()
1776
+ };
1777
+ }
1778
+ function isBuiltinReadTool(pi) {
1779
+ const read = pi.getAllTools().find(({ name }) => name === "read");
1780
+ return read?.sourceInfo.source === "builtin";
1781
+ }
1782
+ function availableSkills(pi, systemSkills) {
1783
+ const result = [...systemSkills];
1784
+ const seen = new Set(result.map(({ name }) => name));
1785
+ const getCommands = pi.getCommands;
1786
+ for (const command of typeof getCommands === "function" ? getCommands.call(pi) : []) {
1787
+ if (command.source !== "skill" || seen.has(command.name.replace(/^skill:/u, ""))) continue;
1788
+ const name = command.name.replace(/^skill:/u, "");
1789
+ seen.add(name);
1790
+ result.push({ name, filePath: command.sourceInfo.path });
1791
+ }
1792
+ return result;
1793
+ }
1794
+ function unavailableMessage() {
1795
+ return [
1796
+ "Local analytics storage could not be initialized safely.",
1797
+ "Existing files were not replaced.",
1798
+ "No analytics are being collected."
1799
+ ].join("\n");
1800
+ }
1801
+ function safeNotify(ctx, message, level) {
1802
+ try {
1803
+ ctx.ui.notify(message, level);
1804
+ } catch {
1805
+ }
1806
+ }
1807
+ var analytics_default = createAnalyticsExtension();
1808
+ export {
1809
+ analytics_default as default
1810
+ };
1811
+ //# sourceMappingURL=index.ts.map