ccx-native 0.0.0-stage → 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 +241 -2
- package/bin/ccx +56 -0
- package/ccx/__init__.py +163 -0
- package/ccx/__main__.py +28 -0
- package/ccx/claude/__init__.py +0 -0
- package/ccx/claude/analytics.py +466 -0
- package/ccx/claude/attach.py +762 -0
- package/ccx/claude/context.py +465 -0
- package/ccx/claude/delivery.py +806 -0
- package/ccx/claude/external.py +253 -0
- package/ccx/claude/host/__init__.py +0 -0
- package/ccx/claude/host/events.py +297 -0
- package/ccx/claude/host/hooks.py +438 -0
- package/ccx/claude/host/pty.py +250 -0
- package/ccx/claude/host/session.py +5184 -0
- package/ccx/claude/host/subagents.py +169 -0
- package/ccx/claude/host/successor.py +267 -0
- package/ccx/claude/managed_turns.py +240 -0
- package/ccx/claude/oauth.py +176 -0
- package/ccx/claude/settings.py +339 -0
- package/ccx/claude/spawn.py +557 -0
- package/ccx/claude/tasks.py +72 -0
- package/ccx/claude/telemetry.py +282 -0
- package/ccx/claude/transcript.py +2281 -0
- package/ccx/claude/usage.py +837 -0
- package/ccx/claude/usage_control.py +152 -0
- package/ccx/cli/__init__.py +0 -0
- package/ccx/cli/codex.py +1009 -0
- package/ccx/cli/commands/__init__.py +0 -0
- package/ccx/cli/commands/_common.py +407 -0
- package/ccx/cli/commands/control.py +161 -0
- package/ccx/cli/commands/create.py +669 -0
- package/ccx/cli/commands/read.py +1020 -0
- package/ccx/cli/commands/turns.py +1081 -0
- package/ccx/cli/controller.py +757 -0
- package/ccx/cli/main.py +301 -0
- package/ccx/cli/parser.py +1324 -0
- package/ccx/cli/skills.py +215 -0
- package/ccx/cli/statusline.py +160 -0
- package/ccx/client.py +1900 -0
- package/ccx/codex/__init__.py +0 -0
- package/ccx/codex/attach_tty.py +267 -0
- package/ccx/codex/client.py +732 -0
- package/ccx/codex/completion.py +121 -0
- package/ccx/codex/control.py +288 -0
- package/ccx/codex/daemon/__init__.py +0 -0
- package/ccx/codex/daemon/__main__.py +4 -0
- package/ccx/codex/daemon/daemon.py +1556 -0
- package/ccx/codex/daemon/models.py +140 -0
- package/ccx/codex/daemon/native.py +1590 -0
- package/ccx/codex/daemon/release.py +367 -0
- package/ccx/codex/daemon/worker.py +2396 -0
- package/ccx/codex/delivery.py +758 -0
- package/ccx/codex/delivery_recovery.py +232 -0
- package/ccx/codex/evidence.py +827 -0
- package/ccx/codex/history.py +158 -0
- package/ccx/codex/notify_history.py +140 -0
- package/ccx/codex/ops.py +666 -0
- package/ccx/codex/paths.py +70 -0
- package/ccx/codex/protocol.py +805 -0
- package/ccx/codex/provenance.py +105 -0
- package/ccx/codex/read.py +29 -0
- package/ccx/codex/records.py +51 -0
- package/ccx/codex/reports.py +177 -0
- package/ccx/codex/runtime.py +1412 -0
- package/ccx/codex/settle.py +363 -0
- package/ccx/codex/telemetry.py +792 -0
- package/ccx/codex/types.py +659 -0
- package/ccx/codex/worker_environment.py +25 -0
- package/ccx/constants.py +288 -0
- package/ccx/controllers/__init__.py +0 -0
- package/ccx/controllers/controller.py +1449 -0
- package/ccx/controllers/live_vars.py +1706 -0
- package/ccx/controllers/live_vars_runner.py +216 -0
- package/ccx/controllers/notify.py +213 -0
- package/ccx/controllers/orchestrator.py +193 -0
- package/ccx/controllers/policy.py +57 -0
- package/ccx/controllers/store.py +681 -0
- package/ccx/errors.py +634 -0
- package/ccx/notify/__init__.py +0 -0
- package/ccx/notify/__main__.py +5 -0
- package/ccx/notify/client.py +116 -0
- package/ccx/notify/daemon.py +480 -0
- package/ccx/notify/deliver.py +304 -0
- package/ccx/notify/paths.py +51 -0
- package/ccx/notify/registry.py +649 -0
- package/ccx/notify/select.py +750 -0
- package/ccx/notify/settle.py +684 -0
- package/ccx/ops/__init__.py +0 -0
- package/ccx/ops/background.py +115 -0
- package/ccx/ops/cancel.py +289 -0
- package/ccx/ops/codex_import.py +96 -0
- package/ccx/ops/common.py +397 -0
- package/ccx/ops/compact.py +337 -0
- package/ccx/ops/compact_ops.py +550 -0
- package/ccx/ops/context_events.py +48 -0
- package/ccx/ops/conversation.py +1540 -0
- package/ccx/ops/conversation_paging.py +1338 -0
- package/ccx/ops/create.py +56 -0
- package/ccx/ops/creation.py +952 -0
- package/ccx/ops/creation_steps.py +579 -0
- package/ccx/ops/event_feed.py +710 -0
- package/ccx/ops/forking.py +711 -0
- package/ccx/ops/input.py +556 -0
- package/ccx/ops/maintenance.py +295 -0
- package/ccx/ops/report.py +770 -0
- package/ccx/ops/semantics.py +490 -0
- package/ccx/ops/send.py +961 -0
- package/ccx/ops/steer_ops.py +867 -0
- package/ccx/ops/terminate.py +383 -0
- package/ccx/ops/turn_reports.py +306 -0
- package/ccx/ops/wait.py +762 -0
- package/ccx/ops/worker_events.py +636 -0
- package/ccx/rpc/__init__.py +0 -0
- package/ccx/rpc/local_rpc.py +1602 -0
- package/ccx/rpc/rpc.py +559 -0
- package/ccx/rpc/serve.py +374 -0
- package/ccx/rpc/transport.py +409 -0
- package/ccx/rpc/view.py +125 -0
- package/ccx/store/__init__.py +0 -0
- package/ccx/store/discovery.py +170 -0
- package/ccx/store/identity.py +105 -0
- package/ccx/store/journal_io.py +79 -0
- package/ccx/store/liveness.py +194 -0
- package/ccx/store/paths.py +254 -0
- package/ccx/store/record_cache.py +40 -0
- package/ccx/store/records.py +309 -0
- package/ccx/store/root.py +804 -0
- package/ccx/store/runtime_presence.py +137 -0
- package/ccx/store/store.py +2332 -0
- package/ccx/store/worker_state.py +86 -0
- package/ccx/system/__init__.py +0 -0
- package/ccx/system/bounded.py +296 -0
- package/ccx/system/cwd_paths.py +25 -0
- package/ccx/system/engine_binary.py +59 -0
- package/ccx/system/executable.py +50 -0
- package/ccx/system/oslayer.py +667 -0
- package/ccx/system/pipes.py +46 -0
- package/ccx/system/sanitize.py +43 -0
- package/ccx/types.py +705 -0
- package/ccx/version.py +29 -0
- package/ccx/workers.py +2081 -0
- package/ccx.py +9 -0
- package/package.json +40 -4
- package/pyproject.toml +87 -0
- package/skills/ccx/SKILL.md +155 -0
- package/skills/ccx/references/controllers.md +244 -0
- package/skills/ccx/references/troubleshooting.md +94 -0
- package/skills/ccx/references/variables.md +62 -0
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
"""Normalized replayable Codex telemetry over sparse CCX evidence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ccx.codex import evidence as codex_evidence
|
|
10
|
+
from ccx.codex import runtime as codex_runtime
|
|
11
|
+
from ccx.codex.evidence import COMPACTION_CONTEXT_METHOD, TOKEN_USAGE_METHOD
|
|
12
|
+
from ccx.ops.conversation import build_codex
|
|
13
|
+
from ccx.errors import InvalidRequest
|
|
14
|
+
|
|
15
|
+
_CURSOR_VERSION = 2
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _cursor(
|
|
19
|
+
short: str,
|
|
20
|
+
journal_instance_id: str | None,
|
|
21
|
+
sequence: int,
|
|
22
|
+
) -> str:
|
|
23
|
+
raw = json.dumps(
|
|
24
|
+
{
|
|
25
|
+
"v": _CURSOR_VERSION,
|
|
26
|
+
"short": short,
|
|
27
|
+
"journal_instance_id": journal_instance_id,
|
|
28
|
+
"sequence": sequence,
|
|
29
|
+
},
|
|
30
|
+
separators=(",", ":"),
|
|
31
|
+
sort_keys=True,
|
|
32
|
+
).encode("utf-8")
|
|
33
|
+
return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _parse_cursor(value: str | None) -> dict[str, Any] | None:
|
|
37
|
+
if value is None:
|
|
38
|
+
return None
|
|
39
|
+
if not isinstance(value, str) or not value:
|
|
40
|
+
raise InvalidRequest("telemetry cursor must be a non-empty opaque string")
|
|
41
|
+
try:
|
|
42
|
+
raw = value + "=" * (-len(value) % 4)
|
|
43
|
+
data = json.loads(base64.urlsafe_b64decode(raw.encode("ascii")))
|
|
44
|
+
except Exception as error:
|
|
45
|
+
raise InvalidRequest("telemetry cursor is invalid") from error
|
|
46
|
+
if (
|
|
47
|
+
not isinstance(data, dict)
|
|
48
|
+
or not isinstance(data.get("sequence"), int)
|
|
49
|
+
or isinstance(data.get("sequence"), bool)
|
|
50
|
+
or data["sequence"] < 0
|
|
51
|
+
):
|
|
52
|
+
raise InvalidRequest("telemetry cursor is invalid")
|
|
53
|
+
version = data.get("v")
|
|
54
|
+
if version == 1:
|
|
55
|
+
return {
|
|
56
|
+
"version": 1,
|
|
57
|
+
"sequence": data["sequence"],
|
|
58
|
+
"short": None,
|
|
59
|
+
"journal_instance_id": None,
|
|
60
|
+
}
|
|
61
|
+
if version != _CURSOR_VERSION:
|
|
62
|
+
raise InvalidRequest("telemetry cursor is invalid")
|
|
63
|
+
short = data.get("short")
|
|
64
|
+
instance = data.get("journal_instance_id")
|
|
65
|
+
if not isinstance(short, str) or not short:
|
|
66
|
+
raise InvalidRequest("telemetry cursor is invalid")
|
|
67
|
+
if instance is not None and (
|
|
68
|
+
not isinstance(instance, str) or not instance
|
|
69
|
+
):
|
|
70
|
+
raise InvalidRequest("telemetry cursor is invalid")
|
|
71
|
+
return {
|
|
72
|
+
"version": _CURSOR_VERSION,
|
|
73
|
+
"sequence": data["sequence"],
|
|
74
|
+
"short": short,
|
|
75
|
+
"journal_instance_id": instance,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _turn_id(event: dict[str, Any]) -> str | None:
|
|
80
|
+
value = event.get("turnId")
|
|
81
|
+
return value if isinstance(value, str) and value else None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _item_id(event: dict[str, Any]) -> str | None:
|
|
85
|
+
value = event.get("itemId")
|
|
86
|
+
return value if isinstance(value, str) and value else None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _usage_projection(event: dict[str, Any]) -> dict[str, Any]:
|
|
90
|
+
params = event.get("params")
|
|
91
|
+
params = params if isinstance(params, dict) else {}
|
|
92
|
+
usage = params.get("tokenUsage")
|
|
93
|
+
usage = usage if isinstance(usage, dict) else {}
|
|
94
|
+
last = usage.get("last")
|
|
95
|
+
last = last if isinstance(last, dict) else {}
|
|
96
|
+
total = usage.get("total")
|
|
97
|
+
total = total if isinstance(total, dict) else {}
|
|
98
|
+
window = usage.get("modelContextWindow")
|
|
99
|
+
input_tokens = last.get("inputTokens")
|
|
100
|
+
zero_last = all(
|
|
101
|
+
int(last.get(key) or 0) == 0
|
|
102
|
+
for key in (
|
|
103
|
+
"inputTokens",
|
|
104
|
+
"cachedInputTokens",
|
|
105
|
+
"outputTokens",
|
|
106
|
+
"reasoningOutputTokens",
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
percent = (
|
|
110
|
+
input_tokens / window * 100
|
|
111
|
+
if (
|
|
112
|
+
isinstance(input_tokens, int)
|
|
113
|
+
and isinstance(window, int)
|
|
114
|
+
and window > 0
|
|
115
|
+
)
|
|
116
|
+
else None
|
|
117
|
+
)
|
|
118
|
+
return {
|
|
119
|
+
"context_after": (
|
|
120
|
+
{
|
|
121
|
+
"tokens_used": input_tokens,
|
|
122
|
+
"window_size": window if isinstance(window, int) else None,
|
|
123
|
+
"percent_used": percent,
|
|
124
|
+
"as_of_turn_ref": None,
|
|
125
|
+
"observed_at_ms": event.get("observedAtMs"),
|
|
126
|
+
}
|
|
127
|
+
if isinstance(input_tokens, int)
|
|
128
|
+
else None
|
|
129
|
+
),
|
|
130
|
+
"cumulative": {
|
|
131
|
+
"input": total.get("inputTokens")
|
|
132
|
+
if isinstance(total.get("inputTokens"), int) else None,
|
|
133
|
+
"output": total.get("outputTokens")
|
|
134
|
+
if isinstance(total.get("outputTokens"), int) else None,
|
|
135
|
+
"cached_input": total.get("cachedInputTokens")
|
|
136
|
+
if isinstance(total.get("cachedInputTokens"), int) else None,
|
|
137
|
+
"reasoning_output": total.get("reasoningOutputTokens")
|
|
138
|
+
if isinstance(total.get("reasoningOutputTokens"), int) else None,
|
|
139
|
+
"total": total.get("totalTokens")
|
|
140
|
+
if isinstance(total.get("totalTokens"), int) else None,
|
|
141
|
+
},
|
|
142
|
+
"zero_last": zero_last,
|
|
143
|
+
"reset": False,
|
|
144
|
+
"sequence": event.get("sequence"),
|
|
145
|
+
"observed_at_ms": event.get("observedAtMs"),
|
|
146
|
+
"source": "codex-thread-token-usage-updated",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _zero_cumulative() -> dict[str, int]:
|
|
151
|
+
return {
|
|
152
|
+
"input": 0,
|
|
153
|
+
"output": 0,
|
|
154
|
+
"cached_input": 0,
|
|
155
|
+
"reasoning_output": 0,
|
|
156
|
+
"total": 0,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _turn_token_delta(
|
|
161
|
+
end: dict[str, Any] | None,
|
|
162
|
+
baseline: dict[str, Any] | None,
|
|
163
|
+
) -> dict[str, int] | None:
|
|
164
|
+
if not isinstance(end, dict) or not isinstance(baseline, dict):
|
|
165
|
+
return None
|
|
166
|
+
out: dict[str, int] = {}
|
|
167
|
+
for key in ("input", "output", "cached_input", "reasoning_output", "total"):
|
|
168
|
+
newer = end.get(key)
|
|
169
|
+
older = baseline.get(key)
|
|
170
|
+
if not isinstance(newer, int) or not isinstance(older, int):
|
|
171
|
+
return None
|
|
172
|
+
delta = newer - older
|
|
173
|
+
if delta < 0:
|
|
174
|
+
return None
|
|
175
|
+
out[key] = delta
|
|
176
|
+
return out
|
|
177
|
+
|
|
178
|
+
def _user_text_items(turn: dict[str, Any]) -> list[dict[str, Any]]:
|
|
179
|
+
items = [
|
|
180
|
+
item
|
|
181
|
+
for item in (turn.get("input") or [])
|
|
182
|
+
if isinstance(item, dict) and item.get("kind") == "user-text"
|
|
183
|
+
]
|
|
184
|
+
return sorted(
|
|
185
|
+
items,
|
|
186
|
+
key=lambda item: (
|
|
187
|
+
item.get("ordinal")
|
|
188
|
+
if isinstance(item.get("ordinal"), int)
|
|
189
|
+
else 1 << 30
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _instruction_and_steers(
|
|
195
|
+
turn: dict[str, Any],
|
|
196
|
+
) -> tuple[dict[str, Any] | None, list[dict[str, Any]]]:
|
|
197
|
+
messages = _user_text_items(turn)
|
|
198
|
+
if not messages:
|
|
199
|
+
return None, []
|
|
200
|
+
|
|
201
|
+
opening_index = 0
|
|
202
|
+
for index, item in enumerate(messages):
|
|
203
|
+
if item.get("origin") == "ccx-start":
|
|
204
|
+
opening_index = index
|
|
205
|
+
break
|
|
206
|
+
opening = messages[opening_index]
|
|
207
|
+
|
|
208
|
+
instruction = {
|
|
209
|
+
"text": opening.get("text") if isinstance(opening.get("text"), str) else "",
|
|
210
|
+
"source": opening.get("origin") or "native-user-message",
|
|
211
|
+
"client_id": opening.get("clientId"),
|
|
212
|
+
"ordinal": opening.get("ordinal"),
|
|
213
|
+
}
|
|
214
|
+
steers = []
|
|
215
|
+
for index, item in enumerate(messages):
|
|
216
|
+
if index == opening_index:
|
|
217
|
+
continue
|
|
218
|
+
steers.append({
|
|
219
|
+
"text": item.get("text") if isinstance(item.get("text"), str) else "",
|
|
220
|
+
"source": item.get("origin") or "native-user-message",
|
|
221
|
+
"client_id": item.get("clientId"),
|
|
222
|
+
"ordinal": item.get("ordinal"),
|
|
223
|
+
"started_at_ms": item.get("startedAtMs"),
|
|
224
|
+
"completed_at_ms": item.get("completedAtMs"),
|
|
225
|
+
})
|
|
226
|
+
return instruction, steers
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _compaction_id(
|
|
230
|
+
*,
|
|
231
|
+
turn_id: str | None,
|
|
232
|
+
item_id: str | None,
|
|
233
|
+
operation_id: str | None,
|
|
234
|
+
sequence: int | None,
|
|
235
|
+
) -> str:
|
|
236
|
+
if item_id:
|
|
237
|
+
return f"item:{item_id}"
|
|
238
|
+
if operation_id:
|
|
239
|
+
return f"operation:{operation_id}"
|
|
240
|
+
if turn_id:
|
|
241
|
+
return f"turn:{turn_id}"
|
|
242
|
+
return f"sequence:{sequence or 0}"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _compactions(
|
|
246
|
+
events: list[dict[str, Any]],
|
|
247
|
+
) -> tuple[list[dict[str, Any]], dict[int, str]]:
|
|
248
|
+
by_id: dict[str, dict[str, Any]] = {}
|
|
249
|
+
sequence_to_id: dict[int, str] = {}
|
|
250
|
+
|
|
251
|
+
for event in events:
|
|
252
|
+
if event.get("method") != "item/completed":
|
|
253
|
+
continue
|
|
254
|
+
params = event.get("params")
|
|
255
|
+
params = params if isinstance(params, dict) else {}
|
|
256
|
+
item = params.get("item")
|
|
257
|
+
item = item if isinstance(item, dict) else {}
|
|
258
|
+
if item.get("type") != "contextCompaction":
|
|
259
|
+
continue
|
|
260
|
+
turn_id = _turn_id(event)
|
|
261
|
+
item_id = _item_id(event) or (
|
|
262
|
+
item.get("id") if isinstance(item.get("id"), str) else None
|
|
263
|
+
)
|
|
264
|
+
sequence = event.get("sequence")
|
|
265
|
+
cid = _compaction_id(
|
|
266
|
+
turn_id=turn_id,
|
|
267
|
+
item_id=item_id,
|
|
268
|
+
operation_id=None,
|
|
269
|
+
sequence=sequence if isinstance(sequence, int) else None,
|
|
270
|
+
)
|
|
271
|
+
by_id[cid] = {
|
|
272
|
+
"compaction_id": cid,
|
|
273
|
+
"turn_id": turn_id,
|
|
274
|
+
"item_id": item_id,
|
|
275
|
+
"operation_id": None,
|
|
276
|
+
"kind": "native",
|
|
277
|
+
"completed_at_ms": (
|
|
278
|
+
params.get("completedAtMs")
|
|
279
|
+
if isinstance(params.get("completedAtMs"), int)
|
|
280
|
+
else event.get("observedAtMs")
|
|
281
|
+
),
|
|
282
|
+
"context_before": None,
|
|
283
|
+
"context_after": None,
|
|
284
|
+
"coverage": {
|
|
285
|
+
"context_before": "unavailable",
|
|
286
|
+
"context_after": "unavailable",
|
|
287
|
+
},
|
|
288
|
+
"sources": ["codex-context-compaction-item"],
|
|
289
|
+
}
|
|
290
|
+
if isinstance(sequence, int):
|
|
291
|
+
sequence_to_id[sequence] = cid
|
|
292
|
+
|
|
293
|
+
for event in events:
|
|
294
|
+
if event.get("method") != COMPACTION_CONTEXT_METHOD:
|
|
295
|
+
continue
|
|
296
|
+
params = event.get("params")
|
|
297
|
+
params = params if isinstance(params, dict) else {}
|
|
298
|
+
item = params.get("item")
|
|
299
|
+
item = item if isinstance(item, dict) else {}
|
|
300
|
+
turn_id = _turn_id(event)
|
|
301
|
+
item_id = _item_id(event) or (
|
|
302
|
+
item.get("id") if isinstance(item.get("id"), str) else None
|
|
303
|
+
)
|
|
304
|
+
operation_id = (
|
|
305
|
+
params.get("operationId")
|
|
306
|
+
if isinstance(params.get("operationId"), str)
|
|
307
|
+
else None
|
|
308
|
+
)
|
|
309
|
+
sequence = event.get("sequence")
|
|
310
|
+
cid = _compaction_id(
|
|
311
|
+
turn_id=turn_id,
|
|
312
|
+
item_id=item_id,
|
|
313
|
+
operation_id=operation_id,
|
|
314
|
+
sequence=sequence if isinstance(sequence, int) else None,
|
|
315
|
+
)
|
|
316
|
+
entry = by_id.setdefault(cid, {
|
|
317
|
+
"compaction_id": cid,
|
|
318
|
+
"turn_id": turn_id,
|
|
319
|
+
"item_id": item_id,
|
|
320
|
+
"operation_id": operation_id,
|
|
321
|
+
"kind": "explicit" if operation_id else "native",
|
|
322
|
+
"completed_at_ms": params.get("completedAtMs"),
|
|
323
|
+
"context_before": None,
|
|
324
|
+
"context_after": None,
|
|
325
|
+
"coverage": {},
|
|
326
|
+
"sources": [],
|
|
327
|
+
})
|
|
328
|
+
entry["operation_id"] = operation_id or entry.get("operation_id")
|
|
329
|
+
if operation_id:
|
|
330
|
+
entry["kind"] = "explicit"
|
|
331
|
+
entry["context_before"] = params.get("contextBefore")
|
|
332
|
+
entry["context_after"] = params.get("contextAfter")
|
|
333
|
+
entry["coverage"] = {
|
|
334
|
+
"context_before": (
|
|
335
|
+
"observed" if params.get("contextBefore") is not None else "unavailable"
|
|
336
|
+
),
|
|
337
|
+
"context_after": (
|
|
338
|
+
"observed"
|
|
339
|
+
if params.get("contextAfter") is not None
|
|
340
|
+
else "pending-next-turn"
|
|
341
|
+
),
|
|
342
|
+
}
|
|
343
|
+
if "ccx-compaction-context" not in entry["sources"]:
|
|
344
|
+
entry["sources"].append("ccx-compaction-context")
|
|
345
|
+
if isinstance(sequence, int):
|
|
346
|
+
sequence_to_id[sequence] = cid
|
|
347
|
+
|
|
348
|
+
return list(by_id.values()), sequence_to_id
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _original_source(store, owner):
|
|
352
|
+
"""Original owned Codex record, thread identity and filtered evidence.
|
|
353
|
+
|
|
354
|
+
Shared with full conversation: evidence cannot attribute an event with a
|
|
355
|
+
missing/foreign native threadId to this worker's original thread.
|
|
356
|
+
"""
|
|
357
|
+
from ccx.store.root import NativeActionDenied
|
|
358
|
+
|
|
359
|
+
source = store.record(owner.worker)
|
|
360
|
+
native = source.get("native") or {}
|
|
361
|
+
activation = store.native_activation(owner.worker)
|
|
362
|
+
thread_id = native.get("threadId")
|
|
363
|
+
expected_source = store.codex_source(owner.worker)
|
|
364
|
+
if (not isinstance(thread_id, str) or not thread_id
|
|
365
|
+
or native.get("threadSource") != expected_source
|
|
366
|
+
or activation.get("state") != "confirmed"
|
|
367
|
+
or activation.get("source") != expected_source
|
|
368
|
+
or (activation.get("nativeBinding") or {}).get("threadId") != thread_id):
|
|
369
|
+
raise NativeActionDenied("Codex telemetry original thread/source unconfirmed")
|
|
370
|
+
record = {**source, "short": owner.short, "threadId": thread_id}
|
|
371
|
+
journal = codex_evidence.read_journal(owner.worker)
|
|
372
|
+
# The owned nofollow journal inode does not attest each native event's
|
|
373
|
+
# threadId; header and CCX-only feed events are not native turn evidence.
|
|
374
|
+
foreign_events = [
|
|
375
|
+
event for event in journal.events
|
|
376
|
+
if event.get("method") not in (
|
|
377
|
+
codex_evidence.HEADER_METHOD, *codex_evidence.FEED_METHODS,
|
|
378
|
+
) and isinstance(event.get("threadId"), str)
|
|
379
|
+
and event["threadId"] and event["threadId"] != thread_id
|
|
380
|
+
]
|
|
381
|
+
journal.events = [
|
|
382
|
+
event for event in journal.events
|
|
383
|
+
if event.get("method") in (
|
|
384
|
+
codex_evidence.HEADER_METHOD, *codex_evidence.FEED_METHODS,
|
|
385
|
+
) or event.get("threadId") == thread_id
|
|
386
|
+
]
|
|
387
|
+
evidence_index = codex_evidence.build_index(
|
|
388
|
+
journal,
|
|
389
|
+
health=(record.get("evidence") if isinstance(record.get("evidence"), dict)
|
|
390
|
+
else None),
|
|
391
|
+
)
|
|
392
|
+
return record, thread_id, journal, foreign_events, evidence_index
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def read_telemetry(
|
|
396
|
+
target: str,
|
|
397
|
+
*,
|
|
398
|
+
after_cursor: str | None = None,
|
|
399
|
+
limit: int = 200,
|
|
400
|
+
timeout: float = 120,
|
|
401
|
+
) -> dict[str, Any]:
|
|
402
|
+
"""Return normalized per-turn telemetry and replay cursor for one worker."""
|
|
403
|
+
if not isinstance(limit, int) or isinstance(limit, bool) or limit <= 0:
|
|
404
|
+
raise InvalidRequest("telemetry limit must be a positive integer")
|
|
405
|
+
if not isinstance(timeout, (int, float)) or isinstance(timeout, bool) or timeout <= 0:
|
|
406
|
+
raise InvalidRequest("telemetry timeout must be greater than zero")
|
|
407
|
+
limit = min(limit, 1000)
|
|
408
|
+
|
|
409
|
+
from ccx import constants
|
|
410
|
+
from ccx.store.root import require_ccx_active
|
|
411
|
+
|
|
412
|
+
store = require_ccx_active(constants.CCX_ROOT)
|
|
413
|
+
owner = store.resolve(target)
|
|
414
|
+
if owner.engine != "codex":
|
|
415
|
+
raise InvalidRequest("Codex telemetry requires an exact Codex full UUID")
|
|
416
|
+
record, thread_id, journal, foreign_events, evidence_index = _original_source(
|
|
417
|
+
store, owner,
|
|
418
|
+
)
|
|
419
|
+
short = owner.short
|
|
420
|
+
cursor_owner = owner.worker_id
|
|
421
|
+
parsed_cursor = _parse_cursor(after_cursor)
|
|
422
|
+
after_sequence = (
|
|
423
|
+
parsed_cursor.get("sequence")
|
|
424
|
+
if isinstance(parsed_cursor, dict)
|
|
425
|
+
else None
|
|
426
|
+
)
|
|
427
|
+
cursor_mismatch = None
|
|
428
|
+
if isinstance(parsed_cursor, dict):
|
|
429
|
+
if parsed_cursor.get("version") != _CURSOR_VERSION:
|
|
430
|
+
cursor_mismatch = "legacy_cursor"
|
|
431
|
+
elif parsed_cursor.get("short") != cursor_owner:
|
|
432
|
+
cursor_mismatch = "cursor_worker_mismatch"
|
|
433
|
+
elif (
|
|
434
|
+
parsed_cursor.get("journal_instance_id")
|
|
435
|
+
!= journal.journal_instance_id
|
|
436
|
+
):
|
|
437
|
+
cursor_mismatch = "cursor_journal_mismatch"
|
|
438
|
+
cursor_ahead = (
|
|
439
|
+
cursor_mismatch is None
|
|
440
|
+
and after_sequence is not None
|
|
441
|
+
and after_sequence > journal.last_sequence
|
|
442
|
+
)
|
|
443
|
+
effective_after_sequence = (
|
|
444
|
+
None if cursor_mismatch is not None or cursor_ahead else after_sequence
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
events = journal.events
|
|
448
|
+
delta_events = [
|
|
449
|
+
event
|
|
450
|
+
for event in events
|
|
451
|
+
if (
|
|
452
|
+
isinstance(event.get("sequence"), int)
|
|
453
|
+
and (
|
|
454
|
+
effective_after_sequence is None
|
|
455
|
+
or event["sequence"] > effective_after_sequence
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
]
|
|
459
|
+
has_more = len(delta_events) > limit
|
|
460
|
+
processed_delta = delta_events[:limit]
|
|
461
|
+
if processed_delta:
|
|
462
|
+
processed_through = int(processed_delta[-1]["sequence"])
|
|
463
|
+
elif effective_after_sequence is not None:
|
|
464
|
+
processed_through = effective_after_sequence
|
|
465
|
+
else:
|
|
466
|
+
processed_through = journal.last_sequence
|
|
467
|
+
if not has_more:
|
|
468
|
+
processed_through = journal.last_sequence
|
|
469
|
+
|
|
470
|
+
event_sequences = [
|
|
471
|
+
event.get("sequence")
|
|
472
|
+
for event in events
|
|
473
|
+
if isinstance(event.get("sequence"), int)
|
|
474
|
+
]
|
|
475
|
+
first_sequence = min(event_sequences) if event_sequences else None
|
|
476
|
+
gap = None
|
|
477
|
+
if cursor_mismatch is not None:
|
|
478
|
+
gap = {
|
|
479
|
+
"reason": cursor_mismatch,
|
|
480
|
+
"requested_after_sequence": after_sequence,
|
|
481
|
+
"first_retained_sequence": first_sequence,
|
|
482
|
+
"last_retained_sequence": journal.last_sequence,
|
|
483
|
+
"requested_worker": (
|
|
484
|
+
parsed_cursor.get("short")
|
|
485
|
+
if isinstance(parsed_cursor, dict) else None
|
|
486
|
+
),
|
|
487
|
+
"current_worker": cursor_owner,
|
|
488
|
+
"requested_journal_instance_id": (
|
|
489
|
+
parsed_cursor.get("journal_instance_id")
|
|
490
|
+
if isinstance(parsed_cursor, dict) else None
|
|
491
|
+
),
|
|
492
|
+
"current_journal_instance_id": journal.journal_instance_id,
|
|
493
|
+
}
|
|
494
|
+
elif cursor_ahead:
|
|
495
|
+
gap = {
|
|
496
|
+
"reason": "cursor_ahead_of_retained_tail",
|
|
497
|
+
"requested_after_sequence": after_sequence,
|
|
498
|
+
"last_retained_sequence": journal.last_sequence,
|
|
499
|
+
"first_retained_sequence": first_sequence,
|
|
500
|
+
}
|
|
501
|
+
elif (
|
|
502
|
+
after_sequence is not None
|
|
503
|
+
and first_sequence is not None
|
|
504
|
+
and after_sequence < first_sequence - 1
|
|
505
|
+
):
|
|
506
|
+
gap = {
|
|
507
|
+
"reason": "cursor_predates_retained_evidence",
|
|
508
|
+
"requested_after_sequence": after_sequence,
|
|
509
|
+
"first_retained_sequence": first_sequence,
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
# Only an explicit successful native thread/start proves history is empty.
|
|
513
|
+
# Legacy records can have native history even when their turn counter is 0.
|
|
514
|
+
if record.get("historyKnownEmpty") is True:
|
|
515
|
+
conversation = {"turns": [], "completeness": {"status": "complete"}}
|
|
516
|
+
else:
|
|
517
|
+
thread = codex_runtime.read_thread(
|
|
518
|
+
thread_id, cwd=record["cwd"], timeout=float(timeout),
|
|
519
|
+
worker_id=owner.worker_id,
|
|
520
|
+
expected_source=store.codex_source(owner.worker),
|
|
521
|
+
)
|
|
522
|
+
conversation = build_codex(
|
|
523
|
+
record,
|
|
524
|
+
thread,
|
|
525
|
+
state=(record.get("lifecycle") or {}).get("state"),
|
|
526
|
+
worker=f"codex:{short}",
|
|
527
|
+
max_tool_result_bytes=0,
|
|
528
|
+
evidence=evidence_index,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
latest_usage: dict[str, dict[str, Any]] = {}
|
|
532
|
+
turn_cumulative: dict[str, dict[str, Any]] = {}
|
|
533
|
+
touched_turns: set[str] = set()
|
|
534
|
+
usage_touched_turns: set[str] = set()
|
|
535
|
+
previous_observed_cumulative: dict[str, Any] | None = None
|
|
536
|
+
for event in events:
|
|
537
|
+
sequence = event.get("sequence")
|
|
538
|
+
tid = _turn_id(event)
|
|
539
|
+
in_delta = (
|
|
540
|
+
tid
|
|
541
|
+
and isinstance(sequence, int)
|
|
542
|
+
and (
|
|
543
|
+
effective_after_sequence is None
|
|
544
|
+
or sequence > effective_after_sequence
|
|
545
|
+
)
|
|
546
|
+
and sequence <= processed_through
|
|
547
|
+
)
|
|
548
|
+
if in_delta:
|
|
549
|
+
touched_turns.add(tid)
|
|
550
|
+
if event.get("method") == TOKEN_USAGE_METHOD and tid:
|
|
551
|
+
projected = _usage_projection(event)
|
|
552
|
+
cumulative = projected.get("cumulative")
|
|
553
|
+
projected["reset"] = bool(
|
|
554
|
+
projected.get("zero_last")
|
|
555
|
+
and isinstance(previous_observed_cumulative, dict)
|
|
556
|
+
and isinstance(cumulative, dict)
|
|
557
|
+
and cumulative == previous_observed_cumulative
|
|
558
|
+
)
|
|
559
|
+
if projected["reset"]:
|
|
560
|
+
projected["context_after"] = None
|
|
561
|
+
if isinstance(cumulative, dict) and any(
|
|
562
|
+
isinstance(cumulative.get(key), int)
|
|
563
|
+
for key in (
|
|
564
|
+
"input",
|
|
565
|
+
"output",
|
|
566
|
+
"cached_input",
|
|
567
|
+
"reasoning_output",
|
|
568
|
+
"total",
|
|
569
|
+
)
|
|
570
|
+
):
|
|
571
|
+
previous_observed_cumulative = dict(cumulative)
|
|
572
|
+
current = latest_usage.get(tid)
|
|
573
|
+
if current is None or int(event.get("sequence") or 0) >= int(
|
|
574
|
+
current.get("sequence") or 0
|
|
575
|
+
):
|
|
576
|
+
latest_usage[tid] = projected
|
|
577
|
+
turn_cumulative[tid] = dict(
|
|
578
|
+
projected.get("cumulative") or {}
|
|
579
|
+
)
|
|
580
|
+
if in_delta:
|
|
581
|
+
usage_touched_turns.add(tid)
|
|
582
|
+
|
|
583
|
+
compactions, compaction_sequence_ids = _compactions(events)
|
|
584
|
+
touched_compactions = {
|
|
585
|
+
cid
|
|
586
|
+
for sequence, cid in compaction_sequence_ids.items()
|
|
587
|
+
if (
|
|
588
|
+
(
|
|
589
|
+
effective_after_sequence is None
|
|
590
|
+
or sequence > effective_after_sequence
|
|
591
|
+
)
|
|
592
|
+
and sequence <= processed_through
|
|
593
|
+
)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
conversation_turns = [
|
|
597
|
+
turn for turn in (conversation.get("turns") or [])
|
|
598
|
+
if isinstance(turn, dict)
|
|
599
|
+
]
|
|
600
|
+
evidence_turns = evidence_index.get("turns") or {}
|
|
601
|
+
lifecycle_coverage_complete = all(
|
|
602
|
+
(
|
|
603
|
+
isinstance(turn.get("turnId"), str)
|
|
604
|
+
and bool(
|
|
605
|
+
(evidence_turns.get(turn.get("turnId")) or {}).get(
|
|
606
|
+
"sawTurnStarted"
|
|
607
|
+
)
|
|
608
|
+
)
|
|
609
|
+
and bool(
|
|
610
|
+
(evidence_turns.get(turn.get("turnId")) or {}).get(
|
|
611
|
+
"sawTurnCompleted"
|
|
612
|
+
)
|
|
613
|
+
)
|
|
614
|
+
)
|
|
615
|
+
for turn in conversation_turns
|
|
616
|
+
)
|
|
617
|
+
evidence_complete = (
|
|
618
|
+
evidence_index.get("status") == "complete"
|
|
619
|
+
and gap is None
|
|
620
|
+
and not foreign_events
|
|
621
|
+
and lifecycle_coverage_complete
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
# tokenUsage.total is cumulative across the native thread. Fold endpoints
|
|
625
|
+
# independently of journal arrival order, then subtract only the immediately
|
|
626
|
+
# preceding native turn endpoint. Missing predecessor evidence keeps a turn
|
|
627
|
+
# partial rather than attributing multiple turns to one delta.
|
|
628
|
+
turn_baseline: dict[str, dict[str, Any] | None] = {}
|
|
629
|
+
previous_turn_id: str | None = None
|
|
630
|
+
previous_endpoint: dict[str, Any] | None = None
|
|
631
|
+
for index, turn in enumerate(conversation_turns):
|
|
632
|
+
turn_id = turn.get("turnId")
|
|
633
|
+
if not isinstance(turn_id, str) or not turn_id:
|
|
634
|
+
previous_turn_id = None
|
|
635
|
+
previous_endpoint = None
|
|
636
|
+
continue
|
|
637
|
+
ev = evidence_turns.get(turn_id) or {}
|
|
638
|
+
if (
|
|
639
|
+
index == 0
|
|
640
|
+
and ev.get("sawTurnStarted")
|
|
641
|
+
):
|
|
642
|
+
baseline: dict[str, Any] | None = _zero_cumulative()
|
|
643
|
+
else:
|
|
644
|
+
baseline = (
|
|
645
|
+
dict(previous_endpoint)
|
|
646
|
+
if isinstance(previous_endpoint, dict)
|
|
647
|
+
else None
|
|
648
|
+
)
|
|
649
|
+
turn_baseline[turn_id] = baseline
|
|
650
|
+
|
|
651
|
+
# A late endpoint for turn N changes the derived consumption of N+1,
|
|
652
|
+
# even though no new native event belongs to N+1. Re-emit that direct
|
|
653
|
+
# successor so cursor consumers can upsert the corrected projection.
|
|
654
|
+
if (
|
|
655
|
+
previous_turn_id in usage_touched_turns
|
|
656
|
+
and turn_id in turn_cumulative
|
|
657
|
+
):
|
|
658
|
+
touched_turns.add(turn_id)
|
|
659
|
+
|
|
660
|
+
endpoint = turn_cumulative.get(turn_id)
|
|
661
|
+
previous_endpoint = (
|
|
662
|
+
dict(endpoint) if isinstance(endpoint, dict) else None
|
|
663
|
+
)
|
|
664
|
+
previous_turn_id = turn_id
|
|
665
|
+
|
|
666
|
+
compaction_turn_ids = {
|
|
667
|
+
item.get("turn_id")
|
|
668
|
+
for item in compactions
|
|
669
|
+
if isinstance(item.get("turn_id"), str)
|
|
670
|
+
}
|
|
671
|
+
turns = []
|
|
672
|
+
for turn in conversation_turns:
|
|
673
|
+
if not isinstance(turn, dict):
|
|
674
|
+
continue
|
|
675
|
+
turn_id = turn.get("turnId")
|
|
676
|
+
ev = (evidence_index.get("turns") or {}).get(turn_id) or {}
|
|
677
|
+
ev_sequences = ev.get("sequences") or []
|
|
678
|
+
first_ev_sequence = min(
|
|
679
|
+
(seq for seq in ev_sequences if isinstance(seq, int)),
|
|
680
|
+
default=None,
|
|
681
|
+
)
|
|
682
|
+
if after_sequence is not None and evidence_complete:
|
|
683
|
+
if turn_id not in touched_turns:
|
|
684
|
+
continue
|
|
685
|
+
elif after_sequence is None and has_more and first_ev_sequence is not None:
|
|
686
|
+
if first_ev_sequence > processed_through:
|
|
687
|
+
continue
|
|
688
|
+
instruction, steers = _instruction_and_steers(turn)
|
|
689
|
+
usage = latest_usage.get(turn_id)
|
|
690
|
+
baseline = turn_baseline.get(turn_id)
|
|
691
|
+
tokens = _turn_token_delta(turn_cumulative.get(turn_id), baseline)
|
|
692
|
+
context_after = usage.get("context_after") if usage else None
|
|
693
|
+
if (
|
|
694
|
+
turn_id in compaction_turn_ids
|
|
695
|
+
and usage is not None
|
|
696
|
+
and usage.get("zero_last")
|
|
697
|
+
):
|
|
698
|
+
context_after = None
|
|
699
|
+
turns.append({
|
|
700
|
+
"turn_id": turn_id,
|
|
701
|
+
"kind": "compaction" if turn_id in compaction_turn_ids else "model",
|
|
702
|
+
"instruction": instruction,
|
|
703
|
+
"steers": steers,
|
|
704
|
+
"started_at": turn.get("startedAt"),
|
|
705
|
+
"completed_at": turn.get("completedAt"),
|
|
706
|
+
"observed_started_at_ms": ev.get("startedAtMs"),
|
|
707
|
+
"observed_completed_at_ms": ev.get("completedAtMs"),
|
|
708
|
+
"outcome": turn.get("status"),
|
|
709
|
+
"tokens": tokens,
|
|
710
|
+
"context_after": context_after,
|
|
711
|
+
"compaction_refs": [
|
|
712
|
+
item["compaction_id"]
|
|
713
|
+
for item in compactions
|
|
714
|
+
if item.get("turn_id") == turn_id
|
|
715
|
+
],
|
|
716
|
+
"coverage": {
|
|
717
|
+
"lifecycle": (
|
|
718
|
+
"observed"
|
|
719
|
+
if ev.get("sawTurnStarted") and ev.get("sawTurnCompleted")
|
|
720
|
+
else "partial"
|
|
721
|
+
),
|
|
722
|
+
"tokens": (
|
|
723
|
+
"observed" if tokens is not None
|
|
724
|
+
else ("partial" if usage is not None else "unavailable")
|
|
725
|
+
),
|
|
726
|
+
"context_after": (
|
|
727
|
+
"observed" if context_after is not None
|
|
728
|
+
else (
|
|
729
|
+
"pending-next-turn"
|
|
730
|
+
if turn_id in compaction_turn_ids
|
|
731
|
+
else "unavailable"
|
|
732
|
+
)
|
|
733
|
+
),
|
|
734
|
+
},
|
|
735
|
+
"sources": [
|
|
736
|
+
"codex-thread-turns-paged",
|
|
737
|
+
*(
|
|
738
|
+
["ccx-evidence-lifecycle"]
|
|
739
|
+
if ev else []
|
|
740
|
+
),
|
|
741
|
+
*(
|
|
742
|
+
["codex-thread-token-usage-updated"]
|
|
743
|
+
if usage else []
|
|
744
|
+
),
|
|
745
|
+
],
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
if evidence_complete:
|
|
749
|
+
compactions = [
|
|
750
|
+
item for item in compactions
|
|
751
|
+
if item["compaction_id"] in touched_compactions
|
|
752
|
+
]
|
|
753
|
+
|
|
754
|
+
warnings = list(evidence_index.get("warnings") or [])
|
|
755
|
+
if foreign_events:
|
|
756
|
+
warnings.append(
|
|
757
|
+
"excluded native telemetry events without the original Codex thread identity"
|
|
758
|
+
)
|
|
759
|
+
if gap:
|
|
760
|
+
warnings.append(
|
|
761
|
+
"requested telemetry cursor is outside the retained evidence range"
|
|
762
|
+
)
|
|
763
|
+
status = (
|
|
764
|
+
"complete"
|
|
765
|
+
if evidence_complete
|
|
766
|
+
else "partial"
|
|
767
|
+
)
|
|
768
|
+
store.revalidate()
|
|
769
|
+
return {
|
|
770
|
+
"worker": {
|
|
771
|
+
"engine": "codex",
|
|
772
|
+
"short": short,
|
|
773
|
+
"name": record.get("name"),
|
|
774
|
+
"thread_id": record.get("threadId"),
|
|
775
|
+
},
|
|
776
|
+
"turns": turns,
|
|
777
|
+
"compactions": compactions,
|
|
778
|
+
"coverage": {
|
|
779
|
+
"status": status,
|
|
780
|
+
"capture_started_at_ms": evidence_index.get("beganAtMs"),
|
|
781
|
+
"gap": gap,
|
|
782
|
+
"warnings": warnings,
|
|
783
|
+
"conversation": conversation.get("completeness"),
|
|
784
|
+
},
|
|
785
|
+
"next_cursor": _cursor(
|
|
786
|
+
cursor_owner, journal.journal_instance_id, processed_through
|
|
787
|
+
),
|
|
788
|
+
"has_more": has_more,
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
__all__ = ["read_telemetry"]
|