opencode-cache-hit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +67 -0
- package/CONTRIBUTING.md +73 -0
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/README.zh-CN.md +186 -0
- package/cache-hit.config.example.json +22 -0
- package/docs/README.md +8 -0
- package/docs/assets/cache-hit-panel.png +0 -0
- package/docs/en/design.md +228 -0
- package/docs/en/timeline.md +253 -0
- package/docs/zh-CN/design.md +229 -0
- package/docs/zh-CN/timeline.md +301 -0
- package/index.tsx +1 -0
- package/package.json +71 -0
- package/scripts/README.md +39 -0
- package/scripts/plot-hit-rate.ts +222 -0
- package/src/agents-view.tsx +55 -0
- package/src/cache-hit-rows.tsx +68 -0
- package/src/child-session-sync.ts +93 -0
- package/src/format-cache-ui.ts +21 -0
- package/src/format-cost.ts +90 -0
- package/src/format-tokens.ts +5 -0
- package/src/i18n.ts +82 -0
- package/src/load-config.ts +29 -0
- package/src/main-session-view.tsx +76 -0
- package/src/message-timing.ts +35 -0
- package/src/plugin-config.ts +116 -0
- package/src/plugin.tsx +33 -0
- package/src/session-list.ts +11 -0
- package/src/sidebar-host.tsx +121 -0
- package/src/stats.ts +141 -0
- package/src/timeline/collector.ts +156 -0
- package/src/timeline/records.ts +73 -0
- package/src/timeline/rotation.ts +47 -0
- package/src/timeline/types.ts +22 -0
- package/src/timeline/writer.ts +134 -0
- package/src/tui-panel/README.md +78 -0
- package/src/tui-panel/README.zh-CN.md +76 -0
- package/src/tui-panel/components.tsx +163 -0
- package/src/tui-panel/index.ts +41 -0
- package/src/tui-panel/layout.ts +107 -0
- package/src/tui-panel/palette.ts +93 -0
- package/src/tui-panel/use-panel-layout.ts +69 -0
- package/src/types.ts +71 -0
- package/src/use-cache-hit-metrics.ts +103 -0
- package/src/version.ts +1 -0
- package/src/widget.tsx +117 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Design
|
|
2
|
+
|
|
3
|
+
For **maintainers** of this plugin: data flow, refresh rules, and boundaries vs [opencode-visual-cache](https://www.npmjs.com/package/opencode-visual-cache). User setup: [README.md](../../README.md).
|
|
4
|
+
|
|
5
|
+
## Relationship to opencode-visual-cache
|
|
6
|
+
|
|
7
|
+
This is a **separate project**, not maintained by the visual-cache authors. It **borrows heavily** from [opencode-visual-cache](https://www.npmjs.com/package/opencode-visual-cache):
|
|
8
|
+
|
|
9
|
+
- **Sidebar panel layout** (`src/tui-panel/`): borders, hit bar, foldable sections, theme palette mapping
|
|
10
|
+
- **Layout**: main session block always visible; collapsible **Agents** section when sub-agents exist
|
|
11
|
+
- **Hit rate convention**: session totals align with visual-cache’s `cache.read / (cache.read + input)` rule
|
|
12
|
+
|
|
13
|
+
**Division of labor**: visual-cache focuses on **main-session context / token distribution estimates**; cache-hit focuses on **per-turn metrics, cost, and sub-agent aggregation**. Install both for a complete picture.
|
|
14
|
+
|
|
15
|
+
See the [documentation index](../README.md).
|
|
16
|
+
|
|
17
|
+
## Product boundary
|
|
18
|
+
|
|
19
|
+
```mermaid
|
|
20
|
+
flowchart TB
|
|
21
|
+
subgraph ours [opencode-cache-hit]
|
|
22
|
+
DISC[Child session discovery]
|
|
23
|
+
AGG[Per-message token/cost aggregation]
|
|
24
|
+
UI[Cache Hit sidebar]
|
|
25
|
+
end
|
|
26
|
+
subgraph ref [opencode-visual-cache reference]
|
|
27
|
+
EST[Context token estimate]
|
|
28
|
+
VCUI[Token Cache sidebar]
|
|
29
|
+
end
|
|
30
|
+
OC[OpenCode session / messages API] --> DISC
|
|
31
|
+
OC --> AGG
|
|
32
|
+
AGG --> UI
|
|
33
|
+
OC -.->|UI patterns only| EST
|
|
34
|
+
EST -.-> VCUI
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
| Role | Responsibility |
|
|
38
|
+
|------|----------------|
|
|
39
|
+
| **cache-hit** | Sub-agent discovery and rollup; cache, tokens, cost for main/child sessions |
|
|
40
|
+
| **visual-cache** | Main-session context and estimates; not maintained in this repo |
|
|
41
|
+
| **Default** | Main block + foldable **Agents** (child-session totals) |
|
|
42
|
+
|
|
43
|
+
## Cost model
|
|
44
|
+
|
|
45
|
+
- OpenCode: `msg.cost` accumulates assistant messages using **USD** list prices from `opencode.json`.
|
|
46
|
+
- Plugin: `createCostFormatter(loadPluginConfig().cost)`; default `costUnit: USD` → `currency: CNY`, `rate: 7.2`.
|
|
47
|
+
- Config file: `cache-hit.config.json` at plugin root (`load-config.ts`); defaults in `plugin-config.ts`.
|
|
48
|
+
|
|
49
|
+
## Runtime architecture
|
|
50
|
+
|
|
51
|
+
```mermaid
|
|
52
|
+
sequenceDiagram
|
|
53
|
+
participant Slot as sidebar_content slot
|
|
54
|
+
participant Host as sidebar-host
|
|
55
|
+
participant API as OpenCode API
|
|
56
|
+
participant W as widget + metrics
|
|
57
|
+
|
|
58
|
+
Slot->>Host: sessionId, display, api
|
|
59
|
+
Host->>API: session.list → childIds
|
|
60
|
+
API-->>Host: message.updated
|
|
61
|
+
Host->>Host: refreshTick++
|
|
62
|
+
Host->>API: session.messages(sid / cid)
|
|
63
|
+
Host->>W: main, messages, subAgents
|
|
64
|
+
W->>W: aggregate / format / TuiPanel
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Module map
|
|
68
|
+
|
|
69
|
+
| File | Role |
|
|
70
|
+
|------|------|
|
|
71
|
+
| `plugin.tsx` | `api.slots.register` (`order: 56`, next to visual-cache); load config |
|
|
72
|
+
| `sidebar-host.tsx` | Bind `sessionId`; `mainSnap` / `mainMessages` / `subAgentList`; `refreshTick` + `message.updated` |
|
|
73
|
+
| `widget.tsx` | Render panel when `sessionId` set; `noData` when empty |
|
|
74
|
+
| `use-cache-hit-metrics.ts` | Hit row, trend, main block visibility |
|
|
75
|
+
| `main-session-view.tsx` / `agents-view.tsx` | Business sections |
|
|
76
|
+
| `cache-hit-rows.tsx` | Shared token rows in Detail |
|
|
77
|
+
| `stats.ts` | Pure aggregation (no UI) |
|
|
78
|
+
| `session-list.ts` | Parse `session.list`, `childSessionIdsForParent` |
|
|
79
|
+
| `format-cost.ts` / `format-tokens.ts` / `format-cache-ui.ts` | Display formatting (`computeHitBarWidth` lives in `tui-panel/layout.ts`) |
|
|
80
|
+
| `message-timing.ts` | SDK time fields |
|
|
81
|
+
| `timeline/` | Per-call JSONL (`records` / `writer` / `collector`) |
|
|
82
|
+
| `plugin-config.ts` / `load-config.ts` | Config normalization |
|
|
83
|
+
|
|
84
|
+
### TUI panel framework (`src/tui-panel/`)
|
|
85
|
+
|
|
86
|
+
Reusable **page** skeleton aligned with visual-cache (layout, colors, foldable sections)—no skills estimate, slash, or kv business logic.
|
|
87
|
+
|
|
88
|
+
| Module | Role |
|
|
89
|
+
|--------|------|
|
|
90
|
+
| `layout.ts` | Column widths, `justifyRow`, `computeHitBarWidth`, separators |
|
|
91
|
+
| `palette.ts` | Theme → panel palette |
|
|
92
|
+
| `use-panel-layout.ts` | `createPanelLayout`, `createSectionFold` |
|
|
93
|
+
| `components.tsx` | `TuiPanel`, `TuiHitRow`, `TuiMetricRow`, … (`@opentui/solid`) |
|
|
94
|
+
| `index.ts` | Barrel export |
|
|
95
|
+
|
|
96
|
+
Import `layout.ts` / `palette.ts` directly from non-JSX code (e.g. `use-cache-hit-metrics`) so `bun test` smoke tests do not pull JSX.
|
|
97
|
+
|
|
98
|
+
See [src/tui-panel/README.md](../../src/tui-panel/README.md) · [中文](../../src/tui-panel/README.zh-CN.md).
|
|
99
|
+
|
|
100
|
+
## Sub-agent discovery
|
|
101
|
+
|
|
102
|
+
Implementation: `src/child-session-sync.ts` (used by `sidebar-host`).
|
|
103
|
+
|
|
104
|
+
```mermaid
|
|
105
|
+
flowchart TD
|
|
106
|
+
A[sessionId changes] --> B[resetForParentChange]
|
|
107
|
+
B --> C[loadChildren: session.list]
|
|
108
|
+
E[message.updated any session] --> R[refreshTick++]
|
|
109
|
+
E --> F{sessionID !== parent?}
|
|
110
|
+
F -->|yes| G[debounce 200ms]
|
|
111
|
+
G --> C2[loadChildren overwrites childIds]
|
|
112
|
+
F -->|no| R
|
|
113
|
+
C --> H[childSessionIdsForParent]
|
|
114
|
+
C2 --> H
|
|
115
|
+
H --> I[re-read messages + aggregate]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- **Single source of truth**: `childIds` always **replaced** from `session.list` (no `session.get` append).
|
|
119
|
+
- **Races**: `listGen` increments on parent change; callbacks check generation and `parentId`.
|
|
120
|
+
- **Streaming**: foreign-session `message.updated` is debounced (`CHILD_LIST_DEBOUNCE_MS` = 200ms).
|
|
121
|
+
- **Depth**: direct children only (`parentID === sid`); nested sub-agents are future work.
|
|
122
|
+
- **Data**: `messages(cid)` → `aggregateSessionFromMessages`; entries without stats are filtered out.
|
|
123
|
+
|
|
124
|
+
### Agents block semantics
|
|
125
|
+
|
|
126
|
+
| Scope | Included in Agents total? |
|
|
127
|
+
|-------|-------------------------|
|
|
128
|
+
| Each child session’s assistant messages | Yes (`aggregateSubAgents`) |
|
|
129
|
+
| Main session assistant messages | **No** (still in `mainSnap` when hidden in UI) |
|
|
130
|
+
|
|
131
|
+
The UI shows `agentsScopeHint` (“sub-sessions only”). This complements visual-cache’s main-session view—it is not a missing-aggregation bug.
|
|
132
|
+
|
|
133
|
+
## Aggregation and refresh
|
|
134
|
+
|
|
135
|
+
### When values recompute
|
|
136
|
+
|
|
137
|
+
| Data | Trigger |
|
|
138
|
+
|------|---------|
|
|
139
|
+
| Main snapshot | `createMemo` reads `refreshTick` + `messages(sid)` |
|
|
140
|
+
| Main messages (Hit trend) | Same |
|
|
141
|
+
| Sub-agent content | `childIds` or `refreshTick` → re-read each `messages(cid)` |
|
|
142
|
+
| Sub-agent ids | `session.list` callback / debounced refresh on foreign activity |
|
|
143
|
+
|
|
144
|
+
`sidebar-host` subscribes to `message.updated` and bumps `refreshTick` so streaming token updates always recompute.
|
|
145
|
+
|
|
146
|
+
### Accumulation rules
|
|
147
|
+
|
|
148
|
+
- Every `role === assistant` message in the session is included—not only the last turn.
|
|
149
|
+
- Streaming updates the same message’s `tokens` repeatedly.
|
|
150
|
+
- `reasoning` tokens are **excluded** from hit-rate denominator.
|
|
151
|
+
- `summary: true`: skipped in `computePerCallHitTrend`; **not** yet excluded in `aggregateSessionFromMessages`.
|
|
152
|
+
|
|
153
|
+
### Sidebar visibility
|
|
154
|
+
|
|
155
|
+
```mermaid
|
|
156
|
+
flowchart TD
|
|
157
|
+
S{sessionId set?}
|
|
158
|
+
S -->|no| H[no panel]
|
|
159
|
+
S -->|yes| P[TuiPanel]
|
|
160
|
+
P --> D{hasData?}
|
|
161
|
+
D -->|no| ND[noData]
|
|
162
|
+
D -->|yes| MAIN[Main / Detail / Model]
|
|
163
|
+
D -->|has subs| AG[Agents (foldable)]
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
| Concept | Implementation |
|
|
167
|
+
|---------|----------------|
|
|
168
|
+
| Whole panel | `widget.tsx`: `Show when={sessionId().length > 0}` |
|
|
169
|
+
| Has data | `mainSessionHasStats(main) \|\| subs.length > 0` |
|
|
170
|
+
| Main **block** | Always rendered |
|
|
171
|
+
| **Agents** | Shown when `subs.length > 0`; foldable |
|
|
172
|
+
|
|
173
|
+
## Hit rate (current)
|
|
174
|
+
|
|
175
|
+
**Session total (Total Hit, aligned with visual-cache)**
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
Sum input and cache.read over assistant messages
|
|
179
|
+
→ cacheRead / (cacheRead + input)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Header Hit (per-turn + trend)**
|
|
183
|
+
|
|
184
|
+
- `computePerCallHitTrend`: one rate per assistant turn; skip `summary: true`.
|
|
185
|
+
- Display the **last** non-summary turn; compare to previous for ↑ / ↓ / `-`.
|
|
186
|
+
|
|
187
|
+
**Combined Hit**
|
|
188
|
+
|
|
189
|
+
- Shown only when main block is visible, sub-agents exist, and combined vs session total differs by ≥ 0.05%.
|
|
190
|
+
|
|
191
|
+
## Time fields (OpenCode SDK v2)
|
|
192
|
+
|
|
193
|
+
| Source | Field | Notes |
|
|
194
|
+
|--------|-------|-------|
|
|
195
|
+
| `AssistantMessage` | `time.created` | ms epoch |
|
|
196
|
+
| `AssistantMessage` | `time.completed?` | end of LLM turn; often missing while streaming |
|
|
197
|
+
| `ReasoningPart` | `time.start` / `time.end?` | thought segments |
|
|
198
|
+
| `ToolStateCompleted` | `time.start` / `time.end` | tool run |
|
|
199
|
+
| `StepFinishPart` | (no time) | tokens/cost; time falls back to message |
|
|
200
|
+
|
|
201
|
+
**Per-call JSONL (Phase 1, default off)**: one row per assistant turn; daily file under `logs/`. See **[timeline.md](./timeline.md)** § Storage, § Rotation and retention.
|
|
202
|
+
|
|
203
|
+
## Testing
|
|
204
|
+
|
|
205
|
+
| Layer | Files | Purpose |
|
|
206
|
+
|-------|-------|---------|
|
|
207
|
+
| Unit | `tests/*.test.ts` | `stats`, `format-*`, layout, timeline |
|
|
208
|
+
| Smoke | `tests/module-load.test.ts` | Real import graph |
|
|
209
|
+
| Runtime | OpenCode logs | JSX / peer load errors |
|
|
210
|
+
|
|
211
|
+
After moving exports: `rg` + `bun test`.
|
|
212
|
+
|
|
213
|
+
## Roadmap (not implemented)
|
|
214
|
+
|
|
215
|
+
| Item | Notes | Doc |
|
|
216
|
+
|------|-------|-----|
|
|
217
|
+
| Per-call JSONL | **Phase 1 done** (`src/timeline/`, default off) | [timeline.md](./timeline.md) |
|
|
218
|
+
| Metric modes | Session / last N / sliding window | timeline.md § Phase 3 |
|
|
219
|
+
| Sub-agents | Recursive children, filter by agent type | timeline.md § Risks |
|
|
220
|
+
|
|
221
|
+
Timeline writes must stay async and non-blocking (see timeline.md).
|
|
222
|
+
|
|
223
|
+
## Plugin cache
|
|
224
|
+
|
|
225
|
+
| Install | After code change |
|
|
226
|
+
|---------|-------------------|
|
|
227
|
+
| Local path | Restart OpenCode |
|
|
228
|
+
| npm package | Restart; clear `~/.cache/opencode/packages/opencode-cache-hit@latest` if needed |
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# Timeline / per-call JSONL
|
|
2
|
+
|
|
3
|
+
For developers. Sidebar aggregation: [design.md](./design.md). User guide: [README.md](../../README.md).
|
|
4
|
+
|
|
5
|
+
**Phase 1 (JSONL on disk) is implemented**; default `timeline.enabled: false`. Phase 2 sidebar Timeline section and Phase 3 metric modes are not done.
|
|
6
|
+
|
|
7
|
+
## Goals and non-goals
|
|
8
|
+
|
|
9
|
+
| Goals | Non-goals |
|
|
10
|
+
|-------|-----------|
|
|
11
|
+
| Inspect each assistant call’s tokens / cache / cost / hit % over time | Replace OpenCode platform logs (`~/.local/share/opencode/log`) |
|
|
12
|
+
| Distinguish main vs child sessions | Spam the TUI with `console.log` |
|
|
13
|
+
| Local JSONL for `jq` / scripts | Cloud upload or team sharing |
|
|
14
|
+
| Same rules as `stats.ts` (including `summary` skip) | SQLite, charts, or recursive sub-agents in v1 |
|
|
15
|
+
|
|
16
|
+
## Core concept
|
|
17
|
+
|
|
18
|
+
**One timeline event = one billable assistant turn**, same source as the sidebar **Hit** row—not tool parts or user messages.
|
|
19
|
+
|
|
20
|
+
```mermaid
|
|
21
|
+
flowchart LR
|
|
22
|
+
MSG[AssistantMessage] --> REC[LlmCallRecord]
|
|
23
|
+
REC --> MEM[in-memory ring last N]
|
|
24
|
+
REC --> JSONL[JSONL append]
|
|
25
|
+
MEM --> UI[sidebar Timeline section optional]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Field | Source |
|
|
29
|
+
|-------|--------|
|
|
30
|
+
| Sort key | `time.completed ?? time.created` (`timingFromAssistantMessage`) |
|
|
31
|
+
| Hit trend eligibility | `summary !== true` and `input + cache.read > 0` |
|
|
32
|
+
| Session totals | `aggregateSessionFromMessages` (may later skip `summary` too) |
|
|
33
|
+
|
|
34
|
+
## Data model
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
export type LlmCallRecord = {
|
|
38
|
+
schema: 1
|
|
39
|
+
recordedAt: number // local write time (ms)
|
|
40
|
+
sessionId: string
|
|
41
|
+
rootSessionId: string // main session; differs for child scope
|
|
42
|
+
scope: "main" | "child"
|
|
43
|
+
messageKey: string
|
|
44
|
+
modelId: string
|
|
45
|
+
created: number
|
|
46
|
+
completedAt?: number
|
|
47
|
+
durationMs?: number
|
|
48
|
+
isComplete: boolean
|
|
49
|
+
input: number
|
|
50
|
+
output: number
|
|
51
|
+
reasoning: number
|
|
52
|
+
cacheRead: number
|
|
53
|
+
cacheWrite: number
|
|
54
|
+
cost: number
|
|
55
|
+
hitPercent: number | null
|
|
56
|
+
skippedForHit: boolean // compaction / summary
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**`messageKey` (dedupe)**
|
|
61
|
+
|
|
62
|
+
1. Prefer `message.id` / `messageID`.
|
|
63
|
+
2. Fallback: `${sessionId}:${created}:${modelID ?? ""}`.
|
|
64
|
+
|
|
65
|
+
**Streaming**
|
|
66
|
+
|
|
67
|
+
- In-memory map keyed by `messageKey` is overwritten on each build.
|
|
68
|
+
- **Disk**: append when `isComplete === true` unless `flushIncomplete: true`.
|
|
69
|
+
|
|
70
|
+
## Building records
|
|
71
|
+
|
|
72
|
+
Module: `src/timeline/records.ts` — `buildCallRecords(sessionId, rootSessionId, scope, messages)`.
|
|
73
|
+
|
|
74
|
+
- Only `role === assistant`.
|
|
75
|
+
- `skippedForHit = msg.summary === true`.
|
|
76
|
+
- `hitPercent` matches `computePerCallHitTrend` per message.
|
|
77
|
+
- Children: `buildCallRecords(cid, rootSid, "child", …)` merged and sorted by `completedAt ?? created`.
|
|
78
|
+
|
|
79
|
+
## Storage
|
|
80
|
+
|
|
81
|
+
**Default layout**
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
~/.config/opencode/plugins/opencode-cache-hit/logs/
|
|
85
|
+
timeline-2026-05-31.jsonl # one active file per local calendar day
|
|
86
|
+
timeline-2026-05-31.jsonl.1 # size rotation backup for that day
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
All main and child sessions for a day share one file; filter by `rootSessionId` / `sessionId` / `scope`. A new date gets a new filename at midnight.
|
|
90
|
+
|
|
91
|
+
Optional `dir` (e.g. `~/.local/share/opencode/cache-hit/logs/`).
|
|
92
|
+
|
|
93
|
+
**Legacy**: older builds used `<rootSessionId>.jsonl` per main session; not migrated automatically.
|
|
94
|
+
|
|
95
|
+
**Config** (`cache-hit.config.json` → `timeline`):
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"timeline": {
|
|
100
|
+
"enabled": false,
|
|
101
|
+
"dir": "",
|
|
102
|
+
"flushIncomplete": false,
|
|
103
|
+
"logSummaryMessages": true,
|
|
104
|
+
"maxMemoryRows": 50,
|
|
105
|
+
"maxLinesPerFile": 100000,
|
|
106
|
+
"rotateMaxBytes": 16777216,
|
|
107
|
+
"retainRotated": 5,
|
|
108
|
+
"maxAgeDays": 30,
|
|
109
|
+
"maxLogFiles": 20
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Example values above; code defaults below (`enabled: false`, rotation `0` except `retainRotated: 5`).
|
|
115
|
+
|
|
116
|
+
| Field | Code default | Description |
|
|
117
|
+
|-------|--------------|-------------|
|
|
118
|
+
| `enabled` | `false` | No IO when off |
|
|
119
|
+
| `dir` | `""` | Empty → plugin `logs/` |
|
|
120
|
+
| `flushIncomplete` | `false` | Write only completed turns |
|
|
121
|
+
| `logSummaryMessages` | `true` | Include summary rows (flagged) |
|
|
122
|
+
| `maxMemoryRows` | `50` | In-memory rows for future UI |
|
|
123
|
+
| `maxLinesPerFile` | `0` | Trim active file to last N lines (`0` = off) |
|
|
124
|
+
| `rotateMaxBytes` | `0` | Size roll to `.jsonl.1` (`0` = off) |
|
|
125
|
+
| `retainRotated` | `5` | Same-day backup files to keep (not counting active) |
|
|
126
|
+
| `maxAgeDays` | `0` | On collector start: delete files older than N days (mtime) |
|
|
127
|
+
| `maxLogFiles` | `0` | Cap total `timeline-*.jsonl*` files (each `.1` counts) |
|
|
128
|
+
|
|
129
|
+
**Write pipeline** (`writer.ts` + `rotation.ts`)
|
|
130
|
+
|
|
131
|
+
1. Optional size roll **before** append.
|
|
132
|
+
2. `appendFile` one JSON line.
|
|
133
|
+
3. Optional line trim **after** append.
|
|
134
|
+
4. Async flush in `queueMicrotask`; `flushedKeys` dedupe by `messageKey` (not cleared on session switch; cleared on date change).
|
|
135
|
+
|
|
136
|
+
## Rotation and retention
|
|
137
|
+
|
|
138
|
+
### Same-day size rotation (`rotateMaxBytes` + `retainRotated`)
|
|
139
|
+
|
|
140
|
+
Before each append, if the active file ≥ threshold:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
active (full) → rename → .1
|
|
144
|
+
old .1 → rename → .2
|
|
145
|
+
old .N → delete when at retainRotated and rolling again
|
|
146
|
+
new empty active file, then append
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| `retainRotated` | Approx. max per day |
|
|
150
|
+
|-----------------|---------------------|
|
|
151
|
+
| `5` (default / example) | active + `.1`…`.5` ≈ 6× `rotateMaxBytes` |
|
|
152
|
+
| `1` | active + `.1` ≈ 2× |
|
|
153
|
+
| `0` | delete active file on roll, no backup |
|
|
154
|
+
|
|
155
|
+
Oldest backup is **deleted** on further rolls; data is gone permanently.
|
|
156
|
+
|
|
157
|
+
### Line cap (`maxLinesPerFile`)
|
|
158
|
+
|
|
159
|
+
Rewrites the **active** file in place; dropped lines are **not** moved to `.1`. With ~500 B/line records, **16MB size roll usually happens before 100k lines**.
|
|
160
|
+
|
|
161
|
+
### Directory cleanup (once per collector start)
|
|
162
|
+
|
|
163
|
+
1. `maxAgeDays`: delete `timeline-*.jsonl*` with mtime older than N days.
|
|
164
|
+
2. `maxLogFiles`: if still over cap, delete **earliest logs first**: oldest **date in filename**, then highest backup index (`.5` before `.1` before active); mtime only as tie-breaker.
|
|
165
|
+
|
|
166
|
+
Does **not** match legacy `ses_*.jsonl` names.
|
|
167
|
+
|
|
168
|
+
### Cross-day
|
|
169
|
+
|
|
170
|
+
New filename after midnight; previous days remain until cleanup runs.
|
|
171
|
+
|
|
172
|
+
### Dedup and session switch
|
|
173
|
+
|
|
174
|
+
- One append per `messageKey` per process (`flushedKeys`).
|
|
175
|
+
- Switching main session: same day file; filter by `rootSessionId`.
|
|
176
|
+
- **Restart** clears `flushedKeys`; completed messages may be written again (no persistent dedupe yet).
|
|
177
|
+
|
|
178
|
+
## Runtime wiring
|
|
179
|
+
|
|
180
|
+
```mermaid
|
|
181
|
+
sequenceDiagram
|
|
182
|
+
participant E as message.updated
|
|
183
|
+
participant H as sidebar-host
|
|
184
|
+
participant B as timeline/build
|
|
185
|
+
participant W as timeline/writer
|
|
186
|
+
|
|
187
|
+
E->>H: refreshTick++
|
|
188
|
+
H->>B: debounce 500ms buildCallRecords
|
|
189
|
+
alt enabled and complete and not flushed
|
|
190
|
+
B->>W: append JSONL
|
|
191
|
+
end
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
- Child ids from `child-session-sync` / `session.list`; timeline only reads `messages()`.
|
|
195
|
+
- Debounce 500ms when enabled.
|
|
196
|
+
- Scope: current TUI root session + its children.
|
|
197
|
+
|
|
198
|
+
## UI phases
|
|
199
|
+
|
|
200
|
+
### Phase 1 — disk only (current)
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
LOG=~/.config/opencode/plugins/opencode-cache-hit/logs/timeline-$(date +%Y-%m-%d).jsonl
|
|
204
|
+
tail -f "$LOG"
|
|
205
|
+
jq -r 'select(.rootSessionId=="YOUR_ROOT") | [.created,.scope,.hitPercent,.cost]|@tsv' "$LOG"
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Charts (optional scripts)** — see [scripts/README.md](../../scripts/README.md):
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# one-liner: call count + average hit %
|
|
212
|
+
python3 -c "import json,sys; r=[json.loads(x) for x in open(sys.argv[1]) if x.strip()]; h=[x['hitPercent'] for x in r if x.get('hitPercent') is not None]; print(f\"{len(r)} calls, avg hit {sum(h)/len(h):.1f}%\")" "$LOG"
|
|
213
|
+
|
|
214
|
+
bun scripts/plot-hit-rate.ts "$LOG" -o /tmp/hit.svg
|
|
215
|
+
bun scripts/plot-hit-rate.ts "$LOG" --by-root -o /tmp/hit-multi.svg
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Phase 2 — sidebar Timeline section (planned)
|
|
219
|
+
|
|
220
|
+
### Phase 3 — metric window linkage (planned)
|
|
221
|
+
|
|
222
|
+
## Module map
|
|
223
|
+
|
|
224
|
+
| Module | Role |
|
|
225
|
+
|--------|------|
|
|
226
|
+
| `message-timing.ts` | `created` / `completed` |
|
|
227
|
+
| `stats.ts` | shared per-message hit % |
|
|
228
|
+
| `sidebar-host.tsx` | `createTimelineCollector` |
|
|
229
|
+
| `plugin.tsx` | reads config |
|
|
230
|
+
|
|
231
|
+
## Tests
|
|
232
|
+
|
|
233
|
+
| Case | File |
|
|
234
|
+
|------|------|
|
|
235
|
+
| `buildCallRecords` | `tests/timeline-records.test.ts` |
|
|
236
|
+
| writer / rotation / purge | `tests/timeline-writer.test.ts`, `timeline-rotation.test.ts` |
|
|
237
|
+
| collector | `tests/timeline-collector.test.ts` |
|
|
238
|
+
|
|
239
|
+
## Risks
|
|
240
|
+
|
|
241
|
+
| Risk | Mitigation |
|
|
242
|
+
|------|------------|
|
|
243
|
+
| Too many writes while streaming | `isComplete` only; debounce |
|
|
244
|
+
| No stable message id | synthetic `messageKey` |
|
|
245
|
+
| Nested sub-agents | flat `child` scope only in v1 |
|
|
246
|
+
| Disk growth | rotation + age + file count caps |
|
|
247
|
+
| SDK changes | `schema: 1` |
|
|
248
|
+
|
|
249
|
+
## Example line
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
{"schema":1,"recordedAt":1717000000000,"sessionId":"sess_main","rootSessionId":"sess_main","scope":"main","messageKey":"sess_main:1716999990000:deepseek/v4","modelId":"deepseek/v4","created":1716999990000,"completedAt":1717000000000,"durationMs":10000,"isComplete":true,"input":1200,"output":80,"reasoning":0,"cacheRead":38000,"cacheWrite":0,"cost":0.012,"hitPercent":96.9,"skippedForHit":false}
|
|
253
|
+
```
|