cctally 1.78.0 → 1.79.1
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/CHANGELOG.md +18 -0
- package/README.md +2 -0
- package/bin/_cctally_cache.py +140 -2
- package/bin/_cctally_config.py +50 -0
- package/bin/_cctally_dashboard.py +177 -20
- package/bin/_cctally_dashboard_conversation.py +1 -1
- package/bin/_cctally_dashboard_envelope.py +12 -0
- package/bin/_cctally_dashboard_sources.py +39 -11
- package/bin/_cctally_doctor.py +16 -1
- package/bin/_cctally_milestone_history.py +966 -0
- package/bin/_cctally_tui.py +23 -0
- package/bin/_cctally_update.py +338 -15
- package/bin/_cctally_weekrefs.py +6 -2
- package/bin/_lib_codex_conversation.py +1342 -18
- package/bin/_lib_codex_conversation_export.py +21 -1
- package/bin/_lib_codex_conversation_query.py +530 -67
- package/bin/_lib_conversation_dispatch.py +2 -2
- package/bin/_lib_doctor.py +38 -0
- package/bin/_lib_milestone_history.py +81 -0
- package/bin/cctally +31 -0
- package/dashboard/static/assets/index-CrIlpAlQ.css +1 -0
- package/dashboard/static/assets/index-Dc2u8c52.js +92 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +3 -1
- package/dashboard/static/assets/index-CkTe6VJt.js +0 -87
- package/dashboard/static/assets/index-DsEXuMpq.css +0 -1
|
@@ -16,6 +16,8 @@ from __future__ import annotations
|
|
|
16
16
|
import dataclasses
|
|
17
17
|
import hashlib
|
|
18
18
|
import json
|
|
19
|
+
import math
|
|
20
|
+
import re
|
|
19
21
|
from typing import Any, Iterable
|
|
20
22
|
|
|
21
23
|
# Reuse the Claude display helpers by IMPORT (never move them). _strip_ansi lives
|
|
@@ -65,6 +67,13 @@ CODEX_TEXT_CAP = 16000
|
|
|
65
67
|
# Display title cap. Equal-by-test to the Claude first-prompt titler's _TITLE_MAX.
|
|
66
68
|
CODEX_TITLE_MAX = _TITLE_MAX
|
|
67
69
|
|
|
70
|
+
# Bumped when the normalized row/item contract changes in a way that requires
|
|
71
|
+
# deterministic replay of retained Codex events. ``sync_codex_conversations``
|
|
72
|
+
# stores this in conversations.db only after a successful full sync; a missing
|
|
73
|
+
# or older value arms the existing byte-zero rebuild path without a schema
|
|
74
|
+
# migration (the store is wholly re-derivable).
|
|
75
|
+
CODEX_CONVERSATION_CONTRACT_VERSION = "5"
|
|
76
|
+
|
|
68
77
|
# Structural wrapper prefixes skipped during title selection (§4.3), pinned from
|
|
69
78
|
# the corpus (title-wrapper-window). Prefix-structural, never content heuristics.
|
|
70
79
|
CODEX_TITLE_SKIP_PREFIXES: tuple[str, ...] = (
|
|
@@ -72,7 +81,7 @@ CODEX_TITLE_SKIP_PREFIXES: tuple[str, ...] = (
|
|
|
72
81
|
"<user_instructions>",
|
|
73
82
|
)
|
|
74
83
|
|
|
75
|
-
|
|
84
|
+
_MIRROR_KINDS = frozenset({"user", "assistant", "reasoning", "meta"})
|
|
76
85
|
|
|
77
86
|
|
|
78
87
|
def _cap(text: str) -> tuple[str, bool]:
|
|
@@ -175,6 +184,11 @@ def _canonical_json(value: Any) -> str:
|
|
|
175
184
|
return ""
|
|
176
185
|
|
|
177
186
|
|
|
187
|
+
def _reject_json_constant(value: str) -> None:
|
|
188
|
+
"""Reject NaN/Infinity tokens so card JSON stays standards-compliant."""
|
|
189
|
+
raise ValueError(f"non-finite JSON constant: {value}")
|
|
190
|
+
|
|
191
|
+
|
|
178
192
|
def _stringify(value: Any) -> str:
|
|
179
193
|
if value is None:
|
|
180
194
|
return ""
|
|
@@ -196,6 +210,636 @@ def _join_content_texts(content: Any) -> str:
|
|
|
196
210
|
return "\n".join(parts)
|
|
197
211
|
|
|
198
212
|
|
|
213
|
+
# ── card-ready native tool shaping (#331 Task A) ─────────────────────────────
|
|
214
|
+
|
|
215
|
+
CODEX_CARD_SCHEMA_VERSION = 1
|
|
216
|
+
_CARD_MAX_COMMANDS = 8
|
|
217
|
+
_CARD_MAX_PARTS = 128
|
|
218
|
+
_CARD_MAX_FILES = 128
|
|
219
|
+
_CARD_MAX_RESULTS = 50
|
|
220
|
+
_CARD_MAX_COLLECTION = 64
|
|
221
|
+
_CARD_HARNESS_PARSE_CAP = 1_000_000
|
|
222
|
+
_AGENT_OPERATIONS = frozenset({
|
|
223
|
+
"spawn_agent", "wait_agent", "send_message", "list_agents",
|
|
224
|
+
"followup_task", "interrupt_agent",
|
|
225
|
+
})
|
|
226
|
+
_EXEC_METADATA_KEYS = frozenset({
|
|
227
|
+
"justification", "login", "max_output_tokens", "prefix_rule",
|
|
228
|
+
"sandbox_permissions", "shell", "tty", "yield_time_ms",
|
|
229
|
+
})
|
|
230
|
+
_HARNESS_STATUS_RE = re.compile(
|
|
231
|
+
r"\A(Script completed|Script failed)\nWall time ([^\n]+)\n\nOutput:\Z")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class _LiteralError(ValueError):
|
|
235
|
+
"""Closed-parser rejection; callers preserve the raw provider payload."""
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class _TextBudget:
|
|
239
|
+
def __init__(self, limit: int):
|
|
240
|
+
self.remaining = max(0, int(limit))
|
|
241
|
+
self.truncated = False
|
|
242
|
+
|
|
243
|
+
def take(self, value: str) -> str:
|
|
244
|
+
if len(value) <= self.remaining:
|
|
245
|
+
self.remaining -= len(value)
|
|
246
|
+
return value
|
|
247
|
+
kept = value[:self.remaining]
|
|
248
|
+
self.remaining = 0
|
|
249
|
+
self.truncated = True
|
|
250
|
+
return kept
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class _HarnessLiteralParser:
|
|
254
|
+
"""Tiny non-executing parser for the JSON-like values in Codex harnesses.
|
|
255
|
+
|
|
256
|
+
It accepts only objects, arrays, double-quoted JSON strings, finite JSON
|
|
257
|
+
numbers, booleans and null. Object keys may be unquoted harness tokens
|
|
258
|
+
(including ``yield_time-ms``-style hyphens). Expressions, identifiers as
|
|
259
|
+
values, templates, comments and trailing code reject the shape.
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
def __init__(self, source: str, pos: int = 0, *, max_depth: int = 8):
|
|
263
|
+
self.source = source
|
|
264
|
+
self.pos = pos
|
|
265
|
+
self.max_depth = max_depth
|
|
266
|
+
|
|
267
|
+
def _ws(self) -> None:
|
|
268
|
+
size = len(self.source)
|
|
269
|
+
while self.pos < size and self.source[self.pos].isspace():
|
|
270
|
+
self.pos += 1
|
|
271
|
+
|
|
272
|
+
def _consume(self, token: str) -> None:
|
|
273
|
+
self._ws()
|
|
274
|
+
if not self.source.startswith(token, self.pos):
|
|
275
|
+
raise _LiteralError(token)
|
|
276
|
+
self.pos += len(token)
|
|
277
|
+
|
|
278
|
+
def _string(self) -> str:
|
|
279
|
+
self._ws()
|
|
280
|
+
if self.pos >= len(self.source) or self.source[self.pos] != '"':
|
|
281
|
+
raise _LiteralError("string")
|
|
282
|
+
try:
|
|
283
|
+
value, end = json.JSONDecoder().raw_decode(self.source, self.pos)
|
|
284
|
+
except (json.JSONDecodeError, TypeError) as exc:
|
|
285
|
+
raise _LiteralError("string") from exc
|
|
286
|
+
if not isinstance(value, str):
|
|
287
|
+
raise _LiteralError("string")
|
|
288
|
+
self.pos = end
|
|
289
|
+
return value
|
|
290
|
+
|
|
291
|
+
def _key(self) -> str:
|
|
292
|
+
self._ws()
|
|
293
|
+
if self.pos < len(self.source) and self.source[self.pos] == '"':
|
|
294
|
+
return self._string()
|
|
295
|
+
match = re.match(r"[A-Za-z_$][A-Za-z0-9_$-]*", self.source[self.pos:])
|
|
296
|
+
if match is None:
|
|
297
|
+
raise _LiteralError("key")
|
|
298
|
+
self.pos += len(match.group(0))
|
|
299
|
+
return match.group(0)
|
|
300
|
+
|
|
301
|
+
def value(self, depth: int = 0) -> Any:
|
|
302
|
+
if depth > self.max_depth:
|
|
303
|
+
raise _LiteralError("depth")
|
|
304
|
+
self._ws()
|
|
305
|
+
if self.pos >= len(self.source):
|
|
306
|
+
raise _LiteralError("value")
|
|
307
|
+
char = self.source[self.pos]
|
|
308
|
+
if char == '"':
|
|
309
|
+
return self._string()
|
|
310
|
+
if char == "{":
|
|
311
|
+
self.pos += 1
|
|
312
|
+
result: dict[str, Any] = {}
|
|
313
|
+
self._ws()
|
|
314
|
+
if self.pos < len(self.source) and self.source[self.pos] == "}":
|
|
315
|
+
self.pos += 1
|
|
316
|
+
return result
|
|
317
|
+
while len(result) < 64:
|
|
318
|
+
key = self._key()
|
|
319
|
+
if key in result:
|
|
320
|
+
raise _LiteralError("duplicate key")
|
|
321
|
+
self._consume(":")
|
|
322
|
+
result[key] = self.value(depth + 1)
|
|
323
|
+
self._ws()
|
|
324
|
+
if self.pos < len(self.source) and self.source[self.pos] == "}":
|
|
325
|
+
self.pos += 1
|
|
326
|
+
return result
|
|
327
|
+
self._consume(",")
|
|
328
|
+
raise _LiteralError("object size")
|
|
329
|
+
if char == "[":
|
|
330
|
+
self.pos += 1
|
|
331
|
+
result_list: list[Any] = []
|
|
332
|
+
self._ws()
|
|
333
|
+
if self.pos < len(self.source) and self.source[self.pos] == "]":
|
|
334
|
+
self.pos += 1
|
|
335
|
+
return result_list
|
|
336
|
+
while len(result_list) < 64:
|
|
337
|
+
result_list.append(self.value(depth + 1))
|
|
338
|
+
self._ws()
|
|
339
|
+
if self.pos < len(self.source) and self.source[self.pos] == "]":
|
|
340
|
+
self.pos += 1
|
|
341
|
+
return result_list
|
|
342
|
+
self._consume(",")
|
|
343
|
+
raise _LiteralError("array size")
|
|
344
|
+
for token, value in (("true", True), ("false", False), ("null", None)):
|
|
345
|
+
if self.source.startswith(token, self.pos):
|
|
346
|
+
self.pos += len(token)
|
|
347
|
+
return value
|
|
348
|
+
match = re.match(r"-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?",
|
|
349
|
+
self.source[self.pos:])
|
|
350
|
+
if match is None:
|
|
351
|
+
raise _LiteralError("literal")
|
|
352
|
+
token = match.group(0)
|
|
353
|
+
if len(token) > 256:
|
|
354
|
+
raise _LiteralError("number length")
|
|
355
|
+
self.pos += len(token)
|
|
356
|
+
try:
|
|
357
|
+
value = float(token) if any(c in token for c in ".eE") else int(token)
|
|
358
|
+
except (OverflowError, ValueError) as exc:
|
|
359
|
+
raise _LiteralError("number") from exc
|
|
360
|
+
if isinstance(value, float) and not (-float("inf") < value < float("inf")):
|
|
361
|
+
raise _LiteralError("finite number")
|
|
362
|
+
return value
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _bounded_metadata(value: Any, budget: _TextBudget) -> Any:
|
|
366
|
+
if isinstance(value, str):
|
|
367
|
+
return budget.take(value)
|
|
368
|
+
if isinstance(value, (bool, int, float)):
|
|
369
|
+
return value
|
|
370
|
+
if (isinstance(value, list) and len(value) <= 32
|
|
371
|
+
and all(isinstance(part, str) for part in value)):
|
|
372
|
+
return [budget.take(part) for part in value]
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _bounded_json(value: Any, budget: _TextBudget, *, depth: int = 0) -> Any:
|
|
377
|
+
"""Retain JSON structure under one shared text/shape budget.
|
|
378
|
+
|
|
379
|
+
Native payload readback remains authoritative; this projection is only the
|
|
380
|
+
bounded card wire. Collection overflow is explicit through the caller's
|
|
381
|
+
``budget.truncated`` flag rather than silently expanding the detail route.
|
|
382
|
+
"""
|
|
383
|
+
if depth > 8:
|
|
384
|
+
budget.truncated = True
|
|
385
|
+
return None
|
|
386
|
+
if isinstance(value, str):
|
|
387
|
+
return budget.take(value)
|
|
388
|
+
if value is None or isinstance(value, (bool, int)):
|
|
389
|
+
return value
|
|
390
|
+
if isinstance(value, float):
|
|
391
|
+
if math.isfinite(value):
|
|
392
|
+
return value
|
|
393
|
+
budget.truncated = True
|
|
394
|
+
return None
|
|
395
|
+
if isinstance(value, list):
|
|
396
|
+
if len(value) > _CARD_MAX_COLLECTION:
|
|
397
|
+
budget.truncated = True
|
|
398
|
+
return [
|
|
399
|
+
_bounded_json(part, budget, depth=depth + 1)
|
|
400
|
+
for part in value[:_CARD_MAX_COLLECTION]
|
|
401
|
+
]
|
|
402
|
+
if isinstance(value, dict):
|
|
403
|
+
if len(value) > _CARD_MAX_COLLECTION:
|
|
404
|
+
budget.truncated = True
|
|
405
|
+
result = {}
|
|
406
|
+
for key, part in list(value.items())[:_CARD_MAX_COLLECTION]:
|
|
407
|
+
if not isinstance(key, str):
|
|
408
|
+
budget.truncated = True
|
|
409
|
+
continue
|
|
410
|
+
result[budget.take(key)] = _bounded_json(
|
|
411
|
+
part, budget, depth=depth + 1)
|
|
412
|
+
return result
|
|
413
|
+
budget.truncated = True
|
|
414
|
+
return None
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _json_object(value: Any) -> dict | None:
|
|
418
|
+
if isinstance(value, dict):
|
|
419
|
+
return value
|
|
420
|
+
if not isinstance(value, str) or len(value) > _CARD_HARNESS_PARSE_CAP:
|
|
421
|
+
return None
|
|
422
|
+
try:
|
|
423
|
+
parsed = json.loads(value, parse_constant=_reject_json_constant)
|
|
424
|
+
except (json.JSONDecodeError, TypeError, ValueError):
|
|
425
|
+
return None
|
|
426
|
+
return parsed if isinstance(parsed, dict) else None
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def decode_secondary_tool_call_card(
|
|
430
|
+
payload: dict, *, text_cap: int = CODEX_TEXT_CAP,
|
|
431
|
+
) -> dict | None:
|
|
432
|
+
"""Bounded additive wire for plans, web actions, and agent operations.
|
|
433
|
+
|
|
434
|
+
Unknown or malformed shapes deliberately return ``None`` so the existing
|
|
435
|
+
provider-name + raw-argument fallback remains visible and payload readback
|
|
436
|
+
stays authoritative.
|
|
437
|
+
"""
|
|
438
|
+
if not isinstance(payload, dict):
|
|
439
|
+
return None
|
|
440
|
+
ptype = payload.get("type")
|
|
441
|
+
name = payload.get("name")
|
|
442
|
+
status = payload.get("status") if isinstance(payload.get("status"), str) else "requested"
|
|
443
|
+
budget = _TextBudget(text_cap)
|
|
444
|
+
if ptype == "function_call" and name == "update_plan":
|
|
445
|
+
arguments = _json_object(payload.get("arguments"))
|
|
446
|
+
plan = arguments.get("plan") if isinstance(arguments, dict) else None
|
|
447
|
+
if not isinstance(plan, list) or len(plan) > _CARD_MAX_COLLECTION:
|
|
448
|
+
return None
|
|
449
|
+
items = []
|
|
450
|
+
for item in plan:
|
|
451
|
+
if not (isinstance(item, dict)
|
|
452
|
+
and isinstance(item.get("step"), str)
|
|
453
|
+
and isinstance(item.get("status"), str)):
|
|
454
|
+
return None
|
|
455
|
+
items.append({
|
|
456
|
+
"step": budget.take(item["step"]),
|
|
457
|
+
"status": budget.take(item["status"]),
|
|
458
|
+
})
|
|
459
|
+
card = {
|
|
460
|
+
"schema_version": CODEX_CARD_SCHEMA_VERSION,
|
|
461
|
+
"type": "plan", "source": "update_plan",
|
|
462
|
+
"call_status": status, "items": items,
|
|
463
|
+
}
|
|
464
|
+
explanation = arguments.get("explanation")
|
|
465
|
+
if explanation is not None:
|
|
466
|
+
if not isinstance(explanation, str):
|
|
467
|
+
return None
|
|
468
|
+
card["explanation"] = budget.take(explanation)
|
|
469
|
+
if budget.truncated:
|
|
470
|
+
card["truncated"] = True
|
|
471
|
+
return card
|
|
472
|
+
if ptype == "web_search_call":
|
|
473
|
+
action = payload.get("action")
|
|
474
|
+
if not isinstance(action, (dict, str)):
|
|
475
|
+
return None
|
|
476
|
+
bounded_action = _bounded_json(action, budget)
|
|
477
|
+
card = {
|
|
478
|
+
"schema_version": CODEX_CARD_SCHEMA_VERSION,
|
|
479
|
+
"type": "web_search", "source": "web_search_call",
|
|
480
|
+
"call_status": status, "action": bounded_action,
|
|
481
|
+
}
|
|
482
|
+
if isinstance(action, dict) and isinstance(action.get("query"), str):
|
|
483
|
+
card["query"] = budget.take(action["query"])
|
|
484
|
+
if budget.truncated:
|
|
485
|
+
card["truncated"] = True
|
|
486
|
+
return card
|
|
487
|
+
if ptype == "function_call" and name in _AGENT_OPERATIONS:
|
|
488
|
+
arguments = _json_object(payload.get("arguments"))
|
|
489
|
+
if arguments is None:
|
|
490
|
+
return None
|
|
491
|
+
bounded = _bounded_json(arguments, budget)
|
|
492
|
+
card = {
|
|
493
|
+
"schema_version": CODEX_CARD_SCHEMA_VERSION,
|
|
494
|
+
"type": "agent", "operation": name,
|
|
495
|
+
"call_status": status, "arguments": bounded,
|
|
496
|
+
}
|
|
497
|
+
if budget.truncated:
|
|
498
|
+
card["truncated"] = True
|
|
499
|
+
return card
|
|
500
|
+
return None
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def decode_secondary_tool_result(
|
|
504
|
+
payload: dict, *, text_cap: int = CODEX_TEXT_CAP,
|
|
505
|
+
) -> dict | None:
|
|
506
|
+
if not isinstance(payload, dict) or payload.get("type") not in _RESPONSE_TOOL_OUTPUTS:
|
|
507
|
+
return None
|
|
508
|
+
value = payload["output"] if "output" in payload else payload.get("tools")
|
|
509
|
+
if isinstance(value, str) and len(value) <= _CARD_HARNESS_PARSE_CAP:
|
|
510
|
+
try:
|
|
511
|
+
parsed = json.loads(value, parse_constant=_reject_json_constant)
|
|
512
|
+
except (json.JSONDecodeError, TypeError, ValueError):
|
|
513
|
+
parsed = value
|
|
514
|
+
else:
|
|
515
|
+
parsed = value
|
|
516
|
+
budget = _TextBudget(text_cap)
|
|
517
|
+
bounded = _bounded_json(parsed, budget)
|
|
518
|
+
status = payload.get("status") if isinstance(payload.get("status"), str) else "returned"
|
|
519
|
+
return {"status": status, "value": bounded, "truncated": budget.truncated}
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def decode_secondary_event_card(
|
|
523
|
+
payload: dict, *, text_cap: int = CODEX_TEXT_CAP,
|
|
524
|
+
) -> dict | None:
|
|
525
|
+
if not isinstance(payload, dict):
|
|
526
|
+
return None
|
|
527
|
+
ptype = payload.get("type")
|
|
528
|
+
budget = _TextBudget(text_cap)
|
|
529
|
+
if ptype == "web_search_end":
|
|
530
|
+
action = payload.get("action")
|
|
531
|
+
query = payload.get("query")
|
|
532
|
+
results = payload.get("results", [])
|
|
533
|
+
if not isinstance(action, (dict, str)) or not isinstance(query, str):
|
|
534
|
+
return None
|
|
535
|
+
if not isinstance(results, list):
|
|
536
|
+
return None
|
|
537
|
+
if len(results) > _CARD_MAX_RESULTS:
|
|
538
|
+
budget.truncated = True
|
|
539
|
+
card = {
|
|
540
|
+
"schema_version": CODEX_CARD_SCHEMA_VERSION,
|
|
541
|
+
"type": "web_search_completion", "source": "web_search_end",
|
|
542
|
+
"status": (payload.get("status")
|
|
543
|
+
if isinstance(payload.get("status"), str) else "returned"),
|
|
544
|
+
"query": budget.take(query),
|
|
545
|
+
"action": _bounded_json(action, budget),
|
|
546
|
+
"results": [
|
|
547
|
+
_bounded_json(result, budget)
|
|
548
|
+
for result in results[:_CARD_MAX_RESULTS]
|
|
549
|
+
],
|
|
550
|
+
}
|
|
551
|
+
if "error" in payload:
|
|
552
|
+
card["error"] = _bounded_json(payload.get("error"), budget)
|
|
553
|
+
if budget.truncated:
|
|
554
|
+
card["truncated"] = True
|
|
555
|
+
return card
|
|
556
|
+
if ptype == "mcp_tool_call_end":
|
|
557
|
+
invocation = payload.get("invocation")
|
|
558
|
+
if not isinstance(invocation, dict):
|
|
559
|
+
return None
|
|
560
|
+
server = invocation.get("server")
|
|
561
|
+
tool = invocation.get("tool") or invocation.get("name")
|
|
562
|
+
if server is not None and not isinstance(server, str):
|
|
563
|
+
return None
|
|
564
|
+
if not isinstance(tool, str):
|
|
565
|
+
return None
|
|
566
|
+
result = payload.get("result")
|
|
567
|
+
if isinstance(result, dict) and "Ok" in result:
|
|
568
|
+
status = "ok"
|
|
569
|
+
elif isinstance(result, dict) and any(key in result for key in ("Err", "Error")):
|
|
570
|
+
status = "error"
|
|
571
|
+
else:
|
|
572
|
+
status = (payload.get("status")
|
|
573
|
+
if isinstance(payload.get("status"), str) else "returned")
|
|
574
|
+
card = {
|
|
575
|
+
"schema_version": CODEX_CARD_SCHEMA_VERSION,
|
|
576
|
+
"type": "mcp_completion", "source": "mcp_tool_call_end",
|
|
577
|
+
"status": status,
|
|
578
|
+
"server": budget.take(server) if isinstance(server, str) else None,
|
|
579
|
+
"tool": budget.take(tool),
|
|
580
|
+
"arguments": _bounded_json(invocation.get("arguments"), budget),
|
|
581
|
+
"result": _bounded_json(result, budget),
|
|
582
|
+
"duration": _bounded_json(payload.get("duration"), budget),
|
|
583
|
+
}
|
|
584
|
+
if budget.truncated:
|
|
585
|
+
card["truncated"] = True
|
|
586
|
+
return card
|
|
587
|
+
return None
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _exec_invocations(source: str, *, budget: _TextBudget) -> list[dict] | None:
|
|
591
|
+
"""Decode only the complete current exec harness statement grammar."""
|
|
592
|
+
commands: list[dict] = []
|
|
593
|
+
pos = 0
|
|
594
|
+
while True:
|
|
595
|
+
prefix = re.match(
|
|
596
|
+
r"\s*const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*"
|
|
597
|
+
r"await\s+tools\.exec_command",
|
|
598
|
+
source[pos:],
|
|
599
|
+
)
|
|
600
|
+
if prefix is None or len(commands) >= _CARD_MAX_COMMANDS:
|
|
601
|
+
return None
|
|
602
|
+
variable = prefix.group(1)
|
|
603
|
+
parser = _HarnessLiteralParser(source, pos + prefix.end())
|
|
604
|
+
try:
|
|
605
|
+
parser._consume("(")
|
|
606
|
+
value = parser.value()
|
|
607
|
+
parser._consume(")")
|
|
608
|
+
except _LiteralError:
|
|
609
|
+
return None
|
|
610
|
+
if not isinstance(value, dict) or not isinstance(value.get("cmd"), str):
|
|
611
|
+
return None
|
|
612
|
+
workdir = value.get("workdir")
|
|
613
|
+
if workdir is not None and not isinstance(workdir, str):
|
|
614
|
+
return None
|
|
615
|
+
command = {
|
|
616
|
+
"workdir": budget.take(workdir) if isinstance(workdir, str) else None,
|
|
617
|
+
"command": "",
|
|
618
|
+
"metadata": {},
|
|
619
|
+
}
|
|
620
|
+
command["command"] = budget.take(value["cmd"])
|
|
621
|
+
for key in sorted(_EXEC_METADATA_KEYS):
|
|
622
|
+
if key not in value:
|
|
623
|
+
continue
|
|
624
|
+
bounded = _bounded_metadata(value[key], budget)
|
|
625
|
+
if bounded is not None:
|
|
626
|
+
command["metadata"][key] = bounded
|
|
627
|
+
commands.append(command)
|
|
628
|
+
suffix = re.match(
|
|
629
|
+
r"\s*;\s*text\s*\(\s*" + re.escape(variable)
|
|
630
|
+
+ r"\.output\s*\)\s*;?",
|
|
631
|
+
source[parser.pos:],
|
|
632
|
+
)
|
|
633
|
+
if suffix is None:
|
|
634
|
+
return None
|
|
635
|
+
pos = parser.pos + suffix.end()
|
|
636
|
+
if not source[pos:].strip():
|
|
637
|
+
return commands
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _decode_apply_patch_program(source: str) -> str | None:
|
|
641
|
+
"""Recognize the exact current ``const patch`` apply_patch harness."""
|
|
642
|
+
match = re.match(r"\A\s*const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*", source)
|
|
643
|
+
if match is None:
|
|
644
|
+
return None
|
|
645
|
+
parser = _HarnessLiteralParser(source, match.end())
|
|
646
|
+
try:
|
|
647
|
+
patch = parser._string()
|
|
648
|
+
except _LiteralError:
|
|
649
|
+
return None
|
|
650
|
+
variable = match.group(1)
|
|
651
|
+
tail = source[parser.pos:]
|
|
652
|
+
pattern = (
|
|
653
|
+
r"\s*;\s*text\s*\(\s*await\s+tools\.apply_patch\s*\(\s*"
|
|
654
|
+
+ re.escape(variable)
|
|
655
|
+
+ r"\s*\)\s*\)\s*;?\s*\Z"
|
|
656
|
+
)
|
|
657
|
+
return patch if re.fullmatch(pattern, tail) else None
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _apply_patch_heredoc(command: str) -> str | None:
|
|
661
|
+
for pattern in (
|
|
662
|
+
r"\A\s*apply_patch\s+<<'([A-Za-z_][A-Za-z0-9_]*)'\n(.*)\n\1\s*\Z",
|
|
663
|
+
r'\A\s*apply_patch\s+<<"([A-Za-z_][A-Za-z0-9_]*)"\n(.*)\n\1\s*\Z',
|
|
664
|
+
r"\A\s*apply_patch\s+<<([A-Za-z_][A-Za-z0-9_]*)\n(.*)\n\1\s*\Z",
|
|
665
|
+
):
|
|
666
|
+
match = re.fullmatch(pattern, command, re.DOTALL)
|
|
667
|
+
if match is not None:
|
|
668
|
+
return match.group(2)
|
|
669
|
+
return None
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _patch_files_from_apply_patch(
|
|
673
|
+
patch: str, budget: _TextBudget | None = None,
|
|
674
|
+
) -> list[dict]:
|
|
675
|
+
files: list[dict] = []
|
|
676
|
+
for line in patch.splitlines():
|
|
677
|
+
match = re.match(r"\*\*\* (Add|Update|Delete) File: (.+)\Z", line)
|
|
678
|
+
if match is not None and len(files) < _CARD_MAX_FILES:
|
|
679
|
+
status = {"Add": "added", "Update": "modified", "Delete": "deleted"}[
|
|
680
|
+
match.group(1)]
|
|
681
|
+
path = budget.take(match.group(2)) if budget is not None else match.group(2)
|
|
682
|
+
files.append({"path": path, "status": status})
|
|
683
|
+
continue
|
|
684
|
+
move = re.match(r"\*\*\* Move to: (.+)\Z", line)
|
|
685
|
+
if move is not None and files:
|
|
686
|
+
files[-1]["move_path"] = (
|
|
687
|
+
budget.take(move.group(1)) if budget is not None else move.group(1))
|
|
688
|
+
files[-1]["status"] = "moved"
|
|
689
|
+
return files
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _complete_apply_patch(patch: str) -> bool:
|
|
693
|
+
"""Require one closed apply_patch envelope with at least one file action."""
|
|
694
|
+
lines = patch.splitlines()
|
|
695
|
+
if (not lines or lines[0] != "*** Begin Patch"
|
|
696
|
+
or lines[-1] != "*** End Patch"):
|
|
697
|
+
return False
|
|
698
|
+
if lines.count("*** Begin Patch") != 1 or lines.count("*** End Patch") != 1:
|
|
699
|
+
return False
|
|
700
|
+
return any(re.fullmatch(r"\*\*\* (?:Add|Update|Delete) File: .+", line)
|
|
701
|
+
for line in lines[1:-1])
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def decode_tool_call_card(payload: dict, *, text_cap: int = CODEX_TEXT_CAP) -> dict | None:
|
|
705
|
+
"""Return the additive card contract for a structurally proven call."""
|
|
706
|
+
if not isinstance(payload, dict) or payload.get("type") != "custom_tool_call":
|
|
707
|
+
return None
|
|
708
|
+
name = payload.get("name")
|
|
709
|
+
value = payload.get("input")
|
|
710
|
+
if not isinstance(name, str) or not isinstance(value, str):
|
|
711
|
+
return None
|
|
712
|
+
if len(value) > _CARD_HARNESS_PARSE_CAP:
|
|
713
|
+
return None
|
|
714
|
+
budget = _TextBudget(text_cap)
|
|
715
|
+
status = payload.get("status") if isinstance(payload.get("status"), str) else "unknown"
|
|
716
|
+
if name == "apply_patch" and _complete_apply_patch(value):
|
|
717
|
+
files = _patch_files_from_apply_patch(value, budget)
|
|
718
|
+
patch = budget.take(value)
|
|
719
|
+
return {
|
|
720
|
+
"schema_version": CODEX_CARD_SCHEMA_VERSION,
|
|
721
|
+
"type": "patch", "source": "apply_patch", "status": status,
|
|
722
|
+
"patch": patch, "files": files,
|
|
723
|
+
"truncated": budget.truncated,
|
|
724
|
+
}
|
|
725
|
+
if name != "exec":
|
|
726
|
+
return None
|
|
727
|
+
patch_value = _decode_apply_patch_program(value)
|
|
728
|
+
if patch_value is not None and _complete_apply_patch(patch_value):
|
|
729
|
+
files = _patch_files_from_apply_patch(patch_value, budget)
|
|
730
|
+
patch = budget.take(patch_value)
|
|
731
|
+
return {
|
|
732
|
+
"schema_version": CODEX_CARD_SCHEMA_VERSION,
|
|
733
|
+
"type": "patch", "source": "tools.apply_patch", "status": status,
|
|
734
|
+
"patch": patch, "files": files,
|
|
735
|
+
"truncated": budget.truncated,
|
|
736
|
+
}
|
|
737
|
+
commands = _exec_invocations(value, budget=budget)
|
|
738
|
+
if commands is None:
|
|
739
|
+
return None
|
|
740
|
+
if len(commands) == 1:
|
|
741
|
+
heredoc = _apply_patch_heredoc(commands[0]["command"])
|
|
742
|
+
if heredoc is not None and _complete_apply_patch(heredoc):
|
|
743
|
+
files = _patch_files_from_apply_patch(heredoc, budget)
|
|
744
|
+
return {
|
|
745
|
+
"schema_version": CODEX_CARD_SCHEMA_VERSION,
|
|
746
|
+
"type": "patch", "source": "exec_apply_patch", "status": status,
|
|
747
|
+
"patch": budget.take(heredoc), "files": files,
|
|
748
|
+
"workdir": commands[0]["workdir"], "truncated": budget.truncated,
|
|
749
|
+
}
|
|
750
|
+
card = {
|
|
751
|
+
"schema_version": CODEX_CARD_SCHEMA_VERSION,
|
|
752
|
+
"type": "terminal", "status": status, "commands": commands,
|
|
753
|
+
}
|
|
754
|
+
if budget.truncated:
|
|
755
|
+
card["truncated"] = True
|
|
756
|
+
return card
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def decode_tool_output_card(
|
|
760
|
+
payload: dict, *, text_cap: int = CODEX_TEXT_CAP,
|
|
761
|
+
) -> tuple[dict, str] | None:
|
|
762
|
+
"""Unwrap supported output envelopes without losing malformed parts."""
|
|
763
|
+
if not isinstance(payload, dict) or payload.get("type") not in _RESPONSE_TOOL_OUTPUTS:
|
|
764
|
+
return None
|
|
765
|
+
value = payload["output"] if "output" in payload else payload.get("tools")
|
|
766
|
+
values = value if isinstance(value, list) else [value]
|
|
767
|
+
budget = _TextBudget(text_cap)
|
|
768
|
+
parts: list[dict] = []
|
|
769
|
+
status = payload.get("status") if isinstance(payload.get("status"), str) else "unknown"
|
|
770
|
+
for index, part in enumerate(values[:_CARD_MAX_PARTS]):
|
|
771
|
+
if isinstance(part, str):
|
|
772
|
+
text = part
|
|
773
|
+
match = _HARNESS_STATUS_RE.fullmatch(text) if index == 0 else None
|
|
774
|
+
if match is not None:
|
|
775
|
+
status = "completed" if match.group(1) == "Script completed" else "failed"
|
|
776
|
+
continue
|
|
777
|
+
parts.append({"type": "text", "stream": "output", "text": budget.take(text)})
|
|
778
|
+
continue
|
|
779
|
+
if isinstance(part, dict) and part.get("type") == "input_text" \
|
|
780
|
+
and isinstance(part.get("text"), str):
|
|
781
|
+
text = part["text"]
|
|
782
|
+
match = _HARNESS_STATUS_RE.fullmatch(text) if index == 0 else None
|
|
783
|
+
if match is not None:
|
|
784
|
+
status = "completed" if match.group(1) == "Script completed" else "failed"
|
|
785
|
+
continue
|
|
786
|
+
stream = part.get("stream") if part.get("stream") in {"stdout", "stderr"} else "output"
|
|
787
|
+
parts.append({"type": "text", "stream": stream, "text": budget.take(text)})
|
|
788
|
+
continue
|
|
789
|
+
raw = _canonical_json(part)
|
|
790
|
+
parts.append({"type": "raw", "stream": "output", "text": budget.take(raw)})
|
|
791
|
+
if len(values) > _CARD_MAX_PARTS:
|
|
792
|
+
budget.truncated = True
|
|
793
|
+
card = {
|
|
794
|
+
"schema_version": CODEX_CARD_SCHEMA_VERSION,
|
|
795
|
+
"type": "terminal_output", "status": status,
|
|
796
|
+
"is_error": status in {"failed", "error"},
|
|
797
|
+
"parts": parts, "truncated": budget.truncated,
|
|
798
|
+
}
|
|
799
|
+
return card, "".join(part["text"] for part in parts)
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def decode_patch_event_card(payload: dict, *, text_cap: int = CODEX_TEXT_CAP) -> dict | None:
|
|
803
|
+
"""Bounded, provider-truthful ``patch_apply_end`` projection."""
|
|
804
|
+
if not isinstance(payload, dict) or payload.get("type") != "patch_apply_end":
|
|
805
|
+
return None
|
|
806
|
+
budget = _TextBudget(text_cap)
|
|
807
|
+
files: list[dict] = []
|
|
808
|
+
changes = payload.get("changes")
|
|
809
|
+
if isinstance(changes, list):
|
|
810
|
+
for change in changes[:_CARD_MAX_FILES]:
|
|
811
|
+
if not isinstance(change, dict):
|
|
812
|
+
files.append({"raw": budget.take(_canonical_json(change))})
|
|
813
|
+
continue
|
|
814
|
+
entry: dict[str, Any] = {}
|
|
815
|
+
for key in ("path", "move_path", "status"):
|
|
816
|
+
if isinstance(change.get(key), str):
|
|
817
|
+
entry[key] = budget.take(change[key])
|
|
818
|
+
if isinstance(change.get("unified_diff"), str):
|
|
819
|
+
entry["unified_diff"] = budget.take(change["unified_diff"])
|
|
820
|
+
unknown = {key: value for key, value in change.items()
|
|
821
|
+
if key not in {"path", "move_path", "status", "unified_diff"}}
|
|
822
|
+
if unknown:
|
|
823
|
+
entry["raw_extra"] = budget.take(_canonical_json(unknown))
|
|
824
|
+
files.append(entry)
|
|
825
|
+
if len(changes) > _CARD_MAX_FILES:
|
|
826
|
+
budget.truncated = True
|
|
827
|
+
elif "changes" in payload:
|
|
828
|
+
files.append({"raw": budget.take(_canonical_json(changes))})
|
|
829
|
+
stdout = budget.take(payload["stdout"]) if isinstance(payload.get("stdout"), str) else None
|
|
830
|
+
stderr = budget.take(payload["stderr"]) if isinstance(payload.get("stderr"), str) else None
|
|
831
|
+
status = payload.get("status") if isinstance(payload.get("status"), str) else "unknown"
|
|
832
|
+
success = payload.get("success") if isinstance(payload.get("success"), bool) else None
|
|
833
|
+
return {
|
|
834
|
+
"schema_version": CODEX_CARD_SCHEMA_VERSION,
|
|
835
|
+
"type": "patch", "source": "patch_apply_end",
|
|
836
|
+
"files": files,
|
|
837
|
+
"has_diff": any(isinstance(entry.get("unified_diff"), str) for entry in files),
|
|
838
|
+
"status": status, "success": success, "stdout": stdout, "stderr": stderr,
|
|
839
|
+
"truncated": budget.truncated,
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
|
|
199
843
|
@dataclasses.dataclass
|
|
200
844
|
class _Extracted:
|
|
201
845
|
kind: str
|
|
@@ -203,6 +847,334 @@ class _Extracted:
|
|
|
203
847
|
column: str # "text" | "search_tool" | "search_thinking"
|
|
204
848
|
detail: dict | None
|
|
205
849
|
touches: list[tuple[str, str]] # (file_path, tool)
|
|
850
|
+
identity_text: str | None = None # raw pre-segmentation mirror/payload identity
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
_REASONING_TITLE_RE = re.compile(r"\A\*\*([^\n]+)\*\*\Z")
|
|
854
|
+
_MARKER_DIRECTIVE_RE = re.compile(r"\A::([a-z0-9-]+)\{(.*)\}\Z")
|
|
855
|
+
_MARKER_ATTR_RE = re.compile(
|
|
856
|
+
r'([A-Za-z][A-Za-z0-9]*)=("(?:[^"\\]|\\.)*"|true|false)')
|
|
857
|
+
_MARKER_DIRECTIVES = {
|
|
858
|
+
"git-create-branch": ({"cwd", "branch"}, "create_branch"),
|
|
859
|
+
"git-stage": ({"cwd"}, "stage"),
|
|
860
|
+
"git-commit": ({"cwd"}, "commit"),
|
|
861
|
+
"git-push": ({"cwd", "branch"}, "push"),
|
|
862
|
+
"git-create-pr": ({"cwd", "branch", "url", "isDraft"}, "create_pr"),
|
|
863
|
+
}
|
|
864
|
+
_MEM_CITATION_RE = re.compile(
|
|
865
|
+
r"\A[^<>\r\n]+:\d+-\d+\|note=\[[^\]\r\n]*\]\Z")
|
|
866
|
+
_ROLLOUT_ID_RE = re.compile(
|
|
867
|
+
r"\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\Z",
|
|
868
|
+
re.IGNORECASE,
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def _reasoning_projection(
|
|
873
|
+
*, source: str, summary: str = "", body: str = "",
|
|
874
|
+
) -> dict | None:
|
|
875
|
+
"""Provider-truthful title/summary/body projection for non-empty reasoning."""
|
|
876
|
+
summary_nonblank = summary.strip()
|
|
877
|
+
body_nonblank = body.strip()
|
|
878
|
+
if not summary_nonblank and not body_nonblank:
|
|
879
|
+
return None
|
|
880
|
+
result = {"schema_version": 1, "source": source}
|
|
881
|
+
match = _REASONING_TITLE_RE.fullmatch(summary_nonblank)
|
|
882
|
+
if match is not None:
|
|
883
|
+
title = match.group(1)
|
|
884
|
+
if title.strip() == title and "**" not in title:
|
|
885
|
+
result["title"] = title
|
|
886
|
+
else:
|
|
887
|
+
result["summary"] = summary_nonblank
|
|
888
|
+
elif summary_nonblank:
|
|
889
|
+
result["summary"] = summary_nonblank
|
|
890
|
+
if body_nonblank:
|
|
891
|
+
result["body"] = body_nonblank
|
|
892
|
+
return result
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _parse_marker_directive(line: str) -> dict | None:
|
|
896
|
+
matched = _MARKER_DIRECTIVE_RE.fullmatch(line)
|
|
897
|
+
if matched is None or matched.group(1) not in _MARKER_DIRECTIVES:
|
|
898
|
+
return None
|
|
899
|
+
name, source = matched.group(1), matched.group(2)
|
|
900
|
+
attrs: dict[str, object] = {}
|
|
901
|
+
pos = 0
|
|
902
|
+
while pos < len(source):
|
|
903
|
+
while pos < len(source) and source[pos] == " ":
|
|
904
|
+
pos += 1
|
|
905
|
+
token = _MARKER_ATTR_RE.match(source, pos)
|
|
906
|
+
if token is None or token.group(1) in attrs:
|
|
907
|
+
return None
|
|
908
|
+
raw = token.group(2)
|
|
909
|
+
try:
|
|
910
|
+
value = json.loads(raw) if raw.startswith('"') else raw == "true"
|
|
911
|
+
except (json.JSONDecodeError, TypeError):
|
|
912
|
+
return None
|
|
913
|
+
attrs[token.group(1)] = value
|
|
914
|
+
pos = token.end()
|
|
915
|
+
if pos < len(source) and source[pos] != " ":
|
|
916
|
+
return None
|
|
917
|
+
required, action = _MARKER_DIRECTIVES[name]
|
|
918
|
+
if set(attrs) != required:
|
|
919
|
+
return None
|
|
920
|
+
if any(not isinstance(attrs[key], str) or not attrs[key]
|
|
921
|
+
for key in required - {"isDraft"}):
|
|
922
|
+
return None
|
|
923
|
+
if name == "git-create-pr":
|
|
924
|
+
if not isinstance(attrs.get("isDraft"), bool):
|
|
925
|
+
return None
|
|
926
|
+
if not str(attrs.get("url", "")).startswith(("https://", "http://")):
|
|
927
|
+
return None
|
|
928
|
+
marker = {"schema_version": 1, "type": "git", "action": action}
|
|
929
|
+
if name == "git-create-pr":
|
|
930
|
+
marker["draft"] = attrs["isDraft"]
|
|
931
|
+
return marker
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def _parse_memory_citation(lines: list[str]) -> dict | None:
|
|
935
|
+
try:
|
|
936
|
+
citation_open = lines.index("<citation_entries>")
|
|
937
|
+
citation_close = lines.index("</citation_entries>")
|
|
938
|
+
rollout_open = lines.index("<rollout_ids>")
|
|
939
|
+
rollout_close = lines.index("</rollout_ids>")
|
|
940
|
+
except ValueError:
|
|
941
|
+
return None
|
|
942
|
+
if not (
|
|
943
|
+
lines[0] == "<oai-mem-citation>"
|
|
944
|
+
and lines[-1] == "</oai-mem-citation>"
|
|
945
|
+
and citation_open == 1
|
|
946
|
+
and citation_open < citation_close < rollout_open < rollout_close
|
|
947
|
+
and rollout_close == len(lines) - 2
|
|
948
|
+
and rollout_open == citation_close + 1
|
|
949
|
+
):
|
|
950
|
+
return None
|
|
951
|
+
citations = lines[citation_open + 1:citation_close]
|
|
952
|
+
rollouts = lines[rollout_open + 1:rollout_close]
|
|
953
|
+
if not citations or not all(_MEM_CITATION_RE.fullmatch(line) for line in citations):
|
|
954
|
+
return None
|
|
955
|
+
if not all(_ROLLOUT_ID_RE.fullmatch(line) for line in rollouts):
|
|
956
|
+
return None
|
|
957
|
+
return {
|
|
958
|
+
"schema_version": 1, "type": "memory_citation",
|
|
959
|
+
"citation_count": len(citations), "rollout_count": len(rollouts),
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def _segment_harness_markers(text: str) -> tuple[str, list[dict]]:
|
|
964
|
+
"""Segment only a closed trailing suffix; authored/fenced lookalikes stay text."""
|
|
965
|
+
lines = text.splitlines()
|
|
966
|
+
end = len(lines)
|
|
967
|
+
while end and not lines[end - 1].strip():
|
|
968
|
+
end -= 1
|
|
969
|
+
reversed_markers: list[dict] = []
|
|
970
|
+
while end:
|
|
971
|
+
marker = _parse_marker_directive(lines[end - 1])
|
|
972
|
+
start = end - 1
|
|
973
|
+
if marker is None and lines[end - 1] == "</oai-mem-citation>":
|
|
974
|
+
starts = [index for index in range(end - 2, -1, -1)
|
|
975
|
+
if lines[index] == "<oai-mem-citation>"]
|
|
976
|
+
if starts:
|
|
977
|
+
start = starts[0]
|
|
978
|
+
marker = _parse_memory_citation(lines[start:end])
|
|
979
|
+
if marker is None:
|
|
980
|
+
break
|
|
981
|
+
# A suffix that begins inside an open Markdown fence is authored code.
|
|
982
|
+
fence: str | None = None
|
|
983
|
+
for line in lines[:start]:
|
|
984
|
+
opened = re.match(r"\s*(`{3,}|~{3,})", line)
|
|
985
|
+
if opened is None:
|
|
986
|
+
continue
|
|
987
|
+
char = opened.group(1)[0]
|
|
988
|
+
if fence is None:
|
|
989
|
+
fence = char
|
|
990
|
+
elif fence == char:
|
|
991
|
+
fence = None
|
|
992
|
+
if fence is not None:
|
|
993
|
+
break
|
|
994
|
+
reversed_markers.append(marker)
|
|
995
|
+
end = start
|
|
996
|
+
while end and not lines[end - 1].strip():
|
|
997
|
+
end -= 1
|
|
998
|
+
if not reversed_markers:
|
|
999
|
+
return text, []
|
|
1000
|
+
return "\n".join(lines[:end]).rstrip(), list(reversed(reversed_markers))
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
_TASK_STARTED_KEYS = frozenset({
|
|
1004
|
+
"type", "collaboration_mode_kind", "model_context_window", "started_at", "turn_id",
|
|
1005
|
+
})
|
|
1006
|
+
_TASK_COMPLETE_KEYS = frozenset({
|
|
1007
|
+
"type", "completed_at", "duration_ms", "last_agent_message", "started_at",
|
|
1008
|
+
"time_to_first_token_ms", "turn_id",
|
|
1009
|
+
})
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def _lifecycle_projection(payload: dict) -> dict:
|
|
1013
|
+
"""Safe lifecycle detail plus internal evidence used by canonical folding."""
|
|
1014
|
+
event = payload.get("type")
|
|
1015
|
+
public: dict[str, object] = {
|
|
1016
|
+
"schema_version": 1, "event": event,
|
|
1017
|
+
"state": "started" if event == "task_started" else "completed",
|
|
1018
|
+
}
|
|
1019
|
+
allowed = _TASK_STARTED_KEYS if event == "task_started" else _TASK_COMPLETE_KEYS
|
|
1020
|
+
valid = set(payload) <= allowed
|
|
1021
|
+
turn_id = payload.get("turn_id")
|
|
1022
|
+
valid = valid and isinstance(turn_id, str) and bool(turn_id)
|
|
1023
|
+
if event == "task_started":
|
|
1024
|
+
for src, dst in (
|
|
1025
|
+
("started_at", "at"),
|
|
1026
|
+
("collaboration_mode_kind", "collaboration_mode_kind"),
|
|
1027
|
+
):
|
|
1028
|
+
value = payload.get(src)
|
|
1029
|
+
if value is not None:
|
|
1030
|
+
if src == "started_at" and (
|
|
1031
|
+
isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
1032
|
+
and math.isfinite(value)
|
|
1033
|
+
):
|
|
1034
|
+
public[dst] = value
|
|
1035
|
+
elif not isinstance(value, str):
|
|
1036
|
+
valid = False
|
|
1037
|
+
else:
|
|
1038
|
+
public[dst] = value
|
|
1039
|
+
window = payload.get("model_context_window")
|
|
1040
|
+
if window is not None:
|
|
1041
|
+
if not isinstance(window, int) or isinstance(window, bool):
|
|
1042
|
+
valid = False
|
|
1043
|
+
else:
|
|
1044
|
+
public["model_context_window"] = window
|
|
1045
|
+
return {"lifecycle": public, "_lifecycle_foldable": valid}
|
|
1046
|
+
|
|
1047
|
+
for src, dst in (("completed_at", "at"), ("started_at", "started_at")):
|
|
1048
|
+
value = payload.get(src)
|
|
1049
|
+
if value is not None:
|
|
1050
|
+
if (isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
1051
|
+
and math.isfinite(value)):
|
|
1052
|
+
public[dst] = value
|
|
1053
|
+
elif not isinstance(value, str):
|
|
1054
|
+
valid = False
|
|
1055
|
+
else:
|
|
1056
|
+
public[dst] = value
|
|
1057
|
+
for key in ("duration_ms", "time_to_first_token_ms"):
|
|
1058
|
+
value = payload.get(key)
|
|
1059
|
+
if value is not None:
|
|
1060
|
+
if (not isinstance(value, (int, float)) or isinstance(value, bool)
|
|
1061
|
+
or not math.isfinite(value)):
|
|
1062
|
+
valid = False
|
|
1063
|
+
else:
|
|
1064
|
+
public[key] = value
|
|
1065
|
+
message = payload.get("last_agent_message")
|
|
1066
|
+
if message is not None and not isinstance(message, str):
|
|
1067
|
+
valid = False
|
|
1068
|
+
message = ""
|
|
1069
|
+
message = message or ""
|
|
1070
|
+
if message.strip():
|
|
1071
|
+
public["message"] = message[:CODEX_TEXT_CAP]
|
|
1072
|
+
error = payload.get("error")
|
|
1073
|
+
if error is not None:
|
|
1074
|
+
valid = False
|
|
1075
|
+
public["state"] = "failed"
|
|
1076
|
+
public["error"] = _stringify(error)[:CODEX_TEXT_CAP]
|
|
1077
|
+
return {
|
|
1078
|
+
"lifecycle": public,
|
|
1079
|
+
"_lifecycle_foldable": valid,
|
|
1080
|
+
"_lifecycle_message_digest": content_digest(message),
|
|
1081
|
+
"_lifecycle_message_len": content_len(message),
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
_SKILL_NAME_RE = re.compile(r"<name>\s*([^<\n]+?)\s*</name>", re.IGNORECASE)
|
|
1086
|
+
_AGENTS_ENVELOPE_RE = re.compile(
|
|
1087
|
+
r"\A\s*# AGENTS\.md instructions for [^\n]+\n\s*"
|
|
1088
|
+
r"<INSTRUCTIONS>.*?</INSTRUCTIONS>\s*\Z",
|
|
1089
|
+
re.DOTALL,
|
|
1090
|
+
)
|
|
1091
|
+
_AGENTS_ENV_BUNDLE_RE = re.compile(
|
|
1092
|
+
r"\A\s*# AGENTS\.md instructions for [^\n]+\n\s*"
|
|
1093
|
+
r"<INSTRUCTIONS>.*?</INSTRUCTIONS>\s*"
|
|
1094
|
+
r"<environment_context>.*?</environment_context>\s*\Z",
|
|
1095
|
+
re.DOTALL,
|
|
1096
|
+
)
|
|
1097
|
+
_CONTEXT_BUNDLE_RE = re.compile(
|
|
1098
|
+
r"\A\s*"
|
|
1099
|
+
r"<recommended_plugins>.*?</recommended_plugins>\s*"
|
|
1100
|
+
r"# AGENTS\.md instructions for [^\n]+\n\s*"
|
|
1101
|
+
r"<INSTRUCTIONS>.*?</INSTRUCTIONS>\s*"
|
|
1102
|
+
r"<environment_context>.*?</environment_context>"
|
|
1103
|
+
r"\s*\Z",
|
|
1104
|
+
re.DOTALL,
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def _exact_wrapper(text: str, opening: str, closing: str) -> bool:
|
|
1109
|
+
stripped = (text or "").strip()
|
|
1110
|
+
return stripped.startswith(opening) and stripped.endswith(closing)
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
def _meta_detail(meta_kind: str, meta_label: str, text: str) -> dict:
|
|
1114
|
+
detail = {"meta_kind": meta_kind, "meta_label": meta_label}
|
|
1115
|
+
if meta_label == "skill":
|
|
1116
|
+
match = _SKILL_NAME_RE.search(text or "")
|
|
1117
|
+
if match:
|
|
1118
|
+
detail["skill_name"] = match.group(1).strip()
|
|
1119
|
+
return detail
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def _classify_injected_message(role: str | None, text: str) -> dict | None:
|
|
1123
|
+
"""Return an explicit neutral meta descriptor for proven harness content.
|
|
1124
|
+
|
|
1125
|
+
Non-human roles are authoritative provider metadata and always context. A
|
|
1126
|
+
user/assistant row is reclassified only by a closed, structural wrapper
|
|
1127
|
+
shape; unknown XML and ordinary prose remain provider-authored content.
|
|
1128
|
+
"""
|
|
1129
|
+
stripped = (text or "").strip()
|
|
1130
|
+
lower = stripped.lower()
|
|
1131
|
+
|
|
1132
|
+
# Some Codex hosts combine three independently injected envelopes into one
|
|
1133
|
+
# user-role message. Match the complete, ordered grammar so an appended
|
|
1134
|
+
# human prompt (or merely XML-looking prose) cannot be hidden as context.
|
|
1135
|
+
if role == "user" and _CONTEXT_BUNDLE_RE.fullmatch(text or ""):
|
|
1136
|
+
detail = _meta_detail("context", "context_bundle", text)
|
|
1137
|
+
detail["meta_sections"] = ["plugins", "agents", "environment"]
|
|
1138
|
+
return detail
|
|
1139
|
+
if role == "user" and _AGENTS_ENV_BUNDLE_RE.fullmatch(text or ""):
|
|
1140
|
+
detail = _meta_detail("context", "context_bundle", text)
|
|
1141
|
+
detail["meta_sections"] = ["agents", "environment"]
|
|
1142
|
+
return detail
|
|
1143
|
+
|
|
1144
|
+
if role not in (None, "user", "assistant"):
|
|
1145
|
+
if _exact_wrapper(lower, "<permissions instructions>",
|
|
1146
|
+
"</permissions instructions>"):
|
|
1147
|
+
return _meta_detail("context", "permissions", text)
|
|
1148
|
+
if _exact_wrapper(lower, "<multi_agent_mode>", "</multi_agent_mode>"):
|
|
1149
|
+
return _meta_detail("context", "mode", text)
|
|
1150
|
+
if _exact_wrapper(lower, "<codex_delegation>", "</codex_delegation>"):
|
|
1151
|
+
return _meta_detail("context", "delegation", text)
|
|
1152
|
+
if _exact_wrapper(lower, "<heartbeat>", "</heartbeat>"):
|
|
1153
|
+
return _meta_detail("notification", "heartbeat", text)
|
|
1154
|
+
if _exact_wrapper(lower, "<model_switch>", "</model_switch>"):
|
|
1155
|
+
return _meta_detail("notification", "model_switch", text)
|
|
1156
|
+
return _meta_detail("context", "role", text)
|
|
1157
|
+
|
|
1158
|
+
wrappers = (
|
|
1159
|
+
("<environment_context>", "</environment_context>", "context", "environment"),
|
|
1160
|
+
("<user_instructions>", "</user_instructions>", "context", "instructions"),
|
|
1161
|
+
("<permissions instructions>", "</permissions instructions>", "context", "permissions"),
|
|
1162
|
+
("<multi_agent_mode>", "</multi_agent_mode>", "context", "mode"),
|
|
1163
|
+
("<codex_delegation>", "</codex_delegation>", "context", "delegation"),
|
|
1164
|
+
("<heartbeat>", "</heartbeat>", "notification", "heartbeat"),
|
|
1165
|
+
("<model_switch>", "</model_switch>", "notification", "model_switch"),
|
|
1166
|
+
("<recommended_plugins>", "</recommended_plugins>", "context", "plugins"),
|
|
1167
|
+
("<skill>", "</skill>", "skill", "skill"),
|
|
1168
|
+
("<oai-mem-citation>", "</oai-mem-citation>", "notification", "memory"),
|
|
1169
|
+
("<memory_context>", "</memory_context>", "context", "memory"),
|
|
1170
|
+
("<memory-context>", "</memory-context>", "context", "memory"),
|
|
1171
|
+
)
|
|
1172
|
+
for opening, closing, meta_kind, label in wrappers:
|
|
1173
|
+
if _exact_wrapper(lower, opening, closing):
|
|
1174
|
+
return _meta_detail(meta_kind, label, text)
|
|
1175
|
+
if role == "user" and _AGENTS_ENVELOPE_RE.fullmatch(text or ""):
|
|
1176
|
+
return _meta_detail("context", "agents", text)
|
|
1177
|
+
return None
|
|
206
1178
|
|
|
207
1179
|
|
|
208
1180
|
def _extract(record_type: str | None, payload: dict) -> _Extracted | None:
|
|
@@ -213,14 +1185,28 @@ def _extract(record_type: str | None, payload: dict) -> _Extracted | None:
|
|
|
213
1185
|
if record_type == "response_item":
|
|
214
1186
|
if ptype == "message":
|
|
215
1187
|
role = payload.get("role")
|
|
216
|
-
kind = "user" if role == "user" else "assistant"
|
|
217
1188
|
text = _join_content_texts(payload.get("content"))
|
|
1189
|
+
meta = _classify_injected_message(
|
|
1190
|
+
role if isinstance(role, str) else None, text)
|
|
1191
|
+
if meta is not None:
|
|
1192
|
+
return _Extracted("meta", text, "text", meta, [])
|
|
1193
|
+
kind = "user" if role == "user" else "assistant"
|
|
1194
|
+
if kind == "assistant":
|
|
1195
|
+
clean, markers = _segment_harness_markers(text)
|
|
1196
|
+
if markers:
|
|
1197
|
+
return _Extracted(
|
|
1198
|
+
kind, clean, "text", {"markers": markers}, [], text)
|
|
218
1199
|
return _Extracted(kind, text, "text", None, [])
|
|
219
1200
|
if ptype == "reasoning":
|
|
220
1201
|
summary = _join_content_texts(payload.get("summary"))
|
|
221
1202
|
body = _join_content_texts(payload.get("content"))
|
|
1203
|
+
reasoning = _reasoning_projection(
|
|
1204
|
+
source="response_item", summary=summary, body=body)
|
|
1205
|
+
if reasoning is None:
|
|
1206
|
+
return None
|
|
222
1207
|
text = "\n".join(p for p in (summary, body) if p)
|
|
223
|
-
return _Extracted(
|
|
1208
|
+
return _Extracted(
|
|
1209
|
+
"reasoning", text, "search_thinking", {"reasoning": reasoning}, [])
|
|
224
1210
|
if ptype in _RESPONSE_TOOL_CALLS:
|
|
225
1211
|
name = payload.get("name") or ptype
|
|
226
1212
|
if ptype == "function_call":
|
|
@@ -231,20 +1217,55 @@ def _extract(record_type: str | None, payload: dict) -> _Extracted | None:
|
|
|
231
1217
|
args = _stringify(payload.get("input") or payload.get("arguments"))
|
|
232
1218
|
text = f"{name}\n{args}" if args else str(name)
|
|
233
1219
|
detail = {"name": name, "args": args[:CODEX_TEXT_CAP]}
|
|
1220
|
+
card = decode_tool_call_card(payload)
|
|
1221
|
+
if card is None:
|
|
1222
|
+
card = decode_secondary_tool_call_card(payload)
|
|
1223
|
+
if card is not None:
|
|
1224
|
+
detail["card"] = card
|
|
234
1225
|
return _Extracted("tool_call", text, "search_tool", detail, [])
|
|
235
1226
|
if ptype in _RESPONSE_TOOL_OUTPUTS:
|
|
236
|
-
|
|
237
|
-
|
|
1227
|
+
value = payload["output"] if "output" in payload else payload.get("tools")
|
|
1228
|
+
shaped = decode_tool_output_card(payload)
|
|
1229
|
+
if shaped is not None:
|
|
1230
|
+
card, _display_body = shaped
|
|
1231
|
+
return _Extracted(
|
|
1232
|
+
"tool_output", _stringify(value), "search_tool", {"card": card}, [])
|
|
1233
|
+
return _Extracted("tool_output", _stringify(value), "search_tool", None, [])
|
|
238
1234
|
return None # unknown response_item subtype: version tolerance
|
|
239
1235
|
if record_type == "event_msg":
|
|
240
1236
|
if ptype in _EVENT_PROSE_KIND:
|
|
241
1237
|
kind = _EVENT_PROSE_KIND[ptype]
|
|
242
1238
|
text = _stringify(payload.get("message") or payload.get("text"))
|
|
1239
|
+
role = "user" if kind == "user" else "assistant"
|
|
1240
|
+
meta = _classify_injected_message(role, text)
|
|
1241
|
+
if meta is not None:
|
|
1242
|
+
return _Extracted("meta", text, "text", meta, [])
|
|
243
1243
|
column = "search_thinking" if kind == "reasoning" else "text"
|
|
1244
|
+
if kind == "reasoning":
|
|
1245
|
+
title_like = _REASONING_TITLE_RE.fullmatch(text.strip()) is not None
|
|
1246
|
+
reasoning = _reasoning_projection(
|
|
1247
|
+
source="agent_reasoning",
|
|
1248
|
+
summary=text if title_like else "",
|
|
1249
|
+
body="" if title_like else text)
|
|
1250
|
+
if reasoning is None:
|
|
1251
|
+
return None
|
|
1252
|
+
return _Extracted(
|
|
1253
|
+
kind, text, column, {"reasoning": reasoning}, [])
|
|
1254
|
+
if kind == "assistant":
|
|
1255
|
+
clean, markers = _segment_harness_markers(text)
|
|
1256
|
+
if markers:
|
|
1257
|
+
return _Extracted(
|
|
1258
|
+
kind, clean, column, {"markers": markers}, [], text)
|
|
244
1259
|
return _Extracted(kind, text, column, None, [])
|
|
245
1260
|
if ptype in _EVENT_CARD_TYPES:
|
|
246
1261
|
text, touches = _event_card(ptype, payload)
|
|
247
1262
|
detail = {"event": ptype}
|
|
1263
|
+
if ptype in {"task_started", "task_complete"}:
|
|
1264
|
+
detail.update(_lifecycle_projection(payload))
|
|
1265
|
+
patch_card = decode_patch_event_card(payload)
|
|
1266
|
+
card = patch_card or decode_secondary_event_card(payload)
|
|
1267
|
+
if card is not None:
|
|
1268
|
+
detail["card"] = card
|
|
248
1269
|
return _Extracted("event", text, "search_tool", detail, touches)
|
|
249
1270
|
return None # token_count (accounting) + unknown event types: no row
|
|
250
1271
|
# session_meta / turn_context / unknown record types: no normalized row
|
|
@@ -268,8 +1289,12 @@ def _event_card(ptype: str, payload: dict) -> tuple[str, list[tuple[str, str]]]:
|
|
|
268
1289
|
text = " ".join(["patch_apply", *paths]) if paths else "patch_apply"
|
|
269
1290
|
elif ptype == "mcp_tool_call_end":
|
|
270
1291
|
invocation = payload.get("invocation")
|
|
271
|
-
|
|
272
|
-
|
|
1292
|
+
server = invocation.get("server") if isinstance(invocation, dict) else None
|
|
1293
|
+
tool = ((invocation.get("tool") or invocation.get("name"))
|
|
1294
|
+
if isinstance(invocation, dict) else None)
|
|
1295
|
+
identity = "/".join(
|
|
1296
|
+
value for value in (server, tool) if isinstance(value, str) and value)
|
|
1297
|
+
text = f"mcp_tool_call {identity}" if identity else "mcp_tool_call"
|
|
273
1298
|
elif ptype == "web_search_end":
|
|
274
1299
|
query = _stringify(payload.get("query"))
|
|
275
1300
|
text = f"web_search {query}".strip()
|
|
@@ -278,6 +1303,75 @@ def _event_card(ptype: str, payload: dict) -> tuple[str, list[tuple[str, str]]]:
|
|
|
278
1303
|
return text, touches
|
|
279
1304
|
|
|
280
1305
|
|
|
1306
|
+
def infer_codex_event_turns(
|
|
1307
|
+
events: Iterable[Any], *, initial_turn: str | None = None
|
|
1308
|
+
) -> tuple[list[str | None], str | None]:
|
|
1309
|
+
"""Infer each physical event's logical turn from native lifecycle anchors.
|
|
1310
|
+
|
|
1311
|
+
``turn_context``/``task_started`` establish a forward turn. A resumed
|
|
1312
|
+
segment can instead begin with response rows and expose its first native
|
|
1313
|
+
proof only on a later patch/task-complete record; in that case only the
|
|
1314
|
+
unanchored prefix since the latest ``session_meta`` is backfilled. Distinct
|
|
1315
|
+
explicit anchors are never merged.
|
|
1316
|
+
"""
|
|
1317
|
+
materialized = list(events)
|
|
1318
|
+
turns: list[str | None] = [None] * len(materialized)
|
|
1319
|
+
current = initial_turn
|
|
1320
|
+
segment_start = 0
|
|
1321
|
+
for index, event in enumerate(materialized):
|
|
1322
|
+
record_type = getattr(event, "record_type", None)
|
|
1323
|
+
try:
|
|
1324
|
+
obj = json.loads(getattr(event, "payload_json", "") or "{}")
|
|
1325
|
+
except (json.JSONDecodeError, TypeError):
|
|
1326
|
+
obj = {}
|
|
1327
|
+
payload = obj.get("payload") if isinstance(obj.get("payload"), dict) else {}
|
|
1328
|
+
ptype = payload.get("type")
|
|
1329
|
+
if record_type == "session_meta":
|
|
1330
|
+
current = None
|
|
1331
|
+
segment_start = index + 1
|
|
1332
|
+
continue
|
|
1333
|
+
explicit = getattr(event, "turn_id", None)
|
|
1334
|
+
if explicit is None and record_type == "turn_context":
|
|
1335
|
+
candidate = payload.get("turn_id")
|
|
1336
|
+
explicit = candidate if isinstance(candidate, str) and candidate else None
|
|
1337
|
+
if explicit is not None:
|
|
1338
|
+
if current is None and _is_late_turn_anchor(
|
|
1339
|
+
record_type, ptype, explicit):
|
|
1340
|
+
for prior in range(segment_start, index):
|
|
1341
|
+
if turns[prior] is None:
|
|
1342
|
+
turns[prior] = explicit
|
|
1343
|
+
current = explicit
|
|
1344
|
+
turns[index] = explicit
|
|
1345
|
+
else:
|
|
1346
|
+
turns[index] = current
|
|
1347
|
+
return turns, current
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
def _is_late_turn_anchor(
|
|
1351
|
+
record_type: str | None, payload_type: str | None, explicit_turn: str | None,
|
|
1352
|
+
) -> bool:
|
|
1353
|
+
"""Whether a native turn id can prove a preceding unanchored prefix."""
|
|
1354
|
+
return (
|
|
1355
|
+
explicit_turn is not None
|
|
1356
|
+
and record_type != "turn_context"
|
|
1357
|
+
and payload_type != "task_started"
|
|
1358
|
+
)
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
def codex_event_is_late_turn_anchor(event: Any) -> bool:
|
|
1362
|
+
"""Public predicate shared by delta persistence and turn inference."""
|
|
1363
|
+
try:
|
|
1364
|
+
obj = json.loads(getattr(event, "payload_json", "") or "{}")
|
|
1365
|
+
except (json.JSONDecodeError, TypeError):
|
|
1366
|
+
obj = {}
|
|
1367
|
+
payload = obj.get("payload") if isinstance(obj.get("payload"), dict) else {}
|
|
1368
|
+
return _is_late_turn_anchor(
|
|
1369
|
+
getattr(event, "record_type", None),
|
|
1370
|
+
payload.get("type"),
|
|
1371
|
+
getattr(event, "turn_id", None),
|
|
1372
|
+
)
|
|
1373
|
+
|
|
1374
|
+
|
|
281
1375
|
def normalize_codex_events(
|
|
282
1376
|
events: Iterable[Any], *, initial: CodexStickyState
|
|
283
1377
|
) -> CodexNormalizationResult:
|
|
@@ -285,10 +1379,13 @@ def normalize_codex_events(
|
|
|
285
1379
|
rows + file touches, replaying sticky turn/model state seeded from ``initial``.
|
|
286
1380
|
|
|
287
1381
|
Returns the terminal sticky state for persistence to codex_session_files."""
|
|
1382
|
+
materialized = list(events)
|
|
1383
|
+
inferred_turns, terminal_turn = infer_codex_event_turns(
|
|
1384
|
+
materialized, initial_turn=initial.turn_id)
|
|
288
1385
|
sticky = CodexStickyState(turn_id=initial.turn_id, model=initial.model)
|
|
289
1386
|
rows: list[CodexNormalizedRow] = []
|
|
290
1387
|
touches: list[CodexFileTouch] = []
|
|
291
|
-
for event in
|
|
1388
|
+
for event_index, event in enumerate(materialized):
|
|
292
1389
|
record_type = getattr(event, "record_type", None)
|
|
293
1390
|
if record_type == "session_meta":
|
|
294
1391
|
sticky.turn_id = None
|
|
@@ -301,7 +1398,9 @@ def normalize_codex_events(
|
|
|
301
1398
|
payload = obj.get("payload") if isinstance(obj.get("payload"), dict) else {}
|
|
302
1399
|
if record_type == "turn_context":
|
|
303
1400
|
own = getattr(event, "turn_id", None)
|
|
304
|
-
|
|
1401
|
+
candidate_turn = own if own is not None else payload.get("turn_id")
|
|
1402
|
+
if isinstance(candidate_turn, str) and candidate_turn:
|
|
1403
|
+
sticky.turn_id = candidate_turn
|
|
305
1404
|
model = payload.get("model")
|
|
306
1405
|
sticky.model = model if isinstance(model, str) and model else None
|
|
307
1406
|
continue
|
|
@@ -313,9 +1412,10 @@ def normalize_codex_events(
|
|
|
313
1412
|
extracted = _extract(record_type, payload)
|
|
314
1413
|
if extracted is None:
|
|
315
1414
|
continue
|
|
316
|
-
|
|
317
|
-
eff_turn = own_turn if own_turn is not None else sticky.turn_id
|
|
1415
|
+
eff_turn = inferred_turns[event_index]
|
|
318
1416
|
text_full = extracted.content_text or ""
|
|
1417
|
+
identity_full = (extracted.identity_text
|
|
1418
|
+
if extracted.identity_text is not None else text_full)
|
|
319
1419
|
capped, truncated = _cap(text_full)
|
|
320
1420
|
text_col = ""
|
|
321
1421
|
search_tool = ""
|
|
@@ -332,6 +1432,11 @@ def normalize_codex_events(
|
|
|
332
1432
|
detail["truncated"] = True
|
|
333
1433
|
source_path = getattr(event, "source_path", "")
|
|
334
1434
|
line_offset = getattr(event, "line_offset", 0)
|
|
1435
|
+
effective_call_id = getattr(event, "call_id", None)
|
|
1436
|
+
if not effective_call_id and record_type == "response_item":
|
|
1437
|
+
candidate = payload.get("id")
|
|
1438
|
+
if isinstance(candidate, str) and candidate:
|
|
1439
|
+
effective_call_id = candidate
|
|
335
1440
|
rows.append(CodexNormalizedRow(
|
|
336
1441
|
conversation_key=conversation_key,
|
|
337
1442
|
source_root_key=getattr(event, "source_root_key", None) or "",
|
|
@@ -339,14 +1444,14 @@ def normalize_codex_events(
|
|
|
339
1444
|
line_offset=line_offset,
|
|
340
1445
|
timestamp_utc=getattr(event, "timestamp_utc", None),
|
|
341
1446
|
turn_id=eff_turn,
|
|
342
|
-
call_id=
|
|
1447
|
+
call_id=effective_call_id,
|
|
343
1448
|
kind=extracted.kind,
|
|
344
1449
|
event_type=getattr(event, "event_type", None),
|
|
345
1450
|
record_family="event_msg" if record_type == "event_msg" else "response_item",
|
|
346
1451
|
model=sticky.model,
|
|
347
1452
|
text=text_col,
|
|
348
|
-
content_digest=content_digest(
|
|
349
|
-
content_len=content_len(
|
|
1453
|
+
content_digest=content_digest(identity_full),
|
|
1454
|
+
content_len=content_len(identity_full),
|
|
350
1455
|
detail_json=_canonical_json(detail) if detail else None,
|
|
351
1456
|
search_tool=search_tool,
|
|
352
1457
|
search_thinking=search_thinking,
|
|
@@ -361,7 +1466,7 @@ def normalize_codex_events(
|
|
|
361
1466
|
))
|
|
362
1467
|
return CodexNormalizationResult(
|
|
363
1468
|
rows=rows, touches=touches,
|
|
364
|
-
terminal=CodexStickyState(turn_id=
|
|
1469
|
+
terminal=CodexStickyState(turn_id=terminal_turn, model=sticky.model),
|
|
365
1470
|
)
|
|
366
1471
|
|
|
367
1472
|
|
|
@@ -383,7 +1488,7 @@ def _pair_mirrors_impl(
|
|
|
383
1488
|
# member (three identical event copies never collapse into one).
|
|
384
1489
|
turned: dict[tuple, dict[str, list[int]]] = {}
|
|
385
1490
|
for i, row in enumerate(rows):
|
|
386
|
-
if row.kind not in
|
|
1491
|
+
if row.kind not in _MIRROR_KINDS or row.turn_id is None:
|
|
387
1492
|
continue
|
|
388
1493
|
key = (row.turn_id, row.kind, row.content_digest, row.content_len)
|
|
389
1494
|
group = turned.setdefault(key, {"R": [], "E": []})
|
|
@@ -399,7 +1504,7 @@ def _pair_mirrors_impl(
|
|
|
399
1504
|
last_prose_by_kind: dict[str, int] = {}
|
|
400
1505
|
paired_response: set[int] = set()
|
|
401
1506
|
for j, row in enumerate(rows):
|
|
402
|
-
if row.kind not in
|
|
1507
|
+
if row.kind not in _MIRROR_KINDS or row.turn_id is not None:
|
|
403
1508
|
continue
|
|
404
1509
|
prev_idx = last_prose_by_kind.get(row.kind)
|
|
405
1510
|
if row.record_family == "event_msg" and prev_idx is not None:
|
|
@@ -446,7 +1551,219 @@ def _item_pos(row: CodexNormalizedRow) -> tuple:
|
|
|
446
1551
|
return (row.timestamp_utc or "", row.source_path, row.line_offset)
|
|
447
1552
|
|
|
448
1553
|
|
|
449
|
-
def
|
|
1554
|
+
def _row_card(row: CodexNormalizedRow) -> dict | None:
|
|
1555
|
+
if not row.detail_json:
|
|
1556
|
+
return None
|
|
1557
|
+
try:
|
|
1558
|
+
detail = json.loads(row.detail_json)
|
|
1559
|
+
except (json.JSONDecodeError, TypeError):
|
|
1560
|
+
return None
|
|
1561
|
+
card = detail.get("card") if isinstance(detail, dict) else None
|
|
1562
|
+
return card if isinstance(card, dict) else None
|
|
1563
|
+
|
|
1564
|
+
|
|
1565
|
+
def _fold_patch_completion_items(items: list[dict]) -> list[dict]:
|
|
1566
|
+
"""Fold only proven ``patch_apply_end`` items into their owning response.
|
|
1567
|
+
|
|
1568
|
+
Same-id ownership wins when unique. Current nested ``tools.apply_patch``
|
|
1569
|
+
events use an inner id, so the second proof is a unique physical bracket in
|
|
1570
|
+
one source file: call < event < that call's uniquely-owned output. Ambiguous
|
|
1571
|
+
or repeated shapes stay separate and inspectable.
|
|
1572
|
+
"""
|
|
1573
|
+
patch_calls: list[
|
|
1574
|
+
tuple[int, CodexNormalizedRow, CodexNormalizedRow | None, int]
|
|
1575
|
+
] = []
|
|
1576
|
+
for item_index, item in enumerate(items):
|
|
1577
|
+
if item["klass"] != "response":
|
|
1578
|
+
continue
|
|
1579
|
+
owner_count: dict[str, int] = {}
|
|
1580
|
+
for row in item["rows"]:
|
|
1581
|
+
if row.kind == "tool_call" and row.call_id:
|
|
1582
|
+
owner_count[row.call_id] = owner_count.get(row.call_id, 0) + 1
|
|
1583
|
+
for row in item["rows"]:
|
|
1584
|
+
card = _row_card(row)
|
|
1585
|
+
if not (row.kind == "tool_call" and isinstance(card, dict)
|
|
1586
|
+
and card.get("type") == "patch"):
|
|
1587
|
+
continue
|
|
1588
|
+
outputs = [
|
|
1589
|
+
candidate for candidate in item["rows"]
|
|
1590
|
+
if candidate.kind == "tool_output" and row.call_id
|
|
1591
|
+
and candidate.call_id == row.call_id
|
|
1592
|
+
and owner_count.get(row.call_id) == 1
|
|
1593
|
+
and _item_pos(row) < _item_pos(candidate)
|
|
1594
|
+
]
|
|
1595
|
+
patch_calls.append((
|
|
1596
|
+
item_index, row, outputs[0] if len(outputs) == 1 else None,
|
|
1597
|
+
owner_count.get(row.call_id, 0),
|
|
1598
|
+
))
|
|
1599
|
+
|
|
1600
|
+
matched_calls: set[tuple[str, int]] = set()
|
|
1601
|
+
remove_items: set[int] = set()
|
|
1602
|
+
for event_index, event_item in enumerate(items):
|
|
1603
|
+
if event_item["klass"] != "event" or len(event_item["rows"]) != 1:
|
|
1604
|
+
continue
|
|
1605
|
+
event_row = event_item["rows"][0]
|
|
1606
|
+
event_card = _row_card(event_row)
|
|
1607
|
+
if not (isinstance(event_card, dict)
|
|
1608
|
+
and event_card.get("source") == "patch_apply_end"):
|
|
1609
|
+
continue
|
|
1610
|
+
same_id = [
|
|
1611
|
+
candidate for candidate in patch_calls
|
|
1612
|
+
if event_row.call_id and candidate[1].call_id == event_row.call_id
|
|
1613
|
+
and candidate[3] == 1
|
|
1614
|
+
and candidate[1].turn_id == event_row.turn_id
|
|
1615
|
+
and (candidate[1].source_path, candidate[1].line_offset) not in matched_calls
|
|
1616
|
+
]
|
|
1617
|
+
candidates = same_id if len(same_id) == 1 else []
|
|
1618
|
+
if not candidates:
|
|
1619
|
+
candidates = [
|
|
1620
|
+
candidate for candidate in patch_calls
|
|
1621
|
+
if candidate[2] is not None
|
|
1622
|
+
and candidate[1].turn_id == event_row.turn_id
|
|
1623
|
+
and candidate[1].source_path == event_row.source_path
|
|
1624
|
+
and candidate[2].source_path == event_row.source_path
|
|
1625
|
+
and candidate[1].line_offset < event_row.line_offset < candidate[2].line_offset
|
|
1626
|
+
and (candidate[1].source_path, candidate[1].line_offset) not in matched_calls
|
|
1627
|
+
]
|
|
1628
|
+
if len(candidates) != 1:
|
|
1629
|
+
continue
|
|
1630
|
+
owner_index, call_row, _output_row, _owner_count = candidates[0]
|
|
1631
|
+
owner = items[owner_index]
|
|
1632
|
+
owner["rows"].append(event_row)
|
|
1633
|
+
owner["rows"].sort(key=_item_pos)
|
|
1634
|
+
owner.setdefault("folded_items", []).append(event_item)
|
|
1635
|
+
matched_calls.add((call_row.source_path, call_row.line_offset))
|
|
1636
|
+
remove_items.add(event_index)
|
|
1637
|
+
return [item for index, item in enumerate(items) if index not in remove_items]
|
|
1638
|
+
|
|
1639
|
+
|
|
1640
|
+
def _fold_secondary_completion_items(items: list[dict]) -> list[dict]:
|
|
1641
|
+
"""Fold web/MCP end events only onto one exact same-turn call id.
|
|
1642
|
+
|
|
1643
|
+
No name, adjacency, or basename inference is allowed: a reused, empty, or
|
|
1644
|
+
unmatched id leaves the native completion visible as its own event card.
|
|
1645
|
+
"""
|
|
1646
|
+
calls_by_id: dict[str, list[tuple[int, CodexNormalizedRow]]] = {}
|
|
1647
|
+
for item_index, item in enumerate(items):
|
|
1648
|
+
if item["klass"] != "response":
|
|
1649
|
+
continue
|
|
1650
|
+
for row in item["rows"]:
|
|
1651
|
+
if row.kind == "tool_call" and row.call_id:
|
|
1652
|
+
calls_by_id.setdefault(row.call_id, []).append((item_index, row))
|
|
1653
|
+
remove_items: set[int] = set()
|
|
1654
|
+
matched_calls: set[tuple[str, int]] = set()
|
|
1655
|
+
for event_index, event_item in enumerate(items):
|
|
1656
|
+
if event_item["klass"] != "event" or len(event_item["rows"]) != 1:
|
|
1657
|
+
continue
|
|
1658
|
+
event_row = event_item["rows"][0]
|
|
1659
|
+
event_card = _row_card(event_row)
|
|
1660
|
+
if not (event_row.call_id and isinstance(event_card, dict)
|
|
1661
|
+
and event_card.get("type") in {
|
|
1662
|
+
"web_search_completion", "mcp_completion"}):
|
|
1663
|
+
continue
|
|
1664
|
+
candidates = [
|
|
1665
|
+
(owner_index, call_row)
|
|
1666
|
+
for owner_index, call_row in calls_by_id.get(event_row.call_id, [])
|
|
1667
|
+
if call_row.turn_id == event_row.turn_id
|
|
1668
|
+
and (call_row.source_path, call_row.line_offset) not in matched_calls
|
|
1669
|
+
]
|
|
1670
|
+
if event_card.get("type") == "web_search_completion":
|
|
1671
|
+
candidates = [
|
|
1672
|
+
candidate for candidate in candidates
|
|
1673
|
+
if (_parse_row_detail(candidate[1]) or {}).get("name")
|
|
1674
|
+
== "web_search_call"
|
|
1675
|
+
]
|
|
1676
|
+
if len(candidates) != 1:
|
|
1677
|
+
continue
|
|
1678
|
+
owner_index, call_row = candidates[0]
|
|
1679
|
+
owner = items[owner_index]
|
|
1680
|
+
owner["rows"].append(event_row)
|
|
1681
|
+
owner["rows"].sort(key=_item_pos)
|
|
1682
|
+
owner.setdefault("folded_items", []).append(event_item)
|
|
1683
|
+
matched_calls.add((call_row.source_path, call_row.line_offset))
|
|
1684
|
+
remove_items.add(event_index)
|
|
1685
|
+
return [item for index, item in enumerate(items) if index not in remove_items]
|
|
1686
|
+
|
|
1687
|
+
|
|
1688
|
+
def _parse_row_detail(row: CodexNormalizedRow) -> dict | None:
|
|
1689
|
+
if not row.detail_json:
|
|
1690
|
+
return None
|
|
1691
|
+
try:
|
|
1692
|
+
detail = json.loads(row.detail_json)
|
|
1693
|
+
except (json.JSONDecodeError, TypeError):
|
|
1694
|
+
return None
|
|
1695
|
+
return detail if isinstance(detail, dict) else None
|
|
1696
|
+
|
|
1697
|
+
|
|
1698
|
+
def _fold_lifecycle_items(items: list[dict]) -> list[dict]:
|
|
1699
|
+
"""Fold only closed, uniquely-owned redundant task lifecycle events.
|
|
1700
|
+
|
|
1701
|
+
The physical rows remain retained and become member-key aliases. A completion
|
|
1702
|
+
is redundant only when its last message is empty or digest/length-identical to
|
|
1703
|
+
a real assistant row in the one owning logical response. Unknown fields,
|
|
1704
|
+
errors, unmatched/ambiguous events, and unique messages stay standalone.
|
|
1705
|
+
"""
|
|
1706
|
+
responses: dict[str, list[tuple[int, dict]]] = {}
|
|
1707
|
+
events: dict[tuple[str, str], list[tuple[int, dict, CodexNormalizedRow, dict]]] = {}
|
|
1708
|
+
for index, item in enumerate(items):
|
|
1709
|
+
turn_id = item.get("turn_id")
|
|
1710
|
+
if item["klass"] == "response" and turn_id:
|
|
1711
|
+
responses.setdefault(turn_id, []).append((index, item))
|
|
1712
|
+
if item["klass"] != "event" or len(item["rows"]) != 1 or not turn_id:
|
|
1713
|
+
continue
|
|
1714
|
+
row = item["rows"][0]
|
|
1715
|
+
detail = _parse_row_detail(row)
|
|
1716
|
+
lifecycle = detail.get("lifecycle") if isinstance(detail, dict) else None
|
|
1717
|
+
if (isinstance(lifecycle, dict)
|
|
1718
|
+
and lifecycle.get("event") in {"task_started", "task_complete"}):
|
|
1719
|
+
events.setdefault((turn_id, lifecycle["event"]), []).append(
|
|
1720
|
+
(index, item, row, detail))
|
|
1721
|
+
|
|
1722
|
+
remove: set[int] = set()
|
|
1723
|
+
for turn_id, owners in responses.items():
|
|
1724
|
+
if len(owners) != 1:
|
|
1725
|
+
continue
|
|
1726
|
+
_owner_index, owner = owners[0]
|
|
1727
|
+
for event_name in ("task_started", "task_complete"):
|
|
1728
|
+
candidates = events.get((turn_id, event_name), [])
|
|
1729
|
+
if len(candidates) != 1:
|
|
1730
|
+
continue
|
|
1731
|
+
event_index, event_item, row, detail = candidates[0]
|
|
1732
|
+
if detail.get("_lifecycle_foldable") is not True:
|
|
1733
|
+
continue
|
|
1734
|
+
if event_name == "task_complete":
|
|
1735
|
+
message_len = detail.get("_lifecycle_message_len")
|
|
1736
|
+
message_digest = detail.get("_lifecycle_message_digest")
|
|
1737
|
+
if message_len:
|
|
1738
|
+
if not any(
|
|
1739
|
+
candidate.kind == "assistant"
|
|
1740
|
+
and candidate.content_len == message_len
|
|
1741
|
+
and candidate.content_digest == message_digest
|
|
1742
|
+
for candidate in owner["rows"]
|
|
1743
|
+
):
|
|
1744
|
+
continue
|
|
1745
|
+
lifecycle = detail["lifecycle"]
|
|
1746
|
+
section = {
|
|
1747
|
+
key: value for key, value in lifecycle.items()
|
|
1748
|
+
if key not in {"schema_version", "event", "state", "message", "error"}
|
|
1749
|
+
}
|
|
1750
|
+
folded = owner.setdefault(
|
|
1751
|
+
"lifecycle", {"schema_version": 1, "state": "started"})
|
|
1752
|
+
folded["started" if event_name == "task_started" else "completed"] = section
|
|
1753
|
+
if event_name == "task_complete":
|
|
1754
|
+
folded["state"] = "completed"
|
|
1755
|
+
owner["rows"].append(row)
|
|
1756
|
+
owner["rows"].sort(key=_item_pos)
|
|
1757
|
+
owner.setdefault("lifecycle_rows", []).append(row)
|
|
1758
|
+
owner["lifecycle_rows"].sort(key=_item_pos)
|
|
1759
|
+
owner.setdefault("folded_items", []).append(event_item)
|
|
1760
|
+
remove.add(event_index)
|
|
1761
|
+
return [item for index, item in enumerate(items) if index not in remove]
|
|
1762
|
+
|
|
1763
|
+
|
|
1764
|
+
def canonical_items(
|
|
1765
|
+
rows: list[CodexNormalizedRow], *, fold_patch_completions: bool = True,
|
|
1766
|
+
) -> list[dict]:
|
|
450
1767
|
"""Group KEPT (post-pairing) rows into canonical rendered items (§5.2).
|
|
451
1768
|
|
|
452
1769
|
Response items bundle a turn's assistant/reasoning/tool rows; prompt/event/
|
|
@@ -460,6 +1777,9 @@ def canonical_items(rows: list[CodexNormalizedRow]) -> list[dict]:
|
|
|
460
1777
|
if row.turn_id is None:
|
|
461
1778
|
items.append({"klass": "unturned", "rows": [row], "turn_id": None,
|
|
462
1779
|
"anchor_row": row})
|
|
1780
|
+
elif row.kind == "meta":
|
|
1781
|
+
items.append({"klass": "meta", "rows": [row], "turn_id": row.turn_id,
|
|
1782
|
+
"anchor_row": row})
|
|
463
1783
|
elif row.kind == "user":
|
|
464
1784
|
items.append({"klass": "prompt", "rows": [row], "turn_id": row.turn_id,
|
|
465
1785
|
"anchor_row": row})
|
|
@@ -474,6 +1794,10 @@ def canonical_items(rows: list[CodexNormalizedRow]) -> list[dict]:
|
|
|
474
1794
|
response_by_turn[row.turn_id] = item
|
|
475
1795
|
items.append(item)
|
|
476
1796
|
item["rows"].append(row)
|
|
1797
|
+
if fold_patch_completions:
|
|
1798
|
+
items = _fold_patch_completion_items(items)
|
|
1799
|
+
items = _fold_secondary_completion_items(items)
|
|
1800
|
+
items = _fold_lifecycle_items(items)
|
|
477
1801
|
items.sort(key=lambda it: _item_pos(it["anchor_row"]))
|
|
478
1802
|
return items
|
|
479
1803
|
|