ccx-native 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +241 -2
- package/bin/ccx +56 -0
- package/ccx/__init__.py +163 -0
- package/ccx/__main__.py +28 -0
- package/ccx/claude/__init__.py +0 -0
- package/ccx/claude/analytics.py +466 -0
- package/ccx/claude/attach.py +762 -0
- package/ccx/claude/context.py +465 -0
- package/ccx/claude/delivery.py +806 -0
- package/ccx/claude/external.py +253 -0
- package/ccx/claude/host/__init__.py +0 -0
- package/ccx/claude/host/events.py +297 -0
- package/ccx/claude/host/hooks.py +438 -0
- package/ccx/claude/host/pty.py +250 -0
- package/ccx/claude/host/session.py +5184 -0
- package/ccx/claude/host/subagents.py +169 -0
- package/ccx/claude/host/successor.py +267 -0
- package/ccx/claude/managed_turns.py +240 -0
- package/ccx/claude/oauth.py +176 -0
- package/ccx/claude/settings.py +339 -0
- package/ccx/claude/spawn.py +557 -0
- package/ccx/claude/tasks.py +72 -0
- package/ccx/claude/telemetry.py +282 -0
- package/ccx/claude/transcript.py +2281 -0
- package/ccx/claude/usage.py +837 -0
- package/ccx/claude/usage_control.py +152 -0
- package/ccx/cli/__init__.py +0 -0
- package/ccx/cli/codex.py +1009 -0
- package/ccx/cli/commands/__init__.py +0 -0
- package/ccx/cli/commands/_common.py +407 -0
- package/ccx/cli/commands/control.py +161 -0
- package/ccx/cli/commands/create.py +669 -0
- package/ccx/cli/commands/read.py +1020 -0
- package/ccx/cli/commands/turns.py +1081 -0
- package/ccx/cli/controller.py +757 -0
- package/ccx/cli/main.py +301 -0
- package/ccx/cli/parser.py +1324 -0
- package/ccx/cli/skills.py +215 -0
- package/ccx/cli/statusline.py +160 -0
- package/ccx/client.py +1900 -0
- package/ccx/codex/__init__.py +0 -0
- package/ccx/codex/attach_tty.py +267 -0
- package/ccx/codex/client.py +732 -0
- package/ccx/codex/completion.py +121 -0
- package/ccx/codex/control.py +288 -0
- package/ccx/codex/daemon/__init__.py +0 -0
- package/ccx/codex/daemon/__main__.py +4 -0
- package/ccx/codex/daemon/daemon.py +1556 -0
- package/ccx/codex/daemon/models.py +140 -0
- package/ccx/codex/daemon/native.py +1590 -0
- package/ccx/codex/daemon/release.py +367 -0
- package/ccx/codex/daemon/worker.py +2396 -0
- package/ccx/codex/delivery.py +758 -0
- package/ccx/codex/delivery_recovery.py +232 -0
- package/ccx/codex/evidence.py +827 -0
- package/ccx/codex/history.py +158 -0
- package/ccx/codex/notify_history.py +140 -0
- package/ccx/codex/ops.py +666 -0
- package/ccx/codex/paths.py +70 -0
- package/ccx/codex/protocol.py +805 -0
- package/ccx/codex/provenance.py +105 -0
- package/ccx/codex/read.py +29 -0
- package/ccx/codex/records.py +51 -0
- package/ccx/codex/reports.py +177 -0
- package/ccx/codex/runtime.py +1412 -0
- package/ccx/codex/settle.py +363 -0
- package/ccx/codex/telemetry.py +792 -0
- package/ccx/codex/types.py +659 -0
- package/ccx/codex/worker_environment.py +25 -0
- package/ccx/constants.py +288 -0
- package/ccx/controllers/__init__.py +0 -0
- package/ccx/controllers/controller.py +1449 -0
- package/ccx/controllers/live_vars.py +1706 -0
- package/ccx/controllers/live_vars_runner.py +216 -0
- package/ccx/controllers/notify.py +213 -0
- package/ccx/controllers/orchestrator.py +193 -0
- package/ccx/controllers/policy.py +57 -0
- package/ccx/controllers/store.py +681 -0
- package/ccx/errors.py +634 -0
- package/ccx/notify/__init__.py +0 -0
- package/ccx/notify/__main__.py +5 -0
- package/ccx/notify/client.py +116 -0
- package/ccx/notify/daemon.py +480 -0
- package/ccx/notify/deliver.py +304 -0
- package/ccx/notify/paths.py +51 -0
- package/ccx/notify/registry.py +649 -0
- package/ccx/notify/select.py +750 -0
- package/ccx/notify/settle.py +684 -0
- package/ccx/ops/__init__.py +0 -0
- package/ccx/ops/background.py +115 -0
- package/ccx/ops/cancel.py +289 -0
- package/ccx/ops/codex_import.py +96 -0
- package/ccx/ops/common.py +397 -0
- package/ccx/ops/compact.py +337 -0
- package/ccx/ops/compact_ops.py +550 -0
- package/ccx/ops/context_events.py +48 -0
- package/ccx/ops/conversation.py +1540 -0
- package/ccx/ops/conversation_paging.py +1338 -0
- package/ccx/ops/create.py +56 -0
- package/ccx/ops/creation.py +952 -0
- package/ccx/ops/creation_steps.py +579 -0
- package/ccx/ops/event_feed.py +710 -0
- package/ccx/ops/forking.py +711 -0
- package/ccx/ops/input.py +556 -0
- package/ccx/ops/maintenance.py +295 -0
- package/ccx/ops/report.py +770 -0
- package/ccx/ops/semantics.py +490 -0
- package/ccx/ops/send.py +961 -0
- package/ccx/ops/steer_ops.py +867 -0
- package/ccx/ops/terminate.py +383 -0
- package/ccx/ops/turn_reports.py +306 -0
- package/ccx/ops/wait.py +762 -0
- package/ccx/ops/worker_events.py +636 -0
- package/ccx/rpc/__init__.py +0 -0
- package/ccx/rpc/local_rpc.py +1602 -0
- package/ccx/rpc/rpc.py +559 -0
- package/ccx/rpc/serve.py +374 -0
- package/ccx/rpc/transport.py +409 -0
- package/ccx/rpc/view.py +125 -0
- package/ccx/store/__init__.py +0 -0
- package/ccx/store/discovery.py +170 -0
- package/ccx/store/identity.py +105 -0
- package/ccx/store/journal_io.py +79 -0
- package/ccx/store/liveness.py +194 -0
- package/ccx/store/paths.py +254 -0
- package/ccx/store/record_cache.py +40 -0
- package/ccx/store/records.py +309 -0
- package/ccx/store/root.py +804 -0
- package/ccx/store/runtime_presence.py +137 -0
- package/ccx/store/store.py +2332 -0
- package/ccx/store/worker_state.py +86 -0
- package/ccx/system/__init__.py +0 -0
- package/ccx/system/bounded.py +296 -0
- package/ccx/system/cwd_paths.py +25 -0
- package/ccx/system/engine_binary.py +59 -0
- package/ccx/system/executable.py +50 -0
- package/ccx/system/oslayer.py +667 -0
- package/ccx/system/pipes.py +46 -0
- package/ccx/system/sanitize.py +43 -0
- package/ccx/types.py +705 -0
- package/ccx/version.py +29 -0
- package/ccx/workers.py +2081 -0
- package/ccx.py +9 -0
- package/package.json +40 -4
- package/pyproject.toml +87 -0
- package/skills/ccx/SKILL.md +155 -0
- package/skills/ccx/references/controllers.md +244 -0
- package/skills/ccx/references/troubleshooting.md +94 -0
- package/skills/ccx/references/variables.md +62 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Exact managed-turn identity and report reads shared by public adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ccx.codex import records as codex_records
|
|
8
|
+
from ccx.codex.reports import native_timestamp_ms, report_from_turn
|
|
9
|
+
from ccx.errors import InvalidRequest, OperationConflict, SessionNotFound, SessionUnavailable
|
|
10
|
+
from ccx.claude.managed_turns import report_window_for_turn_ref, settlement_for_turn_ref, turn_settlements
|
|
11
|
+
from ccx.store.records import read_record as read_claude_record
|
|
12
|
+
from ccx.claude.transcript import exact_assistant_text
|
|
13
|
+
|
|
14
|
+
_CLAUDE_PREFIX = "ccx-turn:claude:v1:"
|
|
15
|
+
_CODEX_PREFIX = "ccx-turn:codex:v1:"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _ref_id(turn_ref: str, *, engine: str) -> str:
|
|
19
|
+
prefix = _CLAUDE_PREFIX if engine == "claude" else _CODEX_PREFIX
|
|
20
|
+
if not isinstance(turn_ref, str) or not turn_ref.startswith(prefix):
|
|
21
|
+
raise InvalidRequest(
|
|
22
|
+
f"{engine.capitalize()} turn_ref must use {prefix}<native-turn-id>"
|
|
23
|
+
if engine == "codex"
|
|
24
|
+
else f"Claude turn_ref must use {prefix}<start-id>"
|
|
25
|
+
)
|
|
26
|
+
value = turn_ref[len(prefix):]
|
|
27
|
+
if not value:
|
|
28
|
+
raise InvalidRequest(f"turn_ref must contain an id after {prefix}")
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def codex_settled_turn(
|
|
33
|
+
value: dict[str, Any] | None,
|
|
34
|
+
*,
|
|
35
|
+
fallback_completed_at: int | None = None,
|
|
36
|
+
) -> dict[str, Any] | None:
|
|
37
|
+
"""Normalize one exact native Codex terminal projection."""
|
|
38
|
+
if not isinstance(value, dict):
|
|
39
|
+
return None
|
|
40
|
+
turn_id = value.get("turnId")
|
|
41
|
+
if not isinstance(turn_id, str) or not turn_id:
|
|
42
|
+
return None
|
|
43
|
+
completed_at = value.get("completedAtMs")
|
|
44
|
+
if not isinstance(completed_at, int) or isinstance(completed_at, bool):
|
|
45
|
+
completed_at = fallback_completed_at
|
|
46
|
+
return {
|
|
47
|
+
"turn_ref": f"{_CODEX_PREFIX}{turn_id}",
|
|
48
|
+
"turn_id": turn_id,
|
|
49
|
+
"outcome": str(value.get("status") or "unknown"),
|
|
50
|
+
"completed_at": completed_at,
|
|
51
|
+
"context": ({**value["context"], "as_of_turn_ref": f"{_CODEX_PREFIX}{turn_id}"}
|
|
52
|
+
if isinstance(value.get("context"), dict) else None),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _claude_settlement(value: Any) -> dict[str, Any] | None:
|
|
57
|
+
"""One usable ``{turnRef, outcome, settledAtMs}`` settlement, else None."""
|
|
58
|
+
if not isinstance(value, dict):
|
|
59
|
+
return None
|
|
60
|
+
turn_ref = value.get("turnRef")
|
|
61
|
+
settled_at = value.get("settledAtMs")
|
|
62
|
+
if not isinstance(turn_ref, str) or not turn_ref.startswith(_CLAUDE_PREFIX):
|
|
63
|
+
return None
|
|
64
|
+
if not isinstance(settled_at, int) or isinstance(settled_at, bool):
|
|
65
|
+
return None
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _latest_claude_settlement(record: dict[str, Any], short: str) -> dict[str, Any] | None:
|
|
70
|
+
"""Exact latest managed settlement, or None when it is unknown.
|
|
71
|
+
|
|
72
|
+
The host stores the latest summary in the record (``lastSettledTurn``)
|
|
73
|
+
together with ``lastSettledAt``. Records written before that field fall
|
|
74
|
+
back to the latest retained sidecar terminal. A retained terminal older
|
|
75
|
+
than the stored summary never replaces it, and one older than the record's
|
|
76
|
+
``lastSettledAt`` is known stale (rotated out, or not yet broadcast), so
|
|
77
|
+
the latest settlement is unknown rather than that older ref.
|
|
78
|
+
"""
|
|
79
|
+
stored = _claude_settlement(record.get("lastSettledTurn"))
|
|
80
|
+
values = turn_settlements(short) or []
|
|
81
|
+
retained = _claude_settlement(values[-1]) if values else None
|
|
82
|
+
latest = stored
|
|
83
|
+
if retained is not None and (
|
|
84
|
+
stored is None
|
|
85
|
+
or (
|
|
86
|
+
retained["turnRef"] != stored["turnRef"]
|
|
87
|
+
and retained["settledAtMs"] > stored["settledAtMs"]
|
|
88
|
+
)
|
|
89
|
+
):
|
|
90
|
+
# Only a strictly newer, different terminal (e.g. broadcast by a host
|
|
91
|
+
# that predates the stored summary) supersedes the record.
|
|
92
|
+
latest = retained
|
|
93
|
+
if latest is None:
|
|
94
|
+
return None
|
|
95
|
+
last_settled_at = record.get("lastSettledAt")
|
|
96
|
+
if (
|
|
97
|
+
isinstance(last_settled_at, int)
|
|
98
|
+
and not isinstance(last_settled_at, bool)
|
|
99
|
+
and latest["settledAtMs"] < last_settled_at
|
|
100
|
+
):
|
|
101
|
+
return None
|
|
102
|
+
return latest
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def last_settled_turn(engine: str, short: str) -> dict[str, Any] | None:
|
|
106
|
+
"""Normalized exact latest settled-turn identity for one owned worker.
|
|
107
|
+
|
|
108
|
+
``None`` means unknown, never "no newer turn": a caller comparing
|
|
109
|
+
checkpoints must treat it as unavailable evidence.
|
|
110
|
+
"""
|
|
111
|
+
if engine == "claude":
|
|
112
|
+
record = read_claude_record(short)
|
|
113
|
+
if record is None:
|
|
114
|
+
return None
|
|
115
|
+
value = _latest_claude_settlement(record, short)
|
|
116
|
+
if value is None:
|
|
117
|
+
return None
|
|
118
|
+
return {
|
|
119
|
+
"turn_ref": value["turnRef"],
|
|
120
|
+
"turn_id": None,
|
|
121
|
+
"outcome": str(value.get("outcome") or "unknown"),
|
|
122
|
+
"completed_at": value["settledAtMs"],
|
|
123
|
+
"context": ({**value["context"], "as_of_turn_ref": value["turnRef"]}
|
|
124
|
+
if isinstance(value.get("context"), dict) else None),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if engine == "codex":
|
|
128
|
+
record = codex_records.read_record(short)
|
|
129
|
+
if record is None:
|
|
130
|
+
return None
|
|
131
|
+
return codex_settled_turn(
|
|
132
|
+
record.get("lastTurn"),
|
|
133
|
+
fallback_completed_at=record.get("lastTurnAt"),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
raise InvalidRequest(f"unknown engine: {engine!r}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def codex_native_turn_evidence(
|
|
140
|
+
child: str, record: dict, thread_id: str, turn_id: str,
|
|
141
|
+
) -> dict[str, Any] | None:
|
|
142
|
+
"""Existing original-owned Codex journal terminal proof; no native read."""
|
|
143
|
+
from ccx.codex.evidence import build_index, read_journal
|
|
144
|
+
|
|
145
|
+
if (not turn_id or (record.get("activeCompact") or {}).get("turnId") == turn_id
|
|
146
|
+
or (record.get("lastCompact") or {}).get("turnId") == turn_id):
|
|
147
|
+
return None
|
|
148
|
+
journal = read_journal(child)
|
|
149
|
+
index = build_index(journal, health=record.get("evidence"))
|
|
150
|
+
turn = index["turns"].get(turn_id)
|
|
151
|
+
if (not index["complete"] or not turn or not turn["sawTurnStarted"]
|
|
152
|
+
or any(item.get("itemType") == "contextCompaction"
|
|
153
|
+
for item in index["items"].get(turn_id, {}).values())):
|
|
154
|
+
return None
|
|
155
|
+
events = [event for event in journal.events if event.get("turnId") == turn_id]
|
|
156
|
+
if not events or any(event.get("threadId") != thread_id for event in events):
|
|
157
|
+
return None
|
|
158
|
+
terminals = [(event.get("params") or {}).get("turn") for event in events
|
|
159
|
+
if event.get("method") == "turn/completed"]
|
|
160
|
+
if not terminals:
|
|
161
|
+
return {"status": None, "error": None}
|
|
162
|
+
if (any(not isinstance(turn, dict) or turn.get("id") != turn_id
|
|
163
|
+
or turn.get("status") not in {"completed", "failed", "interrupted", "cancelled"}
|
|
164
|
+
for turn in terminals)
|
|
165
|
+
or any(turn != terminals[0] for turn in terminals[1:])):
|
|
166
|
+
return None
|
|
167
|
+
return {"status": terminals[0]["status"], "error": terminals[0].get("error")}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def codex_source_report(
|
|
171
|
+
thread: dict, worker_id: str, thread_id: str, turn_id: str,
|
|
172
|
+
*, expected_source: str,
|
|
173
|
+
) -> dict[str, Any]:
|
|
174
|
+
"""Select only the completed original native R from caller-fetched history."""
|
|
175
|
+
if (not isinstance(thread, dict) or thread.get("id") != thread_id
|
|
176
|
+
or thread.get("threadSource") != expected_source):
|
|
177
|
+
raise SessionUnavailable("Codex conversation belongs to another worker; check the worker")
|
|
178
|
+
turns = [turn for turn in (thread.get("turns") or [])
|
|
179
|
+
if isinstance(turn, dict) and turn.get("id") == turn_id]
|
|
180
|
+
if len(turns) != 1 or turns[0].get("status") != "completed":
|
|
181
|
+
raise SessionUnavailable("Completed Codex turn is unavailable; inspect worker status")
|
|
182
|
+
selected = report_from_turn(turns[0])
|
|
183
|
+
return {
|
|
184
|
+
"turn_ref": f"{_CODEX_PREFIX}{turn_id}", "turn_id": turn_id,
|
|
185
|
+
"turn": None, "item_id": selected.get("itemId") if selected else None,
|
|
186
|
+
"phase": selected.get("phase") if selected else None,
|
|
187
|
+
"text": str(selected["text"]) if selected else "",
|
|
188
|
+
"has_final_message": selected is not None,
|
|
189
|
+
"completed_at": (selected.get("completedAt") if selected
|
|
190
|
+
else native_timestamp_ms(turns[0].get("completedAt"))),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def claude_source_report(
|
|
195
|
+
store, child: str, worker_id: str, ref: str,
|
|
196
|
+
window: dict[str, Any],
|
|
197
|
+
) -> dict[str, Any]:
|
|
198
|
+
"""Pinned original-source Claude R window, including accepted EOF recheck.
|
|
199
|
+
|
|
200
|
+
Unlike notification presentation, a verified empty final is "" rather than
|
|
201
|
+
unavailable. All source/window failures are typed instead of coerced to None.
|
|
202
|
+
"""
|
|
203
|
+
from ccx.claude.transcript import open_original_claude_source
|
|
204
|
+
from ccx.store.root import StorageIntegrityError
|
|
205
|
+
|
|
206
|
+
owner = store.resolve(child)
|
|
207
|
+
if owner.engine != "claude" or owner.worker_id != worker_id:
|
|
208
|
+
raise SessionUnavailable("Claude exact report owner/source mismatch")
|
|
209
|
+
record = store.record(child)
|
|
210
|
+
start, stop = window.get("startOffset"), window.get("endOffset")
|
|
211
|
+
if (window.get("turnRef") != ref or not window.get("endOffsetKnown")
|
|
212
|
+
or type(start) is not int or start < 0
|
|
213
|
+
or (stop is not None and (type(stop) is not int or stop < start))):
|
|
214
|
+
raise SessionUnavailable("Claude turn history is incomplete; inspect the worker")
|
|
215
|
+
try:
|
|
216
|
+
with open_original_claude_source(store, owner, record) as source:
|
|
217
|
+
if source is None:
|
|
218
|
+
raise SessionUnavailable("Claude history file is missing; check worker status")
|
|
219
|
+
pinned, _stat, eof, _original_path, _directory_fd = source
|
|
220
|
+
if start > eof or (stop is not None and stop > eof):
|
|
221
|
+
raise SessionUnavailable("Claude turn history changed during reading; retry")
|
|
222
|
+
text = exact_assistant_text(
|
|
223
|
+
pinned, after_offset=start,
|
|
224
|
+
before_offset=stop if stop is not None else eof,
|
|
225
|
+
)
|
|
226
|
+
if stop is None:
|
|
227
|
+
# Lazy to avoid turn_reports -> protocol_ops -> turn_reports cycle.
|
|
228
|
+
from ccx.ops.wait import wait_worker
|
|
229
|
+
later, available = report_window_for_turn_ref(worker_id, ref)
|
|
230
|
+
snapshot = wait_worker(
|
|
231
|
+
child, timeout=.3, expected_generation=store.generation,
|
|
232
|
+
)
|
|
233
|
+
if (not available or later != window
|
|
234
|
+
or (snapshot.get("last_settled_turn") or {}).get("turn_ref") != ref
|
|
235
|
+
or snapshot.get("active_turn_ref") not in {None, ref}):
|
|
236
|
+
raise SessionUnavailable(
|
|
237
|
+
"Claude original report EOF could include a successor turn"
|
|
238
|
+
)
|
|
239
|
+
store.revalidate()
|
|
240
|
+
except (StorageIntegrityError, OSError, ValueError) as exc:
|
|
241
|
+
raise SessionUnavailable("Claude conversation is unavailable; inspect worker status") from exc
|
|
242
|
+
return {
|
|
243
|
+
"turn_ref": ref, "turn_id": None, "turn": None,
|
|
244
|
+
"item_id": None, "phase": None, "text": text,
|
|
245
|
+
"has_final_message": bool(text), "completed_at": None,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def exact_report(
|
|
250
|
+
store, owner, turn_ref: str, *, thread: dict | None = None,
|
|
251
|
+
) -> dict[str, Any]:
|
|
252
|
+
"""Exact CCX-owned native R; caller owns Codex history read transport."""
|
|
253
|
+
turn_id = _ref_id(turn_ref, engine=owner.engine)
|
|
254
|
+
child = owner.worker
|
|
255
|
+
record = store.record(child)
|
|
256
|
+
native = record.get("native") or {}
|
|
257
|
+
activation = store.native_activation(child)
|
|
258
|
+
original = native.get("sessionId" if owner.engine == "claude" else "threadId")
|
|
259
|
+
source = (f"claude-session:{original}" if owner.engine == "claude"
|
|
260
|
+
else store.codex_source(owner.worker))
|
|
261
|
+
if (not isinstance(original, str) or not original
|
|
262
|
+
or activation.get("state") != "confirmed"
|
|
263
|
+
or activation.get("source") != source
|
|
264
|
+
or (activation.get("nativeBinding") or {}).get(
|
|
265
|
+
"sessionId" if owner.engine == "claude" else "threadId"
|
|
266
|
+
) != original
|
|
267
|
+
or (owner.engine == "codex" and native.get("threadSource") != source)):
|
|
268
|
+
raise SessionUnavailable("Conversation source could not be verified; inspect the worker")
|
|
269
|
+
if owner.engine == "claude":
|
|
270
|
+
terminal, available = settlement_for_turn_ref(owner.worker_id, turn_ref)
|
|
271
|
+
window, window_available = report_window_for_turn_ref(owner.worker_id, turn_ref)
|
|
272
|
+
if not available or not window_available:
|
|
273
|
+
raise SessionUnavailable("Claude turn history is unavailable; inspect the worker")
|
|
274
|
+
if terminal is None:
|
|
275
|
+
if window is not None:
|
|
276
|
+
raise OperationConflict("Claude turn is still running; wait for it to finish")
|
|
277
|
+
raise SessionNotFound("Claude turn is no longer saved; inspect recent history")
|
|
278
|
+
if (terminal.get("eventType") != "turn_complete"
|
|
279
|
+
or terminal.get("outcome") != "completed"):
|
|
280
|
+
raise SessionUnavailable("Claude turn is not complete; check worker status")
|
|
281
|
+
if window is None or window.get("turnRef") != turn_ref:
|
|
282
|
+
raise SessionUnavailable("Claude turn history is unavailable; retry later")
|
|
283
|
+
result = claude_source_report(
|
|
284
|
+
store, child, owner.worker_id, turn_ref, window,
|
|
285
|
+
)
|
|
286
|
+
result["turn"] = terminal.get("turn")
|
|
287
|
+
result["completed_at"] = terminal.get("settledAtMs")
|
|
288
|
+
elif owner.engine == "codex":
|
|
289
|
+
proof = codex_native_turn_evidence(child, record, original, turn_id)
|
|
290
|
+
if proof is None:
|
|
291
|
+
raise SessionUnavailable("Codex completion record is missing; check status")
|
|
292
|
+
if proof["status"] is None:
|
|
293
|
+
raise OperationConflict("Codex turn is not complete; wait before reporting")
|
|
294
|
+
if proof["status"] != "completed":
|
|
295
|
+
raise SessionUnavailable("Codex turn ended without completion; inspect worker status")
|
|
296
|
+
if thread is None:
|
|
297
|
+
raise SessionUnavailable("Codex history is unavailable; retry this report")
|
|
298
|
+
result = codex_source_report(
|
|
299
|
+
thread, owner.worker_id, original, turn_id, expected_source=source,
|
|
300
|
+
)
|
|
301
|
+
else:
|
|
302
|
+
raise InvalidRequest(f"unsupported engine {owner.engine!r}")
|
|
303
|
+
store.revalidate()
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
__all__ = ["codex_settled_turn", "exact_report", "last_settled_turn"]
|