opencode-extended-sidebar 0.2.90 → 0.2.91

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.
Files changed (108) hide show
  1. package/.oesignore +17 -17
  2. package/CHANGELOG.md +96 -95
  3. package/LICENSE +21 -21
  4. package/README.md +150 -235
  5. package/oes.json +13 -11
  6. package/package.json +100 -100
  7. package/scripts/install-git-hooks.mjs +29 -29
  8. package/src/pware.oc.core/constants/index.ts +18 -18
  9. package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -29
  10. package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +15 -13
  11. package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -241
  12. package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -55
  13. package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -65
  14. package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -58
  15. package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -63
  16. package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -34
  17. package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -74
  18. package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -69
  19. package/src/pware.oc.core/git/index.ts +7 -7
  20. package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -237
  21. package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -87
  22. package/src/pware.oc.core/index.ts +19 -18
  23. package/src/pware.oc.core/pware.oc.core.bus.ts +42 -42
  24. package/src/pware.oc.core/pware.oc.core.cache.ts +48 -48
  25. package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -97
  26. package/src/pware.oc.core/pware.oc.core.debug.ts +280 -280
  27. package/src/pware.oc.core/pware.oc.core.events.ts +79 -79
  28. package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -135
  29. package/src/pware.oc.core/pware.oc.core.layout.ts +234 -234
  30. package/src/pware.oc.core/pware.oc.core.oes.ts +99 -89
  31. package/src/pware.oc.core/pware.oc.core.paths.ts +240 -240
  32. package/src/pware.oc.core/pware.oc.core.preview.ts +74 -74
  33. package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -726
  34. package/src/pware.oc.core/pware.oc.core.runtimeConfig.ts +41 -0
  35. package/src/pware.oc.core/pware.oc.core.sqlite.ts +164 -175
  36. package/src/pware.oc.core/pware.oc.core.sqliteGateway.ts +89 -0
  37. package/src/pware.oc.core/pware.oc.core.status.ts +214 -214
  38. package/src/pware.oc.core/pware.oc.core.timing.ts +63 -63
  39. package/src/pware.oc.core/pware.oc.core.width.ts +215 -215
  40. package/src/pware.oc.omo/constants/index.ts +15 -15
  41. package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -18
  42. package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -49
  43. package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -29
  44. package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -11
  45. package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -60
  46. package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -49
  47. package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -25
  48. package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -38
  49. package/src/pware.oc.omo/index.ts +7 -7
  50. package/src/pware.oc.omo/resolver/index.ts +19 -19
  51. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -179
  52. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -116
  53. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -34
  54. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -503
  55. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -40
  56. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -269
  57. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -18
  58. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -18
  59. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -155
  60. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +363 -361
  61. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -18
  62. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -11
  63. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -14
  64. package/src/pware.oc.opencode/constants/index.ts +10 -10
  65. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -15
  66. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -18
  67. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -45
  68. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -58
  69. package/src/pware.oc.opencode/index.ts +8 -8
  70. package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -182
  71. package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -399
  72. package/src/pware.oc.opencode/resolver/index.ts +223 -170
  73. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +150 -162
  74. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +149 -185
  75. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +248 -211
  76. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +22 -26
  77. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +224 -236
  78. package/src/pware.oc.perf/index.ts +14 -14
  79. package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -15
  80. package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -284
  81. package/src/pware.oc.perf/pware.oc.perf.reader.ts +962 -927
  82. package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -88
  83. package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -118
  84. package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -79
  85. package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -71
  86. package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -214
  87. package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -179
  88. package/src/pware.oc.perf/pware.oc.perf.view.tsx +463 -462
  89. package/src/pware.oc.runtime/index.ts +13 -13
  90. package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -142
  91. package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +87 -79
  92. package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +282 -253
  93. package/src/pware.oc.runtime/pware.oc.runtime.omoRead.ts +37 -0
  94. package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +83 -95
  95. package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +130 -129
  96. package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -105
  97. package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +44 -43
  98. package/src/pware.oc.runtime/resolver/index.ts +268 -224
  99. package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -146
  100. package/src/pware.oc.ui/index.ts +12 -12
  101. package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +381 -339
  102. package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -144
  103. package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -111
  104. package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -82
  105. package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -843
  106. package/src/pware.oc.ui/pware.oc.ui.sections.tsx +552 -526
  107. package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1718 -1629
  108. package/src/pware.oc.ui.tsx +98 -98
