@jmoyers/harness 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/LICENSE +21 -0
- package/README.md +145 -0
- package/native/ptyd/Cargo.lock +16 -0
- package/native/ptyd/Cargo.toml +7 -0
- package/native/ptyd/src/main.rs +257 -0
- package/package.json +90 -0
- package/scripts/build-ptyd.sh +73 -0
- package/scripts/control-plane-daemon.ts +277 -0
- package/scripts/cursor-hook-relay.ts +82 -0
- package/scripts/harness-animate.ts +469 -0
- package/scripts/harness-bin.js +77 -0
- package/scripts/harness-core.ts +1 -0
- package/scripts/harness-inspector.ts +439 -0
- package/scripts/harness.ts +2493 -0
- package/src/adapters/agent-session-state.ts +390 -0
- package/src/cli/gateway-record.ts +173 -0
- package/src/codex/live-session.ts +872 -0
- package/src/config/config-core.ts +1359 -0
- package/src/config/secrets-core.ts +170 -0
- package/src/control-plane/agent-realtime-api.ts +2441 -0
- package/src/control-plane/codex-session-stream.ts +392 -0
- package/src/control-plane/codex-telemetry.ts +1325 -0
- package/src/control-plane/lifecycle-hooks.ts +706 -0
- package/src/control-plane/session-summary.ts +380 -0
- package/src/control-plane/status/agent-status-reducer.ts +21 -0
- package/src/control-plane/status/reducer-base.ts +170 -0
- package/src/control-plane/status/reducers/claude-status-reducer.ts +37 -0
- package/src/control-plane/status/reducers/codex-status-reducer.ts +48 -0
- package/src/control-plane/status/reducers/critique-status-reducer.ts +15 -0
- package/src/control-plane/status/reducers/cursor-status-reducer.ts +37 -0
- package/src/control-plane/status/reducers/terminal-status-reducer.ts +15 -0
- package/src/control-plane/status/session-status-engine.ts +76 -0
- package/src/control-plane/stream-client.ts +396 -0
- package/src/control-plane/stream-command-parser.ts +1673 -0
- package/src/control-plane/stream-protocol.ts +1808 -0
- package/src/control-plane/stream-server-background.ts +486 -0
- package/src/control-plane/stream-server-command.ts +2557 -0
- package/src/control-plane/stream-server-connection.ts +234 -0
- package/src/control-plane/stream-server-observed-filter.ts +112 -0
- package/src/control-plane/stream-server-session-runtime.ts +566 -0
- package/src/control-plane/stream-server-state-store.ts +15 -0
- package/src/control-plane/stream-server.ts +3192 -0
- package/src/cursor/managed-hooks.ts +282 -0
- package/src/domain/conversations.ts +414 -0
- package/src/domain/directories.ts +78 -0
- package/src/domain/repositories.ts +123 -0
- package/src/domain/tasks.ts +148 -0
- package/src/domain/workspace.ts +156 -0
- package/src/events/normalized-events.ts +124 -0
- package/src/mux/ansi-integrity.ts +103 -0
- package/src/mux/control-plane-op-queue.ts +212 -0
- package/src/mux/conversation-rail.ts +339 -0
- package/src/mux/double-click.ts +78 -0
- package/src/mux/dual-pane-core.ts +435 -0
- package/src/mux/harness-core-ui.ts +817 -0
- package/src/mux/input-shortcuts.ts +667 -0
- package/src/mux/live-mux/actions-conversation.ts +344 -0
- package/src/mux/live-mux/actions-repository.ts +246 -0
- package/src/mux/live-mux/actions-task.ts +115 -0
- package/src/mux/live-mux/args.ts +142 -0
- package/src/mux/live-mux/command-menu.ts +298 -0
- package/src/mux/live-mux/control-plane-records.ts +546 -0
- package/src/mux/live-mux/conversation-state.ts +188 -0
- package/src/mux/live-mux/directory-resolution.ts +34 -0
- package/src/mux/live-mux/event-mapping.ts +96 -0
- package/src/mux/live-mux/gateway-profiler.ts +152 -0
- package/src/mux/live-mux/gateway-render-trace.ts +177 -0
- package/src/mux/live-mux/gateway-status-timeline.ts +166 -0
- package/src/mux/live-mux/git-parsing.ts +131 -0
- package/src/mux/live-mux/git-snapshot.ts +263 -0
- package/src/mux/live-mux/git-state.ts +136 -0
- package/src/mux/live-mux/global-shortcut-handlers.ts +143 -0
- package/src/mux/live-mux/home-pane-actions.ts +58 -0
- package/src/mux/live-mux/home-pane-drop.ts +44 -0
- package/src/mux/live-mux/home-pane-entity-click.ts +96 -0
- package/src/mux/live-mux/home-pane-pointer.ts +96 -0
- package/src/mux/live-mux/input-forwarding.ts +112 -0
- package/src/mux/live-mux/layout.ts +30 -0
- package/src/mux/live-mux/left-nav-activation.ts +103 -0
- package/src/mux/live-mux/left-nav.ts +85 -0
- package/src/mux/live-mux/left-rail-actions.ts +118 -0
- package/src/mux/live-mux/left-rail-conversation-click.ts +82 -0
- package/src/mux/live-mux/left-rail-pointer.ts +74 -0
- package/src/mux/live-mux/modal-command-menu-handler.ts +101 -0
- package/src/mux/live-mux/modal-conversation-handlers.ts +217 -0
- package/src/mux/live-mux/modal-input-reducers.ts +94 -0
- package/src/mux/live-mux/modal-overlays.ts +287 -0
- package/src/mux/live-mux/modal-pointer.ts +70 -0
- package/src/mux/live-mux/modal-prompt-handlers.ts +187 -0
- package/src/mux/live-mux/modal-task-editor-handler.ts +156 -0
- package/src/mux/live-mux/observed-stream.ts +87 -0
- package/src/mux/live-mux/palette-parsing.ts +128 -0
- package/src/mux/live-mux/pointer-routing.ts +108 -0
- package/src/mux/live-mux/process-usage.ts +53 -0
- package/src/mux/live-mux/project-pane-pointer.ts +44 -0
- package/src/mux/live-mux/rail-layout.ts +244 -0
- package/src/mux/live-mux/render-trace-analysis.ts +213 -0
- package/src/mux/live-mux/render-trace-state.ts +84 -0
- package/src/mux/live-mux/repository-folding.ts +207 -0
- package/src/mux/live-mux/runtime-shutdown.ts +51 -0
- package/src/mux/live-mux/selection.ts +411 -0
- package/src/mux/live-mux/startup-utils.ts +187 -0
- package/src/mux/live-mux/status-timeline-state.ts +82 -0
- package/src/mux/live-mux/task-pane-shortcuts.ts +206 -0
- package/src/mux/live-mux/terminal-palette.ts +79 -0
- package/src/mux/new-thread-prompt.ts +165 -0
- package/src/mux/project-tree.ts +295 -0
- package/src/mux/render-frame.ts +113 -0
- package/src/mux/runtime-wiring.ts +185 -0
- package/src/mux/selector-index.ts +160 -0
- package/src/mux/startup-sequencer.ts +238 -0
- package/src/mux/task-composer.ts +289 -0
- package/src/mux/task-focused-pane.ts +417 -0
- package/src/mux/task-screen-keybindings.ts +539 -0
- package/src/mux/terminal-input-modes.ts +35 -0
- package/src/mux/workspace-path.ts +55 -0
- package/src/mux/workspace-rail-model.ts +701 -0
- package/src/mux/workspace-rail.ts +247 -0
- package/src/perf/perf-core.ts +307 -0
- package/src/pty/pty_host.ts +217 -0
- package/src/pty/session-broker.ts +158 -0
- package/src/recording/terminal-recording.ts +383 -0
- package/src/services/control-plane.ts +567 -0
- package/src/services/conversation-lifecycle.ts +176 -0
- package/src/services/conversation-startup-hydration.ts +47 -0
- package/src/services/directory-hydration.ts +49 -0
- package/src/services/event-persistence.ts +104 -0
- package/src/services/mux-ui-state-persistence.ts +82 -0
- package/src/services/output-load-sampler.ts +231 -0
- package/src/services/process-usage-refresh.ts +88 -0
- package/src/services/recording.ts +75 -0
- package/src/services/render-trace-recorder.ts +177 -0
- package/src/services/runtime-control-actions.ts +123 -0
- package/src/services/runtime-control-plane-ops.ts +131 -0
- package/src/services/runtime-conversation-actions.ts +113 -0
- package/src/services/runtime-conversation-activation.ts +78 -0
- package/src/services/runtime-conversation-starter.ts +171 -0
- package/src/services/runtime-conversation-title-edit.ts +149 -0
- package/src/services/runtime-directory-actions.ts +164 -0
- package/src/services/runtime-envelope-handler.ts +198 -0
- package/src/services/runtime-git-state.ts +92 -0
- package/src/services/runtime-input-pipeline.ts +50 -0
- package/src/services/runtime-input-router.ts +202 -0
- package/src/services/runtime-layout-resize.ts +236 -0
- package/src/services/runtime-left-rail-render.ts +159 -0
- package/src/services/runtime-main-pane-input.ts +230 -0
- package/src/services/runtime-modal-input.ts +119 -0
- package/src/services/runtime-navigation-input.ts +207 -0
- package/src/services/runtime-process-wiring.ts +68 -0
- package/src/services/runtime-rail-input.ts +287 -0
- package/src/services/runtime-render-flush.ts +146 -0
- package/src/services/runtime-render-lifecycle.ts +104 -0
- package/src/services/runtime-render-orchestrator.ts +108 -0
- package/src/services/runtime-render-pipeline.ts +167 -0
- package/src/services/runtime-render-state.ts +72 -0
- package/src/services/runtime-repository-actions.ts +197 -0
- package/src/services/runtime-right-pane-render.ts +132 -0
- package/src/services/runtime-shutdown.ts +79 -0
- package/src/services/runtime-stream-subscriptions.ts +56 -0
- package/src/services/runtime-task-composer-persistence.ts +139 -0
- package/src/services/runtime-task-editor-actions.ts +83 -0
- package/src/services/runtime-task-pane-actions.ts +198 -0
- package/src/services/runtime-task-pane-shortcuts.ts +189 -0
- package/src/services/runtime-task-pane.ts +62 -0
- package/src/services/runtime-workspace-actions.ts +153 -0
- package/src/services/runtime-workspace-observed-events.ts +190 -0
- package/src/services/session-projection-instrumentation.ts +190 -0
- package/src/services/startup-background-probe.ts +91 -0
- package/src/services/startup-background-resume.ts +65 -0
- package/src/services/startup-orchestrator.ts +166 -0
- package/src/services/startup-output-tracker.ts +54 -0
- package/src/services/startup-paint-tracker.ts +115 -0
- package/src/services/startup-persisted-conversation-queue.ts +45 -0
- package/src/services/startup-settled-gate.ts +67 -0
- package/src/services/startup-shutdown.ts +53 -0
- package/src/services/startup-span-tracker.ts +77 -0
- package/src/services/startup-state-hydration.ts +94 -0
- package/src/services/startup-visibility.ts +35 -0
- package/src/services/status-timeline-recorder.ts +144 -0
- package/src/services/task-pane-selection-actions.ts +153 -0
- package/src/services/task-planning-hydration.ts +58 -0
- package/src/services/task-planning-observed-events.ts +89 -0
- package/src/services/workspace-observed-events.ts +113 -0
- package/src/store/control-plane-store-normalize.ts +760 -0
- package/src/store/control-plane-store-types.ts +224 -0
- package/src/store/control-plane-store.ts +2951 -0
- package/src/store/event-store.ts +253 -0
- package/src/store/sqlite.ts +81 -0
- package/src/terminal/compat-matrix.ts +345 -0
- package/src/terminal/differential-checkpoints.ts +132 -0
- package/src/terminal/parity-suite.ts +441 -0
- package/src/terminal/snapshot-oracle.ts +1840 -0
- package/src/ui/conversation-input-forwarder.ts +114 -0
- package/src/ui/conversation-selection-input.ts +103 -0
- package/src/ui/debug-footer-notice.ts +39 -0
- package/src/ui/global-shortcut-input.ts +126 -0
- package/src/ui/input-preflight.ts +68 -0
- package/src/ui/input-token-router.ts +312 -0
- package/src/ui/input.ts +238 -0
- package/src/ui/kit.ts +509 -0
- package/src/ui/left-nav-input.ts +80 -0
- package/src/ui/left-rail-pointer-input.ts +148 -0
- package/src/ui/main-pane-pointer-input.ts +150 -0
- package/src/ui/modals/manager.ts +192 -0
- package/src/ui/mux-theme.ts +529 -0
- package/src/ui/panes/conversation.ts +19 -0
- package/src/ui/panes/home-gridfire.ts +302 -0
- package/src/ui/panes/home.ts +109 -0
- package/src/ui/panes/left-rail.ts +12 -0
- package/src/ui/panes/project.ts +44 -0
- package/src/ui/pointer-routing-input.ts +158 -0
- package/src/ui/repository-fold-input.ts +91 -0
- package/src/ui/screen.ts +210 -0
- package/src/ui/surface.ts +224 -0
|
@@ -0,0 +1,1325 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export type CodexTelemetrySource = 'otlp-log' | 'otlp-metric' | 'otlp-trace' | 'history';
|
|
4
|
+
export type CodexStatusHint = 'running' | 'completed' | 'needs-input';
|
|
5
|
+
|
|
6
|
+
export interface ParsedCodexTelemetryEvent {
|
|
7
|
+
readonly source: CodexTelemetrySource;
|
|
8
|
+
readonly observedAt: string;
|
|
9
|
+
readonly eventName: string | null;
|
|
10
|
+
readonly severity: string | null;
|
|
11
|
+
readonly summary: string | null;
|
|
12
|
+
readonly providerThreadId: string | null;
|
|
13
|
+
readonly statusHint: CodexStatusHint | null;
|
|
14
|
+
readonly payload: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CodexTelemetryConfigArgsInput {
|
|
18
|
+
readonly endpointBaseUrl: string;
|
|
19
|
+
readonly token: string;
|
|
20
|
+
readonly logUserPrompt: boolean;
|
|
21
|
+
readonly captureLogs: boolean;
|
|
22
|
+
readonly captureMetrics: boolean;
|
|
23
|
+
readonly captureTraces: boolean;
|
|
24
|
+
readonly historyPersistence: 'save-all' | 'none';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface OtlpAttribute {
|
|
28
|
+
key: string;
|
|
29
|
+
value: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeLookupKey(value: string): string {
|
|
33
|
+
return value.toLowerCase().replace(/[^a-z0-9]/gu, '');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
37
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return value as Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readString(value: unknown): string | null {
|
|
44
|
+
if (typeof value !== 'string') {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readStringTrimmed(value: unknown): string | null {
|
|
51
|
+
const parsed = readString(value);
|
|
52
|
+
if (parsed === null) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const trimmed = parsed.trim();
|
|
56
|
+
if (trimmed.length === 0) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return trimmed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeEpochTimestamp(input: number, fallback: string): string {
|
|
63
|
+
if (!Number.isFinite(input) || input <= 0) {
|
|
64
|
+
return fallback;
|
|
65
|
+
}
|
|
66
|
+
const abs = Math.abs(input);
|
|
67
|
+
let epochMs: number;
|
|
68
|
+
if (abs >= 1e18) {
|
|
69
|
+
epochMs = Math.floor(input / 1_000_000);
|
|
70
|
+
} else if (abs >= 1e15) {
|
|
71
|
+
epochMs = Math.floor(input / 1_000);
|
|
72
|
+
} else if (abs >= 1e12) {
|
|
73
|
+
epochMs = Math.floor(input);
|
|
74
|
+
} else {
|
|
75
|
+
epochMs = Math.floor(input * 1_000);
|
|
76
|
+
}
|
|
77
|
+
if (!Number.isFinite(epochMs) || epochMs <= 0) {
|
|
78
|
+
return fallback;
|
|
79
|
+
}
|
|
80
|
+
const parsed = new Date(epochMs);
|
|
81
|
+
if (!Number.isFinite(parsed.getTime())) {
|
|
82
|
+
return fallback;
|
|
83
|
+
}
|
|
84
|
+
return parsed.toISOString();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeIso(ts: unknown, fallback: string): string {
|
|
88
|
+
if (typeof ts === 'string') {
|
|
89
|
+
const trimmed = ts.trim();
|
|
90
|
+
if (trimmed.length === 0) {
|
|
91
|
+
return fallback;
|
|
92
|
+
}
|
|
93
|
+
if (/^-?\d+(\.\d+)?$/u.test(trimmed)) {
|
|
94
|
+
const numeric = Number(trimmed);
|
|
95
|
+
return normalizeEpochTimestamp(numeric, fallback);
|
|
96
|
+
}
|
|
97
|
+
const parsed = Date.parse(trimmed);
|
|
98
|
+
if (Number.isFinite(parsed)) {
|
|
99
|
+
return new Date(parsed).toISOString();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (typeof ts === 'number' && Number.isFinite(ts)) {
|
|
103
|
+
return normalizeEpochTimestamp(ts, fallback);
|
|
104
|
+
}
|
|
105
|
+
return fallback;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeNanoTimestamp(nanoValue: unknown, fallback: string): string {
|
|
109
|
+
let numericNano: number | null = null;
|
|
110
|
+
if (typeof nanoValue === 'number' && Number.isFinite(nanoValue)) {
|
|
111
|
+
numericNano = nanoValue;
|
|
112
|
+
} else if (typeof nanoValue === 'string') {
|
|
113
|
+
const parsed = Number.parseInt(nanoValue, 10);
|
|
114
|
+
if (Number.isFinite(parsed)) {
|
|
115
|
+
numericNano = parsed;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (numericNano === null || numericNano <= 0) {
|
|
119
|
+
return fallback;
|
|
120
|
+
}
|
|
121
|
+
const epochMs = Math.floor(numericNano / 1_000_000);
|
|
122
|
+
if (!Number.isFinite(epochMs) || epochMs <= 0) {
|
|
123
|
+
return fallback;
|
|
124
|
+
}
|
|
125
|
+
const parsed = new Date(epochMs);
|
|
126
|
+
if (!Number.isFinite(parsed.getTime())) {
|
|
127
|
+
return fallback;
|
|
128
|
+
}
|
|
129
|
+
return parsed.toISOString();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseAnyValue(value: unknown): unknown {
|
|
133
|
+
const record = asRecord(value);
|
|
134
|
+
if (record === null) {
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
if (record['stringValue'] !== undefined) {
|
|
138
|
+
return record['stringValue'];
|
|
139
|
+
}
|
|
140
|
+
if (record['boolValue'] !== undefined) {
|
|
141
|
+
return record['boolValue'];
|
|
142
|
+
}
|
|
143
|
+
if (record['intValue'] !== undefined) {
|
|
144
|
+
const intValue = record['intValue'];
|
|
145
|
+
let parsedIntValue: number | string | null = null;
|
|
146
|
+
if (typeof intValue === 'number') {
|
|
147
|
+
parsedIntValue = intValue;
|
|
148
|
+
} else if (typeof intValue === 'string') {
|
|
149
|
+
const parsed = Number.parseInt(intValue, 10);
|
|
150
|
+
parsedIntValue = Number.isFinite(parsed) ? parsed : intValue;
|
|
151
|
+
}
|
|
152
|
+
if (parsedIntValue !== null) {
|
|
153
|
+
return parsedIntValue;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (record['doubleValue'] !== undefined) {
|
|
157
|
+
return record['doubleValue'];
|
|
158
|
+
}
|
|
159
|
+
if (record['bytesValue'] !== undefined) {
|
|
160
|
+
return record['bytesValue'];
|
|
161
|
+
}
|
|
162
|
+
const arrayValue = asRecord(record['arrayValue']);
|
|
163
|
+
if (arrayValue !== null && Array.isArray(arrayValue['values'])) {
|
|
164
|
+
return arrayValue['values'].map((entry) => parseAnyValue(entry));
|
|
165
|
+
}
|
|
166
|
+
const kvlistValue = asRecord(record['kvlistValue']);
|
|
167
|
+
if (kvlistValue !== null && Array.isArray(kvlistValue['values'])) {
|
|
168
|
+
const out: Record<string, unknown> = {};
|
|
169
|
+
for (const kvEntry of kvlistValue['values']) {
|
|
170
|
+
const kvRecord = asRecord(kvEntry);
|
|
171
|
+
if (kvRecord === null) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const key = readString(kvRecord['key']);
|
|
175
|
+
if (key === null) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
out[key] = parseAnyValue(kvRecord['value']);
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
return record;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseOtlpAttributes(value: unknown): Record<string, unknown> {
|
|
186
|
+
if (!Array.isArray(value)) {
|
|
187
|
+
return {};
|
|
188
|
+
}
|
|
189
|
+
const out: Record<string, unknown> = {};
|
|
190
|
+
for (const entry of value) {
|
|
191
|
+
const item = asRecord(entry) as OtlpAttribute | null;
|
|
192
|
+
if (item === null || typeof item.key !== 'string') {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
out[item.key] = parseAnyValue(item.value);
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function asSummaryText(value: unknown): string | null {
|
|
201
|
+
if (typeof value === 'string') {
|
|
202
|
+
const trimmed = value.trim();
|
|
203
|
+
if (trimmed.length > 0) {
|
|
204
|
+
return trimmed;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
209
|
+
return String(value);
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function readFiniteNumber(value: unknown): number | null {
|
|
215
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
216
|
+
return value;
|
|
217
|
+
}
|
|
218
|
+
if (typeof value === 'string') {
|
|
219
|
+
const parsed = Number(value);
|
|
220
|
+
if (Number.isFinite(parsed)) {
|
|
221
|
+
return parsed;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function compactSummaryText(value: string | null, maxLength = 72): string | null {
|
|
228
|
+
if (value === null) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const normalized = value.replace(/\s+/gu, ' ').trim();
|
|
232
|
+
return normalized.length <= maxLength
|
|
233
|
+
? normalized
|
|
234
|
+
: `${normalized.slice(0, Math.max(0, maxLength - 1))}…`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function findNestedFieldByKey(
|
|
238
|
+
value: unknown,
|
|
239
|
+
targetKeys: ReadonlySet<string>,
|
|
240
|
+
depth = 0,
|
|
241
|
+
maxDepth = 4,
|
|
242
|
+
budget: { remaining: number } = { remaining: 160 },
|
|
243
|
+
): unknown {
|
|
244
|
+
if (budget.remaining <= 0 || depth > maxDepth) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
budget.remaining -= 1;
|
|
248
|
+
if (typeof value !== 'object' || value === null) {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
if (Array.isArray(value)) {
|
|
252
|
+
for (const entry of value) {
|
|
253
|
+
const nested = findNestedFieldByKey(entry, targetKeys, depth + 1, maxDepth, budget);
|
|
254
|
+
if (nested !== undefined) {
|
|
255
|
+
return nested;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
const record = value as Record<string, unknown>;
|
|
261
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
262
|
+
if (targetKeys.has(normalizeLookupKey(key))) {
|
|
263
|
+
return entry;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
for (const entry of Object.values(record)) {
|
|
267
|
+
const nested = findNestedFieldByKey(entry, targetKeys, depth + 1, maxDepth, budget);
|
|
268
|
+
if (nested !== undefined) {
|
|
269
|
+
return nested;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function pickFieldValue(
|
|
276
|
+
attributes: Record<string, unknown>,
|
|
277
|
+
body: unknown,
|
|
278
|
+
keys: readonly string[],
|
|
279
|
+
): unknown {
|
|
280
|
+
const normalizedKeys = new Set(keys.map((key) => normalizeLookupKey(key)));
|
|
281
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
282
|
+
if (normalizedKeys.has(normalizeLookupKey(key))) {
|
|
283
|
+
return value;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return findNestedFieldByKey(body, normalizedKeys);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function pickFieldText(
|
|
290
|
+
attributes: Record<string, unknown>,
|
|
291
|
+
body: unknown,
|
|
292
|
+
keys: readonly string[],
|
|
293
|
+
): string | null {
|
|
294
|
+
return asSummaryText(pickFieldValue(attributes, body, keys));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function pickFieldNumber(
|
|
298
|
+
attributes: Record<string, unknown>,
|
|
299
|
+
body: unknown,
|
|
300
|
+
keys: readonly string[],
|
|
301
|
+
): number | null {
|
|
302
|
+
return readFiniteNumber(pickFieldValue(attributes, body, keys));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function includesAnySubstring(input: string, candidates: readonly string[]): boolean {
|
|
306
|
+
for (const candidate of candidates) {
|
|
307
|
+
if (input.includes(candidate)) {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const NEEDS_INPUT_HINT_TOKENS = [
|
|
315
|
+
'needs-input',
|
|
316
|
+
'needs_input',
|
|
317
|
+
'attention-required',
|
|
318
|
+
'attention_required',
|
|
319
|
+
'input-required',
|
|
320
|
+
'approval-required',
|
|
321
|
+
'approval_required',
|
|
322
|
+
] as const;
|
|
323
|
+
|
|
324
|
+
const LIFECYCLE_TELEMETRY_EVENT_NAMES = new Set([
|
|
325
|
+
'codex.user_prompt',
|
|
326
|
+
'codex.turn.e2e_duration_ms',
|
|
327
|
+
'codex.conversation_starts',
|
|
328
|
+
]);
|
|
329
|
+
|
|
330
|
+
function isLifecycleTelemetryEventName(eventName: string | null): boolean {
|
|
331
|
+
const normalized = eventName?.trim().toLowerCase() ?? '';
|
|
332
|
+
if (normalized.length === 0) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
return LIFECYCLE_TELEMETRY_EVENT_NAMES.has(normalized);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function statusFromOutcomeText(value: string | null): CodexStatusHint | null {
|
|
339
|
+
const normalized = value?.toLowerCase().trim() ?? '';
|
|
340
|
+
if (normalized.length === 0) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
if (includesAnySubstring(normalized, NEEDS_INPUT_HINT_TOKENS)) {
|
|
344
|
+
return 'needs-input';
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function pickEventName(
|
|
350
|
+
explicit: unknown,
|
|
351
|
+
attributes: Record<string, unknown>,
|
|
352
|
+
body: unknown,
|
|
353
|
+
): string | null {
|
|
354
|
+
const candidates = [
|
|
355
|
+
explicit,
|
|
356
|
+
attributes['event.name'],
|
|
357
|
+
attributes['name'],
|
|
358
|
+
attributes['codex.event'],
|
|
359
|
+
attributes['event'],
|
|
360
|
+
asRecord(body)?.['event'],
|
|
361
|
+
asRecord(body)?.['name'],
|
|
362
|
+
asRecord(body)?.['type'],
|
|
363
|
+
body,
|
|
364
|
+
];
|
|
365
|
+
for (const candidate of candidates) {
|
|
366
|
+
const value = asSummaryText(candidate);
|
|
367
|
+
if (value !== null) {
|
|
368
|
+
return value;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function collectThreadIdCandidates(
|
|
375
|
+
value: unknown,
|
|
376
|
+
output: string[],
|
|
377
|
+
directMatch: boolean,
|
|
378
|
+
depth: number,
|
|
379
|
+
maxDepth: number,
|
|
380
|
+
maxValues: number,
|
|
381
|
+
): void {
|
|
382
|
+
if (depth > maxDepth) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (typeof value === 'string') {
|
|
386
|
+
if (directMatch && value.trim().length > 0) {
|
|
387
|
+
output.push(value.trim());
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (typeof value !== 'object' || value === null) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (Array.isArray(value)) {
|
|
395
|
+
for (const entry of value) {
|
|
396
|
+
collectThreadIdCandidates(entry, output, directMatch, depth + 1, maxDepth, maxValues);
|
|
397
|
+
if (output.length >= maxValues) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
404
|
+
const normalizedKey = key.toLowerCase();
|
|
405
|
+
if (
|
|
406
|
+
normalizedKey === 'threadid' ||
|
|
407
|
+
normalizedKey === 'thread_id' ||
|
|
408
|
+
normalizedKey === 'thread-id' ||
|
|
409
|
+
normalizedKey === 'sessionid' ||
|
|
410
|
+
normalizedKey === 'session_id' ||
|
|
411
|
+
normalizedKey === 'session-id' ||
|
|
412
|
+
normalizedKey === 'conversationid' ||
|
|
413
|
+
normalizedKey === 'conversation_id' ||
|
|
414
|
+
normalizedKey === 'conversation-id'
|
|
415
|
+
) {
|
|
416
|
+
collectThreadIdCandidates(nested, output, true, depth + 1, maxDepth, maxValues);
|
|
417
|
+
} else if (
|
|
418
|
+
normalizedKey === 'attributes' ||
|
|
419
|
+
normalizedKey === 'payload' ||
|
|
420
|
+
normalizedKey === 'body' ||
|
|
421
|
+
normalizedKey === 'metadata' ||
|
|
422
|
+
normalizedKey === 'context' ||
|
|
423
|
+
normalizedKey === 'data' ||
|
|
424
|
+
normalizedKey === 'resource' ||
|
|
425
|
+
normalizedKey === 'metric' ||
|
|
426
|
+
normalizedKey === 'span' ||
|
|
427
|
+
normalizedKey === 'entry'
|
|
428
|
+
) {
|
|
429
|
+
collectThreadIdCandidates(nested, output, directMatch, depth + 1, maxDepth, maxValues);
|
|
430
|
+
}
|
|
431
|
+
if (output.length >= maxValues) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function extractCodexThreadId(payload: unknown): string | null {
|
|
438
|
+
const candidates: string[] = [];
|
|
439
|
+
collectThreadIdCandidates(payload, candidates, false, 0, 4, 16);
|
|
440
|
+
if (candidates.length === 0) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
return candidates[0] as string;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function deriveStatusHint(
|
|
447
|
+
eventName: string | null,
|
|
448
|
+
severity: string | null,
|
|
449
|
+
summary: string | null,
|
|
450
|
+
payload: Record<string, unknown>,
|
|
451
|
+
): CodexStatusHint | null {
|
|
452
|
+
const normalizedEventName = eventName?.toLowerCase().trim() ?? '';
|
|
453
|
+
if (normalizedEventName === 'codex.user_prompt') {
|
|
454
|
+
return 'running';
|
|
455
|
+
}
|
|
456
|
+
if (normalizedEventName === 'codex.turn.e2e_duration_ms') {
|
|
457
|
+
return 'completed';
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const payloadAttributes = asRecord(payload['attributes']) ?? {};
|
|
461
|
+
const payloadBody = payload['body'];
|
|
462
|
+
const outcomeHint = statusFromOutcomeText(
|
|
463
|
+
pickFieldText(payloadAttributes, payloadBody, [
|
|
464
|
+
'status',
|
|
465
|
+
'result',
|
|
466
|
+
'outcome',
|
|
467
|
+
'decision',
|
|
468
|
+
'kind',
|
|
469
|
+
'event.kind',
|
|
470
|
+
'event_type',
|
|
471
|
+
'event.type',
|
|
472
|
+
'type',
|
|
473
|
+
]),
|
|
474
|
+
);
|
|
475
|
+
if (outcomeHint !== null) {
|
|
476
|
+
return outcomeHint;
|
|
477
|
+
}
|
|
478
|
+
if (summary !== null) {
|
|
479
|
+
const fromSummary = statusFromOutcomeText(summary);
|
|
480
|
+
if (fromSummary !== null) {
|
|
481
|
+
return fromSummary;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function buildLogSummary(
|
|
488
|
+
eventName: string | null,
|
|
489
|
+
body: unknown,
|
|
490
|
+
attributes: Record<string, unknown>,
|
|
491
|
+
): string | null {
|
|
492
|
+
const normalizedEventName = eventName?.toLowerCase().trim() ?? '';
|
|
493
|
+
const bodyText = asSummaryText(body);
|
|
494
|
+
const eventText = eventName?.trim() ?? null;
|
|
495
|
+
const statusText =
|
|
496
|
+
pickFieldText(attributes, body, ['status', 'result', 'outcome', 'decision']) ??
|
|
497
|
+
asSummaryText(attributes['status']) ??
|
|
498
|
+
asSummaryText(attributes['result']);
|
|
499
|
+
const kindText = pickFieldText(attributes, body, [
|
|
500
|
+
'kind',
|
|
501
|
+
'event.kind',
|
|
502
|
+
'event_type',
|
|
503
|
+
'event.type',
|
|
504
|
+
'type',
|
|
505
|
+
]);
|
|
506
|
+
const toolText = pickFieldText(attributes, body, [
|
|
507
|
+
'tool.name',
|
|
508
|
+
'tool_name',
|
|
509
|
+
'toolName',
|
|
510
|
+
'tool',
|
|
511
|
+
'name',
|
|
512
|
+
]);
|
|
513
|
+
const modelText = pickFieldText(attributes, body, ['model', 'model_name', 'modelName']);
|
|
514
|
+
const durationMs = pickFieldNumber(attributes, body, [
|
|
515
|
+
'duration_ms',
|
|
516
|
+
'durationMs',
|
|
517
|
+
'latency_ms',
|
|
518
|
+
'elapsed_ms',
|
|
519
|
+
]);
|
|
520
|
+
|
|
521
|
+
if (normalizedEventName === 'codex.user_prompt') {
|
|
522
|
+
const promptText = compactSummaryText(bodyText);
|
|
523
|
+
return promptText === null ? 'prompt submitted' : `prompt: ${promptText}`;
|
|
524
|
+
}
|
|
525
|
+
if (normalizedEventName === 'codex.conversation_starts') {
|
|
526
|
+
const model = compactSummaryText(modelText);
|
|
527
|
+
return model === null ? 'conversation started' : `conversation started (${model})`;
|
|
528
|
+
}
|
|
529
|
+
if (normalizedEventName === 'codex.api_request') {
|
|
530
|
+
const outcome = compactSummaryText(statusText);
|
|
531
|
+
if (outcome !== null && durationMs !== null) {
|
|
532
|
+
return `model request ${outcome} (${durationMs.toFixed(0)}ms)`;
|
|
533
|
+
}
|
|
534
|
+
if (outcome !== null) {
|
|
535
|
+
return `model request ${outcome}`;
|
|
536
|
+
}
|
|
537
|
+
if (durationMs !== null) {
|
|
538
|
+
return `model request (${durationMs.toFixed(0)}ms)`;
|
|
539
|
+
}
|
|
540
|
+
return 'model request';
|
|
541
|
+
}
|
|
542
|
+
if (normalizedEventName === 'codex.sse_event') {
|
|
543
|
+
const kind = compactSummaryText(kindText ?? bodyText);
|
|
544
|
+
return kind === null ? 'stream event' : `stream ${kind}`;
|
|
545
|
+
}
|
|
546
|
+
if (normalizedEventName === 'codex.tool_decision') {
|
|
547
|
+
const decision = compactSummaryText(statusText);
|
|
548
|
+
const tool = compactSummaryText(toolText);
|
|
549
|
+
if (decision !== null && tool !== null) {
|
|
550
|
+
return `approval ${decision} (${tool})`;
|
|
551
|
+
}
|
|
552
|
+
if (decision !== null) {
|
|
553
|
+
return `approval ${decision}`;
|
|
554
|
+
}
|
|
555
|
+
if (tool !== null) {
|
|
556
|
+
return `approval (${tool})`;
|
|
557
|
+
}
|
|
558
|
+
return 'approval decision';
|
|
559
|
+
}
|
|
560
|
+
if (normalizedEventName === 'codex.tool_result') {
|
|
561
|
+
const tool = compactSummaryText(toolText);
|
|
562
|
+
const outcome = compactSummaryText(statusText);
|
|
563
|
+
if (tool !== null && outcome !== null && durationMs !== null) {
|
|
564
|
+
return `tool ${tool} ${outcome} (${durationMs.toFixed(0)}ms)`;
|
|
565
|
+
}
|
|
566
|
+
if (tool !== null && outcome !== null) {
|
|
567
|
+
return `tool ${tool} ${outcome}`;
|
|
568
|
+
}
|
|
569
|
+
if (tool !== null && durationMs !== null) {
|
|
570
|
+
return `tool ${tool} (${durationMs.toFixed(0)}ms)`;
|
|
571
|
+
}
|
|
572
|
+
if (tool !== null) {
|
|
573
|
+
return `tool ${tool}`;
|
|
574
|
+
}
|
|
575
|
+
if (outcome !== null) {
|
|
576
|
+
return `tool result ${outcome}`;
|
|
577
|
+
}
|
|
578
|
+
return 'tool result';
|
|
579
|
+
}
|
|
580
|
+
if (normalizedEventName === 'codex.websocket_request') {
|
|
581
|
+
if (durationMs !== null) {
|
|
582
|
+
return `realtime request (${durationMs.toFixed(0)}ms)`;
|
|
583
|
+
}
|
|
584
|
+
return 'realtime request';
|
|
585
|
+
}
|
|
586
|
+
if (normalizedEventName === 'codex.websocket_event') {
|
|
587
|
+
const kind = compactSummaryText(kindText ?? bodyText);
|
|
588
|
+
const outcome = compactSummaryText(statusText);
|
|
589
|
+
if (kind !== null && outcome !== null) {
|
|
590
|
+
return `realtime ${kind} (${outcome})`;
|
|
591
|
+
}
|
|
592
|
+
if (kind !== null) {
|
|
593
|
+
return `realtime ${kind}`;
|
|
594
|
+
}
|
|
595
|
+
if (outcome !== null) {
|
|
596
|
+
return `realtime event (${outcome})`;
|
|
597
|
+
}
|
|
598
|
+
return 'realtime event';
|
|
599
|
+
}
|
|
600
|
+
if (eventText !== null && statusText !== null) {
|
|
601
|
+
return `${eventText} (${statusText})`;
|
|
602
|
+
}
|
|
603
|
+
if (eventText !== null && bodyText !== null && bodyText !== eventText) {
|
|
604
|
+
return `${eventText}: ${bodyText}`;
|
|
605
|
+
}
|
|
606
|
+
return eventText ?? bodyText;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export function parseOtlpLogEvents(
|
|
610
|
+
payload: unknown,
|
|
611
|
+
observedAtFallback: string,
|
|
612
|
+
): readonly ParsedCodexTelemetryEvent[] {
|
|
613
|
+
const root = asRecord(payload);
|
|
614
|
+
if (root === null || !Array.isArray(root['resourceLogs'])) {
|
|
615
|
+
return [];
|
|
616
|
+
}
|
|
617
|
+
const events: ParsedCodexTelemetryEvent[] = [];
|
|
618
|
+
|
|
619
|
+
for (const resourceLog of root['resourceLogs']) {
|
|
620
|
+
const resourceLogRecord = asRecord(resourceLog);
|
|
621
|
+
if (resourceLogRecord === null) {
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
const resourceRecord = asRecord(resourceLogRecord['resource']);
|
|
625
|
+
const resourceAttributes = parseOtlpAttributes(resourceRecord?.['attributes']);
|
|
626
|
+
const scopeLogs = resourceLogRecord['scopeLogs'];
|
|
627
|
+
if (!Array.isArray(scopeLogs)) {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
for (const scopeLog of scopeLogs) {
|
|
631
|
+
const scopeLogRecord = asRecord(scopeLog);
|
|
632
|
+
if (scopeLogRecord === null || !Array.isArray(scopeLogRecord['logRecords'])) {
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
const scopeRecord = asRecord(scopeLogRecord['scope']);
|
|
636
|
+
const scopeAttributes = parseOtlpAttributes(scopeRecord?.['attributes']);
|
|
637
|
+
|
|
638
|
+
for (const logRecord of scopeLogRecord['logRecords']) {
|
|
639
|
+
const item = asRecord(logRecord);
|
|
640
|
+
if (item === null) {
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
const attributes = parseOtlpAttributes(item['attributes']);
|
|
644
|
+
const body = parseAnyValue(item['body']);
|
|
645
|
+
const observedAt = normalizeNanoTimestamp(
|
|
646
|
+
item['timeUnixNano'],
|
|
647
|
+
normalizeNanoTimestamp(item['observedTimeUnixNano'], observedAtFallback),
|
|
648
|
+
);
|
|
649
|
+
const eventName = pickEventName(attributes['event.name'], attributes, body);
|
|
650
|
+
const severity = readStringTrimmed(item['severityText']);
|
|
651
|
+
const payloadRecord: Record<string, unknown> = {
|
|
652
|
+
resource: resourceAttributes,
|
|
653
|
+
scope: scopeAttributes,
|
|
654
|
+
attributes,
|
|
655
|
+
body,
|
|
656
|
+
};
|
|
657
|
+
const summary = buildLogSummary(eventName, body, attributes);
|
|
658
|
+
events.push({
|
|
659
|
+
source: 'otlp-log',
|
|
660
|
+
observedAt,
|
|
661
|
+
eventName,
|
|
662
|
+
severity,
|
|
663
|
+
summary,
|
|
664
|
+
providerThreadId: extractCodexThreadId(payloadRecord),
|
|
665
|
+
statusHint: deriveStatusHint(eventName, severity, summary, payloadRecord),
|
|
666
|
+
payload: payloadRecord,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return events;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function metricDataPoints(metric: Record<string, unknown>): readonly Record<string, unknown>[] {
|
|
676
|
+
const candidates = [
|
|
677
|
+
asRecord(metric['sum'])?.['dataPoints'],
|
|
678
|
+
asRecord(metric['gauge'])?.['dataPoints'],
|
|
679
|
+
asRecord(metric['histogram'])?.['dataPoints'],
|
|
680
|
+
asRecord(metric['exponentialHistogram'])?.['dataPoints'],
|
|
681
|
+
asRecord(metric['summary'])?.['dataPoints'],
|
|
682
|
+
];
|
|
683
|
+
for (const candidate of candidates) {
|
|
684
|
+
if (!Array.isArray(candidate)) {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
return candidate.flatMap((entry) => {
|
|
688
|
+
const record = asRecord(entry);
|
|
689
|
+
return record === null ? [] : [record];
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
return [];
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function readMetricPointValue(point: Record<string, unknown>): number | null {
|
|
696
|
+
const direct = readFiniteNumber(point['asDouble']) ?? readFiniteNumber(point['asInt']);
|
|
697
|
+
if (direct !== null) {
|
|
698
|
+
return direct;
|
|
699
|
+
}
|
|
700
|
+
const sum = readFiniteNumber(point['sum']);
|
|
701
|
+
if (sum !== null) {
|
|
702
|
+
return sum;
|
|
703
|
+
}
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export function parseOtlpMetricEvents(
|
|
708
|
+
payload: unknown,
|
|
709
|
+
observedAtFallback: string,
|
|
710
|
+
): readonly ParsedCodexTelemetryEvent[] {
|
|
711
|
+
const root = asRecord(payload);
|
|
712
|
+
if (root === null || !Array.isArray(root['resourceMetrics'])) {
|
|
713
|
+
return [];
|
|
714
|
+
}
|
|
715
|
+
const events: ParsedCodexTelemetryEvent[] = [];
|
|
716
|
+
for (const resourceMetric of root['resourceMetrics']) {
|
|
717
|
+
const resourceMetricRecord = asRecord(resourceMetric);
|
|
718
|
+
if (resourceMetricRecord === null || !Array.isArray(resourceMetricRecord['scopeMetrics'])) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const resourceRecord = asRecord(resourceMetricRecord['resource']);
|
|
722
|
+
const resourceAttributes = parseOtlpAttributes(resourceRecord?.['attributes']);
|
|
723
|
+
for (const scopeMetric of resourceMetricRecord['scopeMetrics']) {
|
|
724
|
+
const scopeMetricRecord = asRecord(scopeMetric);
|
|
725
|
+
if (scopeMetricRecord === null || !Array.isArray(scopeMetricRecord['metrics'])) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
for (const metricValue of scopeMetricRecord['metrics']) {
|
|
729
|
+
const metric = asRecord(metricValue);
|
|
730
|
+
if (metric === null) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
const metricName = readStringTrimmed(metric['name']);
|
|
734
|
+
const points = metricDataPoints(metric);
|
|
735
|
+
const pointCount = points.length;
|
|
736
|
+
const firstPointValue = points.length > 0 ? readMetricPointValue(points[0]!) : null;
|
|
737
|
+
const payloadRecord: Record<string, unknown> = {
|
|
738
|
+
resource: resourceAttributes,
|
|
739
|
+
metric,
|
|
740
|
+
};
|
|
741
|
+
let summary: string;
|
|
742
|
+
if (metricName === 'codex.turn.e2e_duration_ms' && firstPointValue !== null) {
|
|
743
|
+
summary = `turn complete (${firstPointValue.toFixed(0)}ms)`;
|
|
744
|
+
} else if (metricName === 'codex.conversation.turn.count' && firstPointValue !== null) {
|
|
745
|
+
summary = `turn count ${String(Math.max(0, Math.round(firstPointValue)))}`;
|
|
746
|
+
} else {
|
|
747
|
+
summary =
|
|
748
|
+
metricName === null
|
|
749
|
+
? `metric points=${String(pointCount)}`
|
|
750
|
+
: `${metricName} points=${String(pointCount)}`;
|
|
751
|
+
}
|
|
752
|
+
const statusHint = metricName === 'codex.turn.e2e_duration_ms' ? 'completed' : null;
|
|
753
|
+
events.push({
|
|
754
|
+
source: 'otlp-metric',
|
|
755
|
+
observedAt: observedAtFallback,
|
|
756
|
+
eventName: metricName,
|
|
757
|
+
severity: null,
|
|
758
|
+
summary,
|
|
759
|
+
providerThreadId: extractCodexThreadId(payloadRecord),
|
|
760
|
+
statusHint,
|
|
761
|
+
payload: payloadRecord,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return events;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
export function parseOtlpTraceEvents(
|
|
770
|
+
payload: unknown,
|
|
771
|
+
observedAtFallback: string,
|
|
772
|
+
): readonly ParsedCodexTelemetryEvent[] {
|
|
773
|
+
const root = asRecord(payload);
|
|
774
|
+
if (root === null || !Array.isArray(root['resourceSpans'])) {
|
|
775
|
+
return [];
|
|
776
|
+
}
|
|
777
|
+
const events: ParsedCodexTelemetryEvent[] = [];
|
|
778
|
+
for (const resourceSpan of root['resourceSpans']) {
|
|
779
|
+
const resourceSpanRecord = asRecord(resourceSpan);
|
|
780
|
+
if (resourceSpanRecord === null || !Array.isArray(resourceSpanRecord['scopeSpans'])) {
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
const resourceRecord = asRecord(resourceSpanRecord['resource']);
|
|
784
|
+
const resourceAttributes = parseOtlpAttributes(resourceRecord?.['attributes']);
|
|
785
|
+
for (const scopeSpan of resourceSpanRecord['scopeSpans']) {
|
|
786
|
+
const scopeSpanRecord = asRecord(scopeSpan);
|
|
787
|
+
if (scopeSpanRecord === null || !Array.isArray(scopeSpanRecord['spans'])) {
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
for (const spanValue of scopeSpanRecord['spans']) {
|
|
791
|
+
const span = asRecord(spanValue);
|
|
792
|
+
if (span === null) {
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
const attributes = parseOtlpAttributes(span['attributes']);
|
|
796
|
+
const spanName = readStringTrimmed(span['name']);
|
|
797
|
+
const observedAt = normalizeNanoTimestamp(span['endTimeUnixNano'], observedAtFallback);
|
|
798
|
+
const kind = pickFieldText(attributes, span, [
|
|
799
|
+
'kind',
|
|
800
|
+
'event.kind',
|
|
801
|
+
'event_type',
|
|
802
|
+
'event.type',
|
|
803
|
+
'type',
|
|
804
|
+
]);
|
|
805
|
+
const status = pickFieldText(attributes, span, ['status', 'result', 'outcome']);
|
|
806
|
+
const summary =
|
|
807
|
+
spanName === null
|
|
808
|
+
? (compactSummaryText(kind) ?? compactSummaryText(status) ?? 'span')
|
|
809
|
+
: (compactSummaryText(
|
|
810
|
+
kind === null
|
|
811
|
+
? status === null
|
|
812
|
+
? spanName
|
|
813
|
+
: `${spanName} (${status})`
|
|
814
|
+
: `${spanName}: ${kind}`,
|
|
815
|
+
) as string);
|
|
816
|
+
const payloadRecord: Record<string, unknown> = {
|
|
817
|
+
resource: resourceAttributes,
|
|
818
|
+
attributes,
|
|
819
|
+
span,
|
|
820
|
+
};
|
|
821
|
+
events.push({
|
|
822
|
+
source: 'otlp-trace',
|
|
823
|
+
observedAt,
|
|
824
|
+
eventName: spanName,
|
|
825
|
+
severity: null,
|
|
826
|
+
summary,
|
|
827
|
+
providerThreadId: extractCodexThreadId(payloadRecord),
|
|
828
|
+
statusHint: null,
|
|
829
|
+
payload: payloadRecord,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return events;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function readOtlpTextValue(value: unknown): string | null {
|
|
838
|
+
const record = asRecord(value);
|
|
839
|
+
if (record === null) {
|
|
840
|
+
return asSummaryText(value);
|
|
841
|
+
}
|
|
842
|
+
if (record['stringValue'] !== undefined) {
|
|
843
|
+
return asSummaryText(record['stringValue']);
|
|
844
|
+
}
|
|
845
|
+
if (record['boolValue'] !== undefined) {
|
|
846
|
+
return asSummaryText(record['boolValue']);
|
|
847
|
+
}
|
|
848
|
+
if (record['intValue'] !== undefined) {
|
|
849
|
+
return asSummaryText(record['intValue']);
|
|
850
|
+
}
|
|
851
|
+
if (record['doubleValue'] !== undefined) {
|
|
852
|
+
return asSummaryText(record['doubleValue']);
|
|
853
|
+
}
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function parseOtlpAttributeTextMap(value: unknown): Record<string, string> {
|
|
858
|
+
if (!Array.isArray(value)) {
|
|
859
|
+
return {};
|
|
860
|
+
}
|
|
861
|
+
const out: Record<string, string> = {};
|
|
862
|
+
for (const entry of value) {
|
|
863
|
+
const record = asRecord(entry);
|
|
864
|
+
if (record === null) {
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
const key = readStringTrimmed(record['key']);
|
|
868
|
+
if (key === null) {
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
const parsedValue = readOtlpTextValue(record['value']);
|
|
872
|
+
if (parsedValue === null) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
out[key] = parsedValue;
|
|
876
|
+
}
|
|
877
|
+
return out;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function pickAttributeText(
|
|
881
|
+
attributes: Record<string, string>,
|
|
882
|
+
keys: readonly string[],
|
|
883
|
+
): string | null {
|
|
884
|
+
const normalizedKeys = new Set(keys.map((key) => normalizeLookupKey(key)));
|
|
885
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
886
|
+
if (normalizedKeys.has(normalizeLookupKey(key))) {
|
|
887
|
+
return value;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function lifecycleSummaryFromEventName(
|
|
894
|
+
eventName: string | null,
|
|
895
|
+
statusHint: CodexStatusHint | null,
|
|
896
|
+
attributes: Record<string, string>,
|
|
897
|
+
): string | null {
|
|
898
|
+
const normalizedEventName = eventName?.trim().toLowerCase() ?? '';
|
|
899
|
+
if (normalizedEventName === 'codex.user_prompt') {
|
|
900
|
+
return 'prompt submitted';
|
|
901
|
+
}
|
|
902
|
+
if (normalizedEventName === 'codex.conversation_starts') {
|
|
903
|
+
const model = pickAttributeText(attributes, ['model', 'model_name', 'modelName']);
|
|
904
|
+
if (model !== null) {
|
|
905
|
+
return `conversation started (${compactSummaryText(model)})`;
|
|
906
|
+
}
|
|
907
|
+
return 'conversation started';
|
|
908
|
+
}
|
|
909
|
+
if (normalizedEventName === 'codex.turn.e2e_duration_ms') {
|
|
910
|
+
return 'turn complete';
|
|
911
|
+
}
|
|
912
|
+
if (normalizedEventName === 'codex.sse_event') {
|
|
913
|
+
const kind = pickAttributeText(attributes, [
|
|
914
|
+
'kind',
|
|
915
|
+
'event.kind',
|
|
916
|
+
'event_type',
|
|
917
|
+
'event.type',
|
|
918
|
+
'type',
|
|
919
|
+
]);
|
|
920
|
+
return kind === null ? 'stream event' : `stream ${compactSummaryText(kind)}`;
|
|
921
|
+
}
|
|
922
|
+
return statusHint === 'needs-input' ? 'needs-input' : null;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function lifecycleEventNameFromAttributes(
|
|
926
|
+
attributes: Record<string, string>,
|
|
927
|
+
bodyText: string | null,
|
|
928
|
+
): string | null {
|
|
929
|
+
return (
|
|
930
|
+
pickAttributeText(attributes, ['event.name', 'name', 'codex.event', 'event', 'type']) ??
|
|
931
|
+
compactSummaryText(bodyText)
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function lifecycleThreadIdFromAttributes(attributes: Record<string, string>): string | null {
|
|
936
|
+
return pickAttributeText(attributes, [
|
|
937
|
+
'thread-id',
|
|
938
|
+
'thread_id',
|
|
939
|
+
'threadid',
|
|
940
|
+
'session-id',
|
|
941
|
+
'session_id',
|
|
942
|
+
'sessionid',
|
|
943
|
+
'conversation-id',
|
|
944
|
+
'conversation_id',
|
|
945
|
+
'conversationid',
|
|
946
|
+
]);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function lifecycleStatusHintFromAttributes(
|
|
950
|
+
eventName: string | null,
|
|
951
|
+
attributes: Record<string, string>,
|
|
952
|
+
bodyText: string | null,
|
|
953
|
+
): CodexStatusHint | null {
|
|
954
|
+
const normalizedEventName = eventName?.toLowerCase().trim() ?? '';
|
|
955
|
+
if (normalizedEventName === 'codex.user_prompt') {
|
|
956
|
+
return 'running';
|
|
957
|
+
}
|
|
958
|
+
if (normalizedEventName === 'codex.turn.e2e_duration_ms') {
|
|
959
|
+
return 'completed';
|
|
960
|
+
}
|
|
961
|
+
const statusToken =
|
|
962
|
+
pickAttributeText(attributes, [
|
|
963
|
+
'status',
|
|
964
|
+
'result',
|
|
965
|
+
'outcome',
|
|
966
|
+
'decision',
|
|
967
|
+
'kind',
|
|
968
|
+
'event.kind',
|
|
969
|
+
'event_type',
|
|
970
|
+
'event.type',
|
|
971
|
+
'type',
|
|
972
|
+
]) ?? bodyText;
|
|
973
|
+
const statusHint = statusFromOutcomeText(statusToken);
|
|
974
|
+
if (statusHint !== null) {
|
|
975
|
+
return statusHint;
|
|
976
|
+
}
|
|
977
|
+
return statusFromOutcomeText(eventName);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function shouldRetainLifecycleEvent(
|
|
981
|
+
eventName: string | null,
|
|
982
|
+
statusHint: CodexStatusHint | null,
|
|
983
|
+
): boolean {
|
|
984
|
+
return isLifecycleTelemetryEventName(eventName) || statusHint !== null;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
export function parseOtlpLifecycleLogEvents(
|
|
988
|
+
payload: unknown,
|
|
989
|
+
observedAtFallback: string,
|
|
990
|
+
): readonly ParsedCodexTelemetryEvent[] {
|
|
991
|
+
const root = asRecord(payload);
|
|
992
|
+
if (root === null || !Array.isArray(root['resourceLogs'])) {
|
|
993
|
+
return [];
|
|
994
|
+
}
|
|
995
|
+
const events: ParsedCodexTelemetryEvent[] = [];
|
|
996
|
+
for (const resourceLog of root['resourceLogs']) {
|
|
997
|
+
const resourceLogRecord = asRecord(resourceLog);
|
|
998
|
+
if (resourceLogRecord === null) {
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const scopeLogs = resourceLogRecord['scopeLogs'];
|
|
1002
|
+
if (!Array.isArray(scopeLogs)) {
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
for (const scopeLog of scopeLogs) {
|
|
1006
|
+
const scopeLogRecord = asRecord(scopeLog);
|
|
1007
|
+
if (scopeLogRecord === null || !Array.isArray(scopeLogRecord['logRecords'])) {
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
for (const logRecord of scopeLogRecord['logRecords']) {
|
|
1011
|
+
const item = asRecord(logRecord);
|
|
1012
|
+
if (item === null) {
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
const attributes = parseOtlpAttributeTextMap(item['attributes']);
|
|
1016
|
+
const bodyText = readOtlpTextValue(item['body']);
|
|
1017
|
+
const eventName = lifecycleEventNameFromAttributes(attributes, bodyText);
|
|
1018
|
+
const statusHint = lifecycleStatusHintFromAttributes(eventName, attributes, bodyText);
|
|
1019
|
+
if (!shouldRetainLifecycleEvent(eventName, statusHint)) {
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
const observedAt = normalizeNanoTimestamp(
|
|
1023
|
+
item['timeUnixNano'],
|
|
1024
|
+
normalizeNanoTimestamp(item['observedTimeUnixNano'], observedAtFallback),
|
|
1025
|
+
);
|
|
1026
|
+
const severity = readStringTrimmed(item['severityText']);
|
|
1027
|
+
const providerThreadId = lifecycleThreadIdFromAttributes(attributes);
|
|
1028
|
+
const summary = lifecycleSummaryFromEventName(eventName, statusHint, attributes);
|
|
1029
|
+
events.push({
|
|
1030
|
+
source: 'otlp-log',
|
|
1031
|
+
observedAt,
|
|
1032
|
+
eventName,
|
|
1033
|
+
severity,
|
|
1034
|
+
summary,
|
|
1035
|
+
providerThreadId,
|
|
1036
|
+
statusHint,
|
|
1037
|
+
payload: {
|
|
1038
|
+
attributes,
|
|
1039
|
+
body: bodyText,
|
|
1040
|
+
},
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return events;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function metricDataPointsShallow(
|
|
1049
|
+
metric: Record<string, unknown>,
|
|
1050
|
+
): readonly Record<string, unknown>[] {
|
|
1051
|
+
const candidates = [
|
|
1052
|
+
asRecord(metric['sum'])?.['dataPoints'],
|
|
1053
|
+
asRecord(metric['gauge'])?.['dataPoints'],
|
|
1054
|
+
asRecord(metric['histogram'])?.['dataPoints'],
|
|
1055
|
+
asRecord(metric['exponentialHistogram'])?.['dataPoints'],
|
|
1056
|
+
asRecord(metric['summary'])?.['dataPoints'],
|
|
1057
|
+
];
|
|
1058
|
+
for (const candidate of candidates) {
|
|
1059
|
+
if (!Array.isArray(candidate)) {
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
return candidate.flatMap((entry) => {
|
|
1063
|
+
const record = asRecord(entry);
|
|
1064
|
+
return record === null ? [] : [record];
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
return [];
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function readMetricPointValueShallow(point: Record<string, unknown>): number | null {
|
|
1071
|
+
return (
|
|
1072
|
+
readFiniteNumber(point['asDouble']) ??
|
|
1073
|
+
readFiniteNumber(point['asInt']) ??
|
|
1074
|
+
readFiniteNumber(point['sum'])
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
export function parseOtlpLifecycleMetricEvents(
|
|
1079
|
+
payload: unknown,
|
|
1080
|
+
observedAtFallback: string,
|
|
1081
|
+
): readonly ParsedCodexTelemetryEvent[] {
|
|
1082
|
+
const root = asRecord(payload);
|
|
1083
|
+
if (root === null || !Array.isArray(root['resourceMetrics'])) {
|
|
1084
|
+
return [];
|
|
1085
|
+
}
|
|
1086
|
+
const events: ParsedCodexTelemetryEvent[] = [];
|
|
1087
|
+
for (const resourceMetric of root['resourceMetrics']) {
|
|
1088
|
+
const resourceMetricRecord = asRecord(resourceMetric);
|
|
1089
|
+
if (resourceMetricRecord === null || !Array.isArray(resourceMetricRecord['scopeMetrics'])) {
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
const resourceAttributes = parseOtlpAttributeTextMap(
|
|
1093
|
+
asRecord(resourceMetricRecord['resource'])?.['attributes'],
|
|
1094
|
+
);
|
|
1095
|
+
for (const scopeMetric of resourceMetricRecord['scopeMetrics']) {
|
|
1096
|
+
const scopeMetricRecord = asRecord(scopeMetric);
|
|
1097
|
+
if (scopeMetricRecord === null || !Array.isArray(scopeMetricRecord['metrics'])) {
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
for (const metricValue of scopeMetricRecord['metrics']) {
|
|
1101
|
+
const metric = asRecord(metricValue);
|
|
1102
|
+
if (metric === null) {
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
const eventName = readStringTrimmed(metric['name']);
|
|
1106
|
+
const statusHint = eventName === 'codex.turn.e2e_duration_ms' ? 'completed' : null;
|
|
1107
|
+
if (!shouldRetainLifecycleEvent(eventName, statusHint)) {
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
const dataPoints = metricDataPointsShallow(metric);
|
|
1111
|
+
const firstPoint = dataPoints[0];
|
|
1112
|
+
const pointAttributes =
|
|
1113
|
+
firstPoint === undefined ? {} : parseOtlpAttributeTextMap(firstPoint['attributes']);
|
|
1114
|
+
const providerThreadId =
|
|
1115
|
+
lifecycleThreadIdFromAttributes(pointAttributes) ??
|
|
1116
|
+
lifecycleThreadIdFromAttributes(resourceAttributes);
|
|
1117
|
+
const firstValue =
|
|
1118
|
+
firstPoint === undefined ? null : readMetricPointValueShallow(firstPoint);
|
|
1119
|
+
const summary =
|
|
1120
|
+
eventName === 'codex.turn.e2e_duration_ms' && firstValue !== null
|
|
1121
|
+
? `turn complete (${firstValue.toFixed(0)}ms)`
|
|
1122
|
+
: (compactSummaryText(eventName) ?? 'metric');
|
|
1123
|
+
const observedAt =
|
|
1124
|
+
firstPoint === undefined
|
|
1125
|
+
? observedAtFallback
|
|
1126
|
+
: normalizeNanoTimestamp(firstPoint['timeUnixNano'], observedAtFallback);
|
|
1127
|
+
events.push({
|
|
1128
|
+
source: 'otlp-metric',
|
|
1129
|
+
observedAt,
|
|
1130
|
+
eventName,
|
|
1131
|
+
severity: null,
|
|
1132
|
+
summary,
|
|
1133
|
+
providerThreadId,
|
|
1134
|
+
statusHint,
|
|
1135
|
+
payload: {
|
|
1136
|
+
metricName: eventName,
|
|
1137
|
+
pointCount: dataPoints.length,
|
|
1138
|
+
firstPointValue: firstValue,
|
|
1139
|
+
},
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return events;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
export function parseOtlpLifecycleTraceEvents(
|
|
1148
|
+
payload: unknown,
|
|
1149
|
+
observedAtFallback: string,
|
|
1150
|
+
): readonly ParsedCodexTelemetryEvent[] {
|
|
1151
|
+
const root = asRecord(payload);
|
|
1152
|
+
if (root === null || !Array.isArray(root['resourceSpans'])) {
|
|
1153
|
+
return [];
|
|
1154
|
+
}
|
|
1155
|
+
const events: ParsedCodexTelemetryEvent[] = [];
|
|
1156
|
+
for (const resourceSpan of root['resourceSpans']) {
|
|
1157
|
+
const resourceSpanRecord = asRecord(resourceSpan);
|
|
1158
|
+
if (resourceSpanRecord === null || !Array.isArray(resourceSpanRecord['scopeSpans'])) {
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
for (const scopeSpan of resourceSpanRecord['scopeSpans']) {
|
|
1162
|
+
const scopeSpanRecord = asRecord(scopeSpan);
|
|
1163
|
+
if (scopeSpanRecord === null || !Array.isArray(scopeSpanRecord['spans'])) {
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
for (const spanValue of scopeSpanRecord['spans']) {
|
|
1167
|
+
const span = asRecord(spanValue);
|
|
1168
|
+
if (span === null) {
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
const attributes = parseOtlpAttributeTextMap(span['attributes']);
|
|
1172
|
+
const eventName = readStringTrimmed(span['name']);
|
|
1173
|
+
const statusHint = lifecycleStatusHintFromAttributes(eventName, attributes, null);
|
|
1174
|
+
if (!shouldRetainLifecycleEvent(eventName, statusHint)) {
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
const providerThreadId = lifecycleThreadIdFromAttributes(attributes);
|
|
1178
|
+
const observedAt = normalizeNanoTimestamp(span['endTimeUnixNano'], observedAtFallback);
|
|
1179
|
+
events.push({
|
|
1180
|
+
source: 'otlp-trace',
|
|
1181
|
+
observedAt,
|
|
1182
|
+
eventName,
|
|
1183
|
+
severity: null,
|
|
1184
|
+
summary: compactSummaryText(eventName) ?? 'span',
|
|
1185
|
+
providerThreadId,
|
|
1186
|
+
statusHint,
|
|
1187
|
+
payload: {
|
|
1188
|
+
attributes,
|
|
1189
|
+
spanName: eventName,
|
|
1190
|
+
},
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
return events;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function trimTrailingSlash(value: string): string {
|
|
1199
|
+
return value.endsWith('/') ? value.slice(0, -1) : value;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function tomlString(value: string): string {
|
|
1203
|
+
const escaped = value.replace(/\\/gu, '\\\\').replace(/"/gu, '\\"');
|
|
1204
|
+
return `"${escaped}"`;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
export function buildCodexTelemetryConfigArgs(
|
|
1208
|
+
input: CodexTelemetryConfigArgsInput,
|
|
1209
|
+
): readonly string[] {
|
|
1210
|
+
const baseEndpoint = trimTrailingSlash(input.endpointBaseUrl);
|
|
1211
|
+
const args: string[] = ['-c', `otel.log_user_prompt=${input.logUserPrompt ? 'true' : 'false'}`];
|
|
1212
|
+
if (input.captureLogs) {
|
|
1213
|
+
const endpoint = `${baseEndpoint}/v1/logs/${encodeURIComponent(input.token)}`;
|
|
1214
|
+
args.push('-c', `otel.exporter={otlp-http={endpoint=${tomlString(endpoint)},protocol="json"}}`);
|
|
1215
|
+
}
|
|
1216
|
+
if (input.captureMetrics) {
|
|
1217
|
+
const endpoint = `${baseEndpoint}/v1/metrics/${encodeURIComponent(input.token)}`;
|
|
1218
|
+
args.push(
|
|
1219
|
+
'-c',
|
|
1220
|
+
`otel.metrics_exporter={otlp-http={endpoint=${tomlString(endpoint)},protocol="json"}}`,
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
if (input.captureTraces) {
|
|
1224
|
+
const endpoint = `${baseEndpoint}/v1/traces/${encodeURIComponent(input.token)}`;
|
|
1225
|
+
args.push(
|
|
1226
|
+
'-c',
|
|
1227
|
+
`otel.trace_exporter={otlp-http={endpoint=${tomlString(endpoint)},protocol="json"}}`,
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
args.push('-c', `history.persistence=${tomlString(input.historyPersistence)}`);
|
|
1231
|
+
return args;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function pickHistoryEventName(record: Record<string, unknown>): string | null {
|
|
1235
|
+
const candidates = [record['type'], record['event'], record['name'], record['kind']];
|
|
1236
|
+
for (const candidate of candidates) {
|
|
1237
|
+
const parsed = readStringTrimmed(candidate);
|
|
1238
|
+
if (parsed !== null) {
|
|
1239
|
+
return parsed;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return 'history.entry';
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function pickHistoryObservedAt(record: Record<string, unknown>, fallback: string): string {
|
|
1246
|
+
const candidates = [record['timestamp'], record['ts'], record['time'], record['created_at']];
|
|
1247
|
+
for (const candidate of candidates) {
|
|
1248
|
+
if (candidate === undefined) {
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
const normalized = normalizeIso(candidate, fallback);
|
|
1252
|
+
if (normalized !== fallback) {
|
|
1253
|
+
return normalized;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return fallback;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function pickHistorySummary(record: Record<string, unknown>): string | null {
|
|
1260
|
+
const candidates = [
|
|
1261
|
+
record['summary'],
|
|
1262
|
+
record['message'],
|
|
1263
|
+
record['text'],
|
|
1264
|
+
asRecord(record['entry'])?.['text'],
|
|
1265
|
+
];
|
|
1266
|
+
for (const candidate of candidates) {
|
|
1267
|
+
const parsed = asSummaryText(candidate);
|
|
1268
|
+
if (parsed !== null) {
|
|
1269
|
+
return parsed;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return null;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
export function parseCodexHistoryLine(
|
|
1276
|
+
line: string,
|
|
1277
|
+
observedAtFallback: string,
|
|
1278
|
+
): ParsedCodexTelemetryEvent | null {
|
|
1279
|
+
let parsed: unknown;
|
|
1280
|
+
try {
|
|
1281
|
+
parsed = JSON.parse(line);
|
|
1282
|
+
} catch {
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
const record = asRecord(parsed);
|
|
1286
|
+
if (record === null) {
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
const observedAt = pickHistoryObservedAt(record, observedAtFallback);
|
|
1290
|
+
const eventName = pickHistoryEventName(record);
|
|
1291
|
+
const summary = pickHistorySummary(record);
|
|
1292
|
+
return {
|
|
1293
|
+
source: 'history',
|
|
1294
|
+
observedAt,
|
|
1295
|
+
eventName,
|
|
1296
|
+
severity: null,
|
|
1297
|
+
summary,
|
|
1298
|
+
providerThreadId: extractCodexThreadId(record),
|
|
1299
|
+
statusHint: deriveStatusHint(eventName, null, summary, record),
|
|
1300
|
+
payload: record,
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
export function telemetryFingerprint(event: {
|
|
1305
|
+
source: CodexTelemetrySource;
|
|
1306
|
+
sessionId: string | null;
|
|
1307
|
+
providerThreadId: string | null;
|
|
1308
|
+
eventName: string | null;
|
|
1309
|
+
observedAt: string;
|
|
1310
|
+
payload: Record<string, unknown>;
|
|
1311
|
+
}): string {
|
|
1312
|
+
const hash = createHash('sha1');
|
|
1313
|
+
hash.update(event.source);
|
|
1314
|
+
hash.update('\n');
|
|
1315
|
+
hash.update(event.sessionId ?? '');
|
|
1316
|
+
hash.update('\n');
|
|
1317
|
+
hash.update(event.providerThreadId ?? '');
|
|
1318
|
+
hash.update('\n');
|
|
1319
|
+
hash.update(event.eventName ?? '');
|
|
1320
|
+
hash.update('\n');
|
|
1321
|
+
hash.update(event.observedAt);
|
|
1322
|
+
hash.update('\n');
|
|
1323
|
+
hash.update(JSON.stringify(event.payload));
|
|
1324
|
+
return hash.digest('hex');
|
|
1325
|
+
}
|