@agentsdock/server 1.0.10-beta.1 → 1.0.10-beta.6
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/README.md +6 -0
- package/npm/cli.cjs +29 -3
- package/package.json +1 -1
- package/server/VERSION +1 -1
- package/server/agent_server.py +230 -79
- package/server/claude_goals.py +16 -4
- package/server/codex_app_server.py +4 -0
- package/server/codex_auth.py +31 -0
- package/server/codex_history_repair.py +92 -32
- package/server/codex_provider.py +12 -2
- package/server/execution_activation.py +1 -1
- package/server/execution_control.py +4 -0
- package/server/execution_install.py +48 -7
- package/server/execution_manage.py +304 -0
- package/server/execution_recovery.py +1 -1
- package/server/execution_uninstall.py +19 -0
- package/server/execution_update_status.py +3 -10
- package/server/opencode_agent_client.py +5 -0
- package/server/provider_connections.py +21 -11
- package/server/server_instances.py +211 -4
- package/server/update_handoff.py +6 -11
package/server/claude_goals.py
CHANGED
|
@@ -138,13 +138,19 @@ class ClaudeGoalProjection:
|
|
|
138
138
|
def consume(self, event: Any) -> bool:
|
|
139
139
|
"""Fold one native JSONL record; return whether visible goal changed."""
|
|
140
140
|
if not isinstance(event, dict) or (
|
|
141
|
-
event.get("type")
|
|
141
|
+
event.get("type") not in {"attachment", "system"}
|
|
142
142
|
or event.get("sessionId") != self.provider_session_id
|
|
143
143
|
or event.get("isSidechain") is not False
|
|
144
144
|
):
|
|
145
145
|
return False
|
|
146
146
|
attachment = event.get("attachment")
|
|
147
|
-
|
|
147
|
+
content = event.get("content")
|
|
148
|
+
stopped = (event.get("type") == "system" and event.get("subtype") == "informational"
|
|
149
|
+
and event.get("level") == "warning" and isinstance(content, str)
|
|
150
|
+
and re.match(r"\AA hook blocked the turn from ending [0-9]+ consecutive times [—-] overriding and ending turn\.", content))
|
|
151
|
+
if stopped:
|
|
152
|
+
attachment = {"type": "goal_status", "condition": (self.goal or {}).get("condition"), "met": False}
|
|
153
|
+
elif not isinstance(attachment, dict) or attachment.get("type") != "goal_status":
|
|
148
154
|
return False
|
|
149
155
|
event_id = event.get("uuid")
|
|
150
156
|
if not isinstance(event_id, str) or len(event_id) != 36:
|
|
@@ -175,10 +181,16 @@ class ClaudeGoalProjection:
|
|
|
175
181
|
return False
|
|
176
182
|
goal = dict(previous)
|
|
177
183
|
goal["status"] = "cleared" if sentinel else "achieved" if met else "active"
|
|
184
|
+
if stopped:
|
|
185
|
+
goal["execution_stop_reason"] = "continuation_limit"
|
|
186
|
+
goal["execution_stopped_at"] = timestamp
|
|
187
|
+
else:
|
|
188
|
+
goal.pop("execution_stop_reason", None)
|
|
189
|
+
goal.pop("execution_stopped_at", None)
|
|
178
190
|
if sentinel:
|
|
179
191
|
for field in ("iterations", "duration_ms", "tokens"):
|
|
180
192
|
goal.pop(field, None)
|
|
181
|
-
if not sentinel:
|
|
193
|
+
if not sentinel and not stopped:
|
|
182
194
|
reason = attachment.get("reason")
|
|
183
195
|
if isinstance(reason, str) and reason:
|
|
184
196
|
goal["last_reason"] = reason[:8192]
|
|
@@ -233,7 +245,7 @@ class ClaudeGoalProjection:
|
|
|
233
245
|
if self._discard_line:
|
|
234
246
|
self._discard_line = False
|
|
235
247
|
continue
|
|
236
|
-
if len(line) > MAX_GOAL_RECORD_BYTES or b'"goal_status"'
|
|
248
|
+
if len(line) > MAX_GOAL_RECORD_BYTES or not (b'"goal_status"' in line or b"A hook blocked the turn from ending" in line):
|
|
237
249
|
continue
|
|
238
250
|
try:
|
|
239
251
|
self.consume(json.loads(line))
|
|
@@ -1088,6 +1088,8 @@ class CodexAppServerClient:
|
|
|
1088
1088
|
self._next_id += 1
|
|
1089
1089
|
request_id = self._next_id
|
|
1090
1090
|
future: asyncio.Future[Any] = loop.create_future()
|
|
1091
|
+
# Preserve request ownership for per-chat process handoff.
|
|
1092
|
+
future._codex_thread_id = str((params or {}).get("threadId") or (params or {}).get("ancestorThreadId") or "")
|
|
1091
1093
|
self._pending[request_id] = (
|
|
1092
1094
|
method,
|
|
1093
1095
|
future,
|
|
@@ -1557,6 +1559,7 @@ class CodexAppServerClient:
|
|
|
1557
1559
|
previous,
|
|
1558
1560
|
)
|
|
1559
1561
|
)
|
|
1562
|
+
task._codex_thread_id = thread_id
|
|
1560
1563
|
self._callback_tails[owner] = task
|
|
1561
1564
|
self._callback_tasks.add(task)
|
|
1562
1565
|
task.add_done_callback(
|
|
@@ -1619,6 +1622,7 @@ class CodexAppServerClient:
|
|
|
1619
1622
|
self._handle_server_request(request_id, method, params),
|
|
1620
1623
|
name=f"codex-app-server-request-{request_id}",
|
|
1621
1624
|
)
|
|
1625
|
+
task._codex_thread_id = str(params.get("threadId") or "")
|
|
1622
1626
|
self._server_request_tasks[request_id] = task
|
|
1623
1627
|
task.add_done_callback(
|
|
1624
1628
|
lambda done, rid=request_id: self._finish_server_request(rid, done)
|
package/server/codex_auth.py
CHANGED
|
@@ -28,6 +28,37 @@ HANDOFF_MESSAGE = (
|
|
|
28
28
|
)
|
|
29
29
|
_REVISION_KEY = secrets.token_bytes(32)
|
|
30
30
|
_MAX_AUTH_BYTES = 128 * 1024
|
|
31
|
+
HANDOFF_PROBE_FAILURES = frozenset({"thread_list_unavailable", "goal_state_unavailable",
|
|
32
|
+
"terminal_state_unavailable", "ownership_deadline", "native_unsubscribe_incomplete", "writer_release_failed"})
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def handoff_detail(reason: str) -> dict:
|
|
36
|
+
"""Content-free admission status, distinct from a native auth rejection."""
|
|
37
|
+
reasons = {
|
|
38
|
+
"active_turn": "this chat still has a running turn",
|
|
39
|
+
"maintenance": "this chat is finishing a provider operation",
|
|
40
|
+
"approval": "this chat has an unanswered approval or question",
|
|
41
|
+
"side_chat": "this chat has a running Side chat",
|
|
42
|
+
"active_goal": "this chat has an active goal",
|
|
43
|
+
"native_goal_active": "Codex reports an active goal for this chat",
|
|
44
|
+
"subagents": "this chat has running subagents",
|
|
45
|
+
"callback": "this chat is finishing a provider notification",
|
|
46
|
+
"runtime_request": "a provider request for this chat is still finishing",
|
|
47
|
+
"native_thread_in_use": "Codex still has work using this thread",
|
|
48
|
+
"background_terminals": "this chat has a running background terminal",
|
|
49
|
+
"thread_list_unavailable": "Codex could not report which threads it owns",
|
|
50
|
+
"goal_state_unavailable": "Codex could not report this chat's goal state",
|
|
51
|
+
"terminal_state_unavailable": "Codex could not report this chat's background terminals",
|
|
52
|
+
"ownership_deadline": "Codex took too long to report this chat's state",
|
|
53
|
+
"native_unsubscribe_incomplete": "Codex has not released this thread yet",
|
|
54
|
+
"writer_release_failed": "Codex could not release the old connection to this thread",
|
|
55
|
+
}
|
|
56
|
+
reason = reason if reason in reasons else "runtime_request"
|
|
57
|
+
action = ("Retry the message to check again." if reason in HANDOFF_PROBE_FAILURES
|
|
58
|
+
else "Retry after that operation finishes.")
|
|
59
|
+
return {"code": "codex_login_handoff", "reason": reason, "retryable": True,
|
|
60
|
+
"message": f"Codex sign-in handoff is waiting: {reasons[reason]}. "
|
|
61
|
+
f"Your message has not been sent. {action}"}
|
|
31
62
|
|
|
32
63
|
|
|
33
64
|
@dataclass(frozen=True)
|
|
@@ -202,10 +202,26 @@ def _native_records(path: Path, expected: tuple[int, int, int, int], budget: _Na
|
|
|
202
202
|
|
|
203
203
|
def _native_event_identity(event: dict) -> dict:
|
|
204
204
|
"""Keep bounded public identity, never retain tool bodies or message text."""
|
|
205
|
-
return {key: event[key] for key in ("id", "seq", "type", "phase", "item_id")
|
|
205
|
+
return {key: event[key] for key in ("id", "seq", "type", "phase", "item_id", "provider_thread_id", "provider_turn_id")
|
|
206
206
|
if key in event and (type(event[key]) is int or isinstance(event[key], str) and len(event[key]) <= 256)}
|
|
207
207
|
|
|
208
208
|
|
|
209
|
+
def _native_goal_steer_turn(event: dict) -> str | None:
|
|
210
|
+
"""Accepted goal follow-ups already have a user bubble in this chat.
|
|
211
|
+
|
|
212
|
+
Keep their exact native turn: one goal run can span many provider turns,
|
|
213
|
+
including later genuine messages with identical text.
|
|
214
|
+
"""
|
|
215
|
+
turn = event.get("provider_turn_id")
|
|
216
|
+
if (event.get("type") == "turn_steered" and event.get("backend") == "codex"
|
|
217
|
+
and event.get("purpose") == "codex_goal_resume"
|
|
218
|
+
and event.get("native_goal_steer") is True and event.get("native_steer") is True
|
|
219
|
+
and event.get("provider_user_authored") is True
|
|
220
|
+
and isinstance(turn, str) and 0 < len(turn) <= 256):
|
|
221
|
+
return turn
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
209
225
|
def _native_import_batch(event: dict) -> dict:
|
|
210
226
|
result = {key: event.get(key) for key in ("seq", "provider_session_id", "source_path")}
|
|
211
227
|
checkpoint = event.get("_history_sync_checkpoint")
|
|
@@ -242,10 +258,13 @@ def _native_owned_turn_ids(event: dict) -> set[str]:
|
|
|
242
258
|
if isinstance(value := event.get(field), str) and 0 < len(value) <= 256}
|
|
243
259
|
|
|
244
260
|
|
|
245
|
-
def _native_assistant_item_owners(assistant_items: dict, completed_runs: set[str]) -> dict:
|
|
261
|
+
def _native_assistant_item_owners(assistant_items: dict, completed_runs: set[str], provider_id: str) -> dict:
|
|
246
262
|
owners: dict = {}
|
|
247
|
-
for run, item_id, body_key in assistant_items:
|
|
248
|
-
|
|
263
|
+
for (run, item_id, body_key), event in assistant_items.items():
|
|
264
|
+
# A restart or Stop can omit the completion receipt. An exact public
|
|
265
|
+
# item already delivered by this provider thread still identifies its
|
|
266
|
+
# native run; source reconciliation below supplies the matching turn.
|
|
267
|
+
if run in completed_runs or event.get("provider_thread_id") == provider_id:
|
|
249
268
|
owners.setdefault((item_id, body_key), set()).add(run)
|
|
250
269
|
return owners
|
|
251
270
|
|
|
@@ -856,7 +875,7 @@ def _prove_native_replays(session_id: str, provider_id: str, events: Path, sourc
|
|
|
856
875
|
owned = owners.setdefault((thread, turn), set())
|
|
857
876
|
retained += run not in owned
|
|
858
877
|
owned.add(run)
|
|
859
|
-
kind = "user" if event.get("type") == "turn_started" else "assistant" if event.get("type") in ("assistant_text", "reasoning_summary", "turn_finished") else None
|
|
878
|
+
kind = "user" if event.get("type") == "turn_started" or _native_goal_steer_turn(event) else "assistant" if event.get("type") in ("assistant_text", "reasoning_summary", "turn_finished") else None
|
|
860
879
|
body = event.get("prompt") if kind == "user" else event.get("result_text") if event.get("type") == "turn_finished" else event.get("text")
|
|
861
880
|
if kind and isinstance(body, str) and body and len(body) <= 4 * 1024 * 1024:
|
|
862
881
|
native.setdefault((run, kind, _text_key(body)), []).append(_native_event_identity(event))
|
|
@@ -935,14 +954,14 @@ def _prove_native_source(thread: str, source: Path, root: Path, batches: dict, c
|
|
|
935
954
|
assistant_native_keys = {}
|
|
936
955
|
compactions, candidate_assistants = {}, set()
|
|
937
956
|
compaction_tracker = CodexCompactionSummaryTracker(thread)
|
|
938
|
-
delivery_bodies, delivery_source_ids = {}, {}
|
|
957
|
+
delivery_bodies, delivery_source_ids, full_user_keys = {}, {}, {}
|
|
939
958
|
completed_runs = {run for (owner_thread, _turn), runs in owners.items()
|
|
940
959
|
if owner_thread == thread for run in runs}
|
|
941
960
|
assistant_item_owners = _native_assistant_item_owners({
|
|
942
961
|
(run, item_id, body_key): event
|
|
943
962
|
for (run, kind, body_key), events in native.items() if kind == "assistant"
|
|
944
963
|
for event in events if (item_id := _public_assistant_item_id(event)) is not None
|
|
945
|
-
}, completed_runs)
|
|
964
|
+
}, completed_runs, thread)
|
|
946
965
|
assistant_owner_evidence: dict = {}
|
|
947
966
|
assistant_source_turns: dict = {}
|
|
948
967
|
allowed_header_owners, header_parents = {thread}, {}
|
|
@@ -1016,6 +1035,21 @@ def _prove_native_source(thread: str, source: Path, root: Path, batches: dict, c
|
|
|
1016
1035
|
raise _Unproven()
|
|
1017
1036
|
if (item["kind"], key[2], key[3]) not in candidate_keys:
|
|
1018
1037
|
continue
|
|
1038
|
+
if item["kind"] == "user" and key[3] is not None:
|
|
1039
|
+
# A display-trimmed input still has its complete source in this
|
|
1040
|
+
# checkpoint-verified record. Compare those bytes to the original
|
|
1041
|
+
# native input, never a shared preview or an unverified hash.
|
|
1042
|
+
full_text = None
|
|
1043
|
+
if record.get("type") == "event_msg" and payload.get("type") == "user_message":
|
|
1044
|
+
full_text = payload.get("message")
|
|
1045
|
+
elif record.get("type") == "response_item" and payload.get("role") == "user":
|
|
1046
|
+
blocks = payload.get("content")
|
|
1047
|
+
if (isinstance(blocks, list) and len(blocks) == 1 and isinstance(blocks[0], dict)
|
|
1048
|
+
and blocks[0].get("type") == "input_text"):
|
|
1049
|
+
full_text = blocks[0].get("text")
|
|
1050
|
+
if (isinstance(full_text, str) and full_text
|
|
1051
|
+
and _text_key(" ".join(full_text.split())) == key[3]):
|
|
1052
|
+
full_user_keys[key] = _text_key(full_text)
|
|
1019
1053
|
if item["kind"] == "user" and (body := _async_delivery_body(item["text"])):
|
|
1020
1054
|
delivery_bodies[key] = body
|
|
1021
1055
|
if item["kind"] == "assistant":
|
|
@@ -1073,9 +1107,7 @@ def _prove_native_source(thread: str, source: Path, root: Path, batches: dict, c
|
|
|
1073
1107
|
if len(proofs) > MAX_TARGETS:
|
|
1074
1108
|
raise _Unproven()
|
|
1075
1109
|
continue
|
|
1076
|
-
if len(source_ids) != 1 or source_hash is not None:
|
|
1077
|
-
# Truncated source hashes need a separately retained native full-body
|
|
1078
|
-
# hash. Until one exists, the bounded preview cannot prove equality.
|
|
1110
|
+
if len(source_ids) != 1 or source_hash is not None and key not in full_user_keys:
|
|
1079
1111
|
continue
|
|
1080
1112
|
source_origin = next(iter(source_ids.values()))
|
|
1081
1113
|
if source_origin.get("kind") in ("subagent_notification", "turn_aborted", "provider_notice"):
|
|
@@ -1087,8 +1119,10 @@ def _prove_native_source(thread: str, source: Path, root: Path, batches: dict, c
|
|
|
1087
1119
|
if len(owned_runs) != 1:
|
|
1088
1120
|
continue
|
|
1089
1121
|
native_run = next(iter(owned_runs))
|
|
1090
|
-
|
|
1091
|
-
|
|
1122
|
+
native_body_key = full_user_keys.get(key, body_key)
|
|
1123
|
+
native_matches = [event for event in native.get((native_run, kind, native_body_key), [])
|
|
1124
|
+
if event.get("type") != "turn_steered" or event.get("provider_turn_id") == key[0]]
|
|
1125
|
+
if not native_matches and kind == "user" and source_hash is None:
|
|
1092
1126
|
delivery = deliveries.match(session_id, thread, native_run, source_origin,
|
|
1093
1127
|
delivery_bodies.get(key), delivery_source_ids.get(key[0], set()), body_key)
|
|
1094
1128
|
if delivery is not None:
|
|
@@ -1105,7 +1139,7 @@ def _prove_native_source(thread: str, source: Path, root: Path, batches: dict, c
|
|
|
1105
1139
|
if not native_matches:
|
|
1106
1140
|
continue
|
|
1107
1141
|
representative = min(native_matches, key=lambda event: event["seq"])
|
|
1108
|
-
proofs[target] = {**next(iter(source_ids.values())), "native_event_id": representative["id"], "source_text_sha256":
|
|
1142
|
+
proofs[target] = {**next(iter(source_ids.values())), "native_event_id": representative["id"], "source_text_sha256": native_body_key}
|
|
1109
1143
|
if len(proofs) > MAX_TARGETS:
|
|
1110
1144
|
raise _Unproven()
|
|
1111
1145
|
budget.check()
|
|
@@ -1183,10 +1217,10 @@ class CodexNativeHistoryRepairCache(CodexGoalHistoryRepairCache):
|
|
|
1183
1217
|
**({"_agentsdock_imported_prompt_hidden": True} if event["type"] == "turn_started" else {})}
|
|
1184
1218
|
|
|
1185
1219
|
|
|
1186
|
-
def
|
|
1220
|
+
def _prove_pending_native_inputs(session_id: str, provider_id: str, items: list[dict],
|
|
1187
1221
|
source: Path, root: Path, checkpoint: dict, parse_item: Callable,
|
|
1188
1222
|
previous_seq: int, owners: dict, deliveries: _AsyncDeliveryIndex,
|
|
1189
|
-
budget: _NativeReadBudget) -> dict:
|
|
1223
|
+
budget: _NativeReadBudget, native_inputs: dict | None = None) -> dict:
|
|
1190
1224
|
"""Reuse pinned-prefix proof before broadcast; the synthetic batch is never stored."""
|
|
1191
1225
|
cursor = checkpoint.get("cursor")
|
|
1192
1226
|
if (checkpoint.get("version") != 1 or not isinstance(cursor, dict) or cursor.get("version") != 1
|
|
@@ -1195,16 +1229,21 @@ def _prove_pending_async_deliveries(session_id: str, provider_id: str, items: li
|
|
|
1195
1229
|
raise _Unproven()
|
|
1196
1230
|
run = "import_pending_native_delivery_proof"
|
|
1197
1231
|
candidates, indexes = [], {}
|
|
1232
|
+
steer_keys = {key[2] for key, events in (native_inputs or {}).items()
|
|
1233
|
+
if key[1] == "user" and any(event.get("type") == "turn_steered" for event in events)}
|
|
1198
1234
|
for index, item in enumerate(items):
|
|
1199
1235
|
budget.check()
|
|
1200
1236
|
origin = item.get("provider_origin")
|
|
1201
1237
|
if (item.get("kind") != "user" or not isinstance(item.get("text"), str)
|
|
1202
1238
|
or not isinstance(origin, dict) or origin.get("provider") != "codex" or origin.get("kind") != "user"
|
|
1203
|
-
or origin.get("session_id", provider_id) != provider_id
|
|
1204
|
-
or not (
|
|
1239
|
+
or origin.get("session_id", provider_id) != provider_id
|
|
1240
|
+
or not (item.get("source_text_sha256") is not None
|
|
1241
|
+
or _async_delivery_body(item["text"]) or _text_key(item["text"]) in deliveries.status_body_keys
|
|
1242
|
+
or _text_key(item["text"]) in steer_keys)):
|
|
1205
1243
|
continue
|
|
1206
1244
|
candidate = _replay_target({"seq": previous_seq + index + 2, "id": f"pending:{index}", "run_id": run,
|
|
1207
1245
|
"type": "turn_started", "backend": "codex", "imported": True, "prompt": item["text"],
|
|
1246
|
+
"source_text_sha256": item.get("source_text_sha256"),
|
|
1208
1247
|
"ts": origin.get("timestamp"), "provider_user_authored": _runtime_human_provenance(item)})
|
|
1209
1248
|
if candidate is not None:
|
|
1210
1249
|
candidates.append(candidate)
|
|
@@ -1214,7 +1253,7 @@ def _prove_pending_async_deliveries(session_id: str, provider_id: str, items: li
|
|
|
1214
1253
|
if not candidates:
|
|
1215
1254
|
return {}
|
|
1216
1255
|
proof = _prove_native_source(provider_id, source, root,
|
|
1217
|
-
{run: (previous_seq + 1, previous_seq + len(items) + 2, checkpoint)}, candidates, {},
|
|
1256
|
+
{run: (previous_seq + 1, previous_seq + len(items) + 2, checkpoint)}, candidates, native_inputs or {},
|
|
1218
1257
|
{(provider_id, turn): runs for turn, runs in owners.items()}, parse_item, budget,
|
|
1219
1258
|
session_id=session_id, deliveries=deliveries, require_verified_checkpoint=True)
|
|
1220
1259
|
result = {}
|
|
@@ -1234,10 +1273,10 @@ def filter_native_codex_history_items(session_id: str, provider_id: str, events:
|
|
|
1234
1273
|
parse_item: Callable[[dict], dict | None] | None = None) -> list[dict]:
|
|
1235
1274
|
"""An existing verified import boundary can omit exact completed native copies.
|
|
1236
1275
|
|
|
1237
|
-
Exact native bodies need only the ledger. Legacy async wrappers
|
|
1238
|
-
require the caller's source/checkpoint/parser and
|
|
1239
|
-
|
|
1240
|
-
Missing source context leaves those
|
|
1276
|
+
Exact native bodies need only the ledger. Legacy async wrappers and trimmed
|
|
1277
|
+
user inputs additionally require the caller's source/checkpoint/parser and
|
|
1278
|
+
one verified source-prefix scan. A preview alone never proves equality.
|
|
1279
|
+
Missing source context leaves those inputs visible for later read repair.
|
|
1241
1280
|
Incomplete proof raises; the caller must defer import and its cursor commit.
|
|
1242
1281
|
"""
|
|
1243
1282
|
if not items:
|
|
@@ -1248,6 +1287,7 @@ def filter_native_codex_history_items(session_id: str, provider_id: str, events:
|
|
|
1248
1287
|
budget = _NativeReadBudget(cancelled, deadline)
|
|
1249
1288
|
stamp = _native_stamp(events)
|
|
1250
1289
|
owners, native, assistant_items, wake_keys, native_count = {}, {}, {}, set(), 0
|
|
1290
|
+
native_inputs, steered_inputs = {}, {}
|
|
1251
1291
|
deliveries = _AsyncDeliveryIndex()
|
|
1252
1292
|
prove_deliveries = (source_path is not None and root is not None
|
|
1253
1293
|
and isinstance(sync_checkpoint, dict) and callable(parse_item)
|
|
@@ -1273,24 +1313,31 @@ def filter_native_codex_history_items(session_id: str, provider_id: str, events:
|
|
|
1273
1313
|
owned = owners.setdefault(turn_id, set())
|
|
1274
1314
|
owner_count += run not in owned
|
|
1275
1315
|
owned.add(run)
|
|
1276
|
-
|
|
1316
|
+
steer_turn = _native_goal_steer_turn(event)
|
|
1317
|
+
kind = "user" if event.get("type") == "turn_started" or steer_turn else "assistant" if event.get("type") in ("assistant_text", "reasoning_summary", "turn_finished") else None
|
|
1277
1318
|
body = event.get("prompt") if kind == "user" else event.get("result_text") if event.get("type") == "turn_finished" else event.get("text")
|
|
1278
1319
|
wake_hash = _native_mailbox_wake_hash(event) if kind == "user" else None
|
|
1279
1320
|
if kind and (isinstance(body, str) and body or wake_hash):
|
|
1280
|
-
keys = native.setdefault(run, {})
|
|
1321
|
+
keys = steered_inputs.setdefault((run, steer_turn), {}) if steer_turn else native.setdefault(run, {})
|
|
1281
1322
|
key = (kind, wake_hash or _text_key(body))
|
|
1282
1323
|
native_count += key not in keys
|
|
1283
1324
|
if isinstance(event.get("id"), str) and 0 < len(event["id"]) <= 256:
|
|
1284
1325
|
keys.setdefault(key, event["id"])
|
|
1326
|
+
if prove_deliveries and kind == "user" and isinstance(body, str) and body:
|
|
1327
|
+
inputs = native_inputs.setdefault((run, kind, key[1]), [])
|
|
1328
|
+
if not any(prior.get("type") == event.get("type")
|
|
1329
|
+
and prior.get("provider_turn_id") == event.get("provider_turn_id") for prior in inputs):
|
|
1330
|
+
inputs.append(_native_event_identity(event))
|
|
1331
|
+
native_count += 1
|
|
1285
1332
|
if wake_hash:
|
|
1286
1333
|
wake_keys.add((run, wake_hash))
|
|
1287
1334
|
item_id = _public_assistant_item_id(event)
|
|
1288
1335
|
if item_id is not None:
|
|
1289
|
-
assistant_items[(run, item_id, key[1])] = event
|
|
1336
|
+
assistant_items[(run, item_id, key[1])] = _native_event_identity(event)
|
|
1290
1337
|
if native_count + owner_count + len(assistant_items) + deliveries.count > MAX_KEYS:
|
|
1291
1338
|
raise _Unproven()
|
|
1292
1339
|
assistant_item_owners = _native_assistant_item_owners(
|
|
1293
|
-
assistant_items, {run for runs in owners.values() for run in runs},
|
|
1340
|
+
assistant_items, {run for runs in owners.values() for run in runs}, provider_id,
|
|
1294
1341
|
)
|
|
1295
1342
|
assistant_source_turns: dict = {}
|
|
1296
1343
|
for item in items:
|
|
@@ -1308,10 +1355,17 @@ def filter_native_codex_history_items(session_id: str, provider_id: str, events:
|
|
|
1308
1355
|
for item_key, turns in assistant_source_turns.items():
|
|
1309
1356
|
if len(turns) == 1:
|
|
1310
1357
|
owners.setdefault(next(iter(turns)), set()).update(assistant_item_owners[item_key])
|
|
1311
|
-
|
|
1358
|
+
if prove_deliveries and steered_inputs:
|
|
1359
|
+
# The unmatched import can contain only follow-ups: its assistant
|
|
1360
|
+
# messages were already reconciled. Preserve public item identities
|
|
1361
|
+
# for the source scan to recover earlier turns of a long goal run.
|
|
1362
|
+
for (run, _item_id, body_key), event in assistant_items.items():
|
|
1363
|
+
native_inputs.setdefault((run, "assistant", body_key), []).append(event)
|
|
1364
|
+
delivery_proofs = _prove_pending_native_inputs(
|
|
1312
1365
|
session_id, provider_id, items, source_path, root, sync_checkpoint, parse_item,
|
|
1313
|
-
previous_seq, owners, deliveries, budget,
|
|
1314
|
-
) if prove_deliveries and deliveries.starts
|
|
1366
|
+
previous_seq, owners, deliveries, budget, native_inputs,
|
|
1367
|
+
) if prove_deliveries and (deliveries.starts or steered_inputs or any(
|
|
1368
|
+
item.get("kind") == "user" and item.get("source_text_sha256") is not None for item in items)) else {}
|
|
1315
1369
|
# A hidden wake has no public prompt body. Require a single exact source
|
|
1316
1370
|
# item in this verified import range, in addition to native turn ownership.
|
|
1317
1371
|
wake_source_ids = {}
|
|
@@ -1332,9 +1386,15 @@ def filter_native_codex_history_items(session_id: str, provider_id: str, events:
|
|
|
1332
1386
|
and origin.get("session_id", provider_id) == provider_id and len(runs) == 1
|
|
1333
1387
|
and item.get("source_text_sha256") is None and isinstance(item.get("text"), str))
|
|
1334
1388
|
known = owned and (item["kind"], _text_key(item["text"])) in native.get(next(iter(runs)), {})
|
|
1335
|
-
|
|
1389
|
+
# Steers never use delta-local text equality as proof. An equal
|
|
1390
|
+
# source item can precede this delta's checkpoint in the same
|
|
1391
|
+
# native turn. Only the full verified prefix can establish that
|
|
1392
|
+
# this source occurrence is the already-shown follow-up.
|
|
1393
|
+
# This proof already established exact source bytes, native owner,
|
|
1394
|
+
# original event and checkpoint even when the display is trimmed.
|
|
1395
|
+
delivery = delivery_proofs.get(index)
|
|
1336
1396
|
known = known or delivery is not None
|
|
1337
|
-
if known and (next(iter(runs)), _text_key(item["text"])) in wake_keys:
|
|
1397
|
+
if known and runs and (next(iter(runs)), _text_key(item["text"])) in wake_keys:
|
|
1338
1398
|
known = (bool(origin.get("event_id")) and bool(origin.get("timestamp"))
|
|
1339
1399
|
and len(wake_source_ids.get((origin.get("turn_id"), _text_key(item["text"])), set())) == 1)
|
|
1340
1400
|
if owned and not known and item["kind"] == "assistant":
|
|
@@ -1350,7 +1410,7 @@ def filter_native_codex_history_items(session_id: str, provider_id: str, events:
|
|
|
1350
1410
|
"provider_history_repair": "source_proven_native_replay",
|
|
1351
1411
|
"provider_origin": {**origin, "session_id": provider_id,
|
|
1352
1412
|
"native_event_id": delivery["native_event_id"] if delivery else native[next(iter(runs))][key],
|
|
1353
|
-
"source_text_sha256": key[1]}})
|
|
1413
|
+
"source_text_sha256": delivery["source_text_sha256"] if delivery else key[1]}})
|
|
1354
1414
|
budget.check()
|
|
1355
1415
|
return result
|
|
1356
1416
|
except (OSError, ValueError, TypeError, KeyError, RuntimeError) as exc:
|
package/server/codex_provider.py
CHANGED
|
@@ -814,6 +814,17 @@ def safe_model_label(label, model):
|
|
|
814
814
|
return label.strip() if isinstance(label, str) and label.strip() and len(label) <= 160 and all(ord(c) >= 32 and ord(c) != 127 for c in label) else model
|
|
815
815
|
|
|
816
816
|
|
|
817
|
+
def endpoint_api_base_url(base: str, protocol: str) -> str:
|
|
818
|
+
"""Resolve the API prefix, retaining the gateway's path and saved identity.
|
|
819
|
+
|
|
820
|
+
Anthropic settings accept either a root or its /v1 API prefix. AI SDK
|
|
821
|
+
adapters append /messages, unlike Claude Code which appends /v1/messages.
|
|
822
|
+
OpenAI-compatible settings already specify their complete API prefix.
|
|
823
|
+
"""
|
|
824
|
+
base = base.rstrip("/")
|
|
825
|
+
return base + "/v1" if protocol == "anthropic" and not base.endswith("/v1") else base
|
|
826
|
+
|
|
827
|
+
|
|
817
828
|
def endpoint_model_catalog(selected: dict, *, protocol="responses") -> dict:
|
|
818
829
|
"""Read only this endpoint's inventory. Never use CLI models or infer a default.
|
|
819
830
|
|
|
@@ -823,8 +834,7 @@ def endpoint_model_catalog(selected: dict, *, protocol="responses") -> dict:
|
|
|
823
834
|
base = selected["base_url"].rstrip("/")
|
|
824
835
|
parsed = urlsplit(base)
|
|
825
836
|
router = parsed.scheme == "https" and parsed.netloc in {"openrouter.ai", "eu.openrouter.ai"} and parsed.path in {"/api", "/api/v1"}
|
|
826
|
-
target = (base.removesuffix("/v1") + "/v1/models/user") if router else base +
|
|
827
|
-
"/v1/models" if protocol == "anthropic" and not base.endswith("/v1") else "/models")
|
|
837
|
+
target = (base.removesuffix("/v1") + "/v1/models/user") if router else endpoint_api_base_url(base, protocol) + "/models"
|
|
828
838
|
headers = {"Accept": "application/json", "anthropic-version": "2023-06-01"}
|
|
829
839
|
header = selected.get("auth_header", "bearer")
|
|
830
840
|
headers["x-api-key" if header == "x-api-key" else "Authorization"] = selected["api_key"] if header == "x-api-key" else "Bearer " + selected["api_key"]
|
|
@@ -249,7 +249,7 @@ def require_retired_legacy_intent(layout: files.ExecutionLayout, previous: Any,
|
|
|
249
249
|
|
|
250
250
|
root = layout.install_root
|
|
251
251
|
def no_journal():
|
|
252
|
-
for name in (".activation-transaction", ".execution-transaction", ".execution-uninstall.json"):
|
|
252
|
+
for name in (".activation-transaction", ".execution-transaction", ".execution-uninstall.json", ".execution-lifecycle.json"):
|
|
253
253
|
if (root / name).exists() or (root / name).is_symlink():
|
|
254
254
|
raise RuntimeError("a lifecycle journal still owns the previous recovery intent")
|
|
255
255
|
no_journal()
|
|
@@ -186,6 +186,10 @@ class ExecutionControl:
|
|
|
186
186
|
if length not in {None, 0}:
|
|
187
187
|
raise ExecutionControlError(400, "Status requests do not accept a body")
|
|
188
188
|
result = await self.status_callback()
|
|
189
|
+
# This running worker understands execution-lifecycle startup
|
|
190
|
+
# holds. New CLIs must not stop an older retained worker and
|
|
191
|
+
# discover only after restart that its queue was not held.
|
|
192
|
+
result = {**result, "native_lifecycle_protocol": 1}
|
|
189
193
|
else:
|
|
190
194
|
content_types = _header_values(headers, b"content-type")
|
|
191
195
|
if (len(content_types) != 1 or content_types[0].lower()
|
|
@@ -329,6 +329,36 @@ def layout_manifest(layout: ExecutionLayout) -> dict[str, Any]:
|
|
|
329
329
|
"retained_releases": sorted({str(layout.worker_release), str(layout.gateway_release)})}
|
|
330
330
|
|
|
331
331
|
|
|
332
|
+
def installed_layout(root: Path) -> ExecutionLayout:
|
|
333
|
+
"""Validate authoritative bindings without discarding retained rollback history."""
|
|
334
|
+
root = _path(root)
|
|
335
|
+
data, _mode = _read_file(root / LAYOUT_NAME, private=True)
|
|
336
|
+
value = json.loads(data)
|
|
337
|
+
if not isinstance(value, dict):
|
|
338
|
+
raise ValueError("installed execution layout is invalid")
|
|
339
|
+
layout = ExecutionLayout.from_dict(value.get("layout"))
|
|
340
|
+
expected = layout_manifest(layout)
|
|
341
|
+
retained = value.get("retained_releases")
|
|
342
|
+
if (layout.install_root != root or set(value) != set(expected)
|
|
343
|
+
or {key: item for key, item in value.items() if key != "retained_releases"}
|
|
344
|
+
!= {key: item for key, item in expected.items() if key != "retained_releases"}
|
|
345
|
+
or not isinstance(retained, list) or not all(isinstance(item, str) for item in retained)
|
|
346
|
+
or len(retained) != len(set(retained))
|
|
347
|
+
or not set(expected["retained_releases"]).issubset(retained)):
|
|
348
|
+
raise ValueError("installed execution layout does not match its runtime")
|
|
349
|
+
# Old monolith releases may have no execution_service.py. Retention is not
|
|
350
|
+
# activation authority, but every advisory entry must be an owned, real
|
|
351
|
+
# version directory inside this installation (never an arbitrary path).
|
|
352
|
+
for item in retained:
|
|
353
|
+
path = _path(item)
|
|
354
|
+
if path.parent != root / "releases" or path.name.startswith("."):
|
|
355
|
+
raise ValueError("retained release is outside the installation")
|
|
356
|
+
_owned_directory(path)
|
|
357
|
+
version, _mode = _read_file(path / "VERSION")
|
|
358
|
+
_text(version.decode().strip())
|
|
359
|
+
return layout
|
|
360
|
+
|
|
361
|
+
|
|
332
362
|
def prepare_runtime(layout: ExecutionLayout) -> None:
|
|
333
363
|
"""Create private IPC state; never rotate an existing control token."""
|
|
334
364
|
from execution_transport import ensure_execution_secret
|
|
@@ -399,6 +429,10 @@ def stage(layout: ExecutionLayout, *, scope: str, prior_services: dict[str, Any]
|
|
|
399
429
|
legacy = layout.install_root / ".activation-transaction"
|
|
400
430
|
if legacy.exists() or legacy.is_symlink():
|
|
401
431
|
raise RuntimeError("legacy activation recovery must finish before execution activation")
|
|
432
|
+
for name in (".execution-lifecycle.json", ".execution-uninstall.json"):
|
|
433
|
+
marker = layout.install_root / name
|
|
434
|
+
if marker.exists() or marker.is_symlink():
|
|
435
|
+
raise RuntimeError("native lifecycle ownership must finish before execution activation")
|
|
402
436
|
if layout.transaction_dir.exists() or layout.transaction_dir.is_symlink():
|
|
403
437
|
raise FileExistsError("an execution activation is already pending")
|
|
404
438
|
prior_layout = _snapshot(layout.manifest_path)
|
|
@@ -674,6 +708,19 @@ def pending_worker_operation(root: Path, runtime_root: Path) -> str | None:
|
|
|
674
708
|
journal = root / TRANSACTION_NAME
|
|
675
709
|
outer = root / ".activation-transaction"
|
|
676
710
|
uninstall = root / ".execution-uninstall.json"
|
|
711
|
+
lifecycle = root / ".execution-lifecycle.json"
|
|
712
|
+
if lifecycle.exists() or lifecycle.is_symlink():
|
|
713
|
+
if any(path.exists() or path.is_symlink() for path in (outer, journal)):
|
|
714
|
+
raise RuntimeError("activation and lifecycle both claim execution ownership")
|
|
715
|
+
from execution_manage import pending_lifecycle_operation
|
|
716
|
+
operation = pending_lifecycle_operation(root, runtime_root)
|
|
717
|
+
if uninstall.exists() or uninstall.is_symlink():
|
|
718
|
+
# An interrupted owned-stop -> uninstall adoption may retain both
|
|
719
|
+
# journals, but they must describe the very same sealed operation.
|
|
720
|
+
from execution_uninstall import pending_uninstall_operation
|
|
721
|
+
if pending_uninstall_operation(root, runtime_root) != operation:
|
|
722
|
+
raise RuntimeError("uninstall and lifecycle ownership conflict")
|
|
723
|
+
return operation
|
|
677
724
|
if uninstall.exists() or uninstall.is_symlink():
|
|
678
725
|
if outer.exists() or outer.is_symlink() or journal.exists() or journal.is_symlink():
|
|
679
726
|
raise RuntimeError("activation and uninstall both claim execution ownership")
|
|
@@ -714,13 +761,7 @@ def active_worker_release(root: Path) -> Path | None:
|
|
|
714
761
|
data, _mode = _read_file(root / LAYOUT_NAME, private=True)
|
|
715
762
|
except FileNotFoundError:
|
|
716
763
|
return None
|
|
717
|
-
|
|
718
|
-
if not isinstance(value, dict) or value.get("format") != FORMAT:
|
|
719
|
-
raise ValueError("installed execution layout is invalid")
|
|
720
|
-
layout = ExecutionLayout.from_dict(value.get("layout"))
|
|
721
|
-
if layout.install_root != root or value != layout_manifest(layout):
|
|
722
|
-
raise ValueError("installed execution layout no longer matches its runtime")
|
|
723
|
-
return layout.worker_release
|
|
764
|
+
return installed_layout(root).worker_release
|
|
724
765
|
|
|
725
766
|
|
|
726
767
|
def main(argv: list[str] | None = None) -> None:
|