Skip to content

Commit f5acf9e

Browse files
committed
feat: surface sidecar timing summaries
1 parent 8798555 commit f5acf9e

19 files changed

Lines changed: 533 additions & 18 deletions

File tree

.agents/skills/gettokens-domain-engineering/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ This skill unifies the technical rules for building, styling, and debugging GetT
169169
- Trend chart data window is a fixed count cap, not a fixed time window. Keep only the latest capped request points for the chart model, so new requests push the sequence labels forward (`#50` -> `#51` -> `#52`) while the visible data volume remains bounded.
170170
- Trend chart viewport should be a fixed, non-scrollable audio waveform chart. The visible request count is width-driven: wider surfaces show more recent request bars, narrower surfaces show fewer, and the latest request stays anchored near the right edge. Do not reintroduce horizontal panning or auto-scroll follow logic.
171171
- The timing metric picker below the chart summarizes the same trend window. Show average values for `total / TTFT / first token / stream / queue / auth select / connect / gaps / rates` rather than echoing the latest single request; keep latest-request values in the timeline rows and chart footer.
172+
- The timing metric picker should prefer the sidecar session-level `timingSummary` when present. Treat `timingSummary.window=retained_requests` as the authoritative average window, show `sampleCount` plus `sequenceFrom / sequenceTo`, and mark the UI as `Sidecar summary`. Only use frontend request-window averaging as a fallback for old sidecars or preview data without summary, and label that path as a local estimate.
173+
- Sidecar `timingSummary` must not let stale historical streaming/reconnecting requests grow on every snapshot. Only the current active request may project `totalDurationMs` from the sidecar summary generation time; first-event and first-token fields must remain absent until observed.
172174
- Keep the live-session chart visually inside the page section, not as a nested card. Use the existing Swiss-industrial chart tokens, footer summaries below the graph, and live markers such as dashed strokes/rings for in-flight samples.
173175
- Request timing trend visuals should read like a forward-moving audio waveform, not a finance line, ECG trace, or candlestick chart. Render exactly one centered vertical amplitude bar per request for the selected timing metric; longer durations produce taller bars, and live rings distinguish in-flight samples.
174176
- Timing trend motion should not redraw the full waveform on every live refresh. Keep bars steady; use a short opacity settle only when switching metrics and a subtle breathing ring on the live sample to indicate activity.

app_codex_live_sessions.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type CodexLiveSession struct {
5757
FallbackInferred bool `json:"fallbackInferred,omitempty"`
5858
FallbackConfidence string `json:"fallbackConfidence,omitempty"`
5959
FallbackReason string `json:"fallbackReason,omitempty"`
60+
TimingSummary *CodexLiveTimingSummary `json:"timingSummary,omitempty"`
6061
RecentEvents []CodexLiveTimelineEvent `json:"recentEvents"`
6162
Requests []CodexLiveRequest `json:"requests"`
6263
}
@@ -106,6 +107,31 @@ type CodexLiveTimingMetrics struct {
106107
TotalTokensPerSecond float64 `json:"totalTokensPerSecond,omitempty"`
107108
}
108109

