// @generated by scripts/build-runtime.mjs; do not edit. // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader. // src/analytics.ts import { randomUUID as randomUUID3 } from "node:crypto"; import path3 from "node:path"; import { getAgentDir } from "@earendil-works/pi-coding-agent"; // src/collector.ts import { randomUUID } from "node:crypto"; // src/errors.ts function classifyProviderError(message) { const value = message?.toLowerCase() ?? ""; if (/\b(enotfound|eai_again|dns|getaddrinfo)\b/u.test(value)) return "dns"; if (/\b(etimedout|timeout|timed out)\b/u.test(value)) return "timeout"; if (/\b(econnrefused|connection refused)\b/u.test(value)) return "connection_refused"; if (/\b(econnreset|connection reset|socket hang up)\b/u.test(value)) { return "connection_reset"; } if (/\b(tls|ssl|certificate|cert_|handshake)\b/u.test(value)) return "tls"; if (/\b(fetch failed|network|socket|connection|transport)\b/u.test(value)) { return "network_other"; } return "provider_other"; } // src/collector.ts var ResponseCollector = class { active; hasActiveRun() { return this.active !== void 0; } getActiveRunId() { return this.active?.id; } begin(input) { const interrupted = this.active ? this.finalize(input.now, "interrupted") : void 0; this.active = { id: input.id, startedAtMs: input.now, triggerSource: input.triggerSource, initialModel: input.model, attemptCount: 0, generations: [], generationIds: /* @__PURE__ */ new Set(), tools: [], toolIds: /* @__PURE__ */ new Set(), skills: [], skillIndexes: /* @__PURE__ */ new Map(), providerErrors: [] }; return interrupted; } beginAttempt() { if (this.active) this.active.attemptCount += 1; } beginGeneration(input) { const active = this.active; if (!active || active.generationIds.has(input.id)) return; active.generationIds.add(input.id); active.generations.push({ id: input.id, ordinal: active.generations.length, provider: input.model?.provider, model: input.model?.model, thinkingLevel: input.model?.thinkingLevel, startedAtMs: input.now, outcome: "pending", responses: [] }); } recordProviderResponse(input) { const generation = this.latestGeneration(); if (generation?.outcome !== "pending") return; generation.responses.push({ ordinal: generation.responses.length, occurredAtMs: input.now, status: input.status }); } finishGeneration(input) { const active = this.active; const generation = this.latestGeneration(); if (!active || !generation || generation.outcome !== "pending") return; generation.finishedAtMs = input.now; generation.durationMs = elapsed(generation.startedAtMs, input.now); generation.stopReason = input.stopReason; generation.outcome = generationOutcome(input.stopReason); if (generation.outcome === "error") { active.providerErrors.push({ id: randomUUID(), generationId: generation.id, occurredAtMs: input.now, provider: generation.provider, model: generation.model, category: classifyProviderError(input.errorMessage), recovered: false, terminal: true }); } } beginTool(input) { const active = this.active; if (!active || active.toolIds.has(input.id)) return; active.toolIds.add(input.id); active.tools.push({ id: input.id, ordinal: active.tools.length, name: input.name, provider: input.model?.provider, model: input.model?.model, startedAtMs: input.now, isError: false, completionState: "running" }); } finishTool(input) { const tool = this.active?.tools.find(({ id }) => id === input.id); if (tool?.completionState !== "running") return; tool.finishedAtMs = input.now; tool.durationMs = elapsed(tool.startedAtMs, input.now); tool.isError = input.isError; tool.completionState = "finished"; } activateSkill(input) { const active = this.active; if (!active) return; const existingIndex = active.skillIndexes.get(input.name); if (existingIndex !== void 0) { const existing = active.skills[existingIndex]; if (existing && existing.initiatedBy === "model" && input.initiatedBy === "user") { existing.initiatedBy = "user"; existing.occurredAtMs = input.now; existing.provider = input.model?.provider; existing.model = input.model?.model; } return; } active.skillIndexes.set(input.name, active.skills.length); active.skills.push({ id: randomUUID(), name: input.name, initiatedBy: input.initiatedBy, occurredAtMs: input.now, provider: input.model?.provider, model: input.model?.model }); } settle(now) { return this.finalize(now); } interrupt(now) { return this.finalize(now, "interrupted"); } latestGeneration() { return this.active?.generations.at(-1); } finalize(now, forcedOutcome) { const active = this.active; if (!active) return void 0; this.active = void 0; for (const generation of active.generations) { if (generation.outcome !== "pending") continue; generation.outcome = "interrupted"; generation.finishedAtMs = now; generation.durationMs = elapsed(generation.startedAtMs, now); } for (const tool of active.tools) { if (tool.completionState !== "running") continue; tool.completionState = "interrupted"; tool.finishedAtMs = now; tool.durationMs = elapsed(tool.startedAtMs, now); } const successfulGenerationIndexes = new Set( active.generations.map((generation, index) => ({ generation, index })).filter(({ generation }) => isSuccessfulGeneration(generation)).map(({ index }) => index) ); let recoveredHttpErrors = 0; let httpErrors = 0; for (const [generationIndex, generation] of active.generations.entries()) { const hasLaterSuccess = [...successfulGenerationIndexes].some((index) => index > generationIndex); for (const [responseIndex, response] of generation.responses.entries()) { if (response.status < 400) continue; httpErrors += 1; const laterSuccessInGeneration = generation.responses.slice(responseIndex + 1).some(({ status }) => status >= 200 && status < 400); if (laterSuccessInGeneration || hasLaterSuccess) recoveredHttpErrors += 1; } } for (const error of active.providerErrors) { const generationIndex = active.generations.findIndex(({ id }) => id === error.generationId); error.recovered = [...successfulGenerationIndexes].some((index) => index > generationIndex); error.terminal = !error.recovered; } const recoveredGenerationErrors = active.providerErrors.filter(({ recovered }) => recovered).length; const providerErrorCount = httpErrors + active.providerErrors.length; const recoveredErrorCount = recoveredHttpErrors + recoveredGenerationErrors; const outcome = forcedOutcome ?? deriveOutcome(active.generations, providerErrorCount); return { id: active.id, startedAtMs: active.startedAtMs, finishedAtMs: now, durationMs: elapsed(active.startedAtMs, now), triggerSource: active.triggerSource, initialProvider: active.initialModel?.provider, initialModel: active.initialModel?.model, outcome, attemptCount: active.attemptCount, generations: active.generations, tools: active.tools, skills: active.skills, providerErrors: active.providerErrors, toolErrorCount: active.tools.filter(({ isError }) => isError).length, providerErrorCount, recoveredErrorCount }; } }; function elapsed(start, end) { return Math.max(0, end - start); } function generationOutcome(stopReason) { switch (stopReason) { case "stop": return "stop"; case "toolUse": return "tool_use"; case "error": return "error"; case "aborted": return "aborted"; case "length": return "length"; default: return "interrupted"; } } function isSuccessfulGeneration(generation) { return generation.outcome === "stop" || generation.outcome === "tool_use"; } function deriveOutcome(generations, providerErrors) { const last = generations.at(-1); if (!last) return providerErrors > 0 ? "error" : "success"; switch (last.outcome) { case "stop": case "tool_use": return providerErrors > 0 ? "recovered_success" : "success"; case "error": return "error"; case "aborted": return "aborted"; case "length": return "length"; default: return "interrupted"; } } // src/menu.ts import { stripVTControlCharacters } from "node:util"; // src/storage/queries.ts var DAY_MS = 24 * 60 * 60 * 1e3; var ERROR_CATEGORIES = [ "dns", "timeout", "connection_refused", "connection_reset", "tls", "network_other", "provider_other" ]; function resolveTimeRange(id, now = Date.now()) { let fromMs = 0; if (id === "today") { const date = new Date(now); fromMs = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime(); } else if (id === "7d") fromMs = now - 7 * DAY_MS; else if (id === "30d") fromMs = now - 30 * DAY_MS; return { id, fromMs, toMs: now + 1 }; } async function querySnapshot(runs, range, signal) { const generationCounts = []; const seenRunIds = /* @__PURE__ */ new Set(); const skills = /* @__PURE__ */ new Map(); const tools = /* @__PURE__ */ new Map(); const categories = Object.fromEntries(ERROR_CATEGORIES.map((category) => [category, 0])); let toolErrors = 0; let providerErrors = 0; let recoveredErrors = 0; let http429 = 0; let http5xx = 0; let terminal = 0; for await (const run of runs) { throwIfAborted(signal); if (seenRunIds.has(run.id)) continue; seenRunIds.add(run.id); if (run.startedAtMs < range.fromMs || run.startedAtMs >= range.toMs) continue; generationCounts.push(run.generations.length); toolErrors += run.toolErrorCount; providerErrors += run.providerErrorCount; recoveredErrors += run.recoveredErrorCount; for (const skill of run.skills) { const item = skills.get(skill.name) ?? { name: skill.name, count: 0, modelInitiated: 0, userInitiated: 0, lastOccurredAtMs: 0, models: [] }; item.count += 1; if (skill.initiatedBy === "user") item.userInitiated += 1; else item.modelInitiated += 1; item.lastOccurredAtMs = Math.max(item.lastOccurredAtMs, skill.occurredAtMs); mergeModelCount(item.models, { provider: skill.provider, model: skill.model, count: 1 }); skills.set(skill.name, item); } for (const tool of run.tools) { const item = tools.get(tool.name) ?? { name: tool.name, count: 0, errors: 0, averageDurationMs: 0, totalDurationMs: 0, lastOccurredAtMs: 0, models: [] }; item.count += 1; item.errors += tool.isError ? 1 : 0; item.totalDurationMs += tool.durationMs ?? 0; item.averageDurationMs = item.totalDurationMs / item.count; item.lastOccurredAtMs = Math.max(item.lastOccurredAtMs, tool.startedAtMs); mergeModelCount(item.models, { provider: tool.provider, model: tool.model, count: 1 }); tools.set(tool.name, item); } for (const error of run.providerErrors) { categories[error.category] += 1; terminal += error.terminal ? 1 : 0; } for (const generation of run.generations) { for (const response of generation.responses) { if (response.status === 429) http429 += 1; if (response.status >= 500 && response.status < 600) http5xx += 1; } } } const responses = responseStatistics(generationCounts); return { overview: { responseCycles: responses.count, llmCalls: responses.llmCalls, callsPerResponse: responses.average, p95CallsPerResponse: responses.p95, toolCalls: sum([...tools.values()].map(({ count }) => count)), toolErrors, skillActivations: sum([...skills.values()].map(({ count }) => count)), providerErrors, recoveredErrors }, skills: [...skills.values()].map((item) => ({ ...item, models: sortModels(item.models) })).sort((left, right) => right.count - left.count || left.name.localeCompare(right.name)), tools: [...tools.values()].map(({ totalDurationMs: _, ...item }) => ({ ...item, models: sortModels(item.models) })).sort((left, right) => right.count - left.count || left.name.localeCompare(right.name)), reliability: { http429, http5xx, recovered: recoveredErrors, terminal, categories }, responses }; } function responseStatistics(generationCounts) { const sorted = [...generationCounts].sort((left, right) => left - right); const count = sorted.length; const llmCalls = sum(sorted); const nearestRank = (percentile) => count === 0 ? 0 : sorted[Math.max(0, Math.ceil(percentile * count) - 1)] ?? 0; const median = count === 0 ? 0 : count % 2 === 1 ? sorted[Math.floor(count / 2)] ?? 0 : ((sorted[count / 2 - 1] ?? 0) + (sorted[count / 2] ?? 0)) / 2; return { count, llmCalls, average: count > 0 ? llmCalls / count : 0, median, p95: nearestRank(0.95), maximum: sorted.at(-1) ?? 0, distribution: { one: sorted.filter((value) => value === 1).length, twoToThree: sorted.filter((value) => value >= 2 && value <= 3).length, fourToSix: sorted.filter((value) => value >= 4 && value <= 6).length, sevenPlus: sorted.filter((value) => value >= 7).length } }; } function mergeModelCount(models, next) { const existing = models.find(({ provider, model }) => provider === next.provider && model === next.model); if (existing) existing.count += next.count; else models.push(next); } function sortModels(models) { return models.sort( (left, right) => right.count - left.count || `${left.provider ?? ""}/${left.model ?? ""}`.localeCompare(`${right.provider ?? ""}/${right.model ?? ""}`) ); } function throwIfAborted(signal) { if (signal?.aborted) throw signal.reason ?? new DOMException("Analytics query aborted", "AbortError"); } function sum(values) { return values.reduce((total, value) => total + value, 0); } // src/menu.ts var RANGE_LABELS = { today: "Today", "7d": "Last 7 days", "30d": "Last 30 days", all: "All time" }; function createAnalyticsMenu(source, now = Date.now, options) { let rangeId = "7d"; let cachedState; const loadState = async (signal) => { if (cachedState?.rangeId === rangeId) return cachedState; const range = resolveTimeRange(rangeId, now()); const loaded = { rangeId, range, path: source.path, result: await source.load(range, signal) }; if (!signal.aborted && rangeId === loaded.rangeId) cachedState = loaded; return loaded; }; const getState = ({ signal }) => loadState(signal); const menu = { start: "main", screens: { main: ({ state }) => ({ kind: "actions", title: `Analytics \xB7 ${RANGE_LABELS[state.rangeId]}`, lines: overviewLines(state.result), items: [ { id: "range", label: "Change time range", to: "range" }, { id: "skills", label: "Skills", to: "skills" }, { id: "tools", label: "Tools", to: "tools" }, { id: "reliability", label: "Provider reliability", to: "reliability" }, { id: "responses", label: "Response cycles", to: "responses" }, { id: "privacy", label: "Data & privacy", to: "privacy" }, { id: "close", label: "Close", close: true } ], hint: "close" }), range: ({ state }) => ({ kind: "choice", title: "Analytics time range", items: Object.keys(RANGE_LABELS).map((id) => ({ id, label: RANGE_LABELS[id] })), action: "setRange", currentItemId: state.rangeId, initialItemId: state.rangeId, hint: "back" }), skills: ({ state }) => skillsScreen(state.result), tools: ({ state }) => toolsScreen(state.result), reliability: ({ state }) => ({ kind: "detail", title: `Provider reliability \xB7 ${RANGE_LABELS[state.rangeId]}`, lines: reliabilityLines(state.result), hint: "back" }), responses: ({ state }) => ({ kind: "detail", title: `Response cycles \xB7 ${RANGE_LABELS[state.rangeId]}`, lines: responseLines(state.result), hint: "back" }), privacy: ({ state }) => ({ kind: "actions", title: "Analytics data & privacy", lines: privacyLines(state), items: [ { id: "clear", label: "Clear analytics data\u2026", action: "clearData", disabled: state.result.kind !== "ready" } ], hint: "back" }) }, actions: { setRange: async ({ itemId, signal }) => { if (!isRangeId(itemId)) return { kind: "rejected", error: new Error("Unknown range") }; rangeId = itemId; cachedState = void 0; await loadState(signal); return signal.aborted ? { kind: "close" } : { kind: "to", screen: "main" }; }, clearData: async ({ ctx, state, signal }) => { if (state.result.kind !== "ready") return { kind: "stay" }; if (!options) throw new Error("Analytics confirmation is unavailable"); const count = state.result.snapshot.overview.responseCycles; const confirmation = await options.runConfirmation(ctx, { title: "Delete analytics data?", message: `This will clear all local analytics history from: ${safeDisplayText(state.path)} The selected range currently shows ${count} response cycles. Other running Pi processes may add new records afterward.`, confirmLabel: "Delete data", cancelLabel: "Keep data", signal, isCurrent: options.isCurrent, // Keep the dashboard's existing domain-level error route as the only notifier. onError: () => void 0 }); if (signal.aborted || !options.isCurrent()) return { kind: "close" }; if (confirmation.kind === "closed") { return confirmation.reason === "close" ? { kind: "close" } : { kind: "stay" }; } if (confirmation.kind === "stale") return { kind: "close" }; if (confirmation.kind === "unsupported") { throw new Error(`Analytics confirmation is unavailable in ${confirmation.mode} mode`); } if (confirmation.kind === "error") throw confirmation.error; const result = await source.clearAll(signal); cachedState = void 0; const isCurrent = options.isCurrent(); if (isCurrent) { try { ctx.ui.notify("Cleared local analytics data.", "info"); if (result.cleanupIncomplete) { ctx.ui.notify( "Some obsolete analytics files are still in use. Stop other Pi processes and clear again to remove them.", "warning" ); } } catch { } } return signal.aborted || !isCurrent ? { kind: "close" } : { kind: "to", screen: "main" }; } } }; return { menu, getState, preload: (signal) => loadState(signal), get rangeId() { return rangeId; } }; } async function showAnalyticsMenu(ctx, source, options) { const { runConfirmation, runMenu, runTask } = await import("@narumitw/pi-tui-kit"); if (options.signal.aborted || !options.isCurrent()) return; const controller = createAnalyticsMenu(source, Date.now, { runConfirmation, isCurrent: options.isCurrent }); const loading = await runTask(ctx, { label: "Loading local analytics\u2026", signal: options.signal, isCurrent: options.isCurrent, task: ({ signal }) => controller.preload(signal), onError: () => void 0 }); if (loading.kind !== "completed") { if (loading.kind === "error") { ctx.ui.notify( "Analytics failed: The local analytics query could not be completed. Existing data was not changed.", "error" ); } return; } await runMenu(ctx, controller.menu, { getState: controller.getState, signal: options.signal, isCurrent: options.isCurrent, onError: (_ctx, error) => { ctx.ui.notify(`Analytics failed: ${safeErrorMessage(error)}`, "error"); } }); } function overviewLines(result) { if (result.kind === "unavailable") { return [result.message, "", "No analytics are being collected."]; } const stats = result.snapshot.overview; if (stats.responseCycles === 0) { return [ "No analytics yet.", "Collection is active. Complete one Pi response cycle, then open /analytics again.", "", "Includes settled response cycles only." ]; } return [ metric("Response cycles", stats.responseCycles), metric("LLM calls", stats.llmCalls), metric("Calls per response", `${formatDecimal(stats.callsPerResponse)} \xB7 P95 ${stats.p95CallsPerResponse}`), metric("Tool calls", stats.toolCalls), metric("Tool errors", stats.toolErrors), metric("Skill activations", stats.skillActivations), metric("Provider errors", stats.providerErrors), metric("Recovered errors", stats.recoveredErrors), "", "Includes settled response cycles only." ]; } function skillsScreen(result) { if (result.kind === "unavailable") { return { kind: "browse", title: "Skills", lines: [result.message], items: [], hint: "back" }; } return { kind: "browse", title: "Skills", lines: result.snapshot.skills.length === 0 ? ["No skill activations detected in this time range."] : void 0, items: result.snapshot.skills.map(skillItem), viewportSize: "adaptive", hint: "back" }; } function skillItem(skill) { return { id: skill.name, label: safeDisplayText(skill.name), statusText: `${skill.count} \xB7 ${skill.modelInitiated} model / ${skill.userInitiated} user`, searchText: skill.models.map(modelLabel).join(" "), details: [ metric("Activations", skill.count), metric("Model initiated", skill.modelInitiated), metric("User initiated", skill.userInitiated), "", "By model", ...skill.models.map((model) => `${modelLabel(model)}: ${model.count}`), "", `Last detected: ${formatTimestamp(skill.lastOccurredAtMs)}` ] }; } function toolsScreen(result) { if (result.kind === "unavailable") { return { kind: "browse", title: "Tools", lines: [result.message], items: [], hint: "back" }; } return { kind: "browse", title: "Tools", lines: result.snapshot.tools.length === 0 ? ["No tool calls detected in this time range."] : void 0, items: result.snapshot.tools.map(toolItem), viewportSize: "adaptive", hint: "back" }; } function toolItem(tool) { return { id: tool.name, label: safeDisplayText(tool.name), statusText: `${tool.count} \xB7 ${tool.errors} errors`, searchText: tool.models.map(modelLabel).join(" "), details: [ metric("Calls", tool.count), metric("Errors", tool.errors), `Average duration: ${formatDecimal(tool.averageDurationMs)} ms`, "", "By model", ...tool.models.map((model) => `${modelLabel(model)}: ${model.count}`), "", `Last detected: ${formatTimestamp(tool.lastOccurredAtMs)}` ] }; } function reliabilityLines(result) { if (result.kind === "unavailable") return [result.message]; const value = result.snapshot.reliability; return [ "Observed provider errors only; provider-internal failures may be invisible.", "", metric("HTTP 429", value.http429), metric("HTTP 5xx", value.http5xx), metric("DNS", value.categories.dns), metric("Connection timeout", value.categories.timeout), metric("Connection refused", value.categories.connection_refused), metric("Connection reset", value.categories.connection_reset), metric("TLS", value.categories.tls), metric("Other network", value.categories.network_other), metric("Other provider", value.categories.provider_other), "", metric("Recovered", value.recovered), metric("Terminal failures", value.terminal) ]; } function responseLines(result) { if (result.kind === "unavailable") return [result.message]; const value = result.snapshot.responses; return [ metric("Cycles", value.count), metric("LLM calls", value.llmCalls), metric("Average", formatDecimal(value.average)), metric("Median", formatDecimal(value.median)), metric("P95", value.p95), metric("Maximum", value.maximum), "", "Calls per response", metric("1 call", value.distribution.one), metric("2\u20133 calls", value.distribution.twoToThree), metric("4\u20136 calls", value.distribution.fourToSix), metric("7+ calls", value.distribution.sevenPlus) ]; } function privacyLines(state) { return [ "Local analytics files:", safeDisplayText(state.path), "", "Stored: timestamps, extension-generated record IDs, model/provider IDs, thinking level, tool and skill names, durations, counts, HTTP statuses, and classified errors.", "Not stored: prompts, responses, thinking, tool arguments/results, raw errors, headers, cwd/file paths, session identity, or credentials.", "", "No database server, cloud connection, or other remote telemetry is used.", "Analytics are non-critical derived metadata; a failed local write may be dropped." ]; } function metric(label, value) { return `${label.padEnd(24)} ${value}`; } function formatDecimal(value) { return Number.isInteger(value) ? String(value) : value.toFixed(2).replace(/0+$/u, "").replace(/\.$/u, ""); } function formatTimestamp(value) { return new Date(value).toLocaleString(); } function modelLabel(model) { if (!model.provider && !model.model) return "unknown"; return safeDisplayText(`${model.provider ?? "unknown"}/${model.model ?? "unknown"}`); } function safeDisplayText(value) { return Array.from(stripVTControlCharacters(String(value)), (character) => { const codePoint = character.codePointAt(0) ?? 0; return codePoint <= 31 || codePoint >= 127 && codePoint <= 159 ? " " : character; }).join(""); } function isRangeId(value) { return value === "today" || value === "7d" || value === "30d" || value === "all"; } function safeErrorMessage(_error) { return "The local analytics query could not be completed. Try again; existing data was not changed."; } // src/skills.ts import { realpath } from "node:fs/promises"; import path from "node:path"; function explicitSkillName(text) { return text.match(/^\/skill:([a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)(?:\s|$)/u)?.[1]; } var SkillTracker = class { constructor(cwd, canonicalize = realpath) { this.cwd = cwd; this.canonicalize = canonicalize; } cwd; canonicalize; pending; skillByPath = /* @__PURE__ */ new Map(); availableNames = /* @__PURE__ */ new Set(); observeInput(text, source, now) { if (source === "extension") return; const name = explicitSkillName(text); this.pending = name ? { name, observedAtMs: now, source } : void 0; } consumeExplicitSkill() { const pending = this.pending; this.pending = void 0; return pending; } clearPending() { this.pending = void 0; } hasAvailableSkill(name) { return this.availableNames.has(name); } async setAvailableSkills(skills) { this.skillByPath.clear(); this.availableNames.clear(); const seenNames = /* @__PURE__ */ new Set(); for (const skill of skills) { if (seenNames.has(skill.name)) continue; seenNames.add(skill.name); this.availableNames.add(skill.name); const canonical = await this.canonicalize(skill.filePath).catch(() => path.resolve(this.cwd, skill.filePath)); this.skillByPath.set(canonical, skill.name); } } async matchSuccessfulRead(input) { if (input.toolName !== "read" || input.isError || !isRecord(input.input)) return void 0; const rawPath = input.input.path; if (typeof rawPath !== "string" || rawPath.length === 0) return void 0; const normalized = rawPath.startsWith("@") ? rawPath.slice(1) : rawPath; const absolute = path.resolve(this.cwd, normalized); const canonical = await this.canonicalize(absolute).catch(() => absolute); return this.skillByPath.get(canonical); } }; function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } // src/storage/files.ts import { randomUUID as randomUUID2 } from "node:crypto"; import { createReadStream } from "node:fs"; import { chmod, lstat, mkdir, open, readdir, readFile, rename, rm, rmdir, unlink, writeFile } from "node:fs/promises"; import path2 from "node:path"; // src/storage/format.ts var MAX_STORED_RUN_BYTES = 1024 * 1024; var MAX_STRING_LENGTH = 4096; var MAX_NESTED_RECORDS = 2e4; var TRIGGER_SOURCES = ["interactive", "rpc", "extension", "unknown"]; var RUN_OUTCOMES = ["success", "recovered_success", "error", "aborted", "length", "interrupted"]; var GENERATION_OUTCOMES = ["pending", "stop", "tool_use", "error", "aborted", "length", "interrupted"]; var ERROR_CATEGORIES2 = [ "dns", "timeout", "connection_refused", "connection_reset", "tls", "network_other", "provider_other" ]; var AnalyticsStorageFormatError = class extends Error { constructor(message, options = {}) { super(message, options); this.name = "AnalyticsStorageFormatError"; } }; function encodeStoredRun(run) { const encoded = `${JSON.stringify({ formatVersion: 1, run: parseRun(run, { remaining: MAX_NESTED_RECORDS }) })} `; if (Buffer.byteLength(encoded) > MAX_STORED_RUN_BYTES) { throw new AnalyticsStorageFormatError("Analytics record is too large to store safely."); } return encoded; } function decodeStoredRun(line) { if (Buffer.byteLength(line) > MAX_STORED_RUN_BYTES) { throw new AnalyticsStorageFormatError("Analytics record is too large to read safely."); } let value; try { value = JSON.parse(line); } catch (error) { throw new AnalyticsStorageFormatError("Analytics record contains invalid JSON.", { cause: error }); } const envelope = asRecord(value, "analytics record"); if (envelope.formatVersion !== 1) { throw new AnalyticsStorageFormatError("Analytics record uses an unsupported format version."); } return parseRun(envelope.run, { remaining: MAX_NESTED_RECORDS }); } function parseRun(value, budget) { const run = asRecord(value, "run"); return { id: requiredString(run.id, "run.id"), startedAtMs: timestampValue(run.startedAtMs, "run.startedAtMs"), finishedAtMs: timestampValue(run.finishedAtMs, "run.finishedAtMs"), durationMs: durationValue(run.durationMs, "run.durationMs"), triggerSource: enumValue(run.triggerSource, TRIGGER_SOURCES, "run.triggerSource"), ...optionalProperty("initialProvider", optionalString(run.initialProvider, "run.initialProvider")), ...optionalProperty("initialModel", optionalString(run.initialModel, "run.initialModel")), outcome: enumValue(run.outcome, RUN_OUTCOMES, "run.outcome"), attemptCount: boundedCount(run.attemptCount, "run.attemptCount"), generations: boundedArray(run.generations, "run.generations", budget).map( (item, index) => parseGeneration(item, index, budget) ), tools: boundedArray(run.tools, "run.tools", budget).map(parseTool), skills: boundedArray(run.skills, "run.skills", budget).map(parseSkill), providerErrors: boundedArray(run.providerErrors, "run.providerErrors", budget).map(parseProviderError), toolErrorCount: boundedCount(run.toolErrorCount, "run.toolErrorCount"), providerErrorCount: boundedCount(run.providerErrorCount, "run.providerErrorCount"), recoveredErrorCount: boundedCount(run.recoveredErrorCount, "run.recoveredErrorCount") }; } function parseGeneration(value, index, budget) { const item = asRecord(value, `run.generations[${index}]`); const prefix = `run.generations[${index}]`; return { id: requiredString(item.id, `${prefix}.id`), ordinal: boundedCount(item.ordinal, `${prefix}.ordinal`), ...optionalProperty("provider", optionalString(item.provider, `${prefix}.provider`)), ...optionalProperty("model", optionalString(item.model, `${prefix}.model`)), ...optionalProperty("thinkingLevel", optionalString(item.thinkingLevel, `${prefix}.thinkingLevel`)), startedAtMs: timestampValue(item.startedAtMs, `${prefix}.startedAtMs`), ...optionalProperty("finishedAtMs", optionalTimestamp(item.finishedAtMs, `${prefix}.finishedAtMs`)), ...optionalProperty("durationMs", optionalDuration(item.durationMs, `${prefix}.durationMs`)), ...optionalProperty("stopReason", optionalString(item.stopReason, `${prefix}.stopReason`)), outcome: enumValue(item.outcome, GENERATION_OUTCOMES, `${prefix}.outcome`), responses: boundedArray(item.responses, `${prefix}.responses`, budget).map(parseProviderResponse) }; } function parseProviderResponse(value, index) { const item = asRecord(value, `provider response ${index}`); return { ordinal: boundedCount(item.ordinal, "providerResponse.ordinal"), occurredAtMs: timestampValue(item.occurredAtMs, "providerResponse.occurredAtMs"), status: boundedInteger(item.status, "providerResponse.status", 999) }; } function parseTool(value, index) { const item = asRecord(value, `run.tools[${index}]`); const prefix = `run.tools[${index}]`; const ordinal = boundedCount(item.ordinal, `${prefix}.ordinal`); return { id: `tool-${ordinal}`, ordinal, name: requiredString(item.name, `${prefix}.name`), ...optionalProperty("provider", optionalString(item.provider, `${prefix}.provider`)), ...optionalProperty("model", optionalString(item.model, `${prefix}.model`)), startedAtMs: timestampValue(item.startedAtMs, `${prefix}.startedAtMs`), ...optionalProperty("finishedAtMs", optionalTimestamp(item.finishedAtMs, `${prefix}.finishedAtMs`)), ...optionalProperty("durationMs", optionalDuration(item.durationMs, `${prefix}.durationMs`)), isError: booleanValue(item.isError, `${prefix}.isError`), completionState: enumValue( item.completionState, ["running", "finished", "interrupted"], `${prefix}.completionState` ) }; } function parseSkill(value, index) { const item = asRecord(value, `run.skills[${index}]`); const prefix = `run.skills[${index}]`; return { id: requiredString(item.id, `${prefix}.id`), name: requiredString(item.name, `${prefix}.name`), initiatedBy: enumValue(item.initiatedBy, ["user", "model"], `${prefix}.initiatedBy`), occurredAtMs: timestampValue(item.occurredAtMs, `${prefix}.occurredAtMs`), ...optionalProperty("provider", optionalString(item.provider, `${prefix}.provider`)), ...optionalProperty("model", optionalString(item.model, `${prefix}.model`)) }; } function parseProviderError(value, index) { const item = asRecord(value, `run.providerErrors[${index}]`); const prefix = `run.providerErrors[${index}]`; return { id: requiredString(item.id, `${prefix}.id`), ...optionalProperty("generationId", optionalString(item.generationId, `${prefix}.generationId`)), occurredAtMs: timestampValue(item.occurredAtMs, `${prefix}.occurredAtMs`), ...optionalProperty("provider", optionalString(item.provider, `${prefix}.provider`)), ...optionalProperty("model", optionalString(item.model, `${prefix}.model`)), category: enumValue(item.category, ERROR_CATEGORIES2, `${prefix}.category`), recovered: booleanValue(item.recovered, `${prefix}.recovered`), terminal: booleanValue(item.terminal, `${prefix}.terminal`) }; } function asRecord(value, name) { if (typeof value !== "object" || value === null || Array.isArray(value)) invalid(name); return value; } function boundedArray(value, name, budget) { if (!Array.isArray(value)) invalid(name); budget.remaining -= value.length; if (budget.remaining < 0) { throw new AnalyticsStorageFormatError("Analytics record is too large to process safely."); } return value; } function requiredString(value, name) { if (typeof value !== "string" || value.length === 0 || value.length > MAX_STRING_LENGTH) { invalid(name); } return value; } function optionalString(value, name) { return value === void 0 ? void 0 : requiredString(value, name); } function boundedInteger(value, name, maximum) { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > maximum) { invalid(name); } return value; } function timestampValue(value, name) { return boundedInteger(value, name, Number.MAX_SAFE_INTEGER); } function optionalTimestamp(value, name) { return value === void 0 ? void 0 : timestampValue(value, name); } function durationValue(value, name) { return boundedInteger(value, name, Math.floor(Number.MAX_SAFE_INTEGER / MAX_NESTED_RECORDS)); } function optionalDuration(value, name) { return value === void 0 ? void 0 : durationValue(value, name); } function boundedCount(value, name) { return boundedInteger(value, name, MAX_NESTED_RECORDS); } function booleanValue(value, name) { if (typeof value !== "boolean") invalid(name); return value; } function enumValue(value, values, name) { if (typeof value !== "string" || !values.includes(value)) invalid(name); return value; } function optionalProperty(key, value) { return value === void 0 ? {} : { [key]: value }; } function invalid(name) { throw new AnalyticsStorageFormatError(`Analytics record has an invalid ${name}.`); } // src/storage/files.ts 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; var DEFAULT_WRITE_TIMEOUT_MS = 5e3; var YIELD_EVERY_RECORDS = 100; var AnalyticsGenerationChangedError = class extends Error { constructor() { super("The active analytics generation changed during the read."); this.name = "AnalyticsGenerationChangedError"; } }; var AnalyticsRunFiles = class { constructor(path4, options = {}) { this.path = path4; this.createId = options.createId ?? randomUUID2; this.writeTimeoutMs = options.writeTimeoutMs ?? DEFAULT_WRITE_TIMEOUT_MS; this.beforeAppend = options.beforeAppend; this.beforeCleanupEntry = options.beforeCleanupEntry; this.beforeReadFile = options.beforeReadFile; this.writerId = validCreatedId(this.createId()); } path; createId; writeTimeoutMs; beforeAppend; beforeCleanupEntry; beforeReadFile; writerId; mutationTail = Promise.resolve(); lifecycle = new AbortController(); closed = false; append(run, signal) { if (this.closed) return Promise.reject(new Error("Analytics storage is closed.")); const frame = encodeStoredRun(run); return this.enqueueMutation( () => withDeadline( signal, this.lifecycle.signal, this.writeTimeoutMs, (operationSignal) => this.appendFrame(frame, operationSignal) ) ); } async *read(signal) { throwIfAborted2(signal); const generation = await this.readOrCreateGeneration(signal); const directory = this.generationPath(generation); let entries; try { entries = await readdir(directory, { withFileTypes: true }); } catch (error) { if (isNodeError(error) && error.code === "ENOENT") { await this.assertGenerationUnchanged(generation, signal); throw new AnalyticsGenerationChangedError(); } throw error; } let count = 0; for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) { throwIfAborted2(signal); if (!entry.name.endsWith(".jsonl")) continue; const filePath = path2.join(directory, entry.name); try { await this.beforeReadFile?.(signal); throwIfAborted2(signal); await assertPrivateRegularFile(filePath); for await (const run of readFrames(filePath, signal)) { yield run; count += 1; if (count % YIELD_EVERY_RECORDS === 0) await yieldToEventLoop(signal); } } catch (error) { if (isNodeError(error) && error.code === "ENOENT") { await this.assertGenerationUnchanged(generation, signal); throw new AnalyticsGenerationChangedError(); } throw error; } } await this.assertGenerationUnchanged(generation, signal); } clear(signal) { if (this.closed) return Promise.reject(new Error("Analytics storage is closed.")); let result = { cleanupIncomplete: false }; return this.enqueueMutation( () => withLinkedSignals(signal, this.lifecycle.signal, async (operationSignal) => { throwIfAborted2(operationSignal); await this.readOrCreateGeneration(operationSignal); const next = validCreatedId(this.createId()); await this.publishGeneration(next); this.writerId = validCreatedId(this.createId()); const current = await readGenerationMarker(path2.join(this.path, "current")); await ensurePrivateDirectory(this.generationPath(current)); if (!await this.cleanupObsoleteGenerations(operationSignal)) { result = { cleanupIncomplete: true }; } }) ).then(() => result); } async close() { if (this.closed) return; this.closed = true; this.lifecycle.abort(new DOMException("Analytics storage closed", "AbortError")); await this.mutationTail.catch(() => void 0); } enqueueMutation(operation) { const result = this.mutationTail.then(operation); this.mutationTail = result.catch(() => void 0); return result; } async appendFrame(frame, signal) { throwIfAborted2(signal); let obsoleteDirectory; for (let attempt = 0; attempt < 3; attempt += 1) { const generation = await this.readOrCreateGeneration(signal); const directory = this.generationPath(generation); await ensurePrivateDirectory(directory); const filePath = path2.join(directory, `${this.writerId}.jsonl`); await assertOptionalPrivateRegularFile(filePath); throwIfAborted2(signal); await this.beforeAppend?.(generation, signal); throwIfAborted2(signal); try { await writeFile(filePath, frame, { encoding: "utf8", flag: "a", mode: 384, signal }); if (process.platform !== "win32") await chmod(filePath, 384); const current = await readGenerationMarker(path2.join(this.path, "current"), signal); if (current === generation) { if (obsoleteDirectory) { await cleanupGeneration(obsoleteDirectory, signal).catch(() => void 0); } return; } obsoleteDirectory = directory; } catch (error) { this.writerId = validCreatedId(this.createId()); throwIfAborted2(signal); if (!isNodeError(error) || error.code !== "ENOENT") throw error; } } throw new AnalyticsGenerationChangedError(); } async readOrCreateGeneration(signal) { throwIfAborted2(signal); await ensurePrivateDirectory(this.path); await ensurePrivateDirectory(path2.join(this.path, "generations")); const markerPath = path2.join(this.path, "current"); for (let attempt = 0; attempt < 3; attempt += 1) { let generation; try { generation = await readGenerationMarker(markerPath, signal); } catch (error) { if (!isNodeError(error) || error.code !== "ENOENT") throw error; generation = validCreatedId(this.createId()); throwIfAborted2(signal); try { await createPrivateFile(markerPath, `${generation} `); } catch (createError) { if (!isNodeError(createError) || createError.code !== "EEXIST") throw createError; generation = await readGenerationMarker(markerPath, signal); } } await ensurePrivateDirectory(this.generationPath(generation)); const current = await readGenerationMarker(markerPath, signal); if (current === generation) return generation; } throw new AnalyticsGenerationChangedError(); } async publishGeneration(generation) { const markerPath = path2.join(this.path, "current"); const temporaryPath = path2.join(this.path, `.current.${validCreatedId(this.createId())}.tmp`); await createPrivateFile(temporaryPath, `${generation} `); try { await rename(temporaryPath, markerPath); if (process.platform !== "win32") await chmod(markerPath, 384); } catch (error) { await rm(temporaryPath, { force: true }).catch(() => void 0); throw error; } } async cleanupObsoleteGenerations(signal) { let complete = true; const root = path2.join(this.path, "generations"); for (const entry of await readdir(root, { withFileTypes: true })) { const active2 = await readGenerationMarker(path2.join(this.path, "current")); if (entry.name === active2) continue; if (!entry.isDirectory() || !GENERATION_PATTERN.test(entry.name)) { complete = false; continue; } try { await cleanupGeneration(path2.join(root, entry.name), signal, this.beforeCleanupEntry); } catch { complete = false; if (signal?.aborted) break; } } const active = await readGenerationMarker(path2.join(this.path, "current")); const remaining = await readdir(root, { withFileTypes: true }); return complete && remaining.every((entry) => entry.isDirectory() && entry.name === active); } async assertGenerationUnchanged(generation, signal) { const current = await readGenerationMarker(path2.join(this.path, "current"), signal); if (current !== generation) throw new AnalyticsGenerationChangedError(); } generationPath(generation) { return path2.join(this.path, "generations", generation); } }; async function* readFrames(filePath, signal) { let pending = ""; const stream = createReadStream(filePath, { encoding: "utf8", signal }); for await (const chunk of stream) { throwIfAborted2(signal); pending += String(chunk); while (true) { const newline = pending.indexOf("\n"); if (newline < 0) break; const line = pending.slice(0, newline); pending = pending.slice(newline + 1); if (!line) continue; yield decodeStoredRun(line); } if (Buffer.byteLength(pending) > MAX_STORED_RUN_BYTES) { throw new AnalyticsStorageFormatError("Analytics record is too large to read safely."); } } } async function readGenerationMarker(markerPath, signal) { await assertPrivateRegularFile(markerPath); throwIfAborted2(signal); const value = (await readFile(markerPath, { encoding: "utf8", signal })).trim(); if (!GENERATION_PATTERN.test(value)) { throw new AnalyticsStorageFormatError("Analytics generation marker is invalid."); } return value; } async function ensurePrivateDirectory(directoryPath) { try { const metadata = await lstat(directoryPath); if (!metadata.isDirectory() || metadata.isSymbolicLink()) { throw new Error("Analytics storage paths must be regular directories, not links."); } } catch (error) { if (!isNodeError(error) || error.code !== "ENOENT") throw error; await mkdir(directoryPath, { recursive: true, mode: 448 }); const metadata = await lstat(directoryPath); if (!metadata.isDirectory() || metadata.isSymbolicLink()) { throw new Error("Analytics storage paths must be regular directories, not links."); } } if (process.platform !== "win32") await chmod(directoryPath, 448); } async function assertOptionalPrivateRegularFile(filePath) { try { await assertPrivateRegularFile(filePath); } catch (error) { if (isNodeError(error) && error.code === "ENOENT") return; throw error; } } async function assertPrivateRegularFile(filePath) { const metadata = await lstat(filePath); if (!metadata.isFile() || metadata.isSymbolicLink()) { throw new Error("Analytics storage files must be regular files, not links."); } if (process.platform !== "win32") await chmod(filePath, 384); } async function createPrivateFile(filePath, content) { const handle = await open(filePath, "wx", 384); try { await handle.writeFile(content, "utf8"); await handle.sync(); } finally { await handle.close(); } } async function cleanupGeneration(directoryPath, signal, beforeEntry) { throwIfAborted2(signal); let entries; try { entries = await readdir(directoryPath, { withFileTypes: true }); } catch (error) { if (isNodeError(error) && error.code === "ENOENT") return; throw error; } for (const entry of entries) { throwIfAborted2(signal); await beforeEntry?.(signal); throwIfAborted2(signal); if (!entry.isFile() || !entry.name.endsWith(".jsonl")) { throw new Error("Analytics generation contains an unexpected storage entry."); } await unlink(path2.join(directoryPath, entry.name)); } throwIfAborted2(signal); await rmdir(directoryPath); } async function withLinkedSignals(callerSignal, lifecycleSignal, operation) { throwIfAborted2(callerSignal); throwIfAborted2(lifecycleSignal); const controller = new AbortController(); const abort = (signal) => controller.abort(signal.reason ?? new DOMException("Analytics operation aborted", "AbortError")); const callerAbort = () => callerSignal && abort(callerSignal); const lifecycleAbort = () => abort(lifecycleSignal); callerSignal?.addEventListener("abort", callerAbort, { once: true }); lifecycleSignal.addEventListener("abort", lifecycleAbort, { once: true }); try { return await operation(controller.signal); } finally { callerSignal?.removeEventListener("abort", callerAbort); lifecycleSignal.removeEventListener("abort", lifecycleAbort); } } async function withDeadline(callerSignal, lifecycleSignal, timeoutMs, operation) { throwIfAborted2(callerSignal); throwIfAborted2(lifecycleSignal); const controller = new AbortController(); const abort = (signal) => controller.abort(signal.reason ?? new DOMException("Analytics operation aborted", "AbortError")); const callerAbort = () => callerSignal && abort(callerSignal); const lifecycleAbort = () => abort(lifecycleSignal); callerSignal?.addEventListener("abort", callerAbort, { once: true }); lifecycleSignal.addEventListener("abort", lifecycleAbort, { once: true }); const timer = setTimeout( () => controller.abort(new DOMException("Analytics write timed out", "TimeoutError")), Math.max(1, timeoutMs) ); try { return await operation(controller.signal); } catch (error) { if (controller.signal.aborted && error instanceof Error && error.name === "AbortError" && error.cause === controller.signal.reason) { throw controller.signal.reason; } throw error; } finally { clearTimeout(timer); callerSignal?.removeEventListener("abort", callerAbort); lifecycleSignal.removeEventListener("abort", lifecycleAbort); } } async function yieldToEventLoop(signal) { await new Promise((resolve) => setImmediate(resolve)); throwIfAborted2(signal); } function validCreatedId(value) { if (!GENERATION_PATTERN.test(value)) throw new Error("Analytics storage received an invalid ID."); return value; } function throwIfAborted2(signal) { if (signal?.aborted) { throw signal.reason ?? new DOMException("Analytics operation aborted", "AbortError"); } } function isNodeError(error) { return error instanceof Error && "code" in error; } // src/storage/store.ts var AnalyticsStore = class { files; constructor(rootPath, dependencies = {}) { this.files = dependencies.files ?? new AnalyticsRunFiles(rootPath, { createId: dependencies.createId, writeTimeoutMs: dependencies.writeTimeoutMs }); } get path() { return this.files.path; } recordRun(run, signal) { return this.files.append(run, signal); } async getSnapshot(range, signal) { for (let attempt = 0; attempt < 2; attempt += 1) { try { return await querySnapshot(this.files.read(signal), range, signal); } catch (error) { if (!(error instanceof AnalyticsGenerationChangedError) || attempt > 0) throw error; } } throw new AnalyticsGenerationChangedError(); } clearAll(signal) { return this.files.clear(signal); } close() { return this.files.close(); } }; // src/analytics.ts var STORAGE_DIRECTORY = "pi-analytics"; function createAnalyticsExtension(dependencies = {}) { const deps = { createStore: dependencies.createStore ?? ((rootPath) => new AnalyticsStore(rootPath)), createSkillTracker: dependencies.createSkillTracker ?? ((cwd) => new SkillTracker(cwd)), getAgentDir: dependencies.getAgentDir ?? getAgentDir, now: dependencies.now ?? Date.now, createId: dependencies.createId ?? randomUUID3 }; return function analyticsExtension(pi) { let sessionGeneration = 0; let sessionController = new AbortController(); let collector = new ResponseCollector(); let skillTracker; let store; let storageFailure; const retiredCloseTasks = /* @__PURE__ */ new Set(); let writeFailureActive = false; let pendingTriggerSource = "unknown"; let pendingAttemptWithoutRun = false; let pendingProviderGeneration; pi.registerCommand("analytics", { description: "Open local Pi usage analytics", handler: async (args, ctx) => { if (args.trim()) { if (!ctx.hasUI || ctx.mode !== "tui" && ctx.mode !== "rpc") { throw new Error("/analytics does not accept arguments."); } ctx.ui.notify("/analytics does not accept arguments.", "warning"); return; } if (!ctx.hasUI || ctx.mode !== "tui" && ctx.mode !== "rpc") { throw new Error("/analytics requires Pi TUI or RPC mode."); } const generation = sessionGeneration; const owner = sessionController; const source = menuSource(generation, owner.signal); await showAnalyticsMenu(ctx, source, { signal: owner.signal, isCurrent: () => generation === sessionGeneration && !owner.signal.aborted }); } }); pi.on("session_start", (_event, ctx) => { ++sessionGeneration; const previousStore = store; sessionController.abort(new DOMException("Analytics session replaced", "AbortError")); if (previousStore) retire(previousStore); sessionController = new AbortController(); collector = new ResponseCollector(); skillTracker = deps.createSkillTracker(ctx.cwd); store = void 0; storageFailure = void 0; writeFailureActive = false; pendingTriggerSource = "unknown"; pendingAttemptWithoutRun = false; pendingProviderGeneration = void 0; const storageRoot = path3.join(deps.getAgentDir(), STORAGE_DIRECTORY); try { store = deps.createStore(storageRoot); } catch { storageFailure = unavailableMessage(); safeNotify(ctx, storageFailure, "warning"); } }); pi.on("input", (event, ctx) => { const now = deps.now(); const tracker = skillTracker; tracker?.observeInput(event.text, event.source, now); if (event.source !== "extension") pendingTriggerSource = event.source; if (!tracker || !collector.hasActiveRun()) return; const explicit = tracker.consumeExplicitSkill(); if (!explicit || !tracker.hasAvailableSkill(explicit.name)) return; collector.activateSkill({ name: explicit.name, initiatedBy: "user", now: explicit.observedAtMs, model: modelIdentity(ctx, pi) }); }); pi.on("before_agent_start", async (event, ctx) => { pendingProviderGeneration = void 0; const generation = sessionGeneration; const tracker = skillTracker; const activeCollector = collector; if (!tracker) return; const skills = availableSkills(pi, event.systemPromptOptions.skills ?? []); await tracker.setAvailableSkills(skills); if (generation !== sessionGeneration || tracker !== skillTracker || activeCollector !== collector) { return; } const explicit = tracker.consumeExplicitSkill(); const interrupted = activeCollector.begin({ id: deps.createId(), now: deps.now(), triggerSource: explicit?.source ?? pendingTriggerSource, model: modelIdentity(ctx, pi) }); pendingTriggerSource = "unknown"; if (interrupted) { await persistRun(interrupted, ctx, generation, sessionController.signal); if (generation !== sessionGeneration || tracker !== skillTracker || activeCollector !== collector) { return; } } if (explicit && skills.some(({ name }) => name === explicit.name)) { activeCollector.activateSkill({ name: explicit.name, initiatedBy: "user", now: explicit.observedAtMs, model: modelIdentity(ctx, pi) }); } }); pi.on("agent_start", () => { if (collector.hasActiveRun()) collector.beginAttempt(); else pendingAttemptWithoutRun = true; }); pi.on("turn_start", (_event, ctx) => ensureRun(ctx, "extension")); pi.on("before_provider_request", (_event, ctx) => { pendingProviderGeneration = { id: deps.createId(), startedAtMs: deps.now(), model: modelIdentity(ctx, pi), responses: [] }; }); pi.on("after_provider_response", (event) => { pendingProviderGeneration?.responses.push({ status: event.status, occurredAtMs: deps.now() }); }); pi.on("message_start", (event, ctx) => { if (event.message.role === "assistant") claimPendingProviderGeneration(ctx); }); pi.on("message_end", (event, ctx) => { if (event.message.role !== "assistant") return; claimPendingProviderGeneration(ctx); collector.finishGeneration({ now: deps.now(), stopReason: event.message.stopReason, errorMessage: event.message.errorMessage }); }); pi.on("tool_execution_start", (event, ctx) => { ensureRun(ctx, "extension"); collector.beginTool({ id: event.toolCallId, name: event.toolName, now: deps.now(), model: modelIdentity(ctx, pi) }); }); pi.on("tool_result", async (event, ctx) => { if (event.toolName === "read" && !isBuiltinReadTool(pi)) return; const generation = sessionGeneration; const tracker = skillTracker; const activeCollector = collector; const runId = activeCollector.getActiveRunId(); if (!tracker || !runId) return; const name = await tracker.matchSuccessfulRead({ toolName: event.toolName, input: event.input, isError: event.isError }); if (!name || generation !== sessionGeneration || tracker !== skillTracker || activeCollector !== collector || activeCollector.getActiveRunId() !== runId) { return; } activeCollector.activateSkill({ name, initiatedBy: "model", now: deps.now(), model: modelIdentity(ctx, pi) }); }); pi.on("tool_execution_end", (event) => { collector.finishTool({ id: event.toolCallId, now: deps.now(), isError: event.isError }); }); pi.on("agent_settled", async (_event, ctx) => { const generation = sessionGeneration; const owner = sessionController; const run = collector.settle(deps.now()); pendingAttemptWithoutRun = false; pendingProviderGeneration = void 0; pendingTriggerSource = "unknown"; skillTracker?.clearPending(); if (run) await persistRun(run, ctx, generation, owner.signal); }); pi.on("session_shutdown", async (_event, ctx) => { const activeStore = store; ++sessionGeneration; sessionController.abort(new DOMException("Analytics session shut down", "AbortError")); skillTracker?.clearPending(); skillTracker = void 0; store = void 0; pendingProviderGeneration = void 0; collector.interrupt(deps.now()); const closing = activeStore ? [closeResult(activeStore), ...retiredCloseTasks] : [...retiredCloseTasks]; const results = await Promise.all(closing); if (results.some((closed) => !closed)) { safeNotify(ctx, "Analytics storage shutdown was incomplete.", "warning"); } }); function retire(retiredStore) { const task = closeResult(retiredStore).finally(() => retiredCloseTasks.delete(task)); retiredCloseTasks.add(task); } async function closeResult(activeStore) { try { await activeStore.close(); return true; } catch { return false; } } function claimPendingProviderGeneration(ctx) { const pending = pendingProviderGeneration; if (!pending) return; pendingProviderGeneration = void 0; ensureRun(ctx, "extension"); collector.beginGeneration({ id: pending.id, now: pending.startedAtMs, model: pending.model }); for (const response of pending.responses) { collector.recordProviderResponse({ status: response.status, now: response.occurredAtMs }); } } function ensureRun(ctx, triggerSource) { if (collector.hasActiveRun()) return; collector.begin({ id: deps.createId(), now: deps.now(), triggerSource, model: modelIdentity(ctx, pi) }); if (pendingAttemptWithoutRun) { pendingAttemptWithoutRun = false; collector.beginAttempt(); } } async function persistRun(run, ctx, generation, signal) { const activeStore = store; if (!activeStore || signal.aborted) return; try { await activeStore.recordRun(run, signal); if (generation !== sessionGeneration || activeStore !== store || signal.aborted) return; if (writeFailureActive) { writeFailureActive = false; safeNotify(ctx, "Local analytics storage recovered.", "info"); } } catch { if (generation !== sessionGeneration || activeStore !== store || signal.aborted || writeFailureActive) { return; } writeFailureActive = true; safeNotify(ctx, "Analytics could not save this response cycle; its metrics were dropped.", "warning"); } } function menuSource(generation, signal) { return { path: store?.path ?? path3.join(deps.getAgentDir(), STORAGE_DIRECTORY), async load(range, actionSignal) { assertCurrent(generation, signal); const activeStore = store; if (!activeStore) { return { kind: "unavailable", message: storageFailure ?? unavailableMessage() }; } const snapshot = await activeStore.getSnapshot(range, actionSignal); assertCurrent(generation, signal); return { kind: "ready", snapshot }; }, async clearAll(actionSignal) { assertCurrent(generation, signal); const activeStore = store; if (!activeStore) return { cleanupIncomplete: false }; return activeStore.clearAll(actionSignal); } }; } function assertCurrent(generation, signal) { if (generation !== sessionGeneration || signal.aborted) { throw new DOMException("Analytics interaction replaced", "AbortError"); } } }; } function modelIdentity(ctx, pi) { if (!ctx.model) return void 0; return { provider: ctx.model.provider, model: ctx.model.id, thinkingLevel: pi.getThinkingLevel() }; } function isBuiltinReadTool(pi) { const read = pi.getAllTools().find(({ name }) => name === "read"); return read?.sourceInfo.source === "builtin"; } function availableSkills(pi, systemSkills) { const result = [...systemSkills]; const seen = new Set(result.map(({ name }) => name)); const getCommands = pi.getCommands; for (const command of typeof getCommands === "function" ? getCommands.call(pi) : []) { if (command.source !== "skill" || seen.has(command.name.replace(/^skill:/u, ""))) continue; const name = command.name.replace(/^skill:/u, ""); seen.add(name); result.push({ name, filePath: command.sourceInfo.path }); } return result; } function unavailableMessage() { return [ "Local analytics storage could not be initialized safely.", "Existing files were not replaced.", "No analytics are being collected." ].join("\n"); } function safeNotify(ctx, message, level) { try { ctx.ui.notify(message, level); } catch { } } var analytics_default = createAnalyticsExtension(); export { analytics_default as default }; //# sourceMappingURL=index.ts.map