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,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
|