110+
type CodexLiveTimingSummary struct {
111+
Window string `json:"window"`
112+
SampleCount int `json:"sampleCount"`
113+
SequenceFrom int `json:"sequenceFrom,omitempty"`
114+
SequenceTo int `json:"sequenceTo,omitempty"`
115+
ActiveIncluded bool `json:"activeIncluded,omitempty"`
116+
GeneratedAt string `json:"generatedAt"`
117+
Averages CodexLiveTimingSummaryAverages `json:"averages"`
118+
}
119+
120+
type CodexLiveTimingSummaryAverages struct {
121+
QueueWaitMs *int64 `json:"queueWaitMs,omitempty"`
122+
AuthSelectMs *int64 `json:"authSelectMs,omitempty"`
123+
UpstreamConnectMs *int64 `json:"upstreamConnectMs,omitempty"`
124+
FirstEventMs *int64 `json:"firstEventMs,omitempty"`
125+
FirstTokenMs *int64 `json:"firstTokenMs,omitempty"`
126+
AverageEventGapMs *int64 `json:"averageEventGapMs,omitempty"`
127+
LongestEventGapMs *int64 `json:"longestEventGapMs,omitempty"`
128+
StreamDurationMs *int64 `json:"streamDurationMs,omitempty"`
129+
TotalDurationMs *int64 `json:"totalDurationMs,omitempty"`
130+
ReconnectCount *int `json:"reconnectCount,omitempty"`
131+
OutputTokensPerSecond *float64 `json:"outputTokensPerSecond,omitempty"`
132+
TotalTokensPerSecond *float64 `json:"totalTokensPerSecond,omitempty"`
133+
}
134+
109135
type CodexLiveErrorSummary struct {
110136
StatusCode int `json:"statusCode,omitempty"`
111137
Code string `json:"code,omitempty"`
@@ -193,6 +219,7 @@ func mapCodexLiveSessions(items []wailsapp.CodexLiveSession) []CodexLiveSession
193219
FallbackInferred: item.FallbackInferred,
194220
FallbackConfidence: item.FallbackConfidence,
195221
FallbackReason: item.FallbackReason,
222+
TimingSummary: mapCodexLiveTimingSummary(item.TimingSummary),
196223
RecentEvents: mapCodexLiveTimelineEvents(item.RecentEvents),
197224
Requests: mapCodexLiveRequests(item.Requests),
198225
})
@@ -274,6 +301,34 @@ func mapCodexLiveTiming(item wailsapp.CodexLiveTimingMetrics) CodexLiveTimingMet
274301
}
275302
}
276303

