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.
- package/LICENSE +21 -0
- package/README.md +240 -4
- package/bin/ccx +55 -2
- package/ccx/__init__.py +167 -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 +807 -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 +269 -0
- package/ccx/claude/host/session.py +5244 -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 +2535 -0
- package/ccx/claude/usage.py +796 -0
- package/ccx/claude/usage_control.py +204 -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/models.py +53 -0
- package/ccx/cli/commands/read.py +1104 -0
- package/ccx/cli/commands/turns.py +1081 -0
- package/ccx/cli/controller.py +757 -0
- package/ccx/cli/main.py +302 -0
- package/ccx/cli/parser.py +1439 -0
- package/ccx/cli/skills.py +215 -0
- package/ccx/cli/statusline.py +160 -0
- package/ccx/client.py +1904 -0
- package/ccx/codex/__init__.py +0 -0
- package/ccx/codex/attach_tty.py +267 -0
- package/ccx/codex/client.py +738 -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 +1558 -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 +2424 -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/history_store.py +131 -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 +1454 -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/models.py +225 -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 +296 -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 +1593 -0
- package/ccx/ops/conversation_paging.py +1339 -0
- package/ccx/ops/create.py +56 -0
- package/ccx/ops/creation.py +952 -0
- package/ccx/ops/creation_steps.py +575 -0
- package/ccx/ops/event_feed.py +710 -0
- package/ccx/ops/forking.py +711 -0
- package/ccx/ops/history.py +1041 -0
- package/ccx/ops/input.py +556 -0
- package/ccx/ops/maintenance.py +295 -0
- package/ccx/ops/report.py +773 -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 +348 -0
- package/ccx/ops/wait.py +762 -0
- package/ccx/ops/worker_events.py +640 -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 +250 -0
- package/ccx/store/record_cache.py +40 -0
- package/ccx/store/records.py +309 -0
- package/ccx/store/root.py +789 -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/private_json.py +54 -0
- package/ccx/system/sanitize.py +43 -0
- package/ccx/types.py +809 -0
- package/ccx/version.py +29 -0
- package/ccx/workers.py +2093 -0
- package/ccx.py +9 -0
- package/package.json +39 -7
- package/pyproject.toml +87 -0
- package/skills/ccx/SKILL.md +166 -0
- package/skills/ccx/references/controllers.md +244 -0
- package/skills/ccx/references/history.md +50 -0
- package/skills/ccx/references/troubleshooting.md +94 -0
- package/skills/ccx/references/variables.md +62 -0
package/ccx/constants.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import pathlib
|
|
5
|
+
import shutil
|
|
6
|
+
|
|
7
|
+
from ccx.store.root import canonical_root
|
|
8
|
+
|
|
9
|
+
# Roots are made absolute once, here: the managed host chdirs into the worker
|
|
10
|
+
# cwd before its first path write, and the hooks relay runs in that cwd too, so
|
|
11
|
+
# a relative root would name a different directory in each process.
|
|
12
|
+
# Canonicalize ancestors only; a linked root leaf must fail the no-follow walk.
|
|
13
|
+
_ORIGINAL_CCX_ROOT_ENV = os.environ.get("CCX_ROOT")
|
|
14
|
+
CCX_ROOT = canonical_root(os.environ.get("CCX_ROOT") or "~/.ccx")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def select_root(path: pathlib.Path | str) -> pathlib.Path:
|
|
18
|
+
"""Select an explicit process root and pin the same identity for children."""
|
|
19
|
+
global CCX_ROOT
|
|
20
|
+
CCX_ROOT = canonical_root(path)
|
|
21
|
+
os.environ["CCX_ROOT"] = str(CCX_ROOT)
|
|
22
|
+
return CCX_ROOT
|
|
23
|
+
|
|
24
|
+
_CLAUDE_CONFIG_OVERRIDE = os.environ.get("CLAUDE_CONFIG_DIR")
|
|
25
|
+
CLAUDE_HOME = pathlib.Path(os.path.realpath(os.path.expanduser(_CLAUDE_CONFIG_OVERRIDE or "~/.claude")))
|
|
26
|
+
# Always pin the canonical root for detached hosts, daemon and hook children,
|
|
27
|
+
# even when the caller used the default rather than exporting CCX_ROOT.
|
|
28
|
+
os.environ["CCX_ROOT"] = str(CCX_ROOT)
|
|
29
|
+
if _CLAUDE_CONFIG_OVERRIDE:
|
|
30
|
+
os.environ["CLAUDE_CONFIG_DIR"] = str(CLAUDE_HOME)
|
|
31
|
+
# Claude 2.1.293 reads global metadata (including per-cwd folder trust) from
|
|
32
|
+
# the effective native profile. With a custom config directory, native Claude
|
|
33
|
+
# reads <CLAUDE_CONFIG_DIR>/.claude.json, NOT HOME/.claude.json. Both initial
|
|
34
|
+
# worker creation and respawn call settings.ensure_trusted_paths.
|
|
35
|
+
CLAUDE_DOTFILE = (
|
|
36
|
+
CLAUDE_HOME / ".claude.json"
|
|
37
|
+
if _CLAUDE_CONFIG_OVERRIDE
|
|
38
|
+
else pathlib.Path(os.path.expanduser("~/.claude.json"))
|
|
39
|
+
)
|
|
40
|
+
def _default_claude_bin() -> str:
|
|
41
|
+
# The native installer's location first; npm/Homebrew installs put claude
|
|
42
|
+
# elsewhere on PATH. Keep the native path when neither exists so error
|
|
43
|
+
# messages name the conventional location.
|
|
44
|
+
native = os.path.expanduser("~/.local/bin/claude")
|
|
45
|
+
if os.path.exists(native):
|
|
46
|
+
return native
|
|
47
|
+
return shutil.which("claude") or native
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
CLAUDE_BIN = os.environ.get("CCX_CLAUDE_BIN") or _default_claude_bin()
|
|
51
|
+
|
|
52
|
+
SCHEMA_VERSION = 1
|
|
53
|
+
# Long-lived clients and daemons can span package upgrades. Fence requests and
|
|
54
|
+
# replies in both directions; bump when required fields or operation meanings change.
|
|
55
|
+
CODEXD_RPC_GENERATION = 21
|
|
56
|
+
# Inspection and teardown answer any client, whatever generation it speaks:
|
|
57
|
+
# they are how an operator sees an incompatible daemon and gets rid of it.
|
|
58
|
+
# Every other operation is fenced in both directions before it runs.
|
|
59
|
+
CODEXD_RECOVERY_OPS = frozenset({
|
|
60
|
+
"status", "compact-observe", "kill", "interrupt", "daemon-status", "daemon-stop",
|
|
61
|
+
})
|
|
62
|
+
# Notification supervisors also outlive package upgrades.
|
|
63
|
+
NOTIFYD_PROTOCOL = 19
|
|
64
|
+
# notifyd is a codexd client, so a codexd RPC bump must also retire a running
|
|
65
|
+
# notifyd (otherwise it keeps old code and its Codex parent wakes are deferred).
|
|
66
|
+
NOTIFYD_GENERATION = NOTIFYD_PROTOCOL * 100 + CODEXD_RPC_GENERATION
|
|
67
|
+
# Exact managed caller full identity replaces old short-routing authority.
|
|
68
|
+
SELF_WORKER_ID_ENV = "CCX_SELF_WORKER_ID"
|
|
69
|
+
SELF_ENGINE_ENV = "CCX_SELF_ENGINE"
|
|
70
|
+
SELF_SHORT_ENV = "CCX_SELF_SHORT" # historical, refused by new runtime
|
|
71
|
+
DEFAULT_IDLE_TIMEOUT_S = 30 * 60 # 30 minutes
|
|
72
|
+
# Boot watchdog: a worker leaves S.BOOTING only on a SessionStart hook. If Claude
|
|
73
|
+
# blocks before that (e.g. an un-pre-seeded --add-dir trust dialog or onboarding
|
|
74
|
+
# gate) no hook ever fires and the idle-killer — gated on S.IDLE — never reaps it,
|
|
75
|
+
# so the boot hangs indefinitely. Healthy boots fire SessionStart within seconds;
|
|
76
|
+
# 90s is generously above observed boot times. Env-overridable for tests/tuning.
|
|
77
|
+
BOOT_TIMEOUT_S = float(os.environ.get("CCX_BOOT_TIMEOUT_S", 90))
|
|
78
|
+
DEFAULT_PTY_COLS = 120
|
|
79
|
+
DEFAULT_PTY_ROWS = 40
|
|
80
|
+
WARMUP_S = 4.0 # post-IDLE settle before allowing sends (after a turn)
|
|
81
|
+
PTY_QUIESCENCE_S = 1.5 # PTY must be quiet this long after IDLE-entry → Ink-ready
|
|
82
|
+
PTY_WRITE_TIMEOUT_S = 2.0 # bounded drain for a complete write to the non-blocking PTY
|
|
83
|
+
PASTE_SUBMIT_DELAY_S = 0.01 # let Ink apply a completed bracketed paste before Enter
|
|
84
|
+
# Ink turns large/multiline bracketed pastes into a collapsed attachment
|
|
85
|
+
# asynchronously. An Enter sent during that conversion is silently ignored.
|
|
86
|
+
# Only the single-paste fallback (see PASTE_INLINE_*) collapses: it stages large
|
|
87
|
+
# IDLE sends and delays submission in proportion to their UTF-8 size while the
|
|
88
|
+
# host continues draining PTY output.
|
|
89
|
+
PASTE_STAGED_THRESHOLD_BYTES = 8 * 1024
|
|
90
|
+
PASTE_STAGED_BYTES_PER_S = 2 * 1024
|
|
91
|
+
PASTE_STAGED_MAX_DELAY_S = 30.0
|
|
92
|
+
# Claude collapses a bracketed paste longer than 800 UTF-16 units or with more
|
|
93
|
+
# than min(rows - 10, 2) line breaks into a `[Pasted text]` attachment, and
|
|
94
|
+
# submits attachments wrapped in <pasted_content> tags that its system prompt
|
|
95
|
+
# tells the model to treat as possibly not user-authored. Smaller pastes are
|
|
96
|
+
# inserted as typed text, so prompts are framed as a run of pastes under both
|
|
97
|
+
# limits (2.1.294: FY=800, S8=min(rows-10,2)). The unit cap keeps a margin.
|
|
98
|
+
PASTE_INLINE_MAX_UNITS = 640
|
|
99
|
+
PASTE_INLINE_ROWS_RESERVED = 10
|
|
100
|
+
PASTE_INLINE_MAX_NEWLINES = 2
|
|
101
|
+
SEND_FIRST_ACK_S = 5.0 # if no UserPromptSubmit within this of first paste, retry once
|
|
102
|
+
SEND_RETRY_ACK_S = 30.0 # if no UPS within this of retry paste, CRASHED
|
|
103
|
+
CLEAR_INPUT_MIN_GAP_S = 1.5 # min gap between input-clearing Ctrl-C presses: Claude
|
|
104
|
+
# exits on a second Ctrl-C within 800ms of one that
|
|
105
|
+
# found the editor empty
|
|
106
|
+
CLEAR_KILL_REPEAT = 48 # Ctrl-U presses in the /compact residue clear — drains a multi-line
|
|
107
|
+
# input buffer line-by-line (Ctrl-U = delete-to-line-start,
|
|
108
|
+
# not whole-buffer); no-op once empty. See drain_bytes().
|
|
109
|
+
POST_CLEAR_SETTLE_S = 0.3 # min gap after a retry clear before re-pasting, so the
|
|
110
|
+
# buffer-empty has rendered and isn't raced by the paste
|
|
111
|
+
COMPACT_WAIT_DEFAULT_S = 5 * 60 # max wait for native /compact completion
|
|
112
|
+
COMPACT_POLL_S = 0.2 # transcript poll cadence while compacting
|
|
113
|
+
COMPACT_SUBMIT_DELAY_S = 1.0 # gap between pasting "/compact" and the Enter that runs it —
|
|
114
|
+
# pasting a slash command opens Ink's autocomplete menu, and a
|
|
115
|
+
# \r in the same write is swallowed by the menu instead of
|
|
116
|
+
# submitting (LIVE-verified on Claude 2.1.207)
|
|
117
|
+
COMPACT_SUBMIT_RETRY_S = 3.0 # re-press Enter at this cadence until the transcript records
|
|
118
|
+
# the /compact command as submitted
|
|
119
|
+
POST_COMPACT_HOLD_GRACE_S = 10.0 # a compact that settles while a real turn is already running
|
|
120
|
+
# holds BUSY for that turn instead of releasing to IDLE. If the
|
|
121
|
+
# inference was wrong the worker would be pinned BUSY forever,
|
|
122
|
+
# so the hold is bounded: this much combined hook AND PTY
|
|
123
|
+
# silence releases it. A live turn renders its spinner
|
|
124
|
+
# continuously, so it never approaches the grace.
|
|
125
|
+
BOUNDARY_SEND_ACK_S = 4.0 # hook relay RPC timeout headroom; singleton steer uses its
|
|
126
|
+
# own short boundary ACK budget below
|
|
127
|
+
BOUNDARY_SEND_POLL_S = 0.02 # native singleton steer enqueue-ack poll cadence
|
|
128
|
+
HOOK_RELAY_RETRY_S = 30.0 # a parent hook relay retries a busy host (full listen backlog) this long
|
|
129
|
+
HOOK_RELAY_RETRY_POLL_S = 0.05 # pause between those connect attempts
|
|
130
|
+
# Durable steering (`ccx steer`). A steer is journaled before it is written to
|
|
131
|
+
# the PTY, so absence of quick evidence is never a reason to write it again.
|
|
132
|
+
STEER_BOUNDARY_ACK_S = 1.0 # max PostToolUse/SubagentStop hold waiting for the native
|
|
133
|
+
# enqueue of a durable steer. Short on purpose: the delivery
|
|
134
|
+
# is already journaled, so a slow enqueue is reconciled off
|
|
135
|
+
# the hook path instead of blocking Claude's tool loop.
|
|
136
|
+
STEER_EVIDENCE_GRACE_S = 300.0 # a delivery pasted with no native evidence at all for this long
|
|
137
|
+
# becomes durable `unknown` (never a silent re-paste)
|
|
138
|
+
STEER_OBSERVE_DEFAULT_S = 120.0 # caller-side observation window for stronger placement evidence
|
|
139
|
+
STEER_OBSERVE_POLL_S = 0.5 # caller-side poll cadence while observing
|
|
140
|
+
# Native status is a liveness fallback only; elapsed time never proves terminality.
|
|
141
|
+
INTERRUPT_NATIVE_POLL_S = 0.10
|
|
142
|
+
INTERRUPT_CONTINUED_ACTIVITY_GRACE_S = 0.60
|
|
143
|
+
SEMANTIC_EVIDENCE_GRACE_S = 0.60
|
|
144
|
+
PASSIVE_NATIVE_IDLE_GRACE_S = 2.0 # avoid outpacing normal Stop and blocked UPS hooks
|
|
145
|
+
LATE_HOOK_BACKSTOP_S = 1.60
|
|
146
|
+
LATE_HOOK_PROMPT_ID_S = 60.0 # exact old prompt IDs outlive the heuristic Stop grace
|
|
147
|
+
|
|
148
|
+
INTERRUPT_CONFIRM_S = 12.0 # after an `interrupt` op, how long the host watches the
|
|
149
|
+
# transcript for Claude's "[Request interrupted by user"
|
|
150
|
+
# marker before presuming the Escape was a no-op (a
|
|
151
|
+
# cancelled turn fires no Stop hook, so this watcher is
|
|
152
|
+
# what settles the turn to IDLE)
|
|
153
|
+
RESPAWN_IDLE_DEADLINE_S = 180 # _respawn waits this long for state=IDLE post-host-spawn
|
|
154
|
+
# There is deliberately no default turn-wait bound. A turn is model work with no
|
|
155
|
+
# natural length, so every verb that starts or resumes one (new/send/steer's
|
|
156
|
+
# delivery/answer/approve/reject/wait --next-turn) waits unbounded by default and
|
|
157
|
+
# ends on the worker's own lifecycle. Callers who want a deadline pass an explicit
|
|
158
|
+
# --timeout / timeout=. Operational waits (compact, kill, RPC acks) keep theirs.
|
|
159
|
+
# Optional managed `send` payload bound (UTF-8 bytes of the prompt text).
|
|
160
|
+
# Unlimited by default: CLI and Python callers must be able to relay complete
|
|
161
|
+
# worker output without a CCX-owned length rejection. Operators can opt into a
|
|
162
|
+
# defensive PTY-paste guard with CCX_SEND_MAX_BYTES=N; <= 0 stays unlimited.
|
|
163
|
+
# The PTY transport drains short writes, but operators may still choose a bound
|
|
164
|
+
# for pathological payloads that cannot be delivered within PTY_WRITE_TIMEOUT_S.
|
|
165
|
+
SEND_TEXT_MAX_BYTES = int(os.environ.get("CCX_SEND_MAX_BYTES", "0"))
|
|
166
|
+
HOST_SOCK_WAIT_NEW_S = 5.0 # detach launcher: max wait for a fresh host's socket
|
|
167
|
+
HOST_SOCK_WAIT_RESPAWN_S = 10.0 # detach launcher: max wait for a revived host's socket
|
|
168
|
+
SPAWN_LAUNCHER_GRACE_S = 20.0 # headroom over sock-wait for the launcher subprocess itself
|
|
169
|
+
# (interpreter start + record read + double-fork)
|
|
170
|
+
NB_TERMINAL_CONFIRM_S = 30 # confirm a real user turn lands after terminal-bg queuedPrompt+respawn
|
|
171
|
+
PERMISSION_HOOK_TIMEOUT_S = float(os.environ.get("CCX_PERMISSION_HOOK_TIMEOUT_S", 6 * 60 * 60))
|
|
172
|
+
PERMISSION_POLL_INTERVAL_S = 0.25
|
|
173
|
+
|
|
174
|
+
# --- generic CCX event stream ----------------------------------------------
|
|
175
|
+
# Every event a host broadcasts (state changes, tool boundaries, subagent
|
|
176
|
+
# finishes, …) is stamped with an envelope (v/type/short/seq/epoch/ts) and tee'd
|
|
177
|
+
# to a per-session sidecar NDJSON so dashboards/orchestrators can read history
|
|
178
|
+
# for dead sessions and replay-then-subscribe for live ones. Event streaming enables this by
|
|
179
|
+
# default with no config knobs; persistence is bounded by amortized rotation.
|
|
180
|
+
EVENT_SCHEMA_VERSION = 1 # bumped if the envelope/event shapes change
|
|
181
|
+
CCX_EVENTS_MAX = 5000 # rotate the sidecar once it exceeds this many lines
|
|
182
|
+
CCX_EVENTS_KEEP = 4000 # lines retained (most recent) after a rotation
|
|
183
|
+
CCX_EVENTS_ROTATE_CHECK_INTERVAL = 256 # events between rotation-size checks (amortization)
|
|
184
|
+
EVENT_PREVIEW_LIMIT = 500 # max chars of a bounded input/result/message preview
|
|
185
|
+
BACKGROUND_TASK_SURFACE_LIMIT = 10 # max task objects exposed on peek/report/wait surfaces
|
|
186
|
+
|
|
187
|
+
# --- usage / budget caps ---------------------------------------------------
|
|
188
|
+
# Claude plan usage defaults to a promptless Claude Code control request.
|
|
189
|
+
# Direct OAuth is an explicit per-CCX_ROOT opt-in, never the default.
|
|
190
|
+
USAGE_URL = "https://api.anthropic.com/api/oauth/usage"
|
|
191
|
+
USAGE_BETA = "oauth-2025-04-20"
|
|
192
|
+
# Per-user and private by default: a fixed /tmp name lets other local users
|
|
193
|
+
# read the account's usage or plant data that cap enforcement trusts. Set
|
|
194
|
+
# CCX_USAGE_CACHE lets external *readers* consume one CCX-owned cache.
|
|
195
|
+
def usage_cache_path() -> pathlib.Path:
|
|
196
|
+
"""A current-process root path, not a snapshot captured before select_root."""
|
|
197
|
+
return pathlib.Path(os.path.abspath(os.path.expanduser(
|
|
198
|
+
os.environ.get("CCX_USAGE_CACHE") or str(CCX_ROOT / "usage-cache.json")
|
|
199
|
+
)))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def usage_source_path() -> pathlib.Path:
|
|
203
|
+
return CCX_ROOT / "usage-source.json"
|
|
204
|
+
USAGE_MIN_FETCH_INTERVAL_S = 60.0 # shared minimum, even with force/max_age=0
|
|
205
|
+
CAP_POLL_INTERVAL_S = 120.0 # capped hosts check/refresh at most every 120s
|
|
206
|
+
USAGE_STALE_LIMIT_S = 120.0 # caps fail open when the last good fetch is older
|
|
207
|
+
# Direct OAuth measured 2026-10-26: about 5 reads per 10 minutes, then HTTP 429.
|
|
208
|
+
# Space its reads evenly instead and let caps accept the correspondingly older sample.
|
|
209
|
+
OAUTH_USAGE_MIN_FETCH_INTERVAL_S = 120.0
|
|
210
|
+
OAUTH_USAGE_STALE_LIMIT_S = 300.0
|
|
211
|
+
BUDGET_EXIT = 7 # exit code from blocking new/send when a cap hard-stops a run
|
|
212
|
+
# Cap flag key → legacy field name in the usage payload. Per-model cap
|
|
213
|
+
# lookup also follows the dynamic ``limits`` catalog when the legacy field is
|
|
214
|
+
# absent/null.
|
|
215
|
+
CAP_METRICS = {"5h": "five_hour", "7d": "seven_day", "opus": "seven_day_opus"}
|
|
216
|
+
|
|
217
|
+
# --- opt-in gates for unofficial provider internals --------------------------
|
|
218
|
+
# Paths that MUTATE undocumented Anthropic/Claude Code state stay off unless the
|
|
219
|
+
# operator opts in explicitly. Read-only usage and native-bg discovery are not
|
|
220
|
+
# gated. Read at call time (not import time) so tests and callers can toggle them.
|
|
221
|
+
UNOFFICIAL_RESET_ENV = "CCX_ENABLE_UNOFFICIAL_RESET" # 5-hour reset POST
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def opt_in_enabled(env_name: str) -> bool:
|
|
225
|
+
"""True only for an explicit truthy opt-in (1/true/yes/on)."""
|
|
226
|
+
return os.environ.get(env_name, "").strip().lower() in ("1", "true", "yes", "on")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# Model family aliases understood by `claude --model`. This is NOT an allowlist
|
|
230
|
+
# for --model, which stays a free-form passthrough (new model ids ship on
|
|
231
|
+
# Claude Code's cadence, not ccx's). Its only job is to catch the `--agent
|
|
232
|
+
# <model>` mixup at `ccx new`: `--agent` takes a *subagent* name, and a worker
|
|
233
|
+
# launched with `--agent opus` dies at boot with "--agent 'opus' not found",
|
|
234
|
+
# which used to surface only as an opaque state=crashed.
|
|
235
|
+
MODEL_ALIASES = ("fable", "opus", "sonnet", "haiku")
|
|
236
|
+
|
|
237
|
+
# --- CLI exit codes --------------------------------------------------------
|
|
238
|
+
# The integer codes every `ccx` and `ccx codex` command exits with. Both engines
|
|
239
|
+
# use one meaning per code, and each typed Python error carries the same code as
|
|
240
|
+
# `exit_code` (see ccx/errors.py and EXIT_CODE_TO_ERROR):
|
|
241
|
+
# 1 EXIT_ERROR bad input or usage (argparse errors included), I/O
|
|
242
|
+
# error, general failure
|
|
243
|
+
# 2 EXIT_NOT_FOUND no such worker, or no transcript / reply yet
|
|
244
|
+
# 3 EXIT_UNAVAILABLE CCX cannot operate on the worker: host or daemon not
|
|
245
|
+
# alive, read-only external session, failed respawn,
|
|
246
|
+
# a recorded active turn nothing serves any more
|
|
247
|
+
# 4 EXIT_WAITING the worker is blocked on input (answer / approve /
|
|
248
|
+
# reject / respond)
|
|
249
|
+
# 5 EXIT_REJECTED the host or daemon refused the request in the
|
|
250
|
+
# worker's current state (busy, no active turn,
|
|
251
|
+
# something in flight); nothing was changed
|
|
252
|
+
# 6 EXIT_TURN_INCOMPLETE did not complete cleanly: a wait or turn timed out,
|
|
253
|
+
# or the awaited turn failed / was interrupted
|
|
254
|
+
# 7 BUDGET_EXIT a usage cap hard-stopped the run (defined above)
|
|
255
|
+
# 8 EXIT_RATE_LIMITED Anthropic rate-limited the account (429). Distinct from
|
|
256
|
+
# BUDGET_EXIT, which is CCX's OWN cap: this one is imposed
|
|
257
|
+
# upstream and clears on its own reset, so a caller that
|
|
258
|
+
# lumps the two together cannot tell "I set this limit"
|
|
259
|
+
# from "wait until the quota returns"
|
|
260
|
+
# 9 EXIT_API_TRANSIENT Anthropic could not serve the request right now, but
|
|
261
|
+
# the identical call could succeed later — a 529
|
|
262
|
+
# (capacity, and free of quota), a 5xx, or a stream that
|
|
263
|
+
# died mid-response. Split from EXIT_RATE_LIMITED because
|
|
264
|
+
# the remedy differs: this clears on its own in minutes,
|
|
265
|
+
# where a rate limit waits for a reset. Both are split
|
|
266
|
+
# from EXIT_UNAVAILABLE, which stays the permanent-failure
|
|
267
|
+
# code (auth, billing, bad request) where retrying is
|
|
268
|
+
# pointless
|
|
269
|
+
EXIT_ERROR = 1
|
|
270
|
+
EXIT_NOT_FOUND = 2
|
|
271
|
+
EXIT_UNAVAILABLE = 3
|
|
272
|
+
EXIT_WAITING = 4
|
|
273
|
+
EXIT_REJECTED = 5
|
|
274
|
+
EXIT_TURN_INCOMPLETE = 6
|
|
275
|
+
EXIT_RATE_LIMITED = 8
|
|
276
|
+
EXIT_API_TRANSIENT = 9
|
|
277
|
+
|
|
278
|
+
# Lifecycle states
|
|
279
|
+
class S:
|
|
280
|
+
BOOTING = "booting"
|
|
281
|
+
IDLE = "idle"
|
|
282
|
+
BUSY = "busy"
|
|
283
|
+
WAITING = "waiting"
|
|
284
|
+
IDLE_KILLED = "idle-killed"
|
|
285
|
+
RESUMING = "resuming"
|
|
286
|
+
CRASHED = "crashed"
|
|
287
|
+
DEAD = "dead"
|
|
288
|
+
BUDGET_STOPPED = "budget-stopped" # hard-stopped because a usage cap was reached
|
|
File without changes
|