@agentsdock/server 1.0.9 → 1.0.10-beta.4
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/npm/cli.cjs +6 -2
- package/package.json +1 -1
- package/server/VERSION +1 -1
- package/server/agent_server.py +1261 -2151
- 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 +144 -39
- package/server/codex_provider.py +52 -2
- package/server/codex_response_stream.py +229 -0
- package/server/install.sh +3 -3
- package/server/server_instances.py +166 -2
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
|
|
|
@@ -659,16 +678,36 @@ class _AsyncDeliveryIndex:
|
|
|
659
678
|
starts: dict = field(default_factory=dict)
|
|
660
679
|
ends: dict = field(default_factory=dict)
|
|
661
680
|
receipts: dict = field(default_factory=dict)
|
|
681
|
+
status_queues: dict = field(default_factory=dict)
|
|
682
|
+
status_body_keys: set = field(default_factory=set)
|
|
662
683
|
steered: set = field(default_factory=set)
|
|
663
684
|
count: int = 0
|
|
664
685
|
|
|
665
686
|
def observe(self, event: dict) -> None:
|
|
666
687
|
event_type = event.get("type")
|
|
667
|
-
if (event_type not in {"turn_started", "turn_finished", "turn_steered", "turn_stopped", "turn_queue_run_now",
|
|
688
|
+
if (event_type not in {"turn_queued", "turn_started", "turn_finished", "turn_steered", "turn_stopped", "turn_queue_run_now",
|
|
668
689
|
"chat_conversation_message_received", "chat_conversation_message_started", "chat_conversation_message_delivered"}
|
|
669
690
|
or event.get("imported") is True or event.get("forked") is True
|
|
670
691
|
or event.get("backend") not in (None, "codex")):
|
|
671
692
|
return
|
|
693
|
+
if event_type == "turn_queued":
|
|
694
|
+
# The private durable request is exact provider input. Public display
|
|
695
|
+
# labels and wrapper-looking text are never evidence by themselves.
|
|
696
|
+
queued_id, body = event.get("queued_id"), event.get("request_prompt")
|
|
697
|
+
if (event.get("purpose") == "cross_chat_handoff_delivery"
|
|
698
|
+
and event.get("cross_chat_exchange_status") is True
|
|
699
|
+
and not _runtime_human_provenance(event)
|
|
700
|
+
and isinstance(queued_id, str) and 0 < len(queued_id) <= 256
|
|
701
|
+
and isinstance(body, str) and 0 < len(body) <= NATIVE_PROOF_LINE_BYTES):
|
|
702
|
+
record = {key: event.get(key) for key in ("seq", "cross_chat_exchange_id",
|
|
703
|
+
"cross_chat_exchange_leg_id", "source_session_id", "target_session_id")}
|
|
704
|
+
record["body_key"] = _text_key(body)
|
|
705
|
+
self.status_queues.setdefault(queued_id, []).append(record)
|
|
706
|
+
self.status_body_keys.add(record["body_key"])
|
|
707
|
+
self.count += 1
|
|
708
|
+
if self.count > MAX_KEYS:
|
|
709
|
+
raise _Unproven()
|
|
710
|
+
return
|
|
672
711
|
run = (event.get("interrupted_run_id") or event.get("run_id")) if event_type == "turn_queue_run_now" else event.get("run_id")
|
|
673
712
|
if (event_type in {"turn_steered", "turn_stopped"} or event.get("native_steer") is True):
|
|
674
713
|
if isinstance(run, str) and 0 < len(run) <= 256 and not run.startswith("import_"):
|
|
@@ -686,7 +725,8 @@ class _AsyncDeliveryIndex:
|
|
|
686
725
|
"provider_thread_id", "provider_turn_id", "cross_chat_envelope_id", "handoff_id", "message_id",
|
|
687
726
|
"source_session_id", "target_session_id", "source_title", "target_run_id", "conversation_mode",
|
|
688
727
|
"kind", "action", "handoff_action", "handoff_body_chars", "handoff_body_sha256",
|
|
689
|
-
"message_revision", "message_edited_by_user", "exchange_id", "cross_chat_exchange_id"
|
|
728
|
+
"message_revision", "message_edited_by_user", "exchange_id", "cross_chat_exchange_id",
|
|
729
|
+
"cross_chat_exchange_leg_id", "cross_chat_exchange_status", "queued_id")
|
|
690
730
|
record = {key: event.get(key) for key in fields
|
|
691
731
|
if event.get(key) is None or type(event.get(key)) in (int, bool)
|
|
692
732
|
or isinstance(event.get(key), str) and len(event[key]) <= 512}
|
|
@@ -710,11 +750,36 @@ class _AsyncDeliveryIndex:
|
|
|
710
750
|
raise _Unproven()
|
|
711
751
|
|
|
712
752
|
def match(self, session_id: str, provider_id: str, run: str, origin: dict,
|
|
713
|
-
body: tuple[str, str, int] | None, source_ids: set) -> dict | None:
|
|
753
|
+
body: tuple[str, str, int] | None, source_ids: set, body_key: str = "") -> dict | None:
|
|
714
754
|
starts, ends = self.starts.get(run, []), self.ends.get(run, [])
|
|
715
|
-
if
|
|
755
|
+
if len(starts) != 1 or len(ends) != 1 or run in self.steered:
|
|
716
756
|
return None
|
|
717
757
|
start, end = starts[0], ends[0]
|
|
758
|
+
if start.get("cross_chat_exchange_status") is True:
|
|
759
|
+
queued = self.status_queues.get(start.get("queued_id"), [])
|
|
760
|
+
if len(queued) != 1:
|
|
761
|
+
return None
|
|
762
|
+
receipt = queued[0]
|
|
763
|
+
identity = ("cross_chat_exchange_id", "cross_chat_exchange_leg_id",
|
|
764
|
+
"source_session_id", "target_session_id")
|
|
765
|
+
if (start.get("malformed") or end.get("malformed") or not start.get("body_key")
|
|
766
|
+
or any(not isinstance(start.get(key), str) or not start[key]
|
|
767
|
+
or receipt.get(key) != start[key] or end.get(key) != start[key] for key in identity)
|
|
768
|
+
or start.get("target_session_id") != session_id or start.get("source_session_id") == session_id
|
|
769
|
+
or start.get("backend") != "codex" or end.get("backend") != "codex"
|
|
770
|
+
or end.get("cross_chat_exchange_status") is not True or end.get("transport") != "app-server"
|
|
771
|
+
or end.get("provider_thread_id") != provider_id or end.get("provider_turn_id") != origin.get("turn_id")
|
|
772
|
+
or receipt["body_key"] != body_key or source_ids != {origin.get("event_id")} or not origin.get("event_id")
|
|
773
|
+
or any(type(record.get("seq")) is not int for record in (receipt, start, end))
|
|
774
|
+
or not receipt["seq"] < start["seq"] < end["seq"]
|
|
775
|
+
or not isinstance(start.get("id"), str) or not start["id"]):
|
|
776
|
+
return None
|
|
777
|
+
times = [_delivery_timestamp(value) for value in (start.get("ts"), origin.get("timestamp"), end.get("ts"))]
|
|
778
|
+
if any(value is None for value in times) or not times[0] <= times[1] <= times[2]:
|
|
779
|
+
return None
|
|
780
|
+
return _native_event_identity(start)
|
|
781
|
+
if body is None:
|
|
782
|
+
return None
|
|
718
783
|
envelope = start.get("cross_chat_envelope_id")
|
|
719
784
|
if (start.get("malformed") or end.get("malformed")
|
|
720
785
|
or not isinstance(envelope, str) or not envelope or start.get("backend") != "codex"
|
|
@@ -810,7 +875,7 @@ def _prove_native_replays(session_id: str, provider_id: str, events: Path, sourc
|
|
|
810
875
|
owned = owners.setdefault((thread, turn), set())
|
|
811
876
|
retained += run not in owned
|
|
812
877
|
owned.add(run)
|
|
813
|
-
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
|
|
814
879
|
body = event.get("prompt") if kind == "user" else event.get("result_text") if event.get("type") == "turn_finished" else event.get("text")
|
|
815
880
|
if kind and isinstance(body, str) and body and len(body) <= 4 * 1024 * 1024:
|
|
816
881
|
native.setdefault((run, kind, _text_key(body)), []).append(_native_event_identity(event))
|
|
@@ -889,14 +954,14 @@ def _prove_native_source(thread: str, source: Path, root: Path, batches: dict, c
|
|
|
889
954
|
assistant_native_keys = {}
|
|
890
955
|
compactions, candidate_assistants = {}, set()
|
|
891
956
|
compaction_tracker = CodexCompactionSummaryTracker(thread)
|
|
892
|
-
delivery_bodies, delivery_source_ids = {}, {}
|
|
957
|
+
delivery_bodies, delivery_source_ids, full_user_keys = {}, {}, {}
|
|
893
958
|
completed_runs = {run for (owner_thread, _turn), runs in owners.items()
|
|
894
959
|
if owner_thread == thread for run in runs}
|
|
895
960
|
assistant_item_owners = _native_assistant_item_owners({
|
|
896
961
|
(run, item_id, body_key): event
|
|
897
962
|
for (run, kind, body_key), events in native.items() if kind == "assistant"
|
|
898
963
|
for event in events if (item_id := _public_assistant_item_id(event)) is not None
|
|
899
|
-
}, completed_runs)
|
|
964
|
+
}, completed_runs, thread)
|
|
900
965
|
assistant_owner_evidence: dict = {}
|
|
901
966
|
assistant_source_turns: dict = {}
|
|
902
967
|
allowed_header_owners, header_parents = {thread}, {}
|
|
@@ -970,6 +1035,21 @@ def _prove_native_source(thread: str, source: Path, root: Path, batches: dict, c
|
|
|
970
1035
|
raise _Unproven()
|
|
971
1036
|
if (item["kind"], key[2], key[3]) not in candidate_keys:
|
|
972
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)
|
|
973
1053
|
if item["kind"] == "user" and (body := _async_delivery_body(item["text"])):
|
|
974
1054
|
delivery_bodies[key] = body
|
|
975
1055
|
if item["kind"] == "assistant":
|
|
@@ -1027,9 +1107,7 @@ def _prove_native_source(thread: str, source: Path, root: Path, batches: dict, c
|
|
|
1027
1107
|
if len(proofs) > MAX_TARGETS:
|
|
1028
1108
|
raise _Unproven()
|
|
1029
1109
|
continue
|
|
1030
|
-
if len(source_ids) != 1 or source_hash is not None:
|
|
1031
|
-
# Truncated source hashes need a separately retained native full-body
|
|
1032
|
-
# 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:
|
|
1033
1111
|
continue
|
|
1034
1112
|
source_origin = next(iter(source_ids.values()))
|
|
1035
1113
|
if source_origin.get("kind") in ("subagent_notification", "turn_aborted", "provider_notice"):
|
|
@@ -1041,10 +1119,12 @@ def _prove_native_source(thread: str, source: Path, root: Path, batches: dict, c
|
|
|
1041
1119
|
if len(owned_runs) != 1:
|
|
1042
1120
|
continue
|
|
1043
1121
|
native_run = next(iter(owned_runs))
|
|
1044
|
-
|
|
1045
|
-
|
|
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:
|
|
1046
1126
|
delivery = deliveries.match(session_id, thread, native_run, source_origin,
|
|
1047
|
-
delivery_bodies.get(key), delivery_source_ids.get(key[0], set()))
|
|
1127
|
+
delivery_bodies.get(key), delivery_source_ids.get(key[0], set()), body_key)
|
|
1048
1128
|
if delivery is not None:
|
|
1049
1129
|
native_matches = [delivery]
|
|
1050
1130
|
if not native_matches and kind == "assistant":
|
|
@@ -1059,7 +1139,7 @@ def _prove_native_source(thread: str, source: Path, root: Path, batches: dict, c
|
|
|
1059
1139
|
if not native_matches:
|
|
1060
1140
|
continue
|
|
1061
1141
|
representative = min(native_matches, key=lambda event: event["seq"])
|
|
1062
|
-
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}
|
|
1063
1143
|
if len(proofs) > MAX_TARGETS:
|
|
1064
1144
|
raise _Unproven()
|
|
1065
1145
|
budget.check()
|
|
@@ -1137,10 +1217,10 @@ class CodexNativeHistoryRepairCache(CodexGoalHistoryRepairCache):
|
|
|
1137
1217
|
**({"_agentsdock_imported_prompt_hidden": True} if event["type"] == "turn_started" else {})}
|
|
1138
1218
|
|
|
1139
1219
|
|
|
1140
|
-
def
|
|
1220
|
+
def _prove_pending_native_inputs(session_id: str, provider_id: str, items: list[dict],
|
|
1141
1221
|
source: Path, root: Path, checkpoint: dict, parse_item: Callable,
|
|
1142
1222
|
previous_seq: int, owners: dict, deliveries: _AsyncDeliveryIndex,
|
|
1143
|
-
budget: _NativeReadBudget) -> dict:
|
|
1223
|
+
budget: _NativeReadBudget, native_inputs: dict | None = None) -> dict:
|
|
1144
1224
|
"""Reuse pinned-prefix proof before broadcast; the synthetic batch is never stored."""
|
|
1145
1225
|
cursor = checkpoint.get("cursor")
|
|
1146
1226
|
if (checkpoint.get("version") != 1 or not isinstance(cursor, dict) or cursor.get("version") != 1
|
|
@@ -1149,16 +1229,21 @@ def _prove_pending_async_deliveries(session_id: str, provider_id: str, items: li
|
|
|
1149
1229
|
raise _Unproven()
|
|
1150
1230
|
run = "import_pending_native_delivery_proof"
|
|
1151
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)}
|
|
1152
1234
|
for index, item in enumerate(items):
|
|
1153
1235
|
budget.check()
|
|
1154
1236
|
origin = item.get("provider_origin")
|
|
1155
1237
|
if (item.get("kind") != "user" or not isinstance(item.get("text"), str)
|
|
1156
1238
|
or not isinstance(origin, dict) or origin.get("provider") != "codex" or origin.get("kind") != "user"
|
|
1157
|
-
or origin.get("session_id", provider_id) != provider_id
|
|
1158
|
-
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)):
|
|
1159
1243
|
continue
|
|
1160
1244
|
candidate = _replay_target({"seq": previous_seq + index + 2, "id": f"pending:{index}", "run_id": run,
|
|
1161
1245
|
"type": "turn_started", "backend": "codex", "imported": True, "prompt": item["text"],
|
|
1246
|
+
"source_text_sha256": item.get("source_text_sha256"),
|
|
1162
1247
|
"ts": origin.get("timestamp"), "provider_user_authored": _runtime_human_provenance(item)})
|
|
1163
1248
|
if candidate is not None:
|
|
1164
1249
|
candidates.append(candidate)
|
|
@@ -1168,7 +1253,7 @@ def _prove_pending_async_deliveries(session_id: str, provider_id: str, items: li
|
|
|
1168
1253
|
if not candidates:
|
|
1169
1254
|
return {}
|
|
1170
1255
|
proof = _prove_native_source(provider_id, source, root,
|
|
1171
|
-
{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 {},
|
|
1172
1257
|
{(provider_id, turn): runs for turn, runs in owners.items()}, parse_item, budget,
|
|
1173
1258
|
session_id=session_id, deliveries=deliveries, require_verified_checkpoint=True)
|
|
1174
1259
|
result = {}
|
|
@@ -1188,10 +1273,10 @@ def filter_native_codex_history_items(session_id: str, provider_id: str, events:
|
|
|
1188
1273
|
parse_item: Callable[[dict], dict | None] | None = None) -> list[dict]:
|
|
1189
1274
|
"""An existing verified import boundary can omit exact completed native copies.
|
|
1190
1275
|
|
|
1191
|
-
Exact native bodies need only the ledger. Legacy async wrappers
|
|
1192
|
-
require the caller's source/checkpoint/parser and
|
|
1193
|
-
|
|
1194
|
-
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.
|
|
1195
1280
|
Incomplete proof raises; the caller must defer import and its cursor commit.
|
|
1196
1281
|
"""
|
|
1197
1282
|
if not items:
|
|
@@ -1202,11 +1287,11 @@ def filter_native_codex_history_items(session_id: str, provider_id: str, events:
|
|
|
1202
1287
|
budget = _NativeReadBudget(cancelled, deadline)
|
|
1203
1288
|
stamp = _native_stamp(events)
|
|
1204
1289
|
owners, native, assistant_items, wake_keys, native_count = {}, {}, {}, set(), 0
|
|
1290
|
+
native_inputs, steered_inputs = {}, {}
|
|
1205
1291
|
deliveries = _AsyncDeliveryIndex()
|
|
1206
1292
|
prove_deliveries = (source_path is not None and root is not None
|
|
1207
1293
|
and isinstance(sync_checkpoint, dict) and callable(parse_item)
|
|
1208
|
-
and any(item.get("kind") == "user" and isinstance(item.get("text"), str)
|
|
1209
|
-
and _async_delivery_body(item["text"]) for item in items))
|
|
1294
|
+
and any(item.get("kind") == "user" and isinstance(item.get("text"), str) for item in items))
|
|
1210
1295
|
previous_seq, owner_count = 0, 0
|
|
1211
1296
|
for event, _offset, _line in _native_records(events, stamp, budget):
|
|
1212
1297
|
seq = event.get("seq")
|
|
@@ -1228,24 +1313,31 @@ def filter_native_codex_history_items(session_id: str, provider_id: str, events:
|
|
|
1228
1313
|
owned = owners.setdefault(turn_id, set())
|
|
1229
1314
|
owner_count += run not in owned
|
|
1230
1315
|
owned.add(run)
|
|
1231
|
-
|
|
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
|
|
1232
1318
|
body = event.get("prompt") if kind == "user" else event.get("result_text") if event.get("type") == "turn_finished" else event.get("text")
|
|
1233
1319
|
wake_hash = _native_mailbox_wake_hash(event) if kind == "user" else None
|
|
1234
1320
|
if kind and (isinstance(body, str) and body or wake_hash):
|
|
1235
|
-
keys = native.setdefault(run, {})
|
|
1321
|
+
keys = steered_inputs.setdefault((run, steer_turn), {}) if steer_turn else native.setdefault(run, {})
|
|
1236
1322
|
key = (kind, wake_hash or _text_key(body))
|
|
1237
1323
|
native_count += key not in keys
|
|
1238
1324
|
if isinstance(event.get("id"), str) and 0 < len(event["id"]) <= 256:
|
|
1239
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
|
|
1240
1332
|
if wake_hash:
|
|
1241
1333
|
wake_keys.add((run, wake_hash))
|
|
1242
1334
|
item_id = _public_assistant_item_id(event)
|
|
1243
1335
|
if item_id is not None:
|
|
1244
|
-
assistant_items[(run, item_id, key[1])] = event
|
|
1336
|
+
assistant_items[(run, item_id, key[1])] = _native_event_identity(event)
|
|
1245
1337
|
if native_count + owner_count + len(assistant_items) + deliveries.count > MAX_KEYS:
|
|
1246
1338
|
raise _Unproven()
|
|
1247
1339
|
assistant_item_owners = _native_assistant_item_owners(
|
|
1248
|
-
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,
|
|
1249
1341
|
)
|
|
1250
1342
|
assistant_source_turns: dict = {}
|
|
1251
1343
|
for item in items:
|
|
@@ -1263,10 +1355,17 @@ def filter_native_codex_history_items(session_id: str, provider_id: str, events:
|
|
|
1263
1355
|
for item_key, turns in assistant_source_turns.items():
|
|
1264
1356
|
if len(turns) == 1:
|
|
1265
1357
|
owners.setdefault(next(iter(turns)), set()).update(assistant_item_owners[item_key])
|
|
1266
|
-
|
|
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(
|
|
1267
1365
|
session_id, provider_id, items, source_path, root, sync_checkpoint, parse_item,
|
|
1268
|
-
previous_seq, owners, deliveries, budget,
|
|
1269
|
-
) 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 {}
|
|
1270
1369
|
# A hidden wake has no public prompt body. Require a single exact source
|
|
1271
1370
|
# item in this verified import range, in addition to native turn ownership.
|
|
1272
1371
|
wake_source_ids = {}
|
|
@@ -1287,9 +1386,15 @@ def filter_native_codex_history_items(session_id: str, provider_id: str, events:
|
|
|
1287
1386
|
and origin.get("session_id", provider_id) == provider_id and len(runs) == 1
|
|
1288
1387
|
and item.get("source_text_sha256") is None and isinstance(item.get("text"), str))
|
|
1289
1388
|
known = owned and (item["kind"], _text_key(item["text"])) in native.get(next(iter(runs)), {})
|
|
1290
|
-
|
|
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)
|
|
1291
1396
|
known = known or delivery is not None
|
|
1292
|
-
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:
|
|
1293
1398
|
known = (bool(origin.get("event_id")) and bool(origin.get("timestamp"))
|
|
1294
1399
|
and len(wake_source_ids.get((origin.get("turn_id"), _text_key(item["text"])), set())) == 1)
|
|
1295
1400
|
if owned and not known and item["kind"] == "assistant":
|
|
@@ -1305,7 +1410,7 @@ def filter_native_codex_history_items(session_id: str, provider_id: str, events:
|
|
|
1305
1410
|
"provider_history_repair": "source_proven_native_replay",
|
|
1306
1411
|
"provider_origin": {**origin, "session_id": provider_id,
|
|
1307
1412
|
"native_event_id": delivery["native_event_id"] if delivery else native[next(iter(runs))][key],
|
|
1308
|
-
"source_text_sha256": key[1]}})
|
|
1413
|
+
"source_text_sha256": delivery["source_text_sha256"] if delivery else key[1]}})
|
|
1309
1414
|
budget.check()
|
|
1310
1415
|
return result
|
|
1311
1416
|
except (OSError, ValueError, TypeError, KeyError, RuntimeError) as exc:
|
package/server/codex_provider.py
CHANGED
|
@@ -195,6 +195,19 @@ def session_choice(value) -> str:
|
|
|
195
195
|
return choice
|
|
196
196
|
|
|
197
197
|
|
|
198
|
+
RUNTIME_FIELDS = ("codex_provider", "codex_provider_revision", "codex_provider_binding", "model", "effort", "_codex_provider_removed")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def requested_session(session: dict) -> dict:
|
|
202
|
+
"""Desired runtime is separate from the owner of an in-flight turn."""
|
|
203
|
+
pending = session.get("_codex_provider_pending")
|
|
204
|
+
return {**session, **pending} if isinstance(pending, dict) else dict(session)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def runtime_selection(session: dict) -> dict:
|
|
208
|
+
return {key: session.get(key) for key in RUNTIME_FIELDS}
|
|
209
|
+
|
|
210
|
+
|
|
198
211
|
class ProviderStore:
|
|
199
212
|
"""Atomic metadata pointer plus private, endpoint-bound credential records."""
|
|
200
213
|
def __init__(self, root: Path):
|
|
@@ -329,6 +342,8 @@ class ProviderStore:
|
|
|
329
342
|
def for_session(self, session: dict, *, include_key=False) -> dict | None:
|
|
330
343
|
if session_choice(session.get("codex_provider")) == "default":
|
|
331
344
|
return None
|
|
345
|
+
if session.get("_codex_provider_removed"):
|
|
346
|
+
raise HTTPException(409, "Configure the custom Codex endpoint before using this chat.")
|
|
332
347
|
revision = session.get("codex_provider_revision")
|
|
333
348
|
selected = self.selection(include_key=include_key, revision=revision, include_revision=True)
|
|
334
349
|
if selected is None:
|
|
@@ -345,6 +360,30 @@ class ProviderStore:
|
|
|
345
360
|
selected["model"] = validate_model(model)
|
|
346
361
|
return selected
|
|
347
362
|
|
|
363
|
+
def control(self, session: dict, *, available=True, summary=False) -> dict:
|
|
364
|
+
requested = requested_session(session)
|
|
365
|
+
pending = isinstance(session.get("_codex_provider_pending"), dict)
|
|
366
|
+
requested_provider = session_choice(requested.get("codex_provider"))
|
|
367
|
+
def endpoint(value):
|
|
368
|
+
if session_choice(value.get("codex_provider")) != "custom" or value.get("_codex_provider_removed"):
|
|
369
|
+
return None
|
|
370
|
+
try:
|
|
371
|
+
selected = self._public_selections.get(value.get("codex_provider_revision")) or self.for_session(value)
|
|
372
|
+
return selected.get("base_url") if selected else None
|
|
373
|
+
except HTTPException:
|
|
374
|
+
return None
|
|
375
|
+
return {"pending": pending,
|
|
376
|
+
"pending_reason": session.get("_codex_provider_pending_reason") if pending else None,
|
|
377
|
+
"requested_provider": requested_provider,
|
|
378
|
+
"active_provider": session_choice(session.get("codex_provider")),
|
|
379
|
+
"requested_model": requested.get("model"), "requested_effort": requested.get("effort"),
|
|
380
|
+
# The next-turn picker must use the requested endpoint's catalog,
|
|
381
|
+
# while active runtime fields continue describing running work.
|
|
382
|
+
# Avoid duplicating the active catalog on ordinary session events.
|
|
383
|
+
"requested_catalog": self.catalog(available=available, session=requested, summary=summary)
|
|
384
|
+
if pending and requested_provider == "custom" else None,
|
|
385
|
+
"requested_base_url": endpoint(requested), "active_base_url": endpoint(session)}
|
|
386
|
+
|
|
348
387
|
def registration(self, *, include_key=False) -> dict | None:
|
|
349
388
|
# A damaged optional endpoint must not prevent normal Codex startup.
|
|
350
389
|
# Custom chat admission and the admin status route still fail closed.
|
|
@@ -575,6 +614,17 @@ class ProviderStore:
|
|
|
575
614
|
if selected:
|
|
576
615
|
self._atomic(self._binding_name(thread_id), {"binding": binding(selected),
|
|
577
616
|
"credential_id": selected.get("credential_id")})
|
|
617
|
+
else:
|
|
618
|
+
self._atomic(self._binding_name(thread_id), {"binding": None, "credential_id": None})
|
|
619
|
+
|
|
620
|
+
def thread_binding(self, thread_id: str) -> dict:
|
|
621
|
+
"""Snapshot rollback metadata without reopening revoked credentials."""
|
|
622
|
+
with self.lock:
|
|
623
|
+
return self._read(self._binding_name(thread_id)) or {"binding": None, "credential_id": None}
|
|
624
|
+
|
|
625
|
+
def restore_thread_binding(self, thread_id: str, record: dict) -> None:
|
|
626
|
+
with self.lock:
|
|
627
|
+
self._atomic(self._binding_name(thread_id), record)
|
|
578
628
|
|
|
579
629
|
def for_thread(self, thread_id: str, *, include_key=False) -> dict | None:
|
|
580
630
|
with self.lock:
|
|
@@ -1076,7 +1126,7 @@ def create_router(*, authorize, store: ProviderStore, mutate, probe, available,
|
|
|
1076
1126
|
session = session_lookup(session_id) if session_lookup else None
|
|
1077
1127
|
if not session:
|
|
1078
1128
|
raise HTTPException(404, "Chat not found.")
|
|
1079
|
-
selected = await asyncio.to_thread(store.for_session, session, include_key=True)
|
|
1129
|
+
selected = await asyncio.to_thread(store.for_session, requested_session(session), include_key=True)
|
|
1080
1130
|
else:
|
|
1081
1131
|
selected = await asyncio.to_thread(store.selection, include_key=True, include_revision=True)
|
|
1082
1132
|
if selected is None:
|
|
@@ -1141,7 +1191,7 @@ def create_router(*, authorize, store: ProviderStore, mutate, probe, available,
|
|
|
1141
1191
|
session = session_lookup(session_id) if session_lookup else None
|
|
1142
1192
|
if not session:
|
|
1143
1193
|
raise HTTPException(404, "Chat not found.")
|
|
1144
|
-
selected = await asyncio.to_thread(store.for_session, session, include_key=True)
|
|
1194
|
+
selected = await asyncio.to_thread(store.for_session, requested_session(session), include_key=True)
|
|
1145
1195
|
else:
|
|
1146
1196
|
selected = await asyncio.to_thread(store.selection, include_key=True, include_revision=True)
|
|
1147
1197
|
if selected is None:
|