ccx-native 0.0.1 → 0.2.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.
Files changed (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -4
  3. package/bin/ccx +55 -2
  4. package/ccx/__init__.py +167 -0
  5. package/ccx/__main__.py +28 -0
  6. package/ccx/claude/__init__.py +0 -0
  7. package/ccx/claude/analytics.py +466 -0
  8. package/ccx/claude/attach.py +807 -0
  9. package/ccx/claude/context.py +465 -0
  10. package/ccx/claude/delivery.py +806 -0
  11. package/ccx/claude/external.py +253 -0
  12. package/ccx/claude/host/__init__.py +0 -0
  13. package/ccx/claude/host/events.py +297 -0
  14. package/ccx/claude/host/hooks.py +438 -0
  15. package/ccx/claude/host/pty.py +269 -0
  16. package/ccx/claude/host/session.py +5244 -0
  17. package/ccx/claude/host/subagents.py +169 -0
  18. package/ccx/claude/host/successor.py +267 -0
  19. package/ccx/claude/managed_turns.py +240 -0
  20. package/ccx/claude/oauth.py +176 -0
  21. package/ccx/claude/settings.py +339 -0
  22. package/ccx/claude/spawn.py +557 -0
  23. package/ccx/claude/tasks.py +72 -0
  24. package/ccx/claude/telemetry.py +282 -0
  25. package/ccx/claude/transcript.py +2535 -0
  26. package/ccx/claude/usage.py +796 -0
  27. package/ccx/claude/usage_control.py +204 -0
  28. package/ccx/cli/__init__.py +0 -0
  29. package/ccx/cli/codex.py +1009 -0
  30. package/ccx/cli/commands/__init__.py +0 -0
  31. package/ccx/cli/commands/_common.py +407 -0
  32. package/ccx/cli/commands/control.py +161 -0
  33. package/ccx/cli/commands/create.py +669 -0
  34. package/ccx/cli/commands/models.py +53 -0
  35. package/ccx/cli/commands/read.py +1104 -0
  36. package/ccx/cli/commands/turns.py +1081 -0
  37. package/ccx/cli/controller.py +757 -0
  38. package/ccx/cli/main.py +302 -0
  39. package/ccx/cli/parser.py +1439 -0
  40. package/ccx/cli/skills.py +215 -0
  41. package/ccx/cli/statusline.py +160 -0
  42. package/ccx/client.py +1904 -0
  43. package/ccx/codex/__init__.py +0 -0
  44. package/ccx/codex/attach_tty.py +267 -0
  45. package/ccx/codex/client.py +738 -0
  46. package/ccx/codex/completion.py +121 -0
  47. package/ccx/codex/control.py +288 -0
  48. package/ccx/codex/daemon/__init__.py +0 -0
  49. package/ccx/codex/daemon/__main__.py +4 -0
  50. package/ccx/codex/daemon/daemon.py +1558 -0
  51. package/ccx/codex/daemon/models.py +140 -0
  52. package/ccx/codex/daemon/native.py +1590 -0
  53. package/ccx/codex/daemon/release.py +367 -0
  54. package/ccx/codex/daemon/worker.py +2424 -0
  55. package/ccx/codex/delivery.py +758 -0
  56. package/ccx/codex/delivery_recovery.py +232 -0
  57. package/ccx/codex/evidence.py +827 -0
  58. package/ccx/codex/history.py +158 -0
  59. package/ccx/codex/history_store.py +131 -0
  60. package/ccx/codex/notify_history.py +140 -0
  61. package/ccx/codex/ops.py +666 -0
  62. package/ccx/codex/paths.py +70 -0
  63. package/ccx/codex/protocol.py +805 -0
  64. package/ccx/codex/provenance.py +105 -0
  65. package/ccx/codex/read.py +29 -0
  66. package/ccx/codex/records.py +51 -0
  67. package/ccx/codex/reports.py +177 -0
  68. package/ccx/codex/runtime.py +1454 -0
  69. package/ccx/codex/settle.py +363 -0
  70. package/ccx/codex/telemetry.py +792 -0
  71. package/ccx/codex/types.py +659 -0
  72. package/ccx/codex/worker_environment.py +25 -0
  73. package/ccx/constants.py +288 -0
  74. package/ccx/controllers/__init__.py +0 -0
  75. package/ccx/controllers/controller.py +1449 -0
  76. package/ccx/controllers/live_vars.py +1706 -0
  77. package/ccx/controllers/live_vars_runner.py +216 -0
  78. package/ccx/controllers/notify.py +213 -0
  79. package/ccx/controllers/orchestrator.py +193 -0
  80. package/ccx/controllers/policy.py +57 -0
  81. package/ccx/controllers/store.py +681 -0
  82. package/ccx/errors.py +634 -0
  83. package/ccx/models.py +225 -0
  84. package/ccx/notify/__init__.py +0 -0
  85. package/ccx/notify/__main__.py +5 -0
  86. package/ccx/notify/client.py +116 -0
  87. package/ccx/notify/daemon.py +480 -0
  88. package/ccx/notify/deliver.py +304 -0
  89. package/ccx/notify/paths.py +51 -0
  90. package/ccx/notify/registry.py +649 -0
  91. package/ccx/notify/select.py +750 -0
  92. package/ccx/notify/settle.py +684 -0
  93. package/ccx/ops/__init__.py +0 -0
  94. package/ccx/ops/background.py +115 -0
  95. package/ccx/ops/cancel.py +296 -0
  96. package/ccx/ops/codex_import.py +96 -0
  97. package/ccx/ops/common.py +397 -0
  98. package/ccx/ops/compact.py +337 -0
  99. package/ccx/ops/compact_ops.py +550 -0
  100. package/ccx/ops/context_events.py +48 -0
  101. package/ccx/ops/conversation.py +1593 -0
  102. package/ccx/ops/conversation_paging.py +1339 -0
  103. package/ccx/ops/create.py +56 -0
  104. package/ccx/ops/creation.py +952 -0
  105. package/ccx/ops/creation_steps.py +575 -0
  106. package/ccx/ops/event_feed.py +710 -0
  107. package/ccx/ops/forking.py +711 -0
  108. package/ccx/ops/history.py +1041 -0
  109. package/ccx/ops/input.py +556 -0
  110. package/ccx/ops/maintenance.py +295 -0
  111. package/ccx/ops/report.py +773 -0
  112. package/ccx/ops/semantics.py +490 -0
  113. package/ccx/ops/send.py +961 -0
  114. package/ccx/ops/steer_ops.py +867 -0
  115. package/ccx/ops/terminate.py +383 -0
  116. package/ccx/ops/turn_reports.py +348 -0
  117. package/ccx/ops/wait.py +762 -0
  118. package/ccx/ops/worker_events.py +640 -0
  119. package/ccx/rpc/__init__.py +0 -0
  120. package/ccx/rpc/local_rpc.py +1602 -0
  121. package/ccx/rpc/rpc.py +559 -0
  122. package/ccx/rpc/serve.py +374 -0
  123. package/ccx/rpc/transport.py +409 -0
  124. package/ccx/rpc/view.py +125 -0
  125. package/ccx/store/__init__.py +0 -0
  126. package/ccx/store/discovery.py +170 -0
  127. package/ccx/store/identity.py +105 -0
  128. package/ccx/store/journal_io.py +79 -0
  129. package/ccx/store/liveness.py +194 -0
  130. package/ccx/store/paths.py +250 -0
  131. package/ccx/store/record_cache.py +40 -0
  132. package/ccx/store/records.py +309 -0
  133. package/ccx/store/root.py +789 -0
  134. package/ccx/store/runtime_presence.py +137 -0
  135. package/ccx/store/store.py +2332 -0
  136. package/ccx/store/worker_state.py +86 -0
  137. package/ccx/system/__init__.py +0 -0
  138. package/ccx/system/bounded.py +296 -0
  139. package/ccx/system/cwd_paths.py +25 -0
  140. package/ccx/system/engine_binary.py +59 -0
  141. package/ccx/system/executable.py +50 -0
  142. package/ccx/system/oslayer.py +667 -0
  143. package/ccx/system/pipes.py +46 -0
  144. package/ccx/system/private_json.py +54 -0
  145. package/ccx/system/sanitize.py +43 -0
  146. package/ccx/types.py +809 -0
  147. package/ccx/version.py +29 -0
  148. package/ccx/workers.py +2093 -0
  149. package/ccx.py +9 -0
  150. package/package.json +39 -7
  151. package/pyproject.toml +87 -0
  152. package/skills/ccx/SKILL.md +166 -0
  153. package/skills/ccx/references/controllers.md +244 -0
  154. package/skills/ccx/references/history.md +50 -0
  155. package/skills/ccx/references/troubleshooting.md +94 -0
  156. package/skills/ccx/references/variables.md +62 -0
@@ -0,0 +1,465 @@
1
+ "Transcript-derived carried-context usage for a worker session.\n\nClaude Code records token usage on every assistant turn (see\n``ccx.claude.transcript._usage_tokens``). The *occupancy* of the model's context window\nat any moment is just the latest completed assistant turn's input footprint:\n\n tokens_used = input + cache_read_input + cache_creation_input\n\nwhich matches the input footprint surfaced by Claude Code's status line\n(``output`` is excluded). We derive this from the cleaned transcript turns, so\nit needs no extra capture, works retroactively, and updates exactly at turn\nboundaries.\n\nThe public telemetry deliberately reports only the carried token count. Window\ncapacity, occupancy percentage, and cost thresholds are policy rather than\nobservations needed by CCX's read surfaces; keeping them out also avoids model-\nspecific capacity guesses.\n"
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+
8
+
9
+ STATUSLINE_CONTEXT_TTL_S = 90
10
+
11
+
12
+ # Settlement must not wait for a transcript render or parse a large history.
13
+ # A single pread of at most this many bytes is the only native work here.
14
+ _SETTLEMENT_TAIL_BYTES = 256 * 1024
15
+
16
+
17
+ def read_claude_settlement_context(
18
+ path: str, *, start_offset: int | None, turn_ref: str, settled_ms: int,
19
+ ) -> dict | None:
20
+ """Best-effort bounded native tail for the exact in-flight managed turn.
21
+
22
+ An unknown start fence is not license to borrow the previous turn's usage.
23
+ Errors and unreadable/incomplete lines never block a settlement; the caller
24
+ persists null when no trustworthy bounded reading can be made.
25
+ """
26
+ import os
27
+ import stat
28
+ from datetime import datetime
29
+
30
+ if type(start_offset) is not int or start_offset < 0:
31
+ return None
32
+ fd = None
33
+ try:
34
+ fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK | getattr(os, "O_NOFOLLOW", 0))
35
+ size = os.fstat(fd)
36
+ if not stat.S_ISREG(size.st_mode) or size.st_size < start_offset:
37
+ return None
38
+ begin = max(start_offset, size.st_size - _SETTLEMENT_TAIL_BYTES)
39
+ raw = os.pread(fd, size.st_size - begin, begin)
40
+ except (OSError, ValueError, TypeError):
41
+ return None
42
+ finally:
43
+ if fd is not None:
44
+ os.close(fd)
45
+ if begin > start_offset:
46
+ # The bounded tail may begin in the middle of a line.
47
+ raw = raw.partition(bytes([10]))[2]
48
+ if not raw.endswith(bytes([10])):
49
+ raw = raw.rpartition(bytes([10]))[0]
50
+ if not raw:
51
+ return {"state": "pending", "tokens_used": None,
52
+ "as_of_ms": settled_ms, "as_of_turn_ref": turn_ref}
53
+ for line in reversed(raw.splitlines()):
54
+ try:
55
+ event = json.loads(line)
56
+ except (ValueError, UnicodeError):
57
+ continue
58
+ if not isinstance(event, dict):
59
+ continue
60
+ stamp = event.get("timestamp")
61
+ try:
62
+ ms = int(datetime.fromisoformat(stamp.replace("Z", "+00:00")).timestamp() * 1000)
63
+ except (AttributeError, ValueError, OverflowError, TypeError):
64
+ ms = settled_ms
65
+ if event.get("type") == "system" and event.get("subtype") == "compact_boundary":
66
+ return {"state": "unknown", "tokens_used": None,
67
+ "reason": "just_compacted", "as_of_ms": ms,
68
+ "as_of_turn_ref": turn_ref}
69
+ if event.get("type") != "assistant":
70
+ continue
71
+ message = event.get("message")
72
+ usage = message.get("usage") if isinstance(message, dict) else None
73
+ if not isinstance(usage, dict):
74
+ continue
75
+ inp = usage.get("input_tokens")
76
+ if type(inp) is not int or inp < 0:
77
+ continue
78
+ cached = usage.get("cache_read_input_tokens") or 0
79
+ created = usage.get("cache_creation_input_tokens") or 0
80
+ if any(type(value) is not int or value < 0 for value in (cached, created)):
81
+ continue
82
+ total = inp + cached + created
83
+ if total:
84
+ return {"state": "known", "tokens_used": total,
85
+ "as_of_ms": ms, "as_of_turn_ref": turn_ref}
86
+ return {"state": "pending", "tokens_used": None,
87
+ "as_of_ms": settled_ms, "as_of_turn_ref": turn_ref}
88
+ _COMPACTION_UNKNOWN_REASONS = {"compacting", "just_compacted"}
89
+
90
+
91
+ def _usable_int(value) -> int | None:
92
+ return value if isinstance(value, int) else None
93
+
94
+
95
+ def compaction_unknown_context(
96
+ reason: str,
97
+ *,
98
+ as_of_ms: int | None = None,
99
+ ) -> dict:
100
+ """Explicitly fence off pre-compaction usage that is no longer current.
101
+
102
+ Claude's compact boundary carries a local ``postTokens`` calculation, but
103
+ neither it nor the statusline is an API-usage measurement for the newly
104
+ compacted conversation. Keep ``tokens_used`` null until a later assistant
105
+ response records a real post-boundary input footprint.
106
+ """
107
+ if reason not in _COMPACTION_UNKNOWN_REASONS:
108
+ raise ValueError(f"unsupported compaction context reason: {reason}")
109
+ return {
110
+ "state": "unknown",
111
+ "tokens_used": None,
112
+ "reason": reason,
113
+ "as_of_turn_ref": None,
114
+ "as_of_ms": as_of_ms,
115
+ }
116
+
117
+
118
+ def public_context(block: dict | None, *, engine: str,
119
+ compacting: bool = False) -> dict:
120
+ """The one flat carried-context projection every ``context`` surface emits.
121
+
122
+ ``block`` is the engine's own measurement: Claude's merged transcript /
123
+ statusline block, or the Codex record's ``context``. Unknowns stay ``None``
124
+ rather than guessed (Claude reports no window capacity). Whether a
125
+ measurement exists is ``reason``, never ``state``, so ``state`` can keep
126
+ meaning the worker lifecycle wherever this block is flattened in:
127
+
128
+ - ``None``: ``tokens_used`` is a current measurement;
129
+ - ``"pending"``: turns exist but none carried usable token data yet;
130
+ - ``"compacting"`` / ``"just_compacted"``: pre-compact usage is fenced off;
131
+ - ``"no-signal"``: there is no measurement at all.
132
+
133
+ ``compacting`` is the Codex record's own ``activeCompact`` fence: its
134
+ measurement predates a compaction still running, so it is withheld the way
135
+ the nested ``peek`` / ``list`` block withholds it.
136
+ """
137
+ block = block if isinstance(block, dict) else None
138
+ codex = engine == "codex"
139
+ if compacting:
140
+ reason = "compacting"
141
+ elif block is None:
142
+ reason = "no-signal"
143
+ elif codex:
144
+ reason = (block.get("reason") if block.get("reason") in _COMPACTION_UNKNOWN_REASONS
145
+ else None if block.get("tokens_used") is not None else "no-signal")
146
+ elif block.get("state") == "pending":
147
+ reason = "pending"
148
+ elif block.get("state") == "unknown":
149
+ reason = block.get("reason")
150
+ else:
151
+ reason = None
152
+ block = block or {}
153
+ return {
154
+ "tokens_used": None if compacting or reason in _COMPACTION_UNKNOWN_REASONS else block.get("tokens_used"),
155
+ "window_size": block.get("window_size") if codex else None,
156
+ "percent_used": (block.get("percent_used") if codex and not compacting
157
+ and reason not in _COMPACTION_UNKNOWN_REASONS else None),
158
+ "as_of_turn_ref": block.get("as_of_turn_ref"),
159
+ "as_of_ms": block.get("as_of_ms"),
160
+ "reason": reason,
161
+ }
162
+
163
+
164
+ def settled_context(record: dict, *, engine: str) -> dict | None:
165
+ """Only the owning process's turn-completion snapshot is a settlement."""
166
+ last = record.get("lastSettledTurn" if engine == "claude" else "lastTurn")
167
+ if not isinstance(last, dict) or not isinstance(last.get("context"), dict):
168
+ return None
169
+ block = dict(last["context"])
170
+ if engine == "claude" and not _valid_claude_measurement(block):
171
+ return None
172
+ ref = (last.get("turnRef") if engine == "claude"
173
+ else "ccx-turn:codex:v1:" + last["turnId"]
174
+ if isinstance(last.get("turnId"), str) and last["turnId"] else None)
175
+ if not ref:
176
+ return None
177
+ block["as_of_turn_ref"] = ref
178
+ return block
179
+
180
+
181
+ def _valid_claude_measurement(block) -> bool:
182
+ """Validate advisory usage independently of its strict source identity."""
183
+ if (not isinstance(block, dict)
184
+ or not isinstance(block.get("state"), str)
185
+ or block["state"] not in {"known", "pending", "unknown"}):
186
+ return False
187
+ tokens = block.get("tokens_used")
188
+ stamp = block.get("as_of_ms")
189
+ return (not (tokens is not None and (type(tokens) is not int or tokens < 0))
190
+ and not (stamp is not None and (type(stamp) is not int or stamp < 0))
191
+ and (tokens is not None) == (block["state"] == "known"))
192
+
193
+
194
+ def current_context(record: dict, *, engine: str,
195
+ live: dict | None = None) -> dict | None:
196
+ """A settled idle turn outranks a debounced newer *live* sidecar.
197
+
198
+ On a running turn, the fresh live measurement wins. A newer compaction
199
+ fence always wins; an unknown old record does not invent settlement.
200
+ """
201
+ settled = settled_context(record, engine=engine)
202
+ lifecycle = record.get("lifecycle")
203
+ lifecycle = lifecycle if isinstance(lifecycle, dict) else {}
204
+ active = (lifecycle.get("activeTurnId") is not None if engine == "codex"
205
+ else lifecycle.get("state") in {"busy", "waiting", "resuming"})
206
+ if engine == "claude":
207
+ # A compact fence is owned by the host, independently of advisory
208
+ # statusline timestamps. A new statusline refresh can still contain
209
+ # pre-compact tokens, so only a later genuinely settled assistant turn
210
+ # is permitted to release the fence.
211
+ fence = record.get("context")
212
+ if (isinstance(fence, dict)
213
+ and fence.get("reason") in _COMPACTION_UNKNOWN_REASONS):
214
+ boundary = fence.get("as_of_ms")
215
+ fresh = (isinstance(settled, dict)
216
+ and type(boundary) is int
217
+ and type(settled.get("as_of_ms")) is int
218
+ and settled["as_of_ms"] > boundary
219
+ and settled.get("tokens_used") is not None)
220
+ newer_boundary = (
221
+ isinstance(live, dict)
222
+ and live.get("reason") in _COMPACTION_UNKNOWN_REASONS
223
+ # The verified native compact boundary outranks a host-side
224
+ # "compacting" latch even if the native timestamp is older
225
+ # than the observation time of the pending local command.
226
+ and (fence.get("reason") == "compacting"
227
+ and live.get("reason") == "just_compacted"
228
+ or type(live.get("as_of_ms")) is int
229
+ and (type(boundary) is not int or live["as_of_ms"] >= boundary)
230
+ and (fence.get("reason") != "just_compacted"
231
+ or live.get("reason") == "just_compacted"))
232
+ )
233
+ if not fresh and newer_boundary:
234
+ return live
235
+ if not fresh:
236
+ return fence
237
+ if engine == "codex" and isinstance(record.get("activeCompact"), dict):
238
+ # Fence tokens, not the independently observed native window capacity.
239
+ previous = live if isinstance(live, dict) else settled or {}
240
+ return {**previous, "state": "unknown", "tokens_used": None,
241
+ "reason": "compacting", "as_of_turn_ref": None}
242
+ if engine == "codex":
243
+ compact = record.get("lastCompact")
244
+ if isinstance(compact, dict) and compact.get("status") == "succeeded":
245
+ compact_ms = compact.get("completedAtMs")
246
+ candidates = (settled, live, compact.get("contextAfter"))
247
+ fresh = next((part for part in candidates
248
+ if isinstance(part, dict)
249
+ and type(part.get("as_of_ms")) is int
250
+ and type(compact_ms) is int
251
+ and part["as_of_ms"] > compact_ms
252
+ and part.get("tokens_used") is not None), None)
253
+ if fresh is None and type(compact_ms) is int:
254
+ previous = (compact.get("contextBefore") or {})
255
+ return {**previous, "state": "unknown", "tokens_used": None,
256
+ "percent_used": None, "reason": "just_compacted",
257
+ "as_of_ms": compact_ms, "as_of_turn_ref": None}
258
+ if isinstance(live, dict):
259
+ live = {key: value for key, value in live.items()
260
+ if key not in {"as_of_turn", "as_of_turn_id", "as_of_turn_ref"}}
261
+ live["as_of_turn_ref"] = None
262
+ if isinstance(live, dict) and live.get("reason") in _COMPACTION_UNKNOWN_REASONS:
263
+ stamp = live.get("as_of_ms")
264
+ previous = settled.get("as_of_ms") if settled else None
265
+ if settled is None or type(stamp) is int and (type(previous) is not int or stamp > previous):
266
+ return live
267
+ if not active and settled is not None:
268
+ return settled
269
+ if not active:
270
+ return live or settled
271
+ # Once a turn is active, any valid live reading wins (its ref is null).
272
+ # The only time comparison is the explicit post-settlement compact fence.
273
+ return live if isinstance(live, dict) else settled
274
+
275
+
276
+ def read_claude_observation(store, owner, record) -> dict | None:
277
+ """Read the small, exact-owner Claude context/model snapshot; never render."""
278
+ from ccx.store.root import StorageIntegrityError
279
+
280
+ if owner.engine != "claude":
281
+ raise StorageIntegrityError("Claude context needs a Claude worker owner")
282
+ doc = store.read_worker_sidecar(owner.worker, "context-observation.json")
283
+ native = record.get("native")
284
+ if doc is not None:
285
+ source = doc.get("source")
286
+ model = doc.get("observedModel")
287
+ block = doc.get("context")
288
+ if doc is not None and (doc.get("schemaVersion") != 1
289
+ or doc.get("workerId") != owner.worker_id
290
+ or not isinstance(native, dict)
291
+ or doc.get("nativeSessionId") != native.get("sessionId")
292
+ or doc.get("generation") != store.generation
293
+ or not isinstance(source, dict)
294
+ or any(type(source.get(key)) is not int or source[key] < 0
295
+ for key in ("dev", "ino", "endOffset", "observedAtMs"))
296
+ or (model is not None and (not isinstance(model, str)
297
+ or not 0 < len(model) <= 256
298
+ or model == "<synthetic>"))):
299
+ raise StorageIntegrityError("Claude observation identity/provenance mismatch")
300
+ if doc is not None and block is not None and not _valid_claude_measurement(block):
301
+ # Only the measurement is bad. Its owner/provenance was checked above;
302
+ # keep its independent model and any valid recorded settlement.
303
+ doc = {**doc, "context": None,
304
+ "note": "invalid Claude context measurement discarded"}
305
+ # The native statusline is advisory and expires quickly. Never let it
306
+ # replace a genuine pending/just-compacted transcript fence.
307
+ status = store.read_worker_sidecar(owner.worker, "statusline.json")
308
+ if status is None or not isinstance(native, dict):
309
+ chosen = current_context(record, engine="claude",
310
+ live=doc.get("context") if doc else None)
311
+ return {**(doc or {}), "context": chosen}
312
+ now = int(time.time() * 1000)
313
+ written = status.get("writtenAtMs")
314
+ model_row = status.get("model")
315
+ cw = status.get("contextWindow")
316
+ if (status.get("schemaVersion") != 1
317
+ or status.get("short") != owner.worker_id
318
+ or status.get("sessionId") != native.get("sessionId")
319
+ or type(written) is not int
320
+ or written < now - STATUSLINE_CONTEXT_TTL_S * 1000
321
+ or written > now + 5000):
322
+ return {**(doc or {}), "context": current_context(
323
+ record, engine="claude", live=doc.get("context") if doc else None)}
324
+ live = statusline_context_from_sidecar(status, now_ms=now)
325
+ if (live is not None and (not isinstance(cw, dict)
326
+ or type(cw.get("total_input_tokens")) is not int
327
+ or cw["total_input_tokens"] < 0)):
328
+ live = None
329
+ reported = model_row.get("id") if isinstance(model_row, dict) else None
330
+ if (not isinstance(reported, str) or not 0 < len(reported) <= 256
331
+ or reported == "<synthetic>"):
332
+ reported = None
333
+ combined = current_context(
334
+ record, engine="claude",
335
+ live=merge_context_usage(doc.get("context") if doc else None, live))
336
+ model = doc.get("observedModel") if doc else None
337
+ if combined is None and model is None and reported is None:
338
+ return {**(doc or {}), "context": current_context(
339
+ record, engine="claude", live=doc.get("context") if doc else None)}
340
+ return {**(doc or {}), "context": combined,
341
+ "observedModel": model or reported}
342
+
343
+
344
+ def compute_context_usage(turns, prefer_model: str | None = None) -> dict | None:
345
+ """Carried context as of the latest trustworthy context anchor.
346
+
347
+ Scans ``turns`` (cleaned-transcript shape, newest last) backwards. A valid
348
+ assistant usage block supplies the current carried input footprint. A newer
349
+ compact boundary instead returns ``unknown / just_compacted``: usage from
350
+ before that boundary is stale, while Claude's statusline has no reliable
351
+ post-compact measurement until another model response runs.
352
+
353
+ ``prefer_model`` is retained for caller compatibility but intentionally
354
+ unused: the public context shape no longer guesses model-window capacity.
355
+
356
+ Returns None when there are no turns at all (no signal). Returns a block with
357
+ ``state: "pending"`` when turns exist but none carry usable token data yet
358
+ (e.g. a freshly-launched worker before its first completed turn).
359
+ """
360
+ turns = turns or []
361
+ if not turns:
362
+ return None
363
+
364
+ found = None
365
+ for turn in reversed(turns):
366
+ if turn.get("role") == "system" and turn.get("kind") == "compact_boundary":
367
+ return compaction_unknown_context(
368
+ "just_compacted",
369
+ as_of_ms=turn.get("ts"),
370
+ )
371
+ if turn.get("role") != "assistant":
372
+ continue
373
+ tokens = turn.get("tokens")
374
+ if not isinstance(tokens, dict):
375
+ continue
376
+ inp = _usable_int(tokens.get("input"))
377
+ if inp is None:
378
+ continue
379
+ # Skip degenerate all-zero turns (rate-limit / API-error synthetic replies
380
+ # written with model "<synthetic>" and every counter zeroed out).
381
+ cache_read = _usable_int(tokens.get("cache_read_input")) or 0
382
+ cache_creation = _usable_int(tokens.get("cache_creation_input")) or 0
383
+ if inp + cache_read + cache_creation == 0:
384
+ continue
385
+ found = turn
386
+ break
387
+
388
+ if found is None:
389
+ return {
390
+ "state": "pending",
391
+ "tokens_used": None,
392
+ "as_of_turn_ref": None,
393
+ "as_of_ms": None,
394
+ }
395
+
396
+ tokens = found["tokens"]
397
+ inp = _usable_int(tokens.get("input")) or 0
398
+ cache_read = _usable_int(tokens.get("cache_read_input")) or 0
399
+ cache_creation = _usable_int(tokens.get("cache_creation_input")) or 0
400
+ tokens_used = inp + cache_read + cache_creation
401
+
402
+ return {
403
+ "state": "known",
404
+ "tokens_used": tokens_used,
405
+ "as_of_turn_ref": None,
406
+ "as_of_ms": found.get("ts"),
407
+ }
408
+
409
+
410
+ def statusline_context_from_sidecar(doc: dict, *, now_ms: int | None = None,
411
+ mtime_ms: int | None = None,
412
+ ttl_s: int = STATUSLINE_CONTEXT_TTL_S) -> dict | None:
413
+ """Normalize a statusline sidecar into the public context block shape.
414
+
415
+ Returns ``None`` when the sidecar is stale or doesn't contain a usable
416
+ ``contextWindow.total_input_tokens``. That keeps the bridge an optional
417
+ live overlay: callers can always fall back to transcript-derived context.
418
+ """
419
+ if not isinstance(doc, dict):
420
+ return None
421
+ written = _usable_int(doc.get("writtenAtMs"))
422
+ freshness_ms = mtime_ms if mtime_ms is not None else written
423
+ if freshness_ms is not None:
424
+ now = now_ms if now_ms is not None else int(time.time() * 1000)
425
+ if now - freshness_ms > ttl_s * 1000:
426
+ return None
427
+
428
+ cw = doc.get("contextWindow")
429
+ if not isinstance(cw, dict):
430
+ return None
431
+ tokens_used = _usable_int(cw.get("total_input_tokens"))
432
+ if tokens_used is None:
433
+ return None
434
+
435
+ return {
436
+ "state": "known",
437
+ "tokens_used": tokens_used,
438
+ "as_of_turn_ref": None,
439
+ "as_of_ms": written,
440
+ }
441
+
442
+
443
+ def merge_context_usage(transcript_ctx: dict | None, statusline_ctx: dict | None) -> dict | None:
444
+ """Prefer live statusline usage unless a compaction fence makes it unsafe."""
445
+ if (
446
+ isinstance(transcript_ctx, dict)
447
+ and transcript_ctx.get("state") == "unknown"
448
+ and transcript_ctx.get("reason") in _COMPACTION_UNKNOWN_REASONS
449
+ ):
450
+ return transcript_ctx
451
+ if isinstance(statusline_ctx, dict):
452
+ transcript_ms = (
453
+ _usable_int(transcript_ctx.get("as_of_ms"))
454
+ if isinstance(transcript_ctx, dict)
455
+ else None
456
+ )
457
+ statusline_ms = _usable_int(statusline_ctx.get("as_of_ms"))
458
+ if (
459
+ transcript_ms is not None
460
+ and statusline_ms is not None
461
+ and statusline_ms < transcript_ms
462
+ ):
463
+ return transcript_ctx
464
+ return statusline_ctx
465
+ return transcript_ctx