ccx-native 0.0.1 → 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.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +241 -4
  3. package/bin/ccx +55 -2
  4. package/ccx/__init__.py +163 -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 +762 -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 +250 -0
  16. package/ccx/claude/host/session.py +5184 -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 +2281 -0
  26. package/ccx/claude/usage.py +837 -0
  27. package/ccx/claude/usage_control.py +152 -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/read.py +1020 -0
  35. package/ccx/cli/commands/turns.py +1081 -0
  36. package/ccx/cli/controller.py +757 -0
  37. package/ccx/cli/main.py +301 -0
  38. package/ccx/cli/parser.py +1324 -0
  39. package/ccx/cli/skills.py +215 -0
  40. package/ccx/cli/statusline.py +160 -0
  41. package/ccx/client.py +1900 -0
  42. package/ccx/codex/__init__.py +0 -0
  43. package/ccx/codex/attach_tty.py +267 -0
  44. package/ccx/codex/client.py +732 -0
  45. package/ccx/codex/completion.py +121 -0
  46. package/ccx/codex/control.py +288 -0
  47. package/ccx/codex/daemon/__init__.py +0 -0
  48. package/ccx/codex/daemon/__main__.py +4 -0
  49. package/ccx/codex/daemon/daemon.py +1556 -0
  50. package/ccx/codex/daemon/models.py +140 -0
  51. package/ccx/codex/daemon/native.py +1590 -0
  52. package/ccx/codex/daemon/release.py +367 -0
  53. package/ccx/codex/daemon/worker.py +2396 -0
  54. package/ccx/codex/delivery.py +758 -0
  55. package/ccx/codex/delivery_recovery.py +232 -0
  56. package/ccx/codex/evidence.py +827 -0
  57. package/ccx/codex/history.py +158 -0
  58. package/ccx/codex/notify_history.py +140 -0
  59. package/ccx/codex/ops.py +666 -0
  60. package/ccx/codex/paths.py +70 -0
  61. package/ccx/codex/protocol.py +805 -0
  62. package/ccx/codex/provenance.py +105 -0
  63. package/ccx/codex/read.py +29 -0
  64. package/ccx/codex/records.py +51 -0
  65. package/ccx/codex/reports.py +177 -0
  66. package/ccx/codex/runtime.py +1412 -0
  67. package/ccx/codex/settle.py +363 -0
  68. package/ccx/codex/telemetry.py +792 -0
  69. package/ccx/codex/types.py +659 -0
  70. package/ccx/codex/worker_environment.py +25 -0
  71. package/ccx/constants.py +288 -0
  72. package/ccx/controllers/__init__.py +0 -0
  73. package/ccx/controllers/controller.py +1449 -0
  74. package/ccx/controllers/live_vars.py +1706 -0
  75. package/ccx/controllers/live_vars_runner.py +216 -0
  76. package/ccx/controllers/notify.py +213 -0
  77. package/ccx/controllers/orchestrator.py +193 -0
  78. package/ccx/controllers/policy.py +57 -0
  79. package/ccx/controllers/store.py +681 -0
  80. package/ccx/errors.py +634 -0
  81. package/ccx/notify/__init__.py +0 -0
  82. package/ccx/notify/__main__.py +5 -0
  83. package/ccx/notify/client.py +116 -0
  84. package/ccx/notify/daemon.py +480 -0
  85. package/ccx/notify/deliver.py +304 -0
  86. package/ccx/notify/paths.py +51 -0
  87. package/ccx/notify/registry.py +649 -0
  88. package/ccx/notify/select.py +750 -0
  89. package/ccx/notify/settle.py +684 -0
  90. package/ccx/ops/__init__.py +0 -0
  91. package/ccx/ops/background.py +115 -0
  92. package/ccx/ops/cancel.py +289 -0
  93. package/ccx/ops/codex_import.py +96 -0
  94. package/ccx/ops/common.py +397 -0
  95. package/ccx/ops/compact.py +337 -0
  96. package/ccx/ops/compact_ops.py +550 -0
  97. package/ccx/ops/context_events.py +48 -0
  98. package/ccx/ops/conversation.py +1540 -0
  99. package/ccx/ops/conversation_paging.py +1338 -0
  100. package/ccx/ops/create.py +56 -0
  101. package/ccx/ops/creation.py +952 -0
  102. package/ccx/ops/creation_steps.py +579 -0
  103. package/ccx/ops/event_feed.py +710 -0
  104. package/ccx/ops/forking.py +711 -0
  105. package/ccx/ops/input.py +556 -0
  106. package/ccx/ops/maintenance.py +295 -0
  107. package/ccx/ops/report.py +770 -0
  108. package/ccx/ops/semantics.py +490 -0
  109. package/ccx/ops/send.py +961 -0
  110. package/ccx/ops/steer_ops.py +867 -0
  111. package/ccx/ops/terminate.py +383 -0
  112. package/ccx/ops/turn_reports.py +306 -0
  113. package/ccx/ops/wait.py +762 -0
  114. package/ccx/ops/worker_events.py +636 -0
  115. package/ccx/rpc/__init__.py +0 -0
  116. package/ccx/rpc/local_rpc.py +1602 -0
  117. package/ccx/rpc/rpc.py +559 -0
  118. package/ccx/rpc/serve.py +374 -0
  119. package/ccx/rpc/transport.py +409 -0
  120. package/ccx/rpc/view.py +125 -0
  121. package/ccx/store/__init__.py +0 -0
  122. package/ccx/store/discovery.py +170 -0
  123. package/ccx/store/identity.py +105 -0
  124. package/ccx/store/journal_io.py +79 -0
  125. package/ccx/store/liveness.py +194 -0
  126. package/ccx/store/paths.py +254 -0
  127. package/ccx/store/record_cache.py +40 -0
  128. package/ccx/store/records.py +309 -0
  129. package/ccx/store/root.py +804 -0
  130. package/ccx/store/runtime_presence.py +137 -0
  131. package/ccx/store/store.py +2332 -0
  132. package/ccx/store/worker_state.py +86 -0
  133. package/ccx/system/__init__.py +0 -0
  134. package/ccx/system/bounded.py +296 -0
  135. package/ccx/system/cwd_paths.py +25 -0
  136. package/ccx/system/engine_binary.py +59 -0
  137. package/ccx/system/executable.py +50 -0
  138. package/ccx/system/oslayer.py +667 -0
  139. package/ccx/system/pipes.py +46 -0
  140. package/ccx/system/sanitize.py +43 -0
  141. package/ccx/types.py +705 -0
  142. package/ccx/version.py +29 -0
  143. package/ccx/workers.py +2081 -0
  144. package/ccx.py +9 -0
  145. package/package.json +39 -7
  146. package/pyproject.toml +87 -0
  147. package/skills/ccx/SKILL.md +155 -0
  148. package/skills/ccx/references/controllers.md +244 -0
  149. package/skills/ccx/references/troubleshooting.md +94 -0
  150. package/skills/ccx/references/variables.md +62 -0
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import os
5
+ import threading
6
+ from typing import Any
7
+ from ccx.codex.protocol import ProtocolError, ProtocolTransportError, error_payload
8
+ from ccx.codex.records import now_ms
9
+
10
+
11
+ _LAUNCH_SETTLE_CAP_S = 10.0
12
+
13
+
14
+ def _native_history_budget(message: dict[str, Any]) -> float:
15
+ """One caller-provided native read budget, not a fixed daemon timeout."""
16
+ budget = message.get("observeSeconds", 30.0)
17
+ if (type(budget) not in (int, float)
18
+ or not math.isfinite(budget) or budget <= 0):
19
+ raise ValueError("native history requires positive observation seconds")
20
+ return float(budget)
21
+
22
+
23
+ def linger_seconds() -> float:
24
+ try:
25
+ return float(os.environ.get("CCX_CODEXD_LINGER", "900"))
26
+ except ValueError:
27
+ return 900.0
28
+
29
+
30
+ def _bounded(value: Any, limit: int = 1000) -> Any:
31
+ if not isinstance(value, str) or len(value) <= limit:
32
+ return value
33
+ return value[: limit - 1] + "…"
34
+
35
+
36
+ def _compact_native_refusal(error: ProtocolError) -> bool:
37
+ if isinstance(error, ProtocolTransportError) or error.request_id is None:
38
+ return False
39
+ data = error.data if isinstance(error.data, dict) else {}
40
+ info = data.get("codexErrorInfo", data)
41
+ kind = info.get("activeTurnNotSteerable") if isinstance(info, dict) else None
42
+ if isinstance(kind, dict):
43
+ return kind.get("turnKind") == "compact"
44
+ # Codex 0.159.3 preserves structured error data on steer, but turn/start
45
+ # wraps the same native refusal as an internal-error string without data.
46
+ # Match only the observed method/code/literal; never all -32603 failures.
47
+ if error.method == "turn/start" and error.code == -32603:
48
+ return error.message == "failed to submit turn input: ActiveTurnNotSteerable { turn_kind: Compact }"
49
+ return error.message == "cannot steer a compact turn"
50
+
51
+
52
+ def _protocol_failure(error: ProtocolError) -> dict[str, Any]:
53
+ """One daemon-global failure envelope.
54
+
55
+ ``error`` stays the human-readable string every existing caller reads;
56
+ ``protocolError`` carries the machine-readable fields that would otherwise
57
+ be flattened away at this process boundary.
58
+ """
59
+ return {
60
+ "ok": False,
61
+ "error": str(error),
62
+ "protocolError": error_payload(error),
63
+ **({"reason": "compaction-busy", "retryable": True}
64
+ if _compact_native_refusal(error) else {}),
65
+ }
66
+
67
+
68
+ def _abandon_active_compact(record: dict[str, Any], reason: str) -> bool:
69
+ """Close a persisted compact that can no longer yield native proof.
70
+
71
+ The compact count does not advance: only native compaction evidence proves
72
+ success. Recording the exact operation as unknown prevents a daemon
73
+ restart/kill from poisoning a later generic ``notify`` (or a compact status
74
+ read) with a stale active marker.
75
+ """
76
+ active = record.get("activeCompact")
77
+ if not isinstance(active, dict):
78
+ return False
79
+ record["lastCompact"] = {
80
+ "operationId": active.get("operationId"),
81
+ "baselineCompactCount": active.get("baselineCompactCount"),
82
+ "targetCompactCount": active.get("targetCompactCount"),
83
+ "startedAtMs": active.get("startedAtMs"),
84
+ "completedAtMs": now_ms(),
85
+ "contextBefore": active.get("contextBefore"),
86
+ "turnId": active.get("turnId"),
87
+ "contextItemId": active.get("contextItemId"),
88
+ "nativeTurnStarted": active.get("nativeTurnStarted") is True,
89
+ "nativeTurnSettled": False,
90
+ "status": "unknown",
91
+ "reason": reason,
92
+ }
93
+ # Original fullUUID effects live exclusively in GenerationStore receipts.
94
+ # Do not create or update a second short-keyed compact receipt.
95
+ record["activeCompact"] = None
96
+ return True
97
+
98
+ def _retire_runtime_markers(record: dict[str, Any], reason: str) -> bool:
99
+ """Drop markers only a vanished app-server connection could resolve."""
100
+ changed = _abandon_active_compact(record, reason)
101
+ if record.get("nativeTurnStart") is not None:
102
+ record["nativeTurnStart"] = None
103
+ changed = True
104
+ return changed
105
+
106
+
107
+ class _TurnLaunch:
108
+ """A ``turn/start`` that is irrevocably in flight.
109
+
110
+ Published under the worker lock in the same critical section that checks the
111
+ cancellation marker, so a concurrent ``kill`` either wins (the loader never
112
+ starts the turn) or sees this and takes responsibility for cancelling it.
113
+ """
114
+
115
+ __slots__ = ("done", "turn_id")
116
+
117
+ def __init__(self) -> None:
118
+ self.done = threading.Event()
119
+ self.turn_id: str | None = None
120
+
121
+
122
+ class ReleasePending(ProtocolError):
123
+ """Codex has not answered an earlier `thread/unsubscribe` of this thread."""
124
+
125
+
126
+ class _ThreadRelease:
127
+ """One `thread/unsubscribe`, fenced from teardown until Codex answers it."""
128
+
129
+ def __init__(self, thread_id: str):
130
+ self.thread_id = thread_id
131
+ self.settled = threading.Event()
132
+
133
+
134
+ class WorkerKilled(Exception):
135
+ """A load was abandoned because the worker was killed while it ran.
136
+
137
+ Internal control flow, never surfaced to a caller as-is: the loader
138
+ turns it into an ordinary refused-load reply. It is deliberately not a
139
+ `ProtocolError` — nothing went wrong with app-server.
140
+ """