304+
func mapCodexLiveTimingSummary(item *wailsapp.CodexLiveTimingSummary) *CodexLiveTimingSummary {
305+
if item == nil {
306+
return nil
307+
}
308+
return &CodexLiveTimingSummary{
309+
Window: item.Window,
310+
SampleCount: item.SampleCount,
311+
SequenceFrom: item.SequenceFrom,
312+
SequenceTo: item.SequenceTo,
313+
ActiveIncluded: item.ActiveIncluded,
314+
GeneratedAt: item.GeneratedAt,
315+
Averages: CodexLiveTimingSummaryAverages{
316+
QueueWaitMs: item.Averages.QueueWaitMs,
317+
AuthSelectMs: item.Averages.AuthSelectMs,
318+
UpstreamConnectMs: item.Averages.UpstreamConnectMs,
319+
FirstEventMs: item.Averages.FirstEventMs,
320+
FirstTokenMs: item.Averages.FirstTokenMs,
321+
AverageEventGapMs: item.Averages.AverageEventGapMs,
322+
LongestEventGapMs: item.Averages.LongestEventGapMs,
323+
StreamDurationMs: item.Averages.StreamDurationMs,
324+
TotalDurationMs: item.Averages.TotalDurationMs,
325+
ReconnectCount: item.Averages.ReconnectCount,
326+
OutputTokensPerSecond: item.Averages.OutputTokensPerSecond,
327+
TotalTokensPerSecond: item.Averages.TotalTokensPerSecond,
328+
},
329+
}
330+
}
331+
277332
func mapCodexLiveError(item *wailsapp.CodexLiveErrorSummary) *CodexLiveErrorSummary {
278333
if item == nil {
279334
return nil

docs-linhay/dev/20260523-session-distillation-codex-live-sessions-ui.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,21 @@ Live sessions 的 detail 面板和筛选区本质上是工作台容器,不是
117117

118118
本轮 browser cache 验证中,最新请求时间线仍显示 `#50 总 8.0s / TTFT 562ms / 首 810ms`,而耗时指标块显示 `总耗时 7.1s / TTFT 740ms / 首 token 1.2s`,口径已区分。
119119

120+
### 10.1 耗时均值摘要由 sidecar 声明
121+
122+
趋势窗口平均值的权威口径应由 CLIProxyAPI live-session tracker 返回,而不是只在前端页面进入后临时解释。
123+
124+
稳定边界:
125+
126+
1. session 级 `timingSummary` 是耗时均值的优先数据源,窗口固定描述为 `retained_requests`
127+
2. summary 必须包含 `sampleCount``sequenceFrom / sequenceTo``activeIncluded``generatedAt``averages`,让 UI 可以解释样本范围与 active request 投影。
128+
3. active request 只允许用 sidecar 的生成时刻投影 `totalDurationMs`;不要凭空生成 first event / first token 等未出现的字段。
129+
4. 历史 stale streaming / reconnecting request 只能使用已记录 timing 或 `completedAt - startedAt`,不能因为 snapshot 刷新继续增长。
130+
5. 前端 `resolveCodexLiveTimingMetricSummary` 优先使用 sidecar summary;缺失时才回退本地 retained request window 聚合,并在 UI 标记 `本地估算`
131+
6. summary 只能包含计数、序号、时间与速率字段,不能包含 payload、headers、token、cookie 或未脱敏错误体。
132+
133+
浏览器预览数据也应带 `timingSummary`,避免 preview 永远只覆盖 fallback 分支。结构差分合并时要把 `timingSummary.generatedAt` 和 active 投影导致的 summary 总耗时/流式耗时变化视为 clock-only refresh,避免高频轮询闪烁。
134+
120135
### 11. 右侧详情列要自包含滚动
121136

122137
长运行会话的详情内容会明显高于视口。宽屏双栏布局下,右侧详情列不能只靠外层 workbench 滚动访问底部,否则用户在请求耗时趋势或请求时间线附近滚动时,会把页头、搜索栏和左侧列表一起卷走。

docs-linhay/memory/2026-05-27.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,13 @@
307307
- 会话整理:本轮把状态页 Provider/Model 下拉的语义边界沉淀为项目级规则:显示和保存必须贴近 `config.toml` 的真实 key/value,UI 不再制造显示名/ID 双轨,也不再把历史 `GT` 兜底当作有效默认模型。
308308
- 写回:已更新 `.agents/skills/gettokens-domain-engineering/SKILL.md``Status Local CLI Config`,并补充 `docs-linhay/dev/20260426-relay-service-config-boundary.md` 第 8 节。
309309
- 不升级:该模式属于 Status 本地 CLI 配置域内规则,暂不升级 `AGENTS.md`
310+
311+
## Codex live-session sidecar timing summary implementation
312+
313+
- 实施:CLIProxyAPI fork 的 live-session tracker 新增 session 级 `timingSummary`,基于 retained requests 返回样本数、sequence 范围、active 是否纳入、生成时间和 timing 均值;active request 只投影 `totalDurationMs`,历史 stale streaming/reconnecting request 不随刷新增长。
314+
- GetTokens 透传:`internal/wailsapp`、root `main.App``frontend/wailsjs/go/models.ts`、前端 adapter 和类型已端到端支持 `timingSummary`;前端 `resolveCodexLiveTimingMetricSummary` 优先用 sidecar summary,缺失时回退本地估算。
315+
- UI:live sessions 详情耗时区域改为 `耗时均值`,展示 `最近请求 <n> · #from-#to · Sidecar 汇总/本地估算`,timeline 和 chart footer 继续保留最新单请求值。
316+
- 验证:`go test ./internal/gettokenshooks -run 'LiveSessions.*TimingSummary|TimingSummary' -count=1``go test ./internal/gettokenshooks -run 'LiveSessions' -count=1``go test ./internal/wailsapp -run 'CodexLiveSessions' -count=1``node --test frontend/src/features/codex-live-sessions/model.test.mjs``npm --prefix frontend run typecheck``npm --prefix frontend run build` 均通过。
317+
- 浏览器验收:Playwright 打开 `http://localhost:5173/#frame=codex&workspace=live-sessions`,详情区显示 `耗时均值``最近请求 50 · #36-#85 · SIDECAR 汇总`;截图为 `docs-linhay/spaces/20260527-codex-live-session-timing-summary/screenshots/20260527/codex-live-sessions/20260527-codex-live-sessions-timing-summary-after-v01.png`。唯一 console error 是 favicon 404,与本功能无关。
318+
- 未覆盖:未重启真实桌面 app 并发起真实 Codex 请求做 runtime 验收;当前完成 sidecar fork、Wails DTO、前端模型/构建和浏览器预览验收。
319+
- 沉淀:已更新 `.agents/skills/gettokens-domain-engineering/SKILL.md``docs-linhay/dev/20260523-session-distillation-codex-live-sessions-ui.md`;不升级 `AGENTS.md`

docs-linhay/references/CLIProxyAPI

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Subproject commit 952cf213eb49965f8e4c055affc71f932e897058
1+
Subproject commit b0cbe42cafa232b30cdedb1271b1bfe4ae6f14ad

docs-linhay/spaces/20260527-codex-live-session-timing-summary/README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,5 +128,25 @@
128128
- 相关 workspace:`#frame=codex&workspace=live-sessions`
129129

130130
## 当前状态
131-
- 状态:requirements-ready
131+
- 状态:implemented-tested
132132
- 最近更新:2026-05-27
133+
134+
## 实施记录
135+
136+
- CLIProxyAPI fork 已在 session 级返回 `timingSummary`,窗口为 `retained_requests`,包含样本数、sequence 范围、active 是否纳入、生成时间和各 timing 字段均值。
137+
- active request 只投影 `totalDurationMs`;历史 stale streaming / reconnecting request 不随 snapshot 刷新继续增长。
138+
- GetTokens `internal/wailsapp`、root `main.App``frontend/wailsjs/go/models.ts` 和前端 adapter 已端到端透传 `timingSummary`
139+
- 前端 `TimingMetrics` 优先使用 sidecar summary;缺失时回退本地 retained request window 聚合,并在 UI 标注 `Sidecar 汇总``本地估算`
140+
- 浏览器验收截图:`screenshots/20260527/codex-live-sessions/20260527-codex-live-sessions-timing-summary-after-v01.png`
141+
142+
## 验证记录
143+
144+
- `go test ./internal/gettokenshooks -run 'LiveSessions.*TimingSummary|TimingSummary' -count=1`
145+
- `go test ./internal/gettokenshooks -run 'LiveSessions' -count=1`
146+
- `go test ./internal/wailsapp -run 'CodexLiveSessions' -count=1`
147+
- `node --test frontend/src/features/codex-live-sessions/model.test.mjs`
148+
- `npm --prefix frontend run typecheck`
149+
- `npm --prefix frontend run build`
150+
- Playwright 验收 `http://localhost:5173/#frame=codex&workspace=live-sessions`:详情区显示 `耗时均值``最近请求 50 · #36-#85 · SIDECAR 汇总`,timeline 仍显示最新单请求耗时。
151+
152+
未执行真实桌面 app 重启与真实 Codex 请求验收;当前完成的是 sidecar fork 单元测试、Wails DTO 测试、前端构建和浏览器预览验收。

docs-linhay/spaces/20260527-codex-live-session-timing-summary/plans/implementation-plan-v01.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,36 @@ qmd update && qmd embed
245245
3. `M3 frontend display`:UI 优先 sidecar summary,fallback 兼容,前端测试通过。
246246
4. `M4 acceptance`:浏览器截图与真实 sidecar 验收完成,文档记忆同步。
247247

248+
## 执行结果
249+
250+
### 已完成
251+
252+
- `M1 sidecar summary contract`:已在 CLIProxyAPI fork 增加 `LiveTimingSummary` 与 retained requests 聚合构建器,focused live-session 测试通过。
253+
- `M2 GetTokens DTO bridge`:已在 `internal/wailsapp`、root `main.App``frontend/wailsjs/go/models.ts` 增加 timing summary DTO 与 mapper,Wails focused 测试通过。
254+
- `M3 frontend display`:前端类型、adapter、preview mock、summary resolver 和详情 UI 已接入;`耗时均值` 优先展示 sidecar summary,缺失时 fallback 本地估算。
255+
- `M4 acceptance`:已完成浏览器预览验收与截图归档;文档、memory、领域 skill 已写回。
256+
257+
### 最终验证
258+
259+
```bash
260+
go test ./internal/gettokenshooks -run 'LiveSessions.*TimingSummary|TimingSummary' -count=1
261+
go test ./internal/gettokenshooks -run 'LiveSessions' -count=1
262+
go test ./internal/wailsapp -run 'CodexLiveSessions' -count=1
263+
node --test frontend/src/features/codex-live-sessions/model.test.mjs
264+
npm --prefix frontend run typecheck
265+
npm --prefix frontend run build
266+
```
267+
268+
浏览器预览验收:
269+
270+
- URL:`http://localhost:5173/#frame=codex&workspace=live-sessions`
271+
- 截图:`../screenshots/20260527/codex-live-sessions/20260527-codex-live-sessions-timing-summary-after-v01.png`
272+
- DOM 结果:详情区显示 `耗时均值``最近请求 50 · #36-#85 · SIDECAR 汇总`,timeline 仍显示最新单请求耗时。
273+
274+
### 未覆盖项
275+
276+
- 未重启真实桌面 app 并发起真实 Codex 请求做端到端 runtime 验收。本期验证覆盖 sidecar fork 单元测试、Wails DTO、前端模型/构建和浏览器预览。
277+
248278
## 开放问题
249279

250280
1. `timingSummary` 放在 session 级即可,还是 request detail/history response 也要独立返回同一 summary?

frontend/src/features/codex-live-sessions/components/CodexLiveSessionDetail.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ import {
3030
} from './requestTimelineSummary';
3131
import type { Translate } from './types';
3232
import {
33-
buildCodexLiveRequestTimingMetricAverages,
3433
buildCodexLiveRequestTimingTrend,
34+
resolveCodexLiveTimingMetricSummary,
3535
type CodexLiveTimingMetricAverages,
3636
type CodexLiveRequestTimingTrendPoint,
3737
type CodexLiveTimingTrendMetric,
@@ -652,13 +652,21 @@ function TimingMetrics({
652652
return () => window.clearInterval(refreshID);
653653
}, []);
654654

655-
const averages = buildCodexLiveRequestTimingMetricAverages(session.requests, request, { nowMs });
655+
const averages = resolveCodexLiveTimingMetricSummary(session, request, { nowMs });
656656
const metrics = buildTimingMetricRows(averages, t);
657+
const summaryMeta = buildTimingSummaryMeta(averages, t);
657658

658659
return (
659660
<div className="border-2 border-[var(--border-color)] bg-[var(--bg-surface)] p-3">
660-
<div className="font-mono text-[length:var(--font-size-ui-sm)] font-black uppercase tracking-[0.16em] text-[var(--text-muted)]">
661-
{t('codex_live_sessions.timing')}
661+
<div className="flex flex-wrap items-center justify-between gap-2">
662+
<div className="font-mono text-[length:var(--font-size-ui-sm)] font-black uppercase tracking-[0.16em] text-[var(--text-muted)]">
663+
{t('codex_live_sessions.timing_average')}
664+
</div>
665+
{summaryMeta ? (
666+
<div className="truncate font-mono text-[length:var(--font-size-ui-xs)] font-black uppercase text-[var(--text-muted)]">
667+
{summaryMeta}
668+
</div>
669+
) : null}
662670
</div>
663671
<div className="mt-3 grid gap-x-5 gap-y-2 md:grid-cols-3 xl:grid-cols-4">
664672
{metrics.map((metric) => {
@@ -697,6 +705,18 @@ function TimingMetrics({
697705
);
698706
}
699707

708+
function buildTimingSummaryMeta(summary: CodexLiveTimingMetricAverages, t: Translate): string {
709+
if (summary.sampleCount <= 0) {
710+
return '';
711+
}
712+
const parts = [`${t('codex_live_sessions.timing_summary_recent')} ${summary.sampleCount}`];
713+
if (typeof summary.sequenceFrom === 'number' && typeof summary.sequenceTo === 'number') {
714+
parts.push(`#${summary.sequenceFrom}-#${summary.sequenceTo}`);
715+
}
716+
parts.push(t(summary.source === 'sidecar' ? 'codex_live_sessions.timing_summary_sidecar' : 'codex_live_sessions.timing_summary_fallback'));
717+
return parts.join(' · ');
718+
}
719+
700720
interface TimingMetricRow {
701721
key: string;
702722
label: string;

0 commit comments

Comments
 (0)