supipowers 1.5.3 → 2.0.1
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/README.md +14 -8
- package/bin/install.mjs +20 -5
- package/bin/install.ts +95 -0
- package/package.json +8 -4
- package/skills/context-mode/SKILL.md +17 -10
- package/skills/harness/SKILL.md +94 -0
- package/skills/ui-design/SKILL.md +63 -0
- package/skills/ui-design/sub-agent-templates/component-builder.md +29 -0
- package/skills/ui-design/sub-agent-templates/design-critic.md +46 -0
- package/skills/ui-design/sub-agent-templates/pencil/component-builder.md +29 -0
- package/skills/ui-design/sub-agent-templates/pencil/design-critic.md +42 -0
- package/skills/ui-design/sub-agent-templates/pencil/section-assembler.md +27 -0
- package/skills/ui-design/sub-agent-templates/section-assembler.md +27 -0
- package/skills/ultraplan-discover/SKILL.md +96 -0
- package/skills/ultraplan-intake/SKILL.md +89 -0
- package/skills/ultraplan-research/SKILL.md +129 -0
- package/skills/ultraplan-review/SKILL.md +86 -0
- package/skills/ultraplan-review-scope/SKILL.md +111 -0
- package/skills/ultraplan-review-structure/SKILL.md +120 -0
- package/skills/ultraplan-review-tdd/SKILL.md +142 -0
- package/skills/ultraplan-scout/SKILL.md +110 -0
- package/skills/ultraplan-synthesize/SKILL.md +124 -0
- package/src/{quality/ai-session.ts → ai/final-message.ts} +27 -0
- package/src/ai/schema-text.ts +129 -0
- package/src/ai/structured-output.ts +274 -0
- package/src/ai/template.ts +27 -0
- package/src/bootstrap.ts +63 -28
- package/src/commands/agents.ts +131 -42
- package/src/commands/ai-review.ts +251 -30
- package/src/commands/clear.ts +434 -0
- package/src/commands/commit.ts +1 -0
- package/src/commands/config.ts +242 -44
- package/src/commands/context.ts +55 -28
- package/src/commands/doctor.ts +234 -6
- package/src/commands/fix-pr.ts +306 -131
- package/src/commands/generate.ts +111 -21
- package/src/commands/memory.ts +192 -0
- package/src/commands/model-picker.ts +28 -21
- package/src/commands/model.ts +18 -8
- package/src/commands/optimize-context.ts +408 -29
- package/src/commands/plan.ts +2 -0
- package/src/commands/qa.ts +312 -137
- package/src/commands/release.ts +259 -76
- package/src/commands/review.ts +293 -59
- package/src/commands/status.ts +200 -13
- package/src/commands/supi.ts +3 -35
- package/src/commands/ui-design.ts +394 -0
- package/src/commands/ultraplan.ts +1518 -0
- package/src/commands/update.ts +86 -0
- package/src/config/defaults.ts +62 -0
- package/src/config/loader.ts +448 -60
- package/src/config/schema.ts +108 -2
- package/src/context/optimizer.ts +25 -33
- package/src/context/rule-renderer.ts +223 -0
- package/src/context/savings.ts +258 -0
- package/src/context/startup-check.ts +380 -0
- package/src/context/startup-optimizer.ts +355 -0
- package/src/context/tokenignore.ts +146 -0
- package/src/context-mode/cache-handle.ts +49 -0
- package/src/context-mode/cache-preview.ts +71 -0
- package/src/context-mode/cache-store.ts +738 -0
- package/src/context-mode/compressor.ts +131 -26
- package/src/context-mode/dedup.ts +108 -0
- package/src/context-mode/detector.ts +35 -4
- package/src/context-mode/event-extractor.ts +14 -12
- package/src/context-mode/event-store.ts +91 -36
- package/src/context-mode/hooks.ts +798 -56
- package/src/context-mode/knowledge/store.ts +255 -11
- package/src/context-mode/memory-store.ts +325 -0
- package/src/context-mode/metrics-recorder.ts +158 -0
- package/src/context-mode/metrics-store.ts +765 -0
- package/src/context-mode/model.ts +24 -0
- package/src/context-mode/processor-keys.ts +29 -0
- package/src/context-mode/processors/build.ts +66 -0
- package/src/context-mode/processors/docker.ts +57 -0
- package/src/context-mode/processors/git.ts +111 -0
- package/src/context-mode/processors/json.ts +112 -0
- package/src/context-mode/processors/k8s.ts +67 -0
- package/src/context-mode/processors/lint.ts +67 -0
- package/src/context-mode/processors/log.ts +86 -0
- package/src/context-mode/processors/registry.ts +116 -0
- package/src/context-mode/processors/test-runner.ts +102 -0
- package/src/context-mode/processors/types.ts +20 -0
- package/src/context-mode/repomap.ts +400 -0
- package/src/context-mode/routing.ts +97 -24
- package/src/context-mode/sandbox/runners.ts +5 -1
- package/src/context-mode/snapshot-builder.ts +106 -11
- package/src/context-mode/source-hash.ts +173 -0
- package/src/context-mode/tool-name.ts +11 -0
- package/src/context-mode/tools.ts +654 -22
- package/src/context-mode/web/fetcher.ts +31 -12
- package/src/debug/logger.ts +2 -1
- package/src/deps/registry.ts +1 -1
- package/src/discipline/failure-summarizer.ts +170 -0
- package/src/discipline/failure-taxonomy.ts +131 -0
- package/src/discipline/workflow-invariants.ts +125 -0
- package/src/discovery/index.ts +31 -0
- package/src/discovery/lsp.ts +87 -0
- package/src/discovery/rank.ts +144 -0
- package/src/discovery/sources.ts +89 -0
- package/src/discovery/workflow.ts +87 -0
- package/src/docs/contracts.ts +39 -0
- package/src/docs/drift.ts +117 -87
- package/src/fix-pr/assessment.ts +200 -0
- package/src/fix-pr/contracts.ts +47 -0
- package/src/fix-pr/fetch-comments.ts +80 -0
- package/src/fix-pr/prompt-builder.ts +58 -40
- package/src/fix-pr/scripts/exec.ts +34 -0
- package/src/fix-pr/scripts/trigger-review.ts +106 -0
- package/src/fix-pr/scripts/wait-and-check.ts +108 -0
- package/src/fix-pr/types.ts +4 -0
- package/src/git/branch-finish.ts +5 -0
- package/src/git/commit-contract.ts +83 -0
- package/src/git/commit.ts +121 -184
- package/src/git/status.ts +62 -8
- package/src/harness/anti_slop/architecture-parser.ts +210 -0
- package/src/harness/anti_slop/backend-factory.ts +30 -0
- package/src/harness/anti_slop/backend.ts +140 -0
- package/src/harness/anti_slop/desloppify-adapter.ts +319 -0
- package/src/harness/anti_slop/fallow-adapter.ts +305 -0
- package/src/harness/anti_slop/installer.ts +227 -0
- package/src/harness/anti_slop/queue.ts +216 -0
- package/src/harness/anti_slop/recommend.ts +84 -0
- package/src/harness/anti_slop/score.ts +180 -0
- package/src/harness/anti_slop/synthetic-edit-test.ts +128 -0
- package/src/harness/artifacts/agents-md.ts +88 -0
- package/src/harness/artifacts/checks-wiring.ts +57 -0
- package/src/harness/artifacts/docs-tree.ts +79 -0
- package/src/harness/artifacts/lint-configs.ts +136 -0
- package/src/harness/artifacts/review-agents.ts +67 -0
- package/src/harness/bare-entry.ts +108 -0
- package/src/harness/command.ts +1010 -0
- package/src/harness/default-agents/design.md +23 -0
- package/src/harness/default-agents/discover.md +18 -0
- package/src/harness/default-agents/implement.md +24 -0
- package/src/harness/default-agents/plan.md +19 -0
- package/src/harness/default-agents/research.md +21 -0
- package/src/harness/default-agents/validate.md +22 -0
- package/src/harness/gc/reporter.ts +28 -0
- package/src/harness/gc/runner.ts +136 -0
- package/src/harness/hooks/layer-context-inject.ts +155 -0
- package/src/harness/hooks/post-session-sweep.ts +130 -0
- package/src/harness/hooks/pre-edit-dupe-probe.ts +224 -0
- package/src/harness/hooks/register.ts +118 -0
- package/src/harness/model.ts +117 -0
- package/src/harness/pipeline.ts +348 -0
- package/src/harness/project-paths.ts +235 -0
- package/src/harness/stage-runner.ts +107 -0
- package/src/harness/stages/design.ts +386 -0
- package/src/harness/stages/discover.ts +454 -0
- package/src/harness/stages/implement.ts +162 -0
- package/src/harness/stages/plan.ts +335 -0
- package/src/harness/stages/research.ts +263 -0
- package/src/harness/stages/validate.ts +684 -0
- package/src/harness/storage.ts +467 -0
- package/src/harness/tools.ts +426 -0
- package/src/lsp/bridge.ts +56 -95
- package/src/lsp/capabilities.ts +108 -0
- package/src/lsp/contracts.ts +35 -0
- package/src/lsp/detector.ts +8 -12
- package/src/markdown-frontmatter.ts +68 -0
- package/src/mempalace/bridge.ts +135 -0
- package/src/mempalace/config.ts +75 -0
- package/src/mempalace/format.ts +163 -0
- package/src/mempalace/hooks.ts +370 -0
- package/src/mempalace/installer-helper.ts +194 -0
- package/src/mempalace/python/mempalace_bridge.py +440 -0
- package/src/mempalace/runtime.ts +565 -0
- package/src/mempalace/schema.ts +268 -0
- package/src/mempalace/session-summary.ts +198 -0
- package/src/mempalace/tool.ts +186 -0
- package/src/mempalace/uv.ts +256 -0
- package/src/migrate/runner.ts +354 -0
- package/src/planning/approval-flow.ts +206 -9
- package/src/planning/plan-writer-prompt.ts +4 -3
- package/src/planning/planning-ask-tool.ts +39 -0
- package/src/planning/render-markdown.ts +74 -0
- package/src/planning/spec.ts +42 -0
- package/src/planning/system-prompt.ts +11 -8
- package/src/planning/validate.ts +84 -0
- package/src/platform/omp.ts +15 -2
- package/src/platform/system-prompt.ts +37 -0
- package/src/platform/test-utils.ts +3 -0
- package/src/platform/types.ts +6 -1
- package/src/qa/config.ts +12 -6
- package/src/qa/detect-app-type.ts +13 -6
- package/src/qa/matrix.ts +12 -6
- package/src/qa/prompt-builder.ts +28 -30
- package/src/qa/scripts/dev-server-utils.ts +72 -0
- package/src/qa/scripts/run-e2e-tests.ts +226 -0
- package/src/qa/scripts/start-dev-server.ts +138 -0
- package/src/qa/scripts/stop-dev-server.ts +77 -0
- package/src/qa/session.ts +13 -7
- package/src/quality/ai-setup.ts +27 -25
- package/src/quality/contracts.ts +34 -0
- package/src/quality/gates/ai-review.ts +20 -58
- package/src/quality/gates/command.ts +249 -46
- package/src/quality/review-gates.ts +18 -2
- package/src/quality/runner.ts +63 -22
- package/src/quality/schemas.ts +37 -2
- package/src/quality/setup.ts +96 -16
- package/src/release/changelog.ts +1 -1
- package/src/release/channels/custom.ts +13 -3
- package/src/release/channels/types.ts +5 -0
- package/src/release/contracts.ts +90 -0
- package/src/release/executor.ts +122 -45
- package/src/release/prompt.ts +18 -2
- package/src/release/targets.ts +86 -0
- package/src/release/version.ts +96 -71
- package/src/review/agent-loader.ts +221 -109
- package/src/review/fixer.ts +10 -6
- package/src/review/multi-agent-runner.ts +114 -13
- package/src/review/output.ts +12 -139
- package/src/review/runner.ts +12 -6
- package/src/review/scope.ts +144 -24
- package/src/review/types.ts +1 -20
- package/src/review/validator.ts +12 -6
- package/src/storage/fix-pr-sessions.ts +21 -14
- package/src/storage/plans.ts +14 -5
- package/src/storage/qa-sessions.ts +25 -19
- package/src/storage/reliability-metrics.ts +180 -0
- package/src/storage/reports.ts +8 -7
- package/src/storage/review-sessions.ts +55 -20
- package/src/tool-catalog/active-tool-controller.ts +164 -0
- package/src/tool-catalog/active-tool-planner.ts +212 -0
- package/src/tool-catalog/tool-groups.ts +102 -0
- package/src/types.ts +1399 -5
- package/src/ui-design/backend-adapter.ts +78 -0
- package/src/ui-design/backends/local-html.ts +82 -0
- package/src/ui-design/backends/pencil-mcp.ts +111 -0
- package/src/ui-design/components-scanner.ts +124 -0
- package/src/ui-design/config.ts +55 -0
- package/src/ui-design/pen-scanner.ts +95 -0
- package/src/ui-design/pen-selector.ts +72 -0
- package/src/ui-design/prompt-builder.ts +73 -0
- package/src/ui-design/scanner.ts +136 -0
- package/src/ui-design/session.ts +974 -0
- package/src/ui-design/system-prompt.ts +312 -0
- package/src/ui-design/tokens-scanner.ts +181 -0
- package/src/ui-design/types.ts +96 -0
- package/src/ultraplan/agent-catalog.ts +522 -0
- package/src/ultraplan/authoring/agent-catalog.ts +310 -0
- package/src/ultraplan/authoring/authoring-tools.ts +552 -0
- package/src/ultraplan/authoring/command-handlers.ts +339 -0
- package/src/ultraplan/authoring/markdown.ts +510 -0
- package/src/ultraplan/authoring/model.ts +162 -0
- package/src/ultraplan/authoring/pipeline.ts +319 -0
- package/src/ultraplan/authoring/stage-runner.ts +141 -0
- package/src/ultraplan/authoring/stages/approve.ts +249 -0
- package/src/ultraplan/authoring/stages/discover.ts +289 -0
- package/src/ultraplan/authoring/stages/intake.ts +203 -0
- package/src/ultraplan/authoring/stages/research.ts +399 -0
- package/src/ultraplan/authoring/stages/review.ts +333 -0
- package/src/ultraplan/authoring/stages/scout.ts +188 -0
- package/src/ultraplan/authoring/stages/synthesize.ts +348 -0
- package/src/ultraplan/authoring/storage.ts +594 -0
- package/src/ultraplan/authoring/synth-gate.ts +165 -0
- package/src/ultraplan/authoring-draft.ts +653 -0
- package/src/ultraplan/authoring-persist.ts +180 -0
- package/src/ultraplan/authoring-tool.ts +608 -0
- package/src/ultraplan/authoring-wizard.ts +587 -0
- package/src/ultraplan/batch/merge.ts +98 -0
- package/src/ultraplan/batch/planner.ts +150 -0
- package/src/ultraplan/batch/presenter.ts +97 -0
- package/src/ultraplan/batch/storage.ts +420 -0
- package/src/ultraplan/batch/supervisor.ts +317 -0
- package/src/ultraplan/batch/worker.ts +26 -0
- package/src/ultraplan/batch/worktree.ts +110 -0
- package/src/ultraplan/contracts.ts +1593 -0
- package/src/ultraplan/default-agents/authoring/discoverer.md +12 -0
- package/src/ultraplan/default-agents/authoring/intake.md +12 -0
- package/src/ultraplan/default-agents/authoring/planner.md +12 -0
- package/src/ultraplan/default-agents/authoring/researcher.md +12 -0
- package/src/ultraplan/default-agents/authoring/scope-checker.md +12 -0
- package/src/ultraplan/default-agents/authoring/scout.md +12 -0
- package/src/ultraplan/default-agents/authoring/structure-checker.md +12 -0
- package/src/ultraplan/default-agents/authoring/tdd-checker.md +12 -0
- package/src/ultraplan/default-agents/backend-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/backend-executor.md +10 -0
- package/src/ultraplan/default-agents/backend-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/backend-tester.md +10 -0
- package/src/ultraplan/default-agents/frontend-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/frontend-executor.md +10 -0
- package/src/ultraplan/default-agents/frontend-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/frontend-tester.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-executor.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-tester.md +10 -0
- package/src/ultraplan/execution/contract.ts +71 -0
- package/src/ultraplan/execution/policy.ts +217 -0
- package/src/ultraplan/execution/runtime-tools.ts +107 -0
- package/src/ultraplan/execution/session-runner.ts +281 -0
- package/src/ultraplan/next-router.ts +85 -0
- package/src/ultraplan/presenter.ts +359 -0
- package/src/ultraplan/project-paths.ts +342 -0
- package/src/ultraplan/runtime/active-execution.ts +72 -0
- package/src/ultraplan/runtime/apply-mutation.ts +416 -0
- package/src/ultraplan/runtime/blockers.ts +243 -0
- package/src/ultraplan/runtime/hook-bridge.ts +486 -0
- package/src/ultraplan/runtime/launch-context.ts +207 -0
- package/src/ultraplan/runtime/migration.ts +524 -0
- package/src/ultraplan/runtime/normalize.ts +281 -0
- package/src/ultraplan/runtime/proof.ts +260 -0
- package/src/ultraplan/runtime/reducer.ts +416 -0
- package/src/ultraplan/runtime/repair.ts +251 -0
- package/src/ultraplan/runtime/tracker-storage.ts +368 -0
- package/src/ultraplan/session-selection.ts +291 -0
- package/src/ultraplan/storage.ts +374 -0
- package/src/utils/editor.ts +38 -0
- package/src/utils/executable.ts +80 -0
- package/src/utils/paths.ts +1 -20
- package/src/utils/shell.ts +31 -0
- package/src/visual/companion.ts +2 -1
- package/src/visual/scripts/frame-template.html +60 -0
- package/src/visual/scripts/index.js +59 -13
- package/src/visual/scripts/package.json +3 -0
- package/src/visual/start-server.ts +2 -1
- package/src/workspace/git-scope.ts +64 -0
- package/src/workspace/locks.ts +23 -0
- package/src/workspace/package-manager.ts +117 -0
- package/src/workspace/path-mapping.ts +75 -0
- package/src/workspace/project-slug.ts +92 -0
- package/src/workspace/repo-root.ts +137 -0
- package/src/workspace/selector.ts +115 -0
- package/src/workspace/state-paths.ts +118 -0
- package/src/workspace/targets.ts +313 -0
- package/src/fix-pr/scripts/diff-comments.sh +0 -33
- package/src/fix-pr/scripts/fetch-pr-comments.sh +0 -25
- package/src/fix-pr/scripts/trigger-review.sh +0 -36
- package/src/fix-pr/scripts/wait-and-check.sh +0 -37
- package/src/qa/scripts/detect-app-type.sh +0 -68
- package/src/qa/scripts/discover-routes.sh +0 -143
- package/src/qa/scripts/run-e2e-tests.sh +0 -131
- package/src/qa/scripts/start-dev-server.sh +0 -46
- package/src/qa/scripts/stop-dev-server.sh +0 -36
- package/src/review/prompts/fix-output-schema.md +0 -18
- package/src/review/prompts/review-output-schema.md +0 -38
- package/src/review/template.ts +0 -15
- /package/src/{review → ai}/prompts/invalid-output-retry.md +0 -0
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
// src/context-mode/metrics-store.ts
|
|
2
|
+
//
|
|
3
|
+
// Sidecar SQLite store for L1 measurement. Lives alongside `events.db` under
|
|
4
|
+
// `<projectStateDir>/sessions/metrics.db`. Mirrors the `event-store.ts`
|
|
5
|
+
// conventions (DELETE journal mode, WAL sidecar cleanup, idempotent migration)
|
|
6
|
+
// but keeps an independent failure mode so a metrics issue cannot regress
|
|
7
|
+
// event tracking.
|
|
8
|
+
//
|
|
9
|
+
// Hot-path contract (see design spec §3 + plan preamble):
|
|
10
|
+
// - `record(row)` is sync-looking; it appends to an in-memory queue and
|
|
11
|
+
// arms a `queueMicrotask` flush. Tests await `flushPendingForTest()`
|
|
12
|
+
// before reading durable state.
|
|
13
|
+
// - Bursts that arrive in the same microtask coalesce into one transaction.
|
|
14
|
+
// - Failures are swallowed; the in-memory failure counter combined with the
|
|
15
|
+
// persisted column lets `/supi:doctor` surface degraded sessions.
|
|
16
|
+
|
|
17
|
+
import { constants, Database } from "bun:sqlite";
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { isDebugEnabled } from "../debug/logger.js";
|
|
21
|
+
|
|
22
|
+
export const SCHEMA_VERSION = 3;
|
|
23
|
+
export const MAX_ROWS_PER_SESSION = 5000;
|
|
24
|
+
export const RETENTION_DAYS = 7;
|
|
25
|
+
|
|
26
|
+
export type LayerKey = "L1" | "L2" | "L3" | "L4" | "L5" | "L6" | "L7";
|
|
27
|
+
|
|
28
|
+
export type ProcessorKey =
|
|
29
|
+
| "bash"
|
|
30
|
+
| "read"
|
|
31
|
+
| "search"
|
|
32
|
+
| "find"
|
|
33
|
+
| "passthrough"
|
|
34
|
+
| "omp-minimizer"
|
|
35
|
+
| "git"
|
|
36
|
+
| "test"
|
|
37
|
+
| "lint"
|
|
38
|
+
| "build"
|
|
39
|
+
| "k8s"
|
|
40
|
+
| "docker"
|
|
41
|
+
| "log"
|
|
42
|
+
| "json"
|
|
43
|
+
| "dedup"
|
|
44
|
+
| "lazy-tools"
|
|
45
|
+
| "startup-optimizer"
|
|
46
|
+
| "cache-store"
|
|
47
|
+
| "cache-spill"
|
|
48
|
+
| "cache-open"
|
|
49
|
+
| "cache-prune"
|
|
50
|
+
| "cache-clear"
|
|
51
|
+
| null;
|
|
52
|
+
|
|
53
|
+
/** A single metric row pending insertion or read from the metrics table. */
|
|
54
|
+
export interface MetricRow {
|
|
55
|
+
session_id: string;
|
|
56
|
+
ts: number;
|
|
57
|
+
layer: LayerKey;
|
|
58
|
+
tool: string;
|
|
59
|
+
processor: ProcessorKey;
|
|
60
|
+
before_bytes: number;
|
|
61
|
+
after_bytes: number;
|
|
62
|
+
cache_hit: 0 | 1;
|
|
63
|
+
unique_source_hash: string | null;
|
|
64
|
+
context_tokens: number | null;
|
|
65
|
+
context_window: number | null;
|
|
66
|
+
context_percent: number | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface SessionMetaMetrics {
|
|
70
|
+
session_id: string;
|
|
71
|
+
cwd: string;
|
|
72
|
+
started_at: number;
|
|
73
|
+
last_event_at: number;
|
|
74
|
+
row_count: number;
|
|
75
|
+
write_failures: number;
|
|
76
|
+
last_clear_at: number | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ProjectMetaMetrics {
|
|
80
|
+
project_slug: string;
|
|
81
|
+
first_run_notice_shown_at: number | null;
|
|
82
|
+
last_prune_at: number | null;
|
|
83
|
+
last_clear_all_at: number | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface MetricsStoreOptions {
|
|
87
|
+
dbPath: string;
|
|
88
|
+
projectSlug: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const SCHEMA = `
|
|
92
|
+
CREATE TABLE IF NOT EXISTS metrics (
|
|
93
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
94
|
+
session_id TEXT NOT NULL,
|
|
95
|
+
ts INTEGER NOT NULL,
|
|
96
|
+
layer TEXT NOT NULL,
|
|
97
|
+
tool TEXT NOT NULL,
|
|
98
|
+
processor TEXT,
|
|
99
|
+
before_bytes INTEGER NOT NULL,
|
|
100
|
+
after_bytes INTEGER NOT NULL,
|
|
101
|
+
cache_hit INTEGER NOT NULL DEFAULT 0,
|
|
102
|
+
unique_source_hash TEXT,
|
|
103
|
+
context_tokens INTEGER,
|
|
104
|
+
context_window INTEGER,
|
|
105
|
+
context_percent REAL
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_session_ts ON metrics(session_id, ts);
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_layer_tool ON metrics(layer, tool);
|
|
110
|
+
|
|
111
|
+
CREATE TABLE IF NOT EXISTS session_meta_metrics (
|
|
112
|
+
session_id TEXT PRIMARY KEY,
|
|
113
|
+
cwd TEXT NOT NULL,
|
|
114
|
+
started_at INTEGER NOT NULL,
|
|
115
|
+
last_event_at INTEGER NOT NULL,
|
|
116
|
+
row_count INTEGER NOT NULL DEFAULT 0,
|
|
117
|
+
write_failures INTEGER NOT NULL DEFAULT 0,
|
|
118
|
+
last_clear_at INTEGER
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
CREATE TABLE IF NOT EXISTS project_meta_metrics (
|
|
122
|
+
project_slug TEXT PRIMARY KEY,
|
|
123
|
+
first_run_notice_shown_at INTEGER,
|
|
124
|
+
last_prune_at INTEGER,
|
|
125
|
+
last_clear_all_at INTEGER
|
|
126
|
+
);
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
function appendTraceLine(filePath: string, entry: Record<string, unknown>): void {
|
|
130
|
+
try {
|
|
131
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
132
|
+
fs.appendFileSync(
|
|
133
|
+
filePath,
|
|
134
|
+
JSON.stringify({ ts: new Date().toISOString(), ...entry }) + "\n",
|
|
135
|
+
);
|
|
136
|
+
} catch {
|
|
137
|
+
// Trace logging must never block the primary flow.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class MetricsStore {
|
|
142
|
+
readonly #dbPath: string;
|
|
143
|
+
readonly #projectSlug: string;
|
|
144
|
+
#db: Database;
|
|
145
|
+
#closed = false;
|
|
146
|
+
|
|
147
|
+
// Microtask-batched write queue + a per-burst flush promise so tests can await it.
|
|
148
|
+
#queue: MetricRow[] = [];
|
|
149
|
+
#flushScheduled = false;
|
|
150
|
+
#flushPromise: Promise<void> | null = null;
|
|
151
|
+
#flushResolve: (() => void) | null = null;
|
|
152
|
+
|
|
153
|
+
// Per-session in-memory write failures, combined with persisted column on read.
|
|
154
|
+
#inMemoryFailures = new Map<string, number>();
|
|
155
|
+
|
|
156
|
+
// Per-instance flush counter — used by tests to confirm batching works.
|
|
157
|
+
#flushCount = 0;
|
|
158
|
+
|
|
159
|
+
// Trace path is computed once on init; null when SUPI_DEBUG is unset.
|
|
160
|
+
#tracePath: string | null = null;
|
|
161
|
+
|
|
162
|
+
// Prepared statements (lazy, initialized on first use).
|
|
163
|
+
#insertStmt: ReturnType<Database["prepare"]> | null = null;
|
|
164
|
+
#upsertSessionStmt: ReturnType<Database["prepare"]> | null = null;
|
|
165
|
+
#incRowCountStmt: ReturnType<Database["prepare"]> | null = null;
|
|
166
|
+
#evictOldestStmt: ReturnType<Database["prepare"]> | null = null;
|
|
167
|
+
#updateRowCountStmt: ReturnType<Database["prepare"]> | null = null;
|
|
168
|
+
#flushTransaction: ((rows: MetricRow[]) => void) | null = null;
|
|
169
|
+
|
|
170
|
+
constructor(opts: MetricsStoreOptions) {
|
|
171
|
+
this.#dbPath = opts.dbPath;
|
|
172
|
+
this.#projectSlug = opts.projectSlug;
|
|
173
|
+
this.#db = new Database(opts.dbPath);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Absolute path to the on-disk SQLite file. */
|
|
177
|
+
get dbPath(): string {
|
|
178
|
+
return this.#dbPath;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Project slug supplied at construction (primary key into `project_meta_metrics`). */
|
|
182
|
+
get projectSlug(): string {
|
|
183
|
+
return this.#projectSlug;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Number of times the microtask flush function has run. Test-only. */
|
|
187
|
+
get flushCountForTest(): number {
|
|
188
|
+
return this.#flushCount;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Trace file path (null when `SUPI_DEBUG` is unset). Test-only. */
|
|
192
|
+
get tracePathForTest(): string | null {
|
|
193
|
+
return this.#tracePath;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
init(): void {
|
|
197
|
+
try {
|
|
198
|
+
this.#ensureDeleteJournalMode();
|
|
199
|
+
// CREATE TABLE IF NOT EXISTS guards every statement in SCHEMA, so it is
|
|
200
|
+
// safe to run before #migrate(). Running the schema first means v1→vN
|
|
201
|
+
// data fixups can assume their target tables exist on every code path.
|
|
202
|
+
this.#db.exec(SCHEMA);
|
|
203
|
+
this.#migrate();
|
|
204
|
+
this.#prepareStatements();
|
|
205
|
+
|
|
206
|
+
if (isDebugEnabled()) {
|
|
207
|
+
this.#tracePath = path.join(path.dirname(this.#dbPath), "metrics-trace.jsonl");
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
this.close();
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Journal mode + WAL handling (mirrors event-store.ts) ────────────
|
|
216
|
+
|
|
217
|
+
#ensureDeleteJournalMode(): void {
|
|
218
|
+
const journalMode = this.#getJournalMode();
|
|
219
|
+
if (journalMode === "delete") return;
|
|
220
|
+
|
|
221
|
+
if (journalMode === "wal") {
|
|
222
|
+
this.#cleanupWalSidecars();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
this.#db.exec("PRAGMA journal_mode = DELETE;");
|
|
227
|
+
} catch {
|
|
228
|
+
// Older WAL-backed databases can stay on WAL for this process.
|
|
229
|
+
// close() still checkpoints them so teardown and the next reopen succeed.
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#getJournalMode(): string {
|
|
234
|
+
const { journal_mode } = this.#db.prepare("PRAGMA journal_mode").get() as {
|
|
235
|
+
journal_mode: string;
|
|
236
|
+
};
|
|
237
|
+
return journal_mode.toLowerCase();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#cleanupWalSidecars(): void {
|
|
241
|
+
try {
|
|
242
|
+
this.#db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);
|
|
243
|
+
this.#db.exec("PRAGMA wal_checkpoint(TRUNCATE);");
|
|
244
|
+
} catch {
|
|
245
|
+
// Best effort only: close() still releases the handle in finally.
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Schema migration ─────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
#migrate(): void {
|
|
252
|
+
const { user_version } = this.#db.prepare("PRAGMA user_version").get() as {
|
|
253
|
+
user_version: number;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
if (user_version === SCHEMA_VERSION) return;
|
|
257
|
+
if (user_version > SCHEMA_VERSION) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`metrics-store: unknown schema version ${user_version} (max supported: ${SCHEMA_VERSION})`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// v0 \u2192 v1: schema-only bump. CREATE TABLE IF NOT EXISTS already ran in
|
|
264
|
+
// init(); no data fixup is required.
|
|
265
|
+
|
|
266
|
+
// v1 \u2192 v2: OMP 14.5.12 renamed the canonical "grep" tool key to "search".
|
|
267
|
+
// Rows persisted under the old name still report `tool='grep'` and
|
|
268
|
+
// `processor='grep'` \u2014 values the type system no longer admits and which
|
|
269
|
+
// /supi:doctor / per-processor breakdowns would silently bucket under a
|
|
270
|
+
// typed-impossible key. Source hashes baked the legacy `grep:` prefix into
|
|
271
|
+
// SHA256, so they cannot collide with post-rename `search:`-prefixed
|
|
272
|
+
// hashes; the privacy contract forbids reconstructing the original path
|
|
273
|
+
// or pattern, so re-hashing is impossible. NULL is the right substitute:
|
|
274
|
+
// `getUniqueSourceShare` already excludes NULL hashes from numerator and
|
|
275
|
+
// denominator, and a NULL hash never collides with new dedup state.
|
|
276
|
+
if (user_version < 2) {
|
|
277
|
+
const tx = this.#db.transaction(() => {
|
|
278
|
+
this.#db.exec(
|
|
279
|
+
`UPDATE metrics SET unique_source_hash = NULL WHERE tool = 'grep'`,
|
|
280
|
+
);
|
|
281
|
+
this.#db.exec(
|
|
282
|
+
`UPDATE metrics SET tool = 'search' WHERE tool = 'grep'`,
|
|
283
|
+
);
|
|
284
|
+
this.#db.exec(
|
|
285
|
+
`UPDATE metrics SET processor = 'search' WHERE processor = 'grep'`,
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
tx();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// v2 → v3: OMP 14.6.0 renamed `search`/`find` tool params from
|
|
292
|
+
// `path: string` / `pattern: string` to `paths: string[]`. Source hashes
|
|
293
|
+
// computed under the old salts (`search:<single-path>:<slug>`,
|
|
294
|
+
// `find:<pattern>:<slug>`) cannot collide with the new salts
|
|
295
|
+
// (`search:<joined-paths>:<pattern>:<slug>`, `find:<joined-paths>:<slug>`)
|
|
296
|
+
// because the input strings differ; but the privacy contract forbids
|
|
297
|
+
// re-hashing, so NULL is the correct substitute. `getUniqueSourceShare`
|
|
298
|
+
// already excludes NULLs from numerator/denominator. Newly-recorded rows
|
|
299
|
+
// from this point on use the post-rename salt.
|
|
300
|
+
if (user_version < 3) {
|
|
301
|
+
this.#db.exec(
|
|
302
|
+
`UPDATE metrics SET unique_source_hash = NULL WHERE tool IN ('search', 'find')`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
this.#db.exec(`PRAGMA user_version = ${SCHEMA_VERSION};`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── Prepared statements ─────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
#prepareStatements(): void {
|
|
312
|
+
this.#insertStmt = this.#db.prepare(
|
|
313
|
+
`INSERT INTO metrics
|
|
314
|
+
(session_id, ts, layer, tool, processor, before_bytes, after_bytes,
|
|
315
|
+
cache_hit, unique_source_hash, context_tokens, context_window, context_percent)
|
|
316
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
this.#upsertSessionStmt = this.#db.prepare(
|
|
320
|
+
`INSERT INTO session_meta_metrics
|
|
321
|
+
(session_id, cwd, started_at, last_event_at, row_count, write_failures, last_clear_at)
|
|
322
|
+
VALUES (?, ?, ?, ?, 0, 0, NULL)
|
|
323
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
324
|
+
last_event_at = excluded.last_event_at`,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
this.#incRowCountStmt = this.#db.prepare(
|
|
328
|
+
`UPDATE session_meta_metrics
|
|
329
|
+
SET row_count = row_count + ?, last_event_at = ?
|
|
330
|
+
WHERE session_id = ?`,
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
this.#updateRowCountStmt = this.#db.prepare(
|
|
334
|
+
`UPDATE session_meta_metrics
|
|
335
|
+
SET row_count = ?
|
|
336
|
+
WHERE session_id = ?`,
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
this.#evictOldestStmt = this.#db.prepare(
|
|
340
|
+
`DELETE FROM metrics
|
|
341
|
+
WHERE id IN (
|
|
342
|
+
SELECT id FROM metrics
|
|
343
|
+
WHERE session_id = ?
|
|
344
|
+
ORDER BY id ASC
|
|
345
|
+
LIMIT ?
|
|
346
|
+
)`,
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
this.#flushTransaction = this.#db.transaction((rows: MetricRow[]) => {
|
|
350
|
+
const perSession = new Map<string, number>();
|
|
351
|
+
const lastTsBySession = new Map<string, number>();
|
|
352
|
+
|
|
353
|
+
for (const row of rows) {
|
|
354
|
+
this.#insertStmt!.run(
|
|
355
|
+
row.session_id,
|
|
356
|
+
row.ts,
|
|
357
|
+
row.layer,
|
|
358
|
+
row.tool,
|
|
359
|
+
row.processor,
|
|
360
|
+
row.before_bytes,
|
|
361
|
+
row.after_bytes,
|
|
362
|
+
row.cache_hit,
|
|
363
|
+
row.unique_source_hash,
|
|
364
|
+
row.context_tokens,
|
|
365
|
+
row.context_window,
|
|
366
|
+
row.context_percent,
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
perSession.set(row.session_id, (perSession.get(row.session_id) ?? 0) + 1);
|
|
370
|
+
const prev = lastTsBySession.get(row.session_id);
|
|
371
|
+
if (prev === undefined || row.ts > prev) {
|
|
372
|
+
lastTsBySession.set(row.session_id, row.ts);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Flush in-memory failures opportunistically: if persistence is healthy,
|
|
377
|
+
// promote any pending in-memory counts into the column and clear the
|
|
378
|
+
// in-memory entries so the doctor surfaces a stable number.
|
|
379
|
+
const failures = [...this.#inMemoryFailures.entries()];
|
|
380
|
+
for (const [session, count] of failures) {
|
|
381
|
+
this.#db.prepare(
|
|
382
|
+
`UPDATE session_meta_metrics
|
|
383
|
+
SET write_failures = write_failures + ?
|
|
384
|
+
WHERE session_id = ?`,
|
|
385
|
+
).run(count, session);
|
|
386
|
+
}
|
|
387
|
+
this.#inMemoryFailures.clear();
|
|
388
|
+
|
|
389
|
+
// Bump row_count and last_event_at per session.
|
|
390
|
+
for (const [session, count] of perSession) {
|
|
391
|
+
const ts = lastTsBySession.get(session) ?? Date.now();
|
|
392
|
+
this.#incRowCountStmt!.run(count, ts, session);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Eviction: per-session row_count cap. Read fresh count from DB to
|
|
396
|
+
// accommodate inserts that arrived in a separate transaction.
|
|
397
|
+
for (const [session] of perSession) {
|
|
398
|
+
const row = this.#db.prepare(
|
|
399
|
+
`SELECT row_count FROM session_meta_metrics WHERE session_id = ?`,
|
|
400
|
+
).get(session) as { row_count: number } | undefined;
|
|
401
|
+
if (!row) continue;
|
|
402
|
+
const overflow = row.row_count - MAX_ROWS_PER_SESSION;
|
|
403
|
+
if (overflow > 0) {
|
|
404
|
+
this.#evictOldestStmt!.run(session, overflow);
|
|
405
|
+
this.#updateRowCountStmt!.run(MAX_ROWS_PER_SESSION, session);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ── Session metadata ────────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
upsertSession(opts: { session_id: string; cwd: string; ts?: number }): void {
|
|
414
|
+
const ts = opts.ts ?? Date.now();
|
|
415
|
+
this.#upsertSessionStmt!.run(opts.session_id, opts.cwd, ts, ts);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
getSessionMeta(sessionId: string): SessionMetaMetrics | null {
|
|
419
|
+
const row = this.#db.prepare(
|
|
420
|
+
`SELECT session_id, cwd, started_at, last_event_at, row_count, write_failures, last_clear_at
|
|
421
|
+
FROM session_meta_metrics
|
|
422
|
+
WHERE session_id = ?`,
|
|
423
|
+
).get(sessionId) as SessionMetaMetrics | undefined;
|
|
424
|
+
return row ?? null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
getProjectMeta(projectSlug: string): ProjectMetaMetrics | null {
|
|
428
|
+
const row = this.#db.prepare(
|
|
429
|
+
`SELECT project_slug, first_run_notice_shown_at, last_prune_at, last_clear_all_at
|
|
430
|
+
FROM project_meta_metrics
|
|
431
|
+
WHERE project_slug = ?`,
|
|
432
|
+
).get(projectSlug) as ProjectMetaMetrics | undefined;
|
|
433
|
+
return row ?? null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
setFirstRunNoticeShown(projectSlug: string, ts?: number): void {
|
|
437
|
+
const value = ts ?? Date.now();
|
|
438
|
+
this.#db.prepare(
|
|
439
|
+
`INSERT INTO project_meta_metrics (project_slug, first_run_notice_shown_at)
|
|
440
|
+
VALUES (?, ?)
|
|
441
|
+
ON CONFLICT(project_slug) DO UPDATE SET
|
|
442
|
+
first_run_notice_shown_at = excluded.first_run_notice_shown_at`,
|
|
443
|
+
).run(projectSlug, value);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ── Hot-path write API ───────────────────────────────────────────────
|
|
447
|
+
|
|
448
|
+
record(row: MetricRow): void {
|
|
449
|
+
if (this.#closed) {
|
|
450
|
+
this.#bumpInMemoryFailure(row.session_id);
|
|
451
|
+
this.#trace("metrics_record_after_close", {
|
|
452
|
+
session_id: row.session_id,
|
|
453
|
+
tool: row.tool,
|
|
454
|
+
});
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
this.#queue.push(row);
|
|
459
|
+
if (!this.#flushScheduled) {
|
|
460
|
+
this.#flushScheduled = true;
|
|
461
|
+
this.#flushPromise = new Promise<void>((resolve) => {
|
|
462
|
+
this.#flushResolve = resolve;
|
|
463
|
+
});
|
|
464
|
+
queueMicrotask(() => this.#flush());
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Drain the pending microtask flush. Tests await this before reading state. */
|
|
469
|
+
async flushPendingForTest(): Promise<void> {
|
|
470
|
+
while (this.#flushScheduled) {
|
|
471
|
+
const promise = this.#flushPromise;
|
|
472
|
+
if (promise) await promise;
|
|
473
|
+
else break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
#flush(): void {
|
|
478
|
+
const rows = this.#queue;
|
|
479
|
+
this.#queue = [];
|
|
480
|
+
this.#flushScheduled = false;
|
|
481
|
+
this.#flushCount += 1;
|
|
482
|
+
|
|
483
|
+
const resolve = this.#flushResolve;
|
|
484
|
+
this.#flushResolve = null;
|
|
485
|
+
this.#flushPromise = null;
|
|
486
|
+
|
|
487
|
+
if (rows.length === 0) {
|
|
488
|
+
if (resolve) resolve();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
let attempts = 0;
|
|
493
|
+
while (true) {
|
|
494
|
+
try {
|
|
495
|
+
this.#flushTransaction!(rows);
|
|
496
|
+
for (const row of rows) {
|
|
497
|
+
this.#trace("metrics_record_flushed", {
|
|
498
|
+
tool: row.tool,
|
|
499
|
+
layer: row.layer,
|
|
500
|
+
before_bytes: row.before_bytes,
|
|
501
|
+
after_bytes: row.after_bytes,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
break;
|
|
505
|
+
} catch (err) {
|
|
506
|
+
const code = (err as NodeJS.ErrnoException)?.code;
|
|
507
|
+
if (attempts === 0 && code === "SQLITE_BUSY") {
|
|
508
|
+
attempts += 1;
|
|
509
|
+
// Linear backoff (sleep ~5ms) before the single retry.
|
|
510
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
for (const row of rows) {
|
|
515
|
+
this.#bumpInMemoryFailure(row.session_id);
|
|
516
|
+
this.#trace("metrics_record_failed", {
|
|
517
|
+
tool: row.tool,
|
|
518
|
+
error: (err as Error)?.message ?? String(err),
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (resolve) resolve();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
#bumpInMemoryFailure(sessionId: string): void {
|
|
529
|
+
this.#inMemoryFailures.set(sessionId, (this.#inMemoryFailures.get(sessionId) ?? 0) + 1);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
#trace(event: string, data: Record<string, unknown>): void {
|
|
533
|
+
if (!this.#tracePath) return;
|
|
534
|
+
appendTraceLine(this.#tracePath, { event, ...data });
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ── Read accessors ───────────────────────────────────────────────────
|
|
538
|
+
|
|
539
|
+
getSessionTotals(sessionId: string): {
|
|
540
|
+
beforeBytes: number;
|
|
541
|
+
afterBytes: number;
|
|
542
|
+
saved: number;
|
|
543
|
+
rowCount: number;
|
|
544
|
+
} {
|
|
545
|
+
const row = this.#db.prepare(
|
|
546
|
+
`SELECT
|
|
547
|
+
COALESCE(SUM(before_bytes), 0) AS beforeBytes,
|
|
548
|
+
COALESCE(SUM(after_bytes), 0) AS afterBytes,
|
|
549
|
+
COALESCE(SUM(before_bytes - after_bytes), 0) AS saved,
|
|
550
|
+
COUNT(*) AS rowCount
|
|
551
|
+
FROM metrics
|
|
552
|
+
WHERE session_id = ?`,
|
|
553
|
+
).get(sessionId) as {
|
|
554
|
+
beforeBytes: number;
|
|
555
|
+
afterBytes: number;
|
|
556
|
+
saved: number;
|
|
557
|
+
rowCount: number;
|
|
558
|
+
};
|
|
559
|
+
return row;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
getTopProcessors(
|
|
563
|
+
sessionId: string,
|
|
564
|
+
limit: number,
|
|
565
|
+
): Array<{ processor: string; saved: number; calls: number }> {
|
|
566
|
+
return this.#db.prepare(
|
|
567
|
+
`SELECT
|
|
568
|
+
COALESCE(processor, tool) AS processor,
|
|
569
|
+
COALESCE(SUM(before_bytes - after_bytes), 0) AS saved,
|
|
570
|
+
COUNT(*) AS calls
|
|
571
|
+
FROM metrics
|
|
572
|
+
WHERE session_id = ?
|
|
573
|
+
GROUP BY COALESCE(processor, tool)
|
|
574
|
+
ORDER BY saved DESC
|
|
575
|
+
LIMIT ?`,
|
|
576
|
+
).all(sessionId, limit) as Array<{ processor: string; saved: number; calls: number }>;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
getPerLayer(
|
|
580
|
+
sessionId: string,
|
|
581
|
+
): Array<{ layer: string; saved: number; rows: number }> {
|
|
582
|
+
return this.#db.prepare(
|
|
583
|
+
`SELECT
|
|
584
|
+
layer,
|
|
585
|
+
COALESCE(SUM(before_bytes - after_bytes), 0) AS saved,
|
|
586
|
+
COUNT(*) AS rows
|
|
587
|
+
FROM metrics
|
|
588
|
+
WHERE session_id = ?
|
|
589
|
+
GROUP BY layer
|
|
590
|
+
ORDER BY layer ASC`,
|
|
591
|
+
).all(sessionId) as Array<{ layer: string; saved: number; rows: number }>;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Unique-source share = COUNT(DISTINCT unique_source_hash) /
|
|
596
|
+
* COUNT(unique_source_hash).
|
|
597
|
+
* Null hashes are excluded from both numerator and denominator so that
|
|
598
|
+
* untracked sources do not skew the rot signal. Returns 0 when no rows
|
|
599
|
+
* have a non-null hash.
|
|
600
|
+
*/
|
|
601
|
+
getUniqueSourceShare(sessionId: string): number {
|
|
602
|
+
const row = this.#db.prepare(
|
|
603
|
+
`SELECT
|
|
604
|
+
COUNT(DISTINCT unique_source_hash) AS distinctCount,
|
|
605
|
+
COUNT(unique_source_hash) AS totalCount
|
|
606
|
+
FROM metrics
|
|
607
|
+
WHERE session_id = ?`,
|
|
608
|
+
).get(sessionId) as { distinctCount: number; totalCount: number };
|
|
609
|
+
|
|
610
|
+
if (row.totalCount === 0) return 0;
|
|
611
|
+
return row.distinctCount / row.totalCount;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** Combined in-memory + persisted write-failure count for the session. */
|
|
615
|
+
getSessionWriteFailures(sessionId: string): number {
|
|
616
|
+
const inMemory = this.#inMemoryFailures.get(sessionId) ?? 0;
|
|
617
|
+
let persisted = 0;
|
|
618
|
+
try {
|
|
619
|
+
persisted = this.getSessionMeta(sessionId)?.write_failures ?? 0;
|
|
620
|
+
} catch {
|
|
621
|
+
// DB may be closed; in-memory counter still surfaces the failure.
|
|
622
|
+
}
|
|
623
|
+
return persisted + inMemory;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ── Maintenance ─────────────────────────────────────────────────────
|
|
627
|
+
|
|
628
|
+
pruneOldSessions(retentionDays = RETENTION_DAYS, now = Date.now()): number {
|
|
629
|
+
const cutoff = now - retentionDays * 24 * 60 * 60 * 1000;
|
|
630
|
+
|
|
631
|
+
const oldSessions = this.#db.prepare(
|
|
632
|
+
`SELECT session_id FROM session_meta_metrics WHERE started_at < ?`,
|
|
633
|
+
).all(cutoff) as Array<{ session_id: string }>;
|
|
634
|
+
|
|
635
|
+
if (oldSessions.length > 0) {
|
|
636
|
+
const placeholders = oldSessions.map(() => "?").join(",");
|
|
637
|
+
const ids = oldSessions.map((r) => r.session_id);
|
|
638
|
+
this.#db.prepare(
|
|
639
|
+
`DELETE FROM metrics WHERE session_id IN (${placeholders})`,
|
|
640
|
+
).run(...ids);
|
|
641
|
+
this.#db.prepare(
|
|
642
|
+
`DELETE FROM session_meta_metrics WHERE session_id IN (${placeholders})`,
|
|
643
|
+
).run(...ids);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
this.#db.prepare(
|
|
647
|
+
`INSERT INTO project_meta_metrics (project_slug, last_prune_at)
|
|
648
|
+
VALUES (?, ?)
|
|
649
|
+
ON CONFLICT(project_slug) DO UPDATE SET
|
|
650
|
+
last_prune_at = excluded.last_prune_at`,
|
|
651
|
+
).run(this.#projectSlug, now);
|
|
652
|
+
|
|
653
|
+
return oldSessions.length;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// \u2500\u2500 Clearing (used by /supi:clear) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
657
|
+
|
|
658
|
+
/** Delete metrics rows for a single session, reset row_count, stamp last_clear_at. */
|
|
659
|
+
clearSession(sessionId: string, now = Date.now()): void {
|
|
660
|
+
const tx = this.#db.transaction(() => {
|
|
661
|
+
this.#db.prepare(`DELETE FROM metrics WHERE session_id = ?`).run(sessionId);
|
|
662
|
+
this.#db.prepare(
|
|
663
|
+
`UPDATE session_meta_metrics
|
|
664
|
+
SET row_count = 0, last_clear_at = ?
|
|
665
|
+
WHERE session_id = ?`,
|
|
666
|
+
).run(now, sessionId);
|
|
667
|
+
});
|
|
668
|
+
tx();
|
|
669
|
+
this.#inMemoryFailures.delete(sessionId);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** Returns one row per session in this project, ordered by started_at desc. */
|
|
673
|
+
listSessions(
|
|
674
|
+
_projectSlug: string,
|
|
675
|
+
): Array<{ session_id: string; row_count: number; started_at: number; cwd: string }> {
|
|
676
|
+
return this.#db
|
|
677
|
+
.prepare(
|
|
678
|
+
`SELECT session_id, row_count, started_at, cwd
|
|
679
|
+
FROM session_meta_metrics
|
|
680
|
+
ORDER BY started_at DESC`,
|
|
681
|
+
)
|
|
682
|
+
.all() as Array<{
|
|
683
|
+
session_id: string;
|
|
684
|
+
row_count: number;
|
|
685
|
+
started_at: number;
|
|
686
|
+
cwd: string;
|
|
687
|
+
}>;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** Delete all metrics rows in the project, reset every session's row_count,
|
|
691
|
+
* preserve session metadata (`started_at`, `cwd`), and stamp
|
|
692
|
+
* `project_meta_metrics.last_clear_all_at`.
|
|
693
|
+
*
|
|
694
|
+
* The `projectSlug` parameter is accepted for API symmetry with the rest of
|
|
695
|
+
* the project-meta accessors, but the store always operates on its bound
|
|
696
|
+
* slug. Callers do not need to pass anything special: the store's own
|
|
697
|
+
* projectSlug is the source of truth.
|
|
698
|
+
*/
|
|
699
|
+
clearProject(_projectSlug?: string, now = Date.now()): void {
|
|
700
|
+
const slug = this.#projectSlug;
|
|
701
|
+
const tx = this.#db.transaction(() => {
|
|
702
|
+
this.#db.exec(`DELETE FROM metrics`);
|
|
703
|
+
this.#db.exec(`UPDATE session_meta_metrics SET row_count = 0`);
|
|
704
|
+
this.#db.prepare(
|
|
705
|
+
`INSERT INTO project_meta_metrics (project_slug, last_clear_all_at)
|
|
706
|
+
VALUES (?, ?)
|
|
707
|
+
ON CONFLICT(project_slug) DO UPDATE SET
|
|
708
|
+
last_clear_all_at = excluded.last_clear_all_at`,
|
|
709
|
+
).run(slug, now);
|
|
710
|
+
});
|
|
711
|
+
tx();
|
|
712
|
+
this.#inMemoryFailures.clear();
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
close(): void {
|
|
716
|
+
if (this.#closed) return;
|
|
717
|
+
|
|
718
|
+
// Drain any queued writes synchronously before closing the DB. `record()`
|
|
719
|
+
// schedules flushes via `queueMicrotask`, so a `record()` followed by an
|
|
720
|
+
// immediate `close()` (e.g. session_shutdown after the last tool_result)
|
|
721
|
+
// would otherwise lose pending rows when the microtask later runs against
|
|
722
|
+
// a closed handle. `#flush()` already swallows write errors and bumps the
|
|
723
|
+
// in-memory failure counter, so this drain cannot throw.
|
|
724
|
+
if (this.#queue.length > 0) {
|
|
725
|
+
this.#flush();
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
this.#closed = true;
|
|
729
|
+
|
|
730
|
+
try {
|
|
731
|
+
try {
|
|
732
|
+
if (this.#getJournalMode() === "wal") {
|
|
733
|
+
this.#cleanupWalSidecars();
|
|
734
|
+
}
|
|
735
|
+
} catch {
|
|
736
|
+
// The DB path may already be gone during teardown.
|
|
737
|
+
}
|
|
738
|
+
} finally {
|
|
739
|
+
this.#db.close();
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// ── Module-level singleton + test seam ─────────────────────────────────
|
|
745
|
+
|
|
746
|
+
let _metricsStoreRef: MetricsStore | null = null;
|
|
747
|
+
|
|
748
|
+
/** Return the active metrics store, or `null` when context-mode is disabled. */
|
|
749
|
+
export function getMetricsStore(): MetricsStore | null {
|
|
750
|
+
return _metricsStoreRef;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Test-only setter. Production code wires the ref through the same path from
|
|
755
|
+
* inside `registerContextModeHooks`. The double-underscore prefix marks this
|
|
756
|
+
* as a private API; do **not** call it from product code.
|
|
757
|
+
*/
|
|
758
|
+
export function __setMetricsStoreForTest(store: MetricsStore | null): void {
|
|
759
|
+
_metricsStoreRef = store;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/** Reset module-level state. Intended for `_resetCache()` in tests. */
|
|
763
|
+
export function _resetMetricsStoreCache(): void {
|
|
764
|
+
_metricsStoreRef = null;
|
|
765
|
+
}
|