@@ -1,71 +1,71 @@
1
- /**
2
- * pware.oc.perf.realtimeSampler
3
- *
4
- * Event-driven sampler: subscribes to `session.updated` events, extracts the
5
- * per-session cumulative token totals, and feeds them to a
6
- * `StatRealtimeTimeline`. The subscription is injected so the class stays
7
- * host-free; the event parsing is a pure exported function.
8
- */
9
- import type { StatRealtimeEventTokens, StatRealtimeTimeline } from "./pware.oc.perf.realtimeTimeline.js"
10
-
11
- /** Subscribe to realtime events: returns a dispose function. */
12
- export type RealtimeEventSubscribe = (handler: (evt: unknown) => void) => () => void
13
-
14
- /** Pull `sessionID` + cumulative `tokens` out of a `session.updated` event; null otherwise. */
15
- export function extractSessionTokens(evt: unknown): { sessionId: string; tokens: StatRealtimeEventTokens } | null {
16
- if (!evt || typeof evt !== "object") return null
17
- const o = evt as Record<string, unknown>
18
- const props = (
19
- o.properties && typeof o.properties === "object" ? o.properties : o
20
- ) as Record<string, unknown>
21
- const sessionId = props.sessionID ?? props.sessionId ?? props.session_id
22
- const info = props.info as Record<string, unknown> | undefined
23
- const tokens = info && typeof info === "object" ? (info.tokens as Record<string, unknown> | undefined) : undefined
24
- if (typeof sessionId !== "string" || !sessionId || !tokens || typeof tokens !== "object") return null
25
- const num = (v: unknown): number => (typeof v === "number" && Number.isFinite(v) ? v : 0)
26
- const cache = (
27
- tokens.cache && typeof tokens.cache === "object" ? tokens.cache : {}
28
- ) as Record<string, unknown>
29
- return {
30
- sessionId,
31
- tokens: {
32
- input: num(tokens.input),
33
- output: num(tokens.output),
34
- reasoning: num(tokens.reasoning),
35
- cacheRead: num(cache.read),
36
- cacheWrite: num(cache.write),
37
- },
38
- }
39
- }
40
-
41
- export class EventDriverSampler {
42
- static create(
43
- timeline: StatRealtimeTimeline,
44
- subscribe: RealtimeEventSubscribe,
45
- now: () => number = Date.now,
46
- ): EventDriverSampler {
47
- return new EventDriverSampler(timeline, subscribe, now)
48
- }
49
-
50
- private off: (() => void) | null = null
51
-
52
- private constructor(
53
- private readonly timeline: StatRealtimeTimeline,
54
- private readonly subscribe: RealtimeEventSubscribe,
55
- private readonly now: () => number,
56
- ) {}
57
-
58
- start(): void {
59
- if (this.off) return
60
- this.off = this.subscribe((evt) => {
61
- const hit = extractSessionTokens(evt)
62
- if (!hit) return
63
- this.timeline.ingest(hit.sessionId, hit.tokens, this.now())
64
- })
65
- }
66
-
67
- stop(): void {
68
- this.off?.()
69
- this.off = null
70
- }
71
- }
1
+ /**
2
+ * pware.oc.perf.realtimeSampler
3
+ *
4
+ * Event-driven sampler: subscribes to `session.updated` events, extracts the
5
+ * per-session cumulative token totals, and feeds them to a
6
+ * `StatRealtimeTimeline`. The subscription is injected so the class stays
7
+ * host-free; the event parsing is a pure exported function.
8
+ */
9
+ import type { StatRealtimeEventTokens, StatRealtimeTimeline } from "./pware.oc.perf.realtimeTimeline.js"
10
+
11
+ /** Subscribe to realtime events: returns a dispose function. */
12
+ export type RealtimeEventSubscribe = (handler: (evt: unknown) => void) => () => void
13
+
14
+ /** Pull `sessionID` + cumulative `tokens` out of a `session.updated` event; null otherwise. */
15
+ export function extractSessionTokens(evt: unknown): { sessionId: string; tokens: StatRealtimeEventTokens } | null {
16
+ if (!evt || typeof evt !== "object") return null
17
+ const o = evt as Record<string, unknown>
18
+ const props = (
19
+ o.properties && typeof o.properties === "object" ? o.properties : o
20
+ ) as Record<string, unknown>
21
+ const sessionId = props.sessionID ?? props.sessionId ?? props.session_id
22
+ const info = props.info as Record<string, unknown> | undefined
23
+ const tokens = info && typeof info === "object" ? (info.tokens as Record<string, unknown> | undefined) : undefined
24
+ if (typeof sessionId !== "string" || !sessionId || !tokens || typeof tokens !== "object") return null
25
+ const num = (v: unknown): number => (typeof v === "number" && Number.isFinite(v) ? v : 0)
26
+ const cache = (
27
+ tokens.cache && typeof tokens.cache === "object" ? tokens.cache : {}
28
+ ) as Record<string, unknown>
29
+ return {
30
+ sessionId,
31
+ tokens: {
32
+ input: num(tokens.input),
33
+ output: num(tokens.output),
34
+ reasoning: num(tokens.reasoning),
35
+ cacheRead: num(cache.read),
36
+ cacheWrite: num(cache.write),
37
+ },
38
+ }
39
+ }
40
+
41
+ export class EventDriverSampler {
42
+ static create(
43
+ timeline: StatRealtimeTimeline,
44
+ subscribe: RealtimeEventSubscribe,
45
+ now: () => number = Date.now,
46
+ ): EventDriverSampler {
47
+ return new EventDriverSampler(timeline, subscribe, now)
48
+ }
49
+
50
+ private off: (() => void) | null = null
51
+
52
+ private constructor(
53
+ private readonly timeline: StatRealtimeTimeline,
54
+ private readonly subscribe: RealtimeEventSubscribe,
55
+ private readonly now: () => number,
56
+ ) {}
57
+
58
+ start(): void {
59
+ if (this.off) return
60
+ this.off = this.subscribe((evt) => {
61
+ const hit = extractSessionTokens(evt)
62
+ if (!hit) return
63
+ this.timeline.ingest(hit.sessionId, hit.tokens, this.now())
64
+ })
65
+ }
66
+
67
+ stop(): void {
68
+ this.off?.()
69
+ this.off = null
70
+ }
71
+ }
@@ -1,214 +1,214 @@
1
- /**
2
- * pware.oc.perf.realtimeTimeline
3
- *
4
- * One shared realtime history for every category the OES realtime chart
5
- * renders (Tokens / Cache / CPU·RAM / Network). A single wall-clock loop calls
6
- * `tick(at)` on the UI heartbeat cadence (`TICK_MS`); every tick folds the
7
- * current per-session cumulative token state and the raw CPU ring into one grid
8
- * sample, so the chart's right edge always reaches "now" instead of waiting for
9
- * a host event. Pure state + injectable timestamps — no host, no database.
10
- *
11
- * Token/cache rates are a windowed derivative over the trailing
12
- * `REALTIME_RATE_WINDOW_MS`: `ingest` (exact `session.updated` totals) and
13
- * `ingestEstimate` (estimated stream deltas for out/reasoning) record level
14
- * changes, and `tick` reports the average over the window — it falls back to
15
- * zero when nothing new arrives instead of holding a stale rate. Estimated
16
- * out/reasoning deltas are reconciled against the exact totals on every
17
- * `session.updated`, so a streamed token is never counted twice: the exact
18
- * delta only adds what the estimate has not covered yet.
19
- */
20
- import { cpuCores, cpuPercentOverWindow, ramMb, type CpuRamReading } from "./pware.oc.perf.realtimeCpuRam.js"
21
- import {
22
- REALTIME_RATE_WINDOW_MS,
23
- REALTIME_WINDOW_MS,
24
- } from "../pware.oc.core/pware.oc.core.timing.js"
25
- import {
26
- emptyStatRealtimeSnapshot,
27
- pushStatRealtimeHistory,
28
- sumSeries,
29
- tokenRateToKbit,
30
- type StatRealtimeSnapshot,
31
- type StatRealtimeSnapshotHistory,
32
- } from "./pware.oc.perf.realtime.js"
33
-
34
- /** Cumulative token totals as reported by one `session.updated` event. */
35
- export type StatRealtimeEventTokens = {
36
- input: number
37
- output: number
38
- reasoning: number
39
- cacheRead: number
40
- cacheWrite: number
41
- }
42
-
43
- /** Which token channel an estimated stream delta feeds. */
44
- export type StatRealtimeEstKind = "out" | "reasoning"
45
-
46
- /** A single cumulative-token or streamed-token level change at `at`. */
47
- type Step = { at: number; n: number }
48
-
49
- /** Per-session state: last exact totals + stream estimates not yet confirmed. */
50
- type SessionState = {
51
- exact: StatRealtimeEventTokens | null
52
- estOut: number
53
- estReasoning: number
54
- lastAt: number
55
- }
56
-
57
- /** Channel names that map 1:1 onto a `StatRealtimeSnapshot` leaf. */
58
- type TokenChannel = "in" | "out" | "reasoning" | "cacheRead" | "cacheWrite"
59
-
60
- const TOKEN_CHANNELS: readonly TokenChannel[] = ["in", "out", "reasoning", "cacheRead", "cacheWrite"]
61
-
62
- function pos(n: number): number {
63
- return Number.isFinite(n) && n > 0 ? n : 0
64
- }
65
-
66
- export class StatRealtimeTimeline {
67
- static build(scope: string | null = null, cores: number = cpuCores()): StatRealtimeTimeline {
68
- return new StatRealtimeTimeline(scope, cores)
69
- }
70
-
71
- private readonly scope: string | null
72
- private readonly cores: number
73
- private readonly bySession = new Map<string, SessionState>()
74
- private readonly steps = new Map<TokenChannel, Step[]>()
75
- private cpuReadings: Array<CpuRamReading & { at: number }> = []
76
- private history: StatRealtimeSnapshotHistory[] = []
77
-
78
- private constructor(scope: string | null, cores: number) {
79
- this.scope = scope
80
- this.cores = cores
81
- for (const c of TOKEN_CHANNELS) this.steps.set(c, [])
82
- }
83
-
84
- /** Exact cumulative totals from one `session.updated` event. */
85
- ingest(sessionId: string, totals: StatRealtimeEventTokens, at: number): void {
86
- if (this.scope !== null && sessionId !== this.scope) return
87
- const st = this.bySession.get(sessionId) ?? {
88
- exact: null,
89
- estOut: 0,
90
- estReasoning: 0,
91
- lastAt: 0,
92
- }
93
- st.lastAt = at
94
- this.bySession.set(sessionId, st)
95
-
96
- const prev = st.exact
97
- if (!prev) {
98
- // First report is the baseline: estimates seen before it are already
99
- // inside the reported cumulative, so they must not suppress the first
100
- // post-baseline delta.
101
- st.exact = totals
102
- st.estOut = 0
103
- st.estReasoning = 0
104
- return
105
- }
106
- this.push("in", totals.input - prev.input, at)
107
- this.push("cacheRead", totals.cacheRead - prev.cacheRead, at)
108
- this.push("cacheWrite", totals.cacheWrite - prev.cacheWrite, at)
109
- this.rebase("out", "estOut", prev.output, totals.output, st, at)
110
- this.rebase("reasoning", "estReasoning", prev.reasoning, totals.reasoning, st, at)
111
- st.exact = totals
112
- }
113
-
114
- /** Estimated streamed delta (text → out, thinking → reasoning). */
115
- ingestEstimate(sessionId: string, kind: StatRealtimeEstKind, n: number, at: number): void {
116
- if (this.scope !== null && sessionId !== this.scope) return
117
- const amount = pos(n)
118
- if (amount === 0) return
119
- this.push(kind, amount, at)
120
- const st = this.bySession.get(sessionId) ?? {
121
- exact: null,
122
- estOut: 0,
123
- estReasoning: 0,
124
- lastAt: 0,
125
- }
126
- st.lastAt = at
127
- if (kind === "out") st.estOut += amount
128
- else st.estReasoning += amount
129
- this.bySession.set(sessionId, st)
130
- }
131
-
132
- /** A raw process CPU/RAM reading, taken on the same cadence as `tick`. */
133
- ingestCpuRam(raw: CpuRamReading, at: number): void {
134
- this.cpuReadings.push({ ...raw, at })
135
- }
136
-
137
- /** Advance the grid: fold all state into one sample at `at`, prune stale state. */
138
- tick(at: number): void {
139
- const cut = at - REALTIME_RATE_WINDOW_MS
140
- const snap = emptyStatRealtimeSnapshot(at)
141
-
142
- for (const c of TOKEN_CHANNELS) {
143
- const kept: Step[] = []
144
- let sum = 0
145
- for (const s of this.steps.get(c)!) {
146
- if (s.at <= cut) continue
147
- kept.push(s)
148
- sum += s.n
149
- }
150
- this.steps.set(c, kept)
151
- this.setChannel(snap, c, sum === 0 ? null : (sum / REALTIME_RATE_WINDOW_MS) * 1000)
152
- }
153
-
154
- // Network is estimated from the same token flow: what the session sends to
155
- // the model (input + cache) is `out`, what streams back (output + reasoning)
156
- // is `in`. Real socket bytes never reach this plugin (model traffic lives in
157
- // the server process), so kbit/s here means "≈ tokens × 4 bytes", not a meter.
158
- snap.network.out = tokenRateToKbit(sumSeries(snap.tokens.in, sumSeries(snap.cache.read, snap.cache.write)))
159
- snap.network.in = tokenRateToKbit(sumSeries(snap.tokens.out, snap.tokens.reasoning))
160
-
161
- this.cpuReadings = this.cpuReadings.filter((r) => r.at > cut)
162
- snap.cpuRam.cpu = cpuPercentOverWindow(this.cpuReadings, this.cores, REALTIME_RATE_WINDOW_MS)
163
- const last = this.cpuReadings[this.cpuReadings.length - 1]
164
- snap.cpuRam.ram = last ? ramMb(last.rss) : null
165
-
166
- this.history = pushStatRealtimeHistory(this.history, snap, REALTIME_WINDOW_MS)
167
- // Drop est-only transient states once idle — a session with an exact
168
- // baseline stays, so its first post-idle burst still diffs correctly.
169
- const stale = at - REALTIME_WINDOW_MS
170
- for (const [sid, st] of this.bySession) {
171
- if (st.exact === null && st.lastAt < stale) this.bySession.delete(sid)
172
- }
173
- }
174
-
175
- /** The rolling grid samples in time order — already on the fixed grid. */
176
- getTimeline(): StatRealtimeSnapshotHistory[] {
177
- return this.history
178
- }
179
-
180
- reset(): void {
181
- this.bySession.clear()
182
- for (const c of TOKEN_CHANNELS) this.steps.set(c, [])
183
- this.cpuReadings = []
184
- this.history = []
185
- }
186
-
187
- private push(channel: TokenChannel, n: number, at: number): void {
188
- const amount = pos(n)
189
- if (amount === 0) return
190
- this.steps.get(channel)!.push({ at, n: amount })
191
- }
192
-
193
- private rebase(
194
- channel: "out" | "reasoning",
195
- estKey: "estOut" | "estReasoning",
196
- prevVal: number,
197
- nextVal: number,
198
- st: SessionState,
199
- at: number,
200
- ): void {
201
- const delta = pos(nextVal - prevVal)
202
- const uncovered = Math.max(0, delta - st[estKey])
203
- st[estKey] = 0
204
- this.push(channel, uncovered, at)
205
- }
206
-
207
- private setChannel(snap: StatRealtimeSnapshot, c: TokenChannel, rate: number | null): void {
208
- if (c === "in") snap.tokens.in = rate
209
- else if (c === "out") snap.tokens.out = rate
210
- else if (c === "reasoning") snap.tokens.reasoning = rate
211
- else if (c === "cacheRead") snap.cache.read = rate
212
- else snap.cache.write = rate
213
- }
214
- }
1
+ /**
2
+ * pware.oc.perf.realtimeTimeline
3
+ *
4
+ * One shared realtime history for every category the OES realtime chart
5
+ * renders (Tokens / Cache / CPU·RAM / Network). A single wall-clock loop calls
6
+ * `tick(at)` on the UI heartbeat cadence (`TICK_MS`); every tick folds the
7
+ * current per-session cumulative token state and the raw CPU ring into one grid
8
+ * sample, so the chart's right edge always reaches "now" instead of waiting for
9
+ * a host event. Pure state + injectable timestamps — no host, no database.
10
+ *
11
+ * Token/cache rates are a windowed derivative over the trailing
12
+ * `REALTIME_RATE_WINDOW_MS`: `ingest` (exact `session.updated` totals) and
13
+ * `ingestEstimate` (estimated stream deltas for out/reasoning) record level
14
+ * changes, and `tick` reports the average over the window — it falls back to
15
+ * zero when nothing new arrives instead of holding a stale rate. Estimated
16
+ * out/reasoning deltas are reconciled against the exact totals on every
17
+ * `session.updated`, so a streamed token is never counted twice: the exact
18
+ * delta only adds what the estimate has not covered yet.
19
+ */
20
+ import { cpuCores, cpuPercentOverWindow, ramMb, type CpuRamReading } from "./pware.oc.perf.realtimeCpuRam.js"
21
+ import {
22
+ REALTIME_RATE_WINDOW_MS,
23
+ REALTIME_WINDOW_MS,
24
+ } from "../pware.oc.core/pware.oc.core.timing.js"
25
+ import {
26
+ emptyStatRealtimeSnapshot,
27
+ pushStatRealtimeHistory,
28
+ sumSeries,
29
+ tokenRateToKbit,
30
+ type StatRealtimeSnapshot,
31
+ type StatRealtimeSnapshotHistory,
32
+ } from "./pware.oc.perf.realtime.js"
33
+
34
+ /** Cumulative token totals as reported by one `session.updated` event. */
35
+ export type StatRealtimeEventTokens = {
36
+ input: number
37
+ output: number
38
+ reasoning: number
39
+ cacheRead: number
40
+ cacheWrite: number
41
+ }
42
+
43
+ /** Which token channel an estimated stream delta feeds. */
44
+ export type StatRealtimeEstKind = "out" | "reasoning"
45
+
46
+ /** A single cumulative-token or streamed-token level change at `at`. */
47
+ type Step = { at: number; n: number }
48
+
49
+ /** Per-session state: last exact totals + stream estimates not yet confirmed. */
50
+ type SessionState = {
51
+ exact: StatRealtimeEventTokens | null
52
+ estOut: number
53
+ estReasoning: number
54
+ lastAt: number
55
+ }
56
+
57
+ /** Channel names that map 1:1 onto a `StatRealtimeSnapshot` leaf. */
58
+ type TokenChannel = "in" | "out" | "reasoning" | "cacheRead" | "cacheWrite"
59
+
60
+ const TOKEN_CHANNELS: readonly TokenChannel[] = ["in", "out", "reasoning", "cacheRead", "cacheWrite"]
61
+
62
+ function pos(n: number): number {
63
+ return Number.isFinite(n) && n > 0 ? n : 0
64
+ }
65
+
66
+ export class StatRealtimeTimeline {
67
+ static build(scope: string | null = null, cores: number = cpuCores()): StatRealtimeTimeline {
68
+ return new StatRealtimeTimeline(scope, cores)
69
+ }
70
+
71
+ private readonly scope: string | null
72
+ private readonly cores: number
73
+ private readonly bySession = new Map<string, SessionState>()
74
+ private readonly steps = new Map<TokenChannel, Step[]>()
75
+ private cpuReadings: Array<CpuRamReading & { at: number }> = []
76
+ private history: StatRealtimeSnapshotHistory[] = []
77
+
78
+ private constructor(scope: string | null, cores: number) {
79
+ this.scope = scope
80
+ this.cores = cores
81
+ for (const c of TOKEN_CHANNELS) this.steps.set(c, [])
82
+ }
83
+
84
+ /** Exact cumulative totals from one `session.updated` event. */
85
+ ingest(sessionId: string, totals: StatRealtimeEventTokens, at: number): void {
86
+ if (this.scope !== null && sessionId !== this.scope) return
87
+ const st = this.bySession.get(sessionId) ?? {
88
+ exact: null,
89
+ estOut: 0,
90
+ estReasoning: 0,
91
+ lastAt: 0,
92
+ }
93
+ st.lastAt = at
94
+ this.bySession.set(sessionId, st)
95
+
96
+ const prev = st.exact
97
+ if (!prev) {
98
+ // First report is the baseline: estimates seen before it are already
99
+ // inside the reported cumulative, so they must not suppress the first
100
+ // post-baseline delta.
101
+ st.exact = totals
102
+ st.estOut = 0
103
+ st.estReasoning = 0
104
+ return
105
+ }
106
+ this.push("in", totals.input - prev.input, at)
107
+ this.push("cacheRead", totals.cacheRead - prev.cacheRead, at)
108
+ this.push("cacheWrite", totals.cacheWrite - prev.cacheWrite, at)
109
+ this.rebase("out", "estOut", prev.output, totals.output, st, at)
110
+ this.rebase("reasoning", "estReasoning", prev.reasoning, totals.reasoning, st, at)
111
+ st.exact = totals
112
+ }
113
+
114
+ /** Estimated streamed delta (text → out, thinking → reasoning). */
115
+ ingestEstimate(sessionId: string, kind: StatRealtimeEstKind, n: number, at: number): void {
116
+ if (this.scope !== null && sessionId !== this.scope) return
117
+ const amount = pos(n)
118
+ if (amount === 0) return
119
+ this.push(kind, amount, at)
120
+ const st = this.bySession.get(sessionId) ?? {
121
+ exact: null,
122
+ estOut: 0,
123
+ estReasoning: 0,
124
+ lastAt: 0,
125
+ }
126
+ st.lastAt = at
127
+ if (kind === "out") st.estOut += amount
128
+ else st.estReasoning += amount
129
+ this.bySession.set(sessionId, st)
130
+ }
131
+
132
+ /** A raw process CPU/RAM reading, taken on the same cadence as `tick`. */
133
+ ingestCpuRam(raw: CpuRamReading, at: number): void {
134
+ this.cpuReadings.push({ ...raw, at })
135
+ }
136
+
137
+ /** Advance the grid: fold all state into one sample at `at`, prune stale state. */
138
+ tick(at: number): void {
139
+ const cut = at - REALTIME_RATE_WINDOW_MS
140
+ const snap = emptyStatRealtimeSnapshot(at)
141
+
142
+ for (const c of TOKEN_CHANNELS) {
143
+ const kept: Step[] = []
144
+ let sum = 0
145
+ for (const s of this.steps.get(c)!) {
146
+ if (s.at <= cut) continue
147
+ kept.push(s)
148
+ sum += s.n
149
+ }
150
+ this.steps.set(c, kept)
151
+ this.setChannel(snap, c, sum === 0 ? null : (sum / REALTIME_RATE_WINDOW_MS) * 1000)
152
+ }
153
+
154
+ // Network is estimated from the same token flow: what the session sends to
155
+ // the model (input + cache) is `out`, what streams back (output + reasoning)
156
+ // is `in`. Real socket bytes never reach this plugin (model traffic lives in
157
+ // the server process), so kbit/s here means "≈ tokens × 4 bytes", not a meter.
158
+ snap.network.out = tokenRateToKbit(sumSeries(snap.tokens.in, sumSeries(snap.cache.read, snap.cache.write)))
159
+ snap.network.in = tokenRateToKbit(sumSeries(snap.tokens.out, snap.tokens.reasoning))
160
+
161
+ this.cpuReadings = this.cpuReadings.filter((r) => r.at > cut)
162
+ snap.cpuRam.cpu = cpuPercentOverWindow(this.cpuReadings, this.cores, REALTIME_RATE_WINDOW_MS)
163
+ const last = this.cpuReadings[this.cpuReadings.length - 1]
164
+ snap.cpuRam.ram = last ? ramMb(last.rss) : null
165
+
166
+ this.history = pushStatRealtimeHistory(this.history, snap, REALTIME_WINDOW_MS)
167
+ // Drop est-only transient states once idle — a session with an exact
168
+ // baseline stays, so its first post-idle burst still diffs correctly.
169
+ const stale = at - REALTIME_WINDOW_MS
170
+ for (const [sid, st] of this.bySession) {
171
+ if (st.exact === null && st.lastAt < stale) this.bySession.delete(sid)
172
+ }
173
+ }
174
+
175
+ /** The rolling grid samples in time order — already on the fixed grid. */
176
+ getTimeline(): StatRealtimeSnapshotHistory[] {
177
+ return this.history
178
+ }
179
+
180
+ reset(): void {
181
+ this.bySession.clear()
182
+ for (const c of TOKEN_CHANNELS) this.steps.set(c, [])
183
+ this.cpuReadings = []
184
+ this.history = []
185
+ }
186
+
187
+ private push(channel: TokenChannel, n: number, at: number): void {
188
+ const amount = pos(n)
189
+ if (amount === 0) return
190
+ this.steps.get(channel)!.push({ at, n: amount })
191
+ }
192
+
193
+ private rebase(
194
+ channel: "out" | "reasoning",
195
+ estKey: "estOut" | "estReasoning",
196
+ prevVal: number,
197
+ nextVal: number,
198
+ st: SessionState,
199
+ at: number,
200
+ ): void {
201
+ const delta = pos(nextVal - prevVal)
202
+ const uncovered = Math.max(0, delta - st[estKey])
203
+ st[estKey] = 0
204
+ this.push(channel, uncovered, at)
205
+ }
206
+
207
+ private setChannel(snap: StatRealtimeSnapshot, c: TokenChannel, rate: number | null): void {
208
+ if (c === "in") snap.tokens.in = rate
209
+ else if (c === "out") snap.tokens.out = rate
210
+ else if (c === "reasoning") snap.tokens.reasoning = rate
211
+ else if (c === "cacheRead") snap.cache.read = rate
212
+ else snap.cache.write = rate
213
+ }
214
+ }