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.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +241 -2
  3. package/bin/ccx +56 -0
  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 +40 -4
  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,1449 @@
1
+ """Durable, task-specific Python controllers over real CCX workers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from concurrent.futures import Future, ThreadPoolExecutor
7
+ from contextvars import ContextVar, copy_context
8
+ from dataclasses import asdict, dataclass, is_dataclass
9
+ from functools import wraps
10
+ import hashlib
11
+ import json
12
+ import os
13
+ from pathlib import Path
14
+ import re
15
+ import runpy
16
+ import subprocess
17
+ import sys
18
+ import threading
19
+ import traceback
20
+ from typing import Any, Callable, Generic, Literal, ParamSpec, TypeVar, overload
21
+ import uuid
22
+
23
+ from ccx.controllers import store as store
24
+ from ccx.errors import (
25
+ CapabilityUnsupported,
26
+ BudgetStopped,
27
+ CcxError,
28
+ CreatePartialFailure,
29
+ CompactFailed,
30
+ ControllerError,
31
+ ControllerLaunchRequired,
32
+ ControllerOperationIndeterminate,
33
+ ControllerReplayMismatch,
34
+ ControllerLiveStateLost,
35
+ DeliveryRejected,
36
+ InvalidRequest,
37
+ SessionNotFound,
38
+ WorkerApiError,
39
+ WorkerRateLimited,
40
+ build_worker_api_error,
41
+ )
42
+
43
+
44
+ P = ParamSpec("P")
45
+ R = TypeVar("R")
46
+
47
+ _MUTATING_WORKER_METHODS = frozenset({
48
+ "create", "fork", "send", "steer", "resume", "compact", "answer",
49
+ "respond_elicitation", "approve", "reject", "stop", "kill",
50
+ })
51
+ # Worker-creating calls: the journal owns their UUID across lost returns.
52
+ _CREATING_WORKER_METHODS = frozenset({"create", "fork"})
53
+ _SAFE_REJECTIONS = (
54
+ InvalidRequest,
55
+ CapabilityUnsupported,
56
+ DeliveryRejected,
57
+ SessionNotFound,
58
+ )
59
+ _SLUG_RE = re.compile(r"[^A-Za-z0-9._-]+")
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class TaskOutput:
64
+ value: Any = None
65
+ report: str | None = None
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class ControllerInput:
70
+ id: str
71
+ created_at: int
72
+ worker: str | None
73
+ worker_name: str | None
74
+ comment: str
75
+
76
+ @classmethod
77
+ def from_record(cls, value: dict[str, Any]) -> "ControllerInput":
78
+ return cls(
79
+ id=str(value["id"]),
80
+ created_at=int(value["createdAt"]),
81
+ worker=str(value["worker"]) if value.get("worker") is not None else None,
82
+ worker_name=value.get("workerName"),
83
+ comment=str(value["comment"]),
84
+ )
85
+
86
+
87
+ def task_output(*, value: Any = None, report: str | None = None) -> TaskOutput:
88
+ if report is not None and not isinstance(report, str):
89
+ raise InvalidRequest("task_output report must be a string or None")
90
+ _canonical_json(value, "task output value")
91
+ return TaskOutput(value=value, report=report)
92
+
93
+
94
+ def _canonical_json(value: Any, label: str) -> str:
95
+ try:
96
+ return json.dumps(
97
+ value, sort_keys=True, separators=(",", ":"), ensure_ascii=False,
98
+ allow_nan=False,
99
+ )
100
+ except (TypeError, ValueError) as error:
101
+ raise InvalidRequest(f"{label} must be JSON-compatible: {error}") from error
102
+
103
+
104
+ def _fingerprint(value: Any, label: str) -> str:
105
+ return hashlib.sha256(_canonical_json(value, label).encode("utf-8")).hexdigest()
106
+
107
+
108
+ def _slug(value: str, fallback: str = "item") -> str:
109
+ rendered = _SLUG_RE.sub("-", value.strip()).strip("-._")
110
+ return (rendered or fallback)[:60]
111
+
112
+
113
+ def _write_text_atomic(path: Path, text: str) -> None:
114
+ temporary = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
115
+ path.parent.mkdir(parents=True, exist_ok=True)
116
+ try:
117
+ temporary.write_text(text, encoding="utf-8")
118
+ os.replace(temporary, path)
119
+ finally:
120
+ try:
121
+ temporary.unlink()
122
+ except FileNotFoundError:
123
+ pass
124
+
125
+
126
+ @dataclass
127
+ class _LaunchContext:
128
+ run_id: str
129
+ run_dir: Path
130
+ script: Path
131
+ resume: bool
132
+ name_override: str | None = None
133
+ registered: bool = False
134
+
135
+
136
+ @dataclass
137
+ class _TaskContext:
138
+ task_id: str
139
+ label: str
140
+ workspace: Path
141
+ operation_count: int = 0
142
+
143
+
144
+ _launch_context: ContextVar[_LaunchContext | None] = ContextVar(
145
+ "ccx_controller_launch", default=None
146
+ )
147
+ _active_runtime: ContextVar["_ControllerRuntime | None"] = ContextVar(
148
+ "ccx_controller_runtime", default=None
149
+ )
150
+ _active_task: ContextVar[_TaskContext | None] = ContextVar(
151
+ "ccx_controller_task", default=None
152
+ )
153
+ _reserved_task_invocation: ContextVar[tuple[object, int] | None] = ContextVar(
154
+ "ccx_controller_reserved_task_invocation", default=None
155
+ )
156
+ _suspend_worker_hook: ContextVar[bool] = ContextVar(
157
+ "ccx_controller_suspend_worker_hook", default=False
158
+ )
159
+
160
+
161
+ class _ControllerRuntime:
162
+ def __init__(self, run_dir: Path):
163
+ self.run_dir = run_dir
164
+ record = store.read_json(store.run_record_path(run_dir))
165
+ store.require_operation_journal_layout(run_dir, record)
166
+ self.run_id = str(record["runId"])
167
+ self.name = str(record["controller"])
168
+ self.base_workspace = Path(str(record["workspace"]))
169
+ self._lock = threading.RLock()
170
+ self._task_counts: dict[str, int] = {}
171
+ self._controller_operation_count = 0
172
+ self._executor = ThreadPoolExecutor(thread_name_prefix=f"ccx-{self.run_id[:8]}")
173
+ self._futures: list[Future[Any]] = []
174
+ self._observed_futures: set[Future[Any]] = set()
175
+ self._live_vars_owner = None
176
+
177
+ def live_vars_owner(self):
178
+ with self._lock:
179
+ if self._live_vars_owner is None:
180
+ from ccx.controllers.live_vars import LiveOwner
181
+ self._live_vars_owner = LiveOwner()
182
+ return self._live_vars_owner
183
+
184
+ def _close_live_vars(self) -> None:
185
+ with self._lock:
186
+ owner = self._live_vars_owner
187
+ self._live_vars_owner = None
188
+ if owner is not None:
189
+ owner.close()
190
+
191
+ def next_task_invocation(self, key: str) -> int:
192
+ with self._lock:
193
+ value = self._task_counts.get(key, 0) + 1
194
+ self._task_counts[key] = value
195
+ return value
196
+
197
+ def submit(self, call: Callable[[], R]) -> Future[R]:
198
+ context = copy_context()
199
+ future = self._executor.submit(context.run, call)
200
+ with self._lock:
201
+ self._futures.append(future)
202
+ return future
203
+
204
+ def observe_future(self, future: Future[Any]) -> None:
205
+ with self._lock:
206
+ self._observed_futures.add(future)
207
+
208
+ def finish_futures(self) -> None:
209
+ checked = 0
210
+ first_error: BaseException | None = None
211
+ while True:
212
+ with self._lock:
213
+ futures = self._futures[checked:]
214
+ if not futures:
215
+ break
216
+ checked += len(futures)
217
+ for future in futures:
218
+ try:
219
+ future.result()
220
+ except BaseException as error:
221
+ with self._lock:
222
+ observed = future in self._observed_futures
223
+ if not observed:
224
+ first_error = first_error or error
225
+ self._executor.shutdown(wait=True)
226
+ self._close_live_vars()
227
+ if first_error is not None:
228
+ raise first_error
229
+
230
+ def shutdown_after_error(self) -> None:
231
+ with self._lock:
232
+ futures = list(self._futures)
233
+ for future in futures:
234
+ future.cancel()
235
+ self._executor.shutdown(wait=True, cancel_futures=True)
236
+ self._close_live_vars()
237
+
238
+ def next_operation(self, task_context: _TaskContext | None) -> tuple[str, int, Path]:
239
+ with self._lock:
240
+ if task_context is None:
241
+ self._controller_operation_count += 1
242
+ number = self._controller_operation_count
243
+ scope = "controller"
244
+ directory = self.run_dir / "operations"
245
+ else:
246
+ task_context.operation_count += 1
247
+ number = task_context.operation_count
248
+ scope = task_context.task_id
249
+ directory = self.run_dir / "tasks" / task_context.task_id / "operations"
250
+ directory.mkdir(parents=True, exist_ok=True)
251
+ return scope, number, directory
252
+
253
+
254
+ class _Current:
255
+ def _runtime(self) -> _ControllerRuntime:
256
+ runtime = _active_runtime.get()
257
+ if runtime is None:
258
+ raise ControllerLaunchRequired("current is only available inside a running CCX controller")
259
+ return runtime
260
+
261
+ @property
262
+ def run_id(self) -> str:
263
+ return self._runtime().run_id
264
+
265
+ @property
266
+ def controller_name(self) -> str:
267
+ return self._runtime().name
268
+
269
+ @property
270
+ def run_dir(self) -> Path:
271
+ return self._runtime().run_dir
272
+
273
+ @property
274
+ def workspace(self) -> Path:
275
+ task_context = _active_task.get()
276
+ return task_context.workspace if task_context is not None else self._runtime().base_workspace
277
+
278
+ @property
279
+ def workers(self) -> tuple[dict[str, Any], ...]:
280
+ record = store.read_json(store.run_record_path(self._runtime().run_dir))
281
+ return tuple(dict(item) for item in record.get("workers", []) if isinstance(item, dict))
282
+
283
+ @property
284
+ def inputs(self) -> tuple[ControllerInput, ...]:
285
+ return tuple(
286
+ ControllerInput.from_record(value)
287
+ for value in store.read_inputs(self._runtime().run_dir)
288
+ )
289
+
290
+ @property
291
+ def latest_input(self) -> ControllerInput | None:
292
+ values = self.inputs
293
+ return values[-1] if values else None
294
+
295
+ def report(
296
+ self,
297
+ *,
298
+ status: str,
299
+ checkpoint: str | None = None,
300
+ summary: str | None = None,
301
+ report: str | None = None,
302
+ data: dict[str, Any] | None = None,
303
+ ) -> dict[str, Any]:
304
+ runtime = self._runtime()
305
+ if not isinstance(status, str) or not status.strip():
306
+ raise InvalidRequest("controller report status must be non-empty")
307
+ if data is not None:
308
+ _canonical_json(data, "controller report data")
309
+ report_path: Path | None = None
310
+ with runtime._lock:
311
+ if report is not None:
312
+ if not isinstance(report, str):
313
+ raise InvalidRequest("controller report text must be a string or None")
314
+ record = store.read_json(store.run_record_path(runtime.run_dir))
315
+ sequence = len(record.get("reports") or []) + 1
316
+ report_path = runtime.run_dir / "reports" / f"controller-{sequence:04d}.md"
317
+ _write_text_atomic(report_path, report)
318
+ report_record = {
319
+ "createdAt": store.now_ms(),
320
+ "status": status.strip(),
321
+ "checkpoint": checkpoint,
322
+ "summary": summary,
323
+ "path": str(report_path) if report_path is not None else None,
324
+ "data": data,
325
+ }
326
+
327
+ def update(value: dict[str, Any]) -> None:
328
+ value["status"] = report_record["status"]
329
+ value["checkpoint"] = checkpoint
330
+ value["summary"] = summary
331
+ value["latestReport"] = report_record
332
+ value.setdefault("reports", []).append(report_record)
333
+
334
+ store.mutate_run(runtime.run_dir, update)
335
+ store.append_event(
336
+ runtime.run_dir, "controller-report", status=status.strip(),
337
+ checkpoint=checkpoint, reportPath=report_record["path"],
338
+ )
339
+ return dict(report_record)
340
+
341
+
342
+ current = _Current()
343
+
344
+
345
+ def _update_task_projection(runtime: _ControllerRuntime, task_record: dict[str, Any]) -> None:
346
+ def update(value: dict[str, Any]) -> None:
347
+ value.setdefault("tasks", {})[str(task_record["id"])] = {
348
+ key: task_record.get(key)
349
+ for key in (
350
+ "id", "name", "invocation", "state", "workspaceMode", "workspace",
351
+ "startedAt", "completedAt", "attempts", "reportPath", "git", "error",
352
+ )
353
+ }
354
+ store.mutate_run(runtime.run_dir, update)
355
+
356
+
357
+ def _git(workspace: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
358
+ proc = subprocess.run(
359
+ ["git", "-C", str(workspace), *args],
360
+ text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
361
+ )
362
+ if check and proc.returncode != 0:
363
+ detail = (proc.stderr or proc.stdout).strip()
364
+ raise ControllerError(f"git {' '.join(args)} failed in {workspace}: {detail}")
365
+ return proc
366
+
367
+
368
+ def _prepare_worktree(
369
+ runtime: _ControllerRuntime, task_id: str, label: str,
370
+ ) -> tuple[Path, dict[str, Any]]:
371
+ persisted = (
372
+ store.read_json(store.run_record_path(runtime.run_dir)).get("worktrees") or {}
373
+ ).get(task_id)
374
+ if isinstance(persisted, dict):
375
+ repository = Path(str(persisted["repository"])).resolve()
376
+ base = str(persisted["base"])
377
+ workspace = Path(str(persisted["path"])).resolve()
378
+ branch = str(persisted["branch"])
379
+ else:
380
+ repo_text = _git(runtime.base_workspace, "rev-parse", "--show-toplevel").stdout.strip()
381
+ repository = Path(repo_text).resolve()
382
+ base = _git(repository, "rev-parse", "HEAD").stdout.strip()
383
+ workspace = runtime.run_dir / "worktrees" / task_id
384
+ branch = f"ccx/{_slug(runtime.name)}/{runtime.run_id[:8]}/{task_id}"
385
+ with store.file_lock(runtime.run_dir / "git.lock"):
386
+ if workspace.exists():
387
+ _git(workspace, "rev-parse", "--is-inside-work-tree")
388
+ else:
389
+ branch_exists = _git(repository, "show-ref", "--verify", "--quiet", f"refs/heads/{branch}", check=False).returncode == 0
390
+ command = ["worktree", "add"]
391
+ if not branch_exists:
392
+ command += ["-b", branch]
393
+ command += [str(workspace), branch if branch_exists else base]
394
+ _git(repository, *command)
395
+ metadata = {
396
+ "repository": str(repository), "path": str(workspace),
397
+ "branch": branch, "base": base, "head": _git(workspace, "rev-parse", "HEAD").stdout.strip(),
398
+ "status": _git(workspace, "status", "--porcelain=v1").stdout,
399
+ "diffStat": _git(workspace, "diff", "--stat", "HEAD").stdout,
400
+ "commit": None,
401
+ }
402
+ store.mutate_run(
403
+ runtime.run_dir,
404
+ lambda value: value.setdefault("worktrees", {}).__setitem__(task_id, metadata),
405
+ )
406
+ return workspace, metadata
407
+
408
+
409
+ def _finish_git(
410
+ runtime: _ControllerRuntime,
411
+ task_id: str,
412
+ label: str,
413
+ workspace: Path,
414
+ metadata: dict[str, Any],
415
+ commit: bool | str,
416
+ ) -> dict[str, Any]:
417
+ dirty = _git(workspace, "status", "--porcelain=v1").stdout
418
+ metadata = dict(metadata)
419
+ metadata["status"] = dirty
420
+ metadata["diffStat"] = _git(workspace, "diff", "--stat", "HEAD").stdout
421
+ if commit and dirty:
422
+ _git(workspace, "add", "-A")
423
+ message = commit if isinstance(commit, str) else f"ccx({runtime.name}): {label}"
424
+ _git(workspace, "commit", "-m", message)
425
+ metadata["commit"] = _git(workspace, "rev-parse", "HEAD").stdout.strip()
426
+ metadata["status"] = _git(workspace, "status", "--porcelain=v1").stdout
427
+ metadata["diffStat"] = _git(workspace, "show", "--stat", "--oneline", "HEAD").stdout
428
+ metadata["head"] = _git(workspace, "rev-parse", "HEAD").stdout.strip()
429
+ store.mutate_run(
430
+ runtime.run_dir,
431
+ lambda value: value.setdefault("worktrees", {}).__setitem__(task_id, metadata),
432
+ )
433
+ return metadata
434
+
435
+
436
+ class TaskFuture(Generic[R]):
437
+ def __init__(self, future: Future[R], runtime: _ControllerRuntime):
438
+ self._future = future
439
+ self._runtime = runtime
440
+
441
+ def result(self, timeout: float | None = None) -> R:
442
+ try:
443
+ return self._future.result(timeout=timeout)
444
+ finally:
445
+ if self._future.done():
446
+ self._runtime.observe_future(self._future)
447
+
448
+ def done(self) -> bool:
449
+ return self._future.done()
450
+
451
+
452
+ class Task(Generic[P, R]):
453
+ def __init__(
454
+ self,
455
+ function: Callable[P, R],
456
+ *,
457
+ name: str | None = None,
458
+ workspace: Literal["shared", "worktree"] = "shared",
459
+ commit: bool | str = False,
460
+ ):
461
+ if workspace not in {"shared", "worktree"}:
462
+ raise InvalidRequest("task workspace must be 'shared' or 'worktree'")
463
+ if commit and workspace != "worktree":
464
+ raise InvalidRequest("task commits require workspace='worktree'")
465
+ self.function = function
466
+ self.name = name or function.__name__
467
+ self.workspace_mode = workspace
468
+ self.commit = commit
469
+ wraps(function)(self)
470
+
471
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
472
+ runtime = _active_runtime.get()
473
+ if runtime is None:
474
+ raise ControllerLaunchRequired("tasks can only run inside a CCX controller")
475
+ key = _slug(self.name)
476
+ reserved = _reserved_task_invocation.get()
477
+ invocation = (
478
+ reserved[1] if reserved is not None and reserved[0] is self
479
+ else runtime.next_task_invocation(key)
480
+ )
481
+ task_id = f"{_slug(self.name)}--{invocation:04d}"
482
+ input_value = {"args": args, "kwargs": kwargs}
483
+ input_fingerprint = _fingerprint(input_value, f"arguments for task {self.name}")
484
+ task_dir = runtime.run_dir / "tasks" / task_id
485
+ task_dir.mkdir(parents=True, exist_ok=True)
486
+ task_path = task_dir / "task.json"
487
+ with store.file_lock(task_dir / "task.lock"):
488
+ if task_path.exists():
489
+ record = store.read_json(task_path)
490
+ if record.get("function") != self.function.__qualname__:
491
+ raise ControllerReplayMismatch(
492
+ f"task {task_id} resolved to {self.function.__qualname__!r}, but the "
493
+ f"stored invocation belongs to {record.get('function')!r}; controller "
494
+ "resumes must make the same task calls in the same order"
495
+ )
496
+ if record.get("inputSha256") != input_fingerprint:
497
+ raise ControllerReplayMismatch(
498
+ f"task {task_id} input changed while replaying controller {runtime.run_id}; "
499
+ "controllers resume from the top and must make the same task calls with "
500
+ "the same arguments in the same order"
501
+ )
502
+ if record.get("state") == "succeeded":
503
+ if record.get("outputKind") == "task-output":
504
+ report_path = record.get("reportPath")
505
+ report = Path(report_path).read_text(encoding="utf-8") if report_path else None
506
+ return TaskOutput(value=record.get("value"), report=report) # type: ignore[return-value]
507
+ return record.get("value") # type: ignore[return-value]
508
+ attempts = int(record.get("attempts") or 0) + 1
509
+ else:
510
+ attempts = 1
511
+ record = {
512
+ "schemaVersion": 1, "id": task_id, "name": self.name,
513
+ "function": self.function.__qualname__, "invocation": invocation,
514
+ "inputSha256": input_fingerprint, "workspaceMode": self.workspace_mode,
515
+ "workspace": None, "state": "pending", "attempts": 0,
516
+ "createdAt": store.now_ms(), "startedAt": None, "completedAt": None,
517
+ "outputKind": None, "value": None, "reportPath": None,
518
+ "git": None, "error": None,
519
+ }
520
+ record.update(state="running", attempts=attempts, startedAt=store.now_ms(), error=None)
521
+ store.write_json_atomic(task_path, record)
522
+ _update_task_projection(runtime, record)
523
+ store.append_event(runtime.run_dir, "task-started", taskId=task_id, attempt=attempts)
524
+
525
+ git_metadata: dict[str, Any] | None = None
526
+ workspace = runtime.base_workspace
527
+ try:
528
+ if self.workspace_mode == "worktree":
529
+ workspace, git_metadata = _prepare_worktree(runtime, task_id, self.name)
530
+ record["workspace"] = str(workspace)
531
+ task_context = _TaskContext(task_id=task_id, label=self.name, workspace=workspace)
532
+ token = _active_task.set(task_context)
533
+ try:
534
+ result = self.function(*args, **kwargs)
535
+ finally:
536
+ _active_task.reset(token)
537
+ if isinstance(result, TaskOutput):
538
+ value = result.value
539
+ report_text = result.report
540
+ output_kind = "task-output"
541
+ else:
542
+ value = result
543
+ report_text = None
544
+ output_kind = "value"
545
+ _canonical_json(value, f"return value for task {self.name}")
546
+ report_path: Path | None = None
547
+ if report_text is not None:
548
+ report_path = task_dir / "report.md"
549
+ _write_text_atomic(report_path, report_text)
550
+ if git_metadata is not None:
551
+ git_metadata = _finish_git(
552
+ runtime, task_id, self.name, workspace, git_metadata, self.commit
553
+ )
554
+ record.update(
555
+ state="succeeded", completedAt=store.now_ms(), outputKind=output_kind,
556
+ value=value, reportPath=str(report_path) if report_path else None,
557
+ git=git_metadata, error=None,
558
+ )
559
+ store.write_json_atomic(task_path, record)
560
+ _update_task_projection(runtime, record)
561
+ store.append_event(runtime.run_dir, "task-succeeded", taskId=task_id)
562
+ return result
563
+ except BaseException as error:
564
+ if git_metadata is not None and workspace.exists():
565
+ try:
566
+ git_metadata = _finish_git(
567
+ runtime, task_id, self.name, workspace, git_metadata, False
568
+ )
569
+ except Exception:
570
+ pass
571
+ record.update(
572
+ state="failed", completedAt=store.now_ms(), git=git_metadata,
573
+ error={"type": type(error).__name__, "message": str(error)},
574
+ )
575
+ store.write_json_atomic(task_path, record)
576
+ _update_task_projection(runtime, record)
577
+ store.append_event(
578
+ runtime.run_dir, "task-failed", taskId=task_id,
579
+ errorType=type(error).__name__, message=str(error),
580
+ )
581
+ raise
582
+
583
+ def submit(self, *args: P.args, **kwargs: P.kwargs) -> TaskFuture[R]:
584
+ runtime = _active_runtime.get()
585
+ if runtime is None:
586
+ raise ControllerLaunchRequired("tasks can only run inside a CCX controller")
587
+ invocation = runtime.next_task_invocation(_slug(self.name))
588
+ def invoke():
589
+ token = _reserved_task_invocation.set((self, invocation))
590
+ try:
591
+ return self(*args, **kwargs)
592
+ finally:
593
+ _reserved_task_invocation.reset(token)
594
+ return TaskFuture(runtime.submit(invoke), runtime)
595
+
596
+
597
+ @overload
598
+ def task(function: Callable[P, R]) -> Task[P, R]: ...
599
+
600
+
601
+ @overload
602
+ def task(
603
+ function: None = None, *, name: str | None = None,
604
+ workspace: Literal["shared", "worktree"] = "shared", commit: bool | str = False,
605
+ ) -> Callable[[Callable[P, R]], Task[P, R]]: ...
606
+
607
+
608
+ def task(function=None, *, name=None, workspace="shared", commit=False):
609
+ def decorate(value):
610
+ return Task(value, name=name, workspace=workspace, commit=commit)
611
+ return decorate(function) if function is not None else decorate
612
+
613
+
614
+ def _original_controller_start() -> str:
615
+ from ccx.store.liveness import proc_start_token
616
+ start = proc_start_token(os.getpid())
617
+ if start is None:
618
+ raise ControllerError("controller cannot prove its original PID/start")
619
+ return start
620
+
621
+
622
+ def _mark_lifecycle(run_dir: Path, state: str, *, error: BaseException | None = None) -> None:
623
+ start = _original_controller_start() if state == "running" else None
624
+
625
+ def update(value: dict[str, Any]) -> None:
626
+ value.setdefault("lifecycle", {}).update(
627
+ state=state, since=store.now_ms(), pid=os.getpid() if state == "running" else None,
628
+ pidStart=start,
629
+ )
630
+ attempts = value.setdefault("attempts", [])
631
+ if attempts:
632
+ attempts[-1]["pid"] = os.getpid()
633
+ if start is not None:
634
+ attempts[-1]["pidStart"] = start
635
+ if state != "running":
636
+ attempts[-1]["completedAt"] = store.now_ms()
637
+ attempts[-1]["lifecycle"] = state
638
+ if error is not None:
639
+ value["error"] = {
640
+ "type": type(error).__name__, "message": str(error),
641
+ "traceback": "".join(traceback.format_exception(error)),
642
+ }
643
+ elif state == "succeeded":
644
+ value["error"] = None
645
+ if value.get("status") is None:
646
+ value["status"] = "completed"
647
+ store.mutate_run(run_dir, update)
648
+ store.append_event(run_dir, f"run-{state}", pid=os.getpid())
649
+
650
+
651
+ def _register_controller(
652
+ launch: _LaunchContext,
653
+ *,
654
+ name: str | None,
655
+ workspace: str | os.PathLike[str] | None,
656
+ ) -> _ControllerRuntime:
657
+ if launch.registered:
658
+ raise ControllerError("a controller script may invoke only one @controller entrypoint")
659
+ # Fence direct entry invocation before publishing a bootstrap.
660
+ store.require_operation_journal_layout(launch.run_dir)
661
+ decorated_name = store.validate_controller_name(name or launch.script.stem)
662
+ effective_name = store.validate_controller_name(
663
+ launch.name_override or decorated_name
664
+ )
665
+ if launch.resume:
666
+ record = store.read_json(store.run_record_path(launch.run_dir))
667
+ recorded_declared = record.get("declaredControllerName")
668
+ if (
669
+ not isinstance(recorded_declared, str)
670
+ or recorded_declared.casefold() != decorated_name.casefold()
671
+ ):
672
+ raise ControllerReplayMismatch(
673
+ f"declared controller name changed from {recorded_declared!r} "
674
+ f"to {decorated_name!r}"
675
+ )
676
+ effective_workspace = (
677
+ Path(workspace).expanduser().resolve()
678
+ if workspace is not None
679
+ else Path(str(record.get("workspace"))).resolve()
680
+ )
681
+ if str(record.get("controller", "")).casefold() != effective_name.casefold():
682
+ raise ControllerReplayMismatch(
683
+ f"effective controller name changed from {record.get('controller')!r} "
684
+ f"to {effective_name!r}"
685
+ )
686
+ if Path(str(record.get("workspace"))).resolve() != effective_workspace:
687
+ raise ControllerReplayMismatch("decorated controller workspace changed during resume")
688
+ else:
689
+ effective_workspace = (
690
+ Path(workspace).expanduser().resolve()
691
+ if workspace is not None else Path.cwd().resolve()
692
+ )
693
+ launch.run_dir = store.publish_bootstrap(
694
+ launch.run_dir, name=effective_name, script=launch.script,
695
+ declared_name=decorated_name, workspace=effective_workspace,
696
+ )
697
+ launch.registered = True
698
+ runtime = _ControllerRuntime(launch.run_dir)
699
+ _mark_lifecycle(launch.run_dir, "running")
700
+ store.write_launch_verdict(
701
+ launch.run_id,
702
+ {"ok": True, "runId": launch.run_id, "controller": runtime.name,
703
+ "runDir": str(launch.run_dir), "pid": os.getpid()},
704
+ )
705
+ return runtime
706
+
707
+
708
+ @overload
709
+ def controller(function: Callable[P, R]) -> Callable[P, R]: ...
710
+
711
+
712
+ @overload
713
+ def controller(
714
+ function: None = None, *, name: str | None = None,
715
+ workspace: str | os.PathLike[str] | None = None,
716
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
717
+
718
+
719
+ def controller(function=None, *, name=None, workspace=None):
720
+ def decorate(value):
721
+ @wraps(value)
722
+ def wrapped(*args, **kwargs):
723
+ launch = _launch_context.get()
724
+ if launch is None:
725
+ raise ControllerLaunchRequired(
726
+ f"{launch_command_for_current_script()} must launch decorated controllers; "
727
+ "direct Python execution is intentionally refused"
728
+ )
729
+ runtime = _register_controller(launch, name=name, workspace=workspace)
730
+ token = _active_runtime.set(runtime)
731
+ try:
732
+ result = value(*args, **kwargs)
733
+ runtime.finish_futures()
734
+ if isinstance(result, TaskOutput):
735
+ _canonical_json(result.value, "controller return value")
736
+ if result.report is not None:
737
+ current.report(status="completed", report=result.report)
738
+ persisted_result = result.value
739
+ else:
740
+ _canonical_json(result, "controller return value")
741
+ persisted_result = result
742
+ store.mutate_run(
743
+ runtime.run_dir,
744
+ lambda record: record.__setitem__("result", persisted_result),
745
+ )
746
+ _mark_lifecycle(runtime.run_dir, "succeeded")
747
+ return result
748
+ except BaseException as error:
749
+ try:
750
+ runtime.shutdown_after_error()
751
+ except BaseException:
752
+ pass
753
+ try:
754
+ existing = store.read_json(store.run_record_path(runtime.run_dir))
755
+ if (existing.get("lifecycle") or {}).get("state") != "stopped":
756
+ _mark_lifecycle(runtime.run_dir, "failed", error=error)
757
+ except Exception:
758
+ pass
759
+ raise
760
+ finally:
761
+ _active_runtime.reset(token)
762
+ return wrapped
763
+ return decorate(function) if function is not None else decorate
764
+
765
+
766
+ def launch_command_for_current_script() -> str:
767
+ script = Path(sys.argv[0]).resolve() if sys.argv else Path("controller.py")
768
+ return f"ccx controller run {script}"
769
+
770
+
771
+ def _host_execute(
772
+ *, run_id: str, run_dir: Path, script: Path, resume: bool, script_args: list[str],
773
+ ) -> int:
774
+ from ccx.controllers.live_vars import sanitize_inherited_shell_bridge_env
775
+ sanitize_inherited_shell_bridge_env()
776
+
777
+ launch = _LaunchContext(
778
+ run_id=run_id,
779
+ run_dir=run_dir,
780
+ script=script,
781
+ resume=resume,
782
+ )
783
+ launch_token = _launch_context.set(launch)
784
+ old_argv = sys.argv[:]
785
+ try:
786
+ record = store.read_json(store.run_record_path(run_dir))
787
+ # This precedes lifecycle writes and, crucially, runpy.run_path: top-level
788
+ # controller source can perform arbitrary side effects before @controller.
789
+ try:
790
+ store.require_operation_journal_layout(run_dir, record)
791
+ except ControllerError as error:
792
+ # Direct _host entry is independently fenced. A layout refusal is
793
+ # not an execution failure: do not overwrite the legacy lifecycle,
794
+ # error, attempt, launch verdict or operation journals.
795
+ sys.stderr.write(f"[ccx controller] {error}\n")
796
+ return 1
797
+ requested_name = record.get("requestedControllerName")
798
+ launch.name_override = (
799
+ str(requested_name) if requested_name is not None else None
800
+ )
801
+ start = _original_controller_start()
802
+ store.mutate_run(
803
+ run_dir,
804
+ lambda value: value.setdefault("lifecycle", {}).update(
805
+ state="starting", since=store.now_ms(), pid=os.getpid(),
806
+ pidStart=start,
807
+ ),
808
+ )
809
+ sys.argv = [str(script), *script_args]
810
+ runpy.run_path(str(script), run_name="__main__")
811
+ if not launch.registered:
812
+ raise ControllerError(
813
+ "controller script did not invoke an @controller-decorated entrypoint"
814
+ )
815
+ return 0
816
+ except SystemExit as error:
817
+ code = error.code if isinstance(error.code, int) else 1
818
+ if not launch.registered:
819
+ try:
820
+ _mark_lifecycle(run_dir, "failed", error=error)
821
+ except Exception:
822
+ pass
823
+ store.write_launch_verdict(
824
+ run_id, {"ok": False, "runId": run_id, "error": f"script exited with {code} before controller startup"},
825
+ )
826
+ return code
827
+ except BaseException as error:
828
+ if launch.registered:
829
+ try:
830
+ _mark_lifecycle(launch.run_dir, "failed", error=error)
831
+ except Exception:
832
+ pass
833
+ else:
834
+ try:
835
+ _mark_lifecycle(run_dir, "failed", error=error)
836
+ except Exception:
837
+ pass
838
+ store.write_launch_verdict(
839
+ run_id,
840
+ {"ok": False, "runId": run_id, "error": f"{type(error).__name__}: {error}"},
841
+ )
842
+ traceback.print_exc()
843
+ return 1
844
+ finally:
845
+ sys.argv = old_argv
846
+ _launch_context.reset(launch_token)
847
+
848
+
849
+ def _operation_metadata(method: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> dict[str, Any]:
850
+ metadata: dict[str, Any] = {"method": method}
851
+ if method == "create":
852
+ metadata.update(
853
+ engine=kwargs.get("engine"), name=kwargs.get("name"), cwd=kwargs.get("cwd"),
854
+ workerId=kwargs.get("worker_id"),
855
+ requestedEngine=kwargs.get("engine"), requestedModel=kwargs.get("model"),
856
+ requestedEffort=kwargs.get("effort"),
857
+ promptBytes=len(str(kwargs.get("prompt") or "").encode("utf-8")),
858
+ )
859
+ elif method == "fork":
860
+ source = args[0] if args else kwargs.get("worker")
861
+ metadata.update(
862
+ source=_worker_handle(source), name=kwargs.get("name"),
863
+ workerId=kwargs.get("worker_id"),
864
+ requestedModel=kwargs.get("model"), requestedEffort=kwargs.get("effort"),
865
+ promptBytes=len(str(kwargs.get("prompt") or "").encode("utf-8")),
866
+ )
867
+ elif method == "send":
868
+ worker = args[0] if args else kwargs.get("worker")
869
+ text = args[1] if len(args) > 1 else kwargs.get("text")
870
+ metadata.update(worker=_worker_handle(worker), deliveryId=kwargs.get("delivery_id"),
871
+ textBytes=len(str(text or "").encode("utf-8")))
872
+ else:
873
+ worker = args[0] if args else kwargs.get("worker")
874
+ if worker is not None:
875
+ metadata["worker"] = _worker_handle(worker)
876
+ if method == "steer":
877
+ metadata["deliveryId"] = kwargs.get("delivery_id")
878
+ if method == "compact":
879
+ metadata["operationId"] = kwargs.get("operation_id")
880
+ variables = kwargs.get("variables")
881
+ if isinstance(variables, Mapping):
882
+ metadata["variableMode"] = "live"
883
+ metadata["liveVariables"] = sorted(str(name) for name in variables)
884
+ variable_source = kwargs.get("variables_from")
885
+ if variable_source is not None:
886
+ metadata["variableMode"] = "durable-shared"
887
+ metadata["variableSource"] = _worker_handle(variable_source)
888
+ return metadata
889
+
890
+
891
+ def _stable_call_value(value: Any) -> Any:
892
+ if value is None or isinstance(value, (bool, int, float, str)):
893
+ return value
894
+ if isinstance(value, Path):
895
+ return {"path": str(value)}
896
+ if isinstance(value, (list, tuple)):
897
+ return [_stable_call_value(item) for item in value]
898
+ if isinstance(value, dict):
899
+ return {str(key): _stable_call_value(item) for key, item in sorted(value.items(), key=lambda item: str(item[0]))}
900
+ if is_dataclass(value):
901
+ return _stable_call_value(asdict(value))
902
+ return {"type": f"{type(value).__module__}.{type(value).__qualname__}", "repr": repr(value)}
903
+
904
+
905
+ def _worker_handle(value: Any) -> str | None:
906
+ if isinstance(value, str):
907
+ return value
908
+ if isinstance(value, dict) and isinstance(value.get("worker"), str):
909
+ return value["worker"]
910
+ return None
911
+
912
+
913
+ def _verified_created_owner(client: Any, result: Any, kwargs: dict[str, Any],
914
+ operation_id: str, *, method: str = "create"):
915
+ """Require the journaled full UUID and its ACTIVE original owner."""
916
+ worker_id = kwargs.get("worker_id")
917
+ engine = kwargs.get("engine")
918
+ if method == "fork":
919
+ try:
920
+ from ccx.store.identity import canonical_worker_id
921
+ canonical_worker_id(worker_id)
922
+ client._uuid_store.revalidate()
923
+ owner = client._uuid_store.resolve(worker_id)
924
+ engine = owner.engine
925
+ except Exception as error:
926
+ raise ControllerOperationIndeterminate(
927
+ f"fork child has no confirmed original ACTIVE owner: {operation_id}",
928
+ operation_id=operation_id,
929
+ ) from error
930
+ handle = f"{engine}:{worker_id}"
931
+ if (not isinstance(result, dict) or result.get("worker") != handle
932
+ or result.get("worker_id") != worker_id
933
+ or result.get("engine") != engine):
934
+ raise ControllerOperationIndeterminate(
935
+ f"create result disagrees with its original full-UUID intent: {operation_id}",
936
+ operation_id=operation_id,
937
+ )
938
+ if method != "fork":
939
+ try:
940
+ from ccx.store.identity import canonical_worker_id
941
+ canonical_worker_id(worker_id)
942
+ client._uuid_store.revalidate()
943
+ owner = client._uuid_store.resolve(handle)
944
+ except Exception as error:
945
+ raise ControllerOperationIndeterminate(
946
+ f"created worker has no confirmed original ACTIVE owner: {operation_id}",
947
+ operation_id=operation_id,
948
+ ) from error
949
+ if owner.worker != handle or owner.worker_id != worker_id or owner.engine != engine:
950
+ raise ControllerOperationIndeterminate(
951
+ f"created owner differs from committed journal identity: {operation_id}",
952
+ operation_id=operation_id,
953
+ )
954
+ return owner
955
+
956
+
957
+ def _registered_cwd(client: Any, handle: str, cwd: Any) -> str | None:
958
+ """Use the exact created owner's record, never the old short-keyed store."""
959
+ if cwd:
960
+ return str(Path(str(cwd)).resolve())
961
+ record = client._uuid_store.record(handle)
962
+ return record.get("cwd")
963
+
964
+
965
+ def _register_worker(
966
+ runtime: _ControllerRuntime,
967
+ client: Any,
968
+ owner: Any,
969
+ result: dict[str, Any],
970
+ kwargs: dict[str, Any],
971
+ task_context: _TaskContext | None,
972
+ operation_id: str,
973
+ ) -> None:
974
+ handle = owner.worker
975
+ name = kwargs.get("name")
976
+ worker_record = {
977
+ "worker": handle,
978
+ "engine": owner.engine,
979
+ "short": owner.short,
980
+ "worker_id": owner.worker_id,
981
+ "name": name,
982
+ "cwd": _registered_cwd(client, handle, kwargs.get("cwd") or result.get("cwd")),
983
+ "taskId": task_context.task_id if task_context is not None else None,
984
+ "createdAt": store.now_ms(),
985
+ }
986
+
987
+ def update(value: dict[str, Any]) -> None:
988
+ workers = value.setdefault("workers", [])
989
+ if not any(item.get("worker") == handle for item in workers):
990
+ workers.append(worker_record)
991
+ store.mutate_run(runtime.run_dir, update)
992
+ store.append_event(runtime.run_dir, "worker-recorded", worker=handle, name=name)
993
+
994
+
995
+ def _budget_stopped_from_record(record: dict[str, Any]) -> BudgetStopped:
996
+ error = record.get("error") or {}
997
+ return BudgetStopped(
998
+ str(error.get("message") or "budget stopped"),
999
+ short=error.get("short"), reason=error.get("reason"),
1000
+ phase=error.get("phase"),
1001
+ )
1002
+
1003
+
1004
+ # Journal `error.type` values that mean "a WorkerApiError of some subclass".
1005
+ # Listed by name because a journal written by an older ccx must still replay;
1006
+ # the exact subclass is then rebuilt from the stored flags, not from the name.
1007
+ _API_ERROR_TYPES = frozenset({
1008
+ "WorkerApiError", "WorkerRateLimited", "WorkerApiTransient",
1009
+ "WorkerApiPermanent", "WorkerOverloaded",
1010
+ })
1011
+
1012
+
1013
+ def _api_error_fields(error: WorkerApiError | None) -> dict[str, Any] | None:
1014
+ """Flatten a typed API error for the journal, or ``None`` when absent."""
1015
+ if error is None:
1016
+ return None
1017
+ return {
1018
+ "type": type(error).__name__, "message": str(error),
1019
+ "short": error.short, "kind": error.kind, "status": error.status,
1020
+ "transient": error.transient,
1021
+ "rateLimited": isinstance(error, WorkerRateLimited),
1022
+ "requestId": error.request_id, "detail": error.message,
1023
+ **({"resetText": error.reset_text}
1024
+ if isinstance(error, WorkerRateLimited) else {}),
1025
+ }
1026
+
1027
+
1028
+ def _api_error_from_fields(fields: Any) -> WorkerApiError | None:
1029
+ """Rebuild the exception from its durable classification flags."""
1030
+ if not isinstance(fields, dict):
1031
+ return None
1032
+ rate_limited = bool(fields.get("rateLimited"))
1033
+ return build_worker_api_error(
1034
+ str(fields.get("message") or "Claude API error"),
1035
+ short=fields.get("short"), kind=fields.get("kind"),
1036
+ status=fields.get("status"),
1037
+ transient=bool(fields.get("transient")),
1038
+ rate_limited=rate_limited,
1039
+ request_id=fields.get("requestId"), detail=fields.get("detail"),
1040
+ reset_text=fields.get("resetText"),
1041
+ )
1042
+
1043
+
1044
+ def _stopped_error_from_record(record: dict[str, Any]) -> CcxError:
1045
+ """Rebuild the error a `stopped` operation died on, for replay.
1046
+
1047
+ Resume must re-raise the same condition it stopped on, typed as it was
1048
+ originally: a controller that branches on ``WorkerRateLimited`` has to see
1049
+ it again on the second run, not a flattened stand-in.
1050
+ """
1051
+ error = record.get("error") or {}
1052
+ kind = error.get("type")
1053
+ message = str(error.get("message") or "Claude API error")
1054
+ if kind == "CompactFailed":
1055
+ return CompactFailed(
1056
+ message, short=error.get("short"), state=error.get("state"),
1057
+ operation_id=error.get("operationId"),
1058
+ api_error=_api_error_from_fields(error.get("apiError")),
1059
+ )
1060
+ if kind == "CreatePartialFailure":
1061
+ result = record.get("result") or {}
1062
+ cause = error.get("causeType")
1063
+ typed_cause = (
1064
+ _budget_stopped_from_record(record) if cause == "BudgetStopped" else
1065
+ _api_error_from_fields(error.get("apiError"))
1066
+ if cause in _API_ERROR_TYPES else None
1067
+ )
1068
+ return CreatePartialFailure(
1069
+ message, info={
1070
+ "short": result.get("short") or error.get("short"),
1071
+ "worker_id": result.get("worker_id") or error.get("workerId"),
1072
+ "worker": result.get("worker") or error.get("worker"),
1073
+ "engine": result.get("engine") or error.get("engine") or "claude",
1074
+ },
1075
+ phase=error.get("phase") or "initial-delivery",
1076
+ delivery_id=error.get("deliveryId"),
1077
+ delivery_status=error.get("deliveryStatus"),
1078
+ notification_intent=bool(error.get("notificationIntent")),
1079
+ cause=typed_cause,
1080
+ )
1081
+ if kind not in _API_ERROR_TYPES:
1082
+ return _budget_stopped_from_record(record)
1083
+ return _api_error_from_fields(error) or _budget_stopped_from_record(record)
1084
+
1085
+
1086
+ def _stopped_create_result(client: Any, error: CcxError,
1087
+ kwargs: dict[str, Any], operation_id: str, *,
1088
+ method: str = "create") -> dict[str, Any] | None:
1089
+ """Record only a positively ACTIVE original worker, never a short alias.
1090
+
1091
+ A partial create may preserve an unconfirmed claimed UUID. Its typed error
1092
+ is durable, but that claim is NOT evidence of native activation or success.
1093
+ """
1094
+ if not isinstance(error, CreatePartialFailure):
1095
+ return None
1096
+ if (error.info.get("activation_state") != "confirmed"
1097
+ or error.phase not in {"initial-delivery", "wait-report", "notification-registration"}):
1098
+ return None
1099
+ result = {"worker": error.worker, "worker_id": error.worker_id,
1100
+ "engine": error.engine, "short": error.worker_id[:8],
1101
+ "cwd": error.info.get("cwd"), "report": None}
1102
+ owner = _verified_created_owner(client, result, kwargs, operation_id, method=method)
1103
+ record = client._uuid_store.record(owner.worker)
1104
+ lifecycle = record.get("lifecycle") or {}
1105
+ result["state"] = lifecycle.get("state")
1106
+ result["native_state"] = (record.get("native") or {}).get("nativeState")
1107
+ return result
1108
+
1109
+
1110
+ def _worker_call_fingerprint_value(
1111
+ args: tuple[Any, ...], kwargs: dict[str, Any],
1112
+ ) -> dict[str, Any]:
1113
+ stable_kwargs = dict(kwargs)
1114
+ variables = stable_kwargs.get("variables")
1115
+ if isinstance(variables, Mapping):
1116
+ stable_kwargs["variables"] = {
1117
+ "mode": "live",
1118
+ "names": sorted(str(name) for name in variables),
1119
+ }
1120
+ variable_source = stable_kwargs.get("variables_from")
1121
+ if variable_source is not None:
1122
+ stable_kwargs["variables_from"] = _worker_handle(variable_source)
1123
+ return {
1124
+ "args": _stable_call_value(args),
1125
+ "kwargs": _stable_call_value(stable_kwargs),
1126
+ }
1127
+
1128
+
1129
+ def _raise_live_state_lost(record: dict[str, Any], operation_id: str) -> None:
1130
+ metadata = record.get("metadata") or {}
1131
+ names = metadata.get("liveVariables")
1132
+ if not isinstance(names, list) or not names:
1133
+ return
1134
+ rendered = ", ".join(str(name) for name in names)
1135
+ raise ControllerLiveStateLost(
1136
+ "worker operation bound live Python variables "
1137
+ f"({rendered}) in a prior controller attempt; CCX will not redeliver "
1138
+ "the worker turn or reconstruct that object graph. Complete the branch "
1139
+ "that consumes live state inside the same successful task/attempt and "
1140
+ "return the JSON-compatible result needed for replay.",
1141
+ operation_id=operation_id,
1142
+ )
1143
+
1144
+
1145
+ def _first_live_binding_committed(client: Any, record: dict, worker_id: str) -> bool:
1146
+ names = (record.get("metadata") or {}).get("liveVariables")
1147
+ if not isinstance(names, list) or not names:
1148
+ return False
1149
+ from ccx.store.identity import UnknownWorker
1150
+ from ccx.store.root import _read_json
1151
+ ctx = client._uuid_store
1152
+ try:
1153
+ owner = ctx.resolve(worker_id)
1154
+ except UnknownWorker:
1155
+ return False
1156
+ try:
1157
+ with ctx.lease():
1158
+ with ctx._gen("variables", "bindings", owner.engine) as directory:
1159
+ prior = _read_json(directory, owner.worker_id + ".json")
1160
+ except FileNotFoundError:
1161
+ return False
1162
+ return (isinstance(prior, dict) and prior.get("schemaVersion") == 1
1163
+ and prior.get("names") == sorted(names))
1164
+
1165
+
1166
+ def observe_worker_call(
1167
+ client: Any,
1168
+ method: str,
1169
+ function: Callable[..., Any],
1170
+ args: tuple[Any, ...],
1171
+ kwargs: dict[str, Any],
1172
+ ) -> Any:
1173
+ runtime = _active_runtime.get()
1174
+ if runtime is None or _suspend_worker_hook.get():
1175
+ return function(client, *args, **kwargs)
1176
+ if method not in _MUTATING_WORKER_METHODS:
1177
+ return function(client, *args, **kwargs)
1178
+ kwargs = dict(kwargs)
1179
+ task_context = _active_task.get()
1180
+ scope, number, directory = runtime.next_operation(task_context)
1181
+ operation_id = f"{scope}:{number:04d}:{method}"
1182
+ path = directory / f"{number:04d}-{method}.json"
1183
+ existing_record = store.read_json(path) if path.exists() else None
1184
+ # A positional journal, not a display name, owns the UUID. Use it on
1185
+ # every lost-return retry even when the caller did not specify an ID.
1186
+ supplied_create_id = kwargs.get("worker_id") if method in _CREATING_WORKER_METHODS else None
1187
+ committed_create_id = (
1188
+ (existing_record.get("metadata") or {}).get("workerId")
1189
+ if method in _CREATING_WORKER_METHODS and existing_record is not None else None
1190
+ )
1191
+ if method not in _CREATING_WORKER_METHODS:
1192
+ # Reserve the ordinal before resolving a changing worker target: a
1193
+ # caught lookup refusal must not displace a later accepted operation.
1194
+ worker = args[0] if args else kwargs.get("worker")
1195
+ owner = client._uuid_owner(worker)
1196
+ if args:
1197
+ args = (owner.worker, *args[1:])
1198
+ else:
1199
+ kwargs["worker"] = owner.worker
1200
+ if existing_record is not None and (
1201
+ (existing_record.get("metadata") or {}).get("worker") != owner.worker
1202
+ ):
1203
+ raise ControllerOperationIndeterminate(
1204
+ f"{method} intent has no proven original worker: {operation_id}",
1205
+ operation_id=operation_id,
1206
+ )
1207
+ if method in {"send", "steer"}:
1208
+ caps = kwargs.get("caps")
1209
+ if caps is not None:
1210
+ from ccx.workers import _check_caps
1211
+ _check_caps(caps, engine=owner.engine)
1212
+ supplied_delivery_id = kwargs.get("delivery_id")
1213
+ stored_delivery_id = ((existing_record.get("metadata") or {}).get("deliveryId")
1214
+ if existing_record is not None else None)
1215
+ if stored_delivery_id is not None and supplied_delivery_id is not None and supplied_delivery_id != stored_delivery_id:
1216
+ raise ControllerReplayMismatch(f"original {method} delivery token changed during replay: {operation_id}")
1217
+ kwargs["delivery_id"] = stored_delivery_id or supplied_delivery_id or str(uuid.uuid4())
1218
+ from ccx.claude import delivery as claude_delivery
1219
+ from ccx.codex import delivery as codex_delivery
1220
+ validator = claude_delivery.valid_delivery_id if owner.engine == "claude" else codex_delivery.valid_delivery_id
1221
+ if not validator(kwargs["delivery_id"]):
1222
+ raise InvalidRequest(f"controller {method} delivery_id must be a bounded native token")
1223
+ if existing_record is not None and stored_delivery_id != kwargs["delivery_id"]:
1224
+ raise ControllerOperationIndeterminate(
1225
+ f"{method} intent has no proven original delivery token: {operation_id}",
1226
+ operation_id=operation_id,
1227
+ )
1228
+ if method == "compact" and owner.engine == "claude" and not kwargs.get("operation_id"):
1229
+ persisted_operation_id = (
1230
+ (existing_record.get("metadata") or {}).get("operationId")
1231
+ if existing_record is not None else None
1232
+ )
1233
+ kwargs["operation_id"] = persisted_operation_id or str(uuid.uuid4())
1234
+
1235
+ def require_original_result(value: Any) -> None:
1236
+ # The native methods own receipt proof; the controller must not
1237
+ # publish/replay a result for a different worker or operation token.
1238
+ if (not isinstance(value, dict) or value.get("worker") != owner.worker
1239
+ or value.get("worker_id", owner.worker_id) != owner.worker_id
1240
+ or value.get("engine", owner.engine) != owner.engine
1241
+ or (method in {"send", "steer"}
1242
+ and value.get("delivery_id", kwargs["delivery_id"]) != kwargs["delivery_id"])
1243
+ or (method == "compact" and owner.engine == "claude"
1244
+ and value.get("operation_id") != kwargs["operation_id"])):
1245
+ raise ControllerOperationIndeterminate(
1246
+ f"{method} result disagrees with its original worker/token: {operation_id}",
1247
+ operation_id=operation_id,
1248
+ )
1249
+ call_fingerprint = _fingerprint(
1250
+ _worker_call_fingerprint_value(args, kwargs),
1251
+ f"WorkerClient.{method} call",
1252
+ )
1253
+ if method in _CREATING_WORKER_METHODS:
1254
+ from ccx.ops.creation import new_worker_id
1255
+ kwargs["worker_id"] = committed_create_id or supplied_create_id or new_worker_id()
1256
+ if existing_record is not None:
1257
+ record = existing_record
1258
+ if record.get("callSha256") != call_fingerprint:
1259
+ raise ControllerReplayMismatch(f"worker operation changed during replay: {operation_id}")
1260
+ state = record.get("state")
1261
+ if (state in {"intent", "indeterminate", "rejected"}
1262
+ and method in _CREATING_WORKER_METHODS
1263
+ and committed_create_id
1264
+ and _first_live_binding_committed(client, record, committed_create_id)):
1265
+ _raise_live_state_lost(record, operation_id)
1266
+ if state == "accepted":
1267
+ _raise_live_state_lost(record, operation_id)
1268
+ result = record.get("result")
1269
+ if method in _CREATING_WORKER_METHODS:
1270
+ owner = _verified_created_owner(
1271
+ client, result, kwargs, operation_id, method=method,
1272
+ )
1273
+ _register_worker(runtime, client, owner, result, kwargs, task_context, operation_id)
1274
+ else:
1275
+ require_original_result(result)
1276
+ return result
1277
+ if state == "stopped":
1278
+ _raise_live_state_lost(record, operation_id)
1279
+ result = record.get("result")
1280
+ if method in _CREATING_WORKER_METHODS and isinstance(result, dict):
1281
+ owner = _verified_created_owner(
1282
+ client, result, kwargs, operation_id, method=method,
1283
+ )
1284
+ _register_worker(runtime, client, owner, result, kwargs, task_context, operation_id)
1285
+ raise _stopped_error_from_record(record)
1286
+ if state == "rejected":
1287
+ pass
1288
+ elif state not in {"intent", "indeterminate"}:
1289
+ raise ControllerOperationIndeterminate(
1290
+ f"worker operation has unknown journal state {state!r}: {operation_id}",
1291
+ operation_id=operation_id,
1292
+ )
1293
+ elif method in _CREATING_WORKER_METHODS:
1294
+ if not committed_create_id:
1295
+ raise ControllerOperationIndeterminate(
1296
+ f"old worker create intent has no replayable UUID: {operation_id}",
1297
+ operation_id=operation_id,
1298
+ )
1299
+ # Fall through: same worker UUID and create:initial receipt reconcile
1300
+ # both activation and first delivery without matching any names.
1301
+ if state in {"intent", "indeterminate"} and method == "compact" and kwargs.get("operation_id"):
1302
+ token = _suspend_worker_hook.set(True)
1303
+ try:
1304
+ status = client.compact_status(
1305
+ args[0] if args else kwargs.get("worker"), kwargs["operation_id"]
1306
+ )
1307
+ finally:
1308
+ _suspend_worker_hook.reset(token)
1309
+ if status.get("operation_status") == "succeeded":
1310
+ require_original_result(status)
1311
+ record.update(state="accepted", acceptedAt=store.now_ms(), result=status, reconciled=True)
1312
+ store.write_json_atomic(path, record)
1313
+ return status
1314
+ if state != "rejected" and method not in _CREATING_WORKER_METHODS:
1315
+ raise ControllerOperationIndeterminate(
1316
+ f"worker operation may have landed and will not be retried: {operation_id}",
1317
+ operation_id=operation_id,
1318
+ )
1319
+
1320
+ record = {
1321
+ "schemaVersion": 1, "id": operation_id, "method": method,
1322
+ "state": "intent", "createdAt": store.now_ms(),
1323
+ "callSha256": call_fingerprint, "metadata": _operation_metadata(method, args, kwargs),
1324
+ "result": None, "error": None,
1325
+ }
1326
+ if existing_record is None or existing_record.get("state") == "rejected":
1327
+ # A safe rejection permits another dispatch, not reuse of its durable
1328
+ # rejection. Fence the retry before it can land and lose its reply.
1329
+ if existing_record is not None:
1330
+ record["createdAt"] = existing_record["createdAt"]
1331
+ store.write_json_atomic(path, record)
1332
+ store.append_event(runtime.run_dir, "worker-operation-intent", operationId=operation_id, method=method)
1333
+ else:
1334
+ record = existing_record
1335
+ try:
1336
+ # One public mutation owns one replay slot. Nested calls should not
1337
+ # create another slot when restoring an accepted outer result.
1338
+ token = _suspend_worker_hook.set(True)
1339
+ try:
1340
+ result = function(client, *args, **kwargs)
1341
+ finally:
1342
+ _suspend_worker_hook.reset(token)
1343
+ # Never persist accepted before proving its original journaled worker.
1344
+ if method in _CREATING_WORKER_METHODS:
1345
+ created_owner = _verified_created_owner(
1346
+ client, result, kwargs, operation_id, method=method,
1347
+ )
1348
+ else:
1349
+ require_original_result(result)
1350
+ except BaseException as error:
1351
+ budget_stopped = isinstance(error, BudgetStopped)
1352
+ partial = isinstance(error, CreatePartialFailure)
1353
+ partial_cause = error.cause if partial else None
1354
+ # An API error journals like a budget stop rather than as a generic
1355
+ # failure. Generic failures become `indeterminate`, which on resume
1356
+ # raises ControllerOperationIndeterminate and loses the real reason —
1357
+ # exactly the wrong answer for a run halted by capacity or a rate limit.
1358
+ # `stopped` keeps the typed error and re-raises it on resume instead.
1359
+ #
1360
+ # Replay stays off the table on purpose: a turn killed by an API error
1361
+ # had already been delivered, so re-running the call would double-send
1362
+ # the prompt. A controller that wants to wait out a rate limit catches
1363
+ # the exception INSIDE its task, where it knows that is safe.
1364
+ stopped = budget_stopped or partial or isinstance(
1365
+ error, (WorkerApiError, CompactFailed),
1366
+ )
1367
+ # The worker may exist even though the call raised — recover it for an
1368
+ # API error exactly as for a budget stop, or the run leaks a live
1369
+ # worker while freeing its name.
1370
+ expected_engine = kwargs.get("engine") if method == "create" else (
1371
+ error.engine if partial else None
1372
+ )
1373
+ if partial and (error.worker_id != kwargs.get("worker_id")
1374
+ or error.engine != expected_engine
1375
+ or error.worker != f"{expected_engine}:{kwargs.get('worker_id')}"):
1376
+ record.update(state="indeterminate", completedAt=store.now_ms(),
1377
+ error={"type": type(error).__name__, "message": str(error),
1378
+ "workerId": error.worker_id, "worker": error.worker})
1379
+ store.write_json_atomic(path, record)
1380
+ raise ControllerOperationIndeterminate(
1381
+ f"partial create returned foreign original worker identity: {operation_id}",
1382
+ operation_id=operation_id,
1383
+ ) from error
1384
+ stopped_result = (
1385
+ _stopped_create_result(client, error, kwargs, operation_id, method=method)
1386
+ if stopped and method in _CREATING_WORKER_METHODS else None
1387
+ )
1388
+ record.update(
1389
+ state=("stopped" if stopped else
1390
+ "rejected" if isinstance(error, _SAFE_REJECTIONS) else "indeterminate"),
1391
+ completedAt=store.now_ms(),
1392
+ error={
1393
+ "type": type(error).__name__, "message": str(error),
1394
+ **({
1395
+ "short": error.short, "reason": error.reason,
1396
+ "phase": error.phase,
1397
+ } if budget_stopped else {}),
1398
+ **(_api_error_fields(error)
1399
+ if isinstance(error, WorkerApiError) else {}),
1400
+ **({
1401
+ "short": error.short, "state": error.state,
1402
+ "operationId": error.operation_id,
1403
+ "apiError": _api_error_fields(error.api_error),
1404
+ } if isinstance(error, CompactFailed) else {}),
1405
+ **({
1406
+ "workerId": error.worker_id, "worker": error.worker,
1407
+ "engine": error.engine, "phase": error.phase,
1408
+ "deliveryId": error.delivery_id,
1409
+ "deliveryStatus": error.delivery_status,
1410
+ "notificationIntent": error.notification_intent,
1411
+ "causeType": type(partial_cause).__name__ if partial_cause else None,
1412
+ "apiError": _api_error_fields(partial_cause)
1413
+ if isinstance(partial_cause, WorkerApiError) else None,
1414
+ "reason": partial_cause.reason if isinstance(partial_cause, BudgetStopped) else None,
1415
+ "short": error.short,
1416
+ } if partial else {}),
1417
+ },
1418
+ result=stopped_result,
1419
+ )
1420
+ store.write_json_atomic(path, record)
1421
+ if method in _CREATING_WORKER_METHODS and stopped and isinstance(stopped_result, dict):
1422
+ stopped_owner = _verified_created_owner(
1423
+ client, stopped_result, kwargs, operation_id, method=method,
1424
+ )
1425
+ _register_worker(runtime, client, stopped_owner, stopped_result, kwargs, task_context, operation_id)
1426
+ store.append_event(
1427
+ runtime.run_dir, "worker-operation-error", operationId=operation_id,
1428
+ state=record["state"], errorType=type(error).__name__,
1429
+ )
1430
+ raise
1431
+ record.update(state="accepted", acceptedAt=store.now_ms(), result=result)
1432
+ store.write_json_atomic(path, record)
1433
+ if method in _CREATING_WORKER_METHODS:
1434
+ _register_worker(runtime, client, created_owner, result, kwargs, task_context, operation_id)
1435
+ store.append_event(runtime.run_dir, "worker-operation-accepted", operationId=operation_id, method=method)
1436
+ return result
1437
+
1438
+
1439
+ __all__ = [
1440
+ "ControllerInput",
1441
+ "Task",
1442
+ "TaskFuture",
1443
+ "TaskOutput",
1444
+ "controller",
1445
+ "current",
1446
+ "observe_worker_call",
1447
+ "task",
1448
+ "task_output",
1449
+ ]