@ericrisco/rsc 0.1.33 → 0.1.34
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/manifest.json +1 -1
- package/package.json +1 -1
- package/skills/roast-me/tools/adapters/__pycache__/__init__.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/base.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/claude.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/codex.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/gemini.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/__pycache__/registry.cpython-312.pyc +0 -0
- package/skills/roast-me/tools/adapters/base.py +12 -0
- package/skills/roast-me/tools/adapters/codex.py +146 -53
- package/skills/roast-me/tools/adapters/gemini.py +102 -81
- package/skills/roast-me/tools/extract_prompts.py +13 -14
package/manifest.json
CHANGED
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -17,10 +17,22 @@ An adapter MUST:
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
+
import re
|
|
20
21
|
from abc import ABC, abstractmethod
|
|
21
22
|
from pathlib import Path
|
|
22
23
|
from typing import Any
|
|
23
24
|
|
|
25
|
+
#: Shared correction detector — a user message matching this right after a prompt
|
|
26
|
+
#: signals the previous turn went wrong. Single source of truth for every adapter
|
|
27
|
+
#: (and re-used by extract_prompts.py) so the heuristic never drifts between runtimes.
|
|
28
|
+
CORRECTION_RE = re.compile(
|
|
29
|
+
r"\b(no[,.]?\s|wrong|instead|actually|don'?t|shouldn'?t|stop|not that|"
|
|
30
|
+
r"I said|I meant|I asked|that'?s not|please don'?t|why did you|"
|
|
31
|
+
r"you should have|that was wrong|incorrect|try again|redo|"
|
|
32
|
+
r"that broke|you broke|revert|undo)\b",
|
|
33
|
+
re.IGNORECASE,
|
|
34
|
+
)
|
|
35
|
+
|
|
24
36
|
|
|
25
37
|
class BaseAdapter(ABC):
|
|
26
38
|
"""Contract every runtime adapter must satisfy."""
|
|
@@ -1,36 +1,71 @@
|
|
|
1
1
|
"""Codex CLI adapter (@openai/codex).
|
|
2
2
|
|
|
3
3
|
Session files: ~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>.jsonl
|
|
4
|
-
Format: JSON Lines. Each line is
|
|
5
|
-
|
|
6
|
-
CONFIRMED (2026-06,
|
|
7
|
-
- Base path: ~/.codex/sessions/
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
4
|
+
Format: JSON Lines. Each line is {"timestamp", "type", "payload"}.
|
|
5
|
+
|
|
6
|
+
CONFIRMED (2026-06, against live ~/.codex/sessions on macOS):
|
|
7
|
+
- Base path / layout / naming: ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl
|
|
8
|
+
- Line envelope: {"timestamp": ISO8601, "type": str, "payload": {...}}
|
|
9
|
+
- The genuine user prompt is an `event_msg` line whose payload.type is
|
|
10
|
+
"user_message", carrying the typed text in payload.message. (Injected
|
|
11
|
+
AGENTS.md / developer preambles appear only as response_item/message lines,
|
|
12
|
+
so keying on user_message excludes them.)
|
|
13
|
+
- Assistant turns: event_msg/agent_message (payload.message) and
|
|
14
|
+
response_item/message role=assistant.
|
|
15
|
+
- Tool calls: response_item/function_call (payload.name).
|
|
16
|
+
- Tool results: response_item/function_call_output (payload.output) — shell
|
|
17
|
+
execs include a "Process exited with code N" line; N != 0 (or common error
|
|
18
|
+
markers) means the command failed.
|
|
19
|
+
- Model: turn_context.model / session_meta payload.
|
|
20
|
+
|
|
21
|
+
Degradation: if ~/.codex/ does not exist, discover() returns []; any unexpected
|
|
22
|
+
line shape is skipped, so parse() never raises.
|
|
19
23
|
"""
|
|
20
24
|
|
|
21
25
|
from __future__ import annotations
|
|
22
26
|
|
|
27
|
+
import datetime
|
|
23
28
|
import json
|
|
29
|
+
import re
|
|
24
30
|
from pathlib import Path
|
|
25
31
|
from typing import Any
|
|
26
32
|
|
|
27
|
-
from .base import BaseAdapter
|
|
33
|
+
from .base import CORRECTION_RE, BaseAdapter
|
|
34
|
+
|
|
35
|
+
# A shell tool result failed when it reports a non-zero exit code, or carries a
|
|
36
|
+
# common error marker with no explicit success code. Conservative on purpose —
|
|
37
|
+
# better to miss a soft error than to invent one.
|
|
38
|
+
_EXIT_CODE_RE = re.compile(r"exited with code\s+(\d+)", re.IGNORECASE)
|
|
39
|
+
_ERROR_MARKER_RE = re.compile(
|
|
40
|
+
r"\b(command not found|no such file|permission denied|traceback \(most recent|"
|
|
41
|
+
r"fatal:|panic:|segmentation fault|unhandled exception)\b",
|
|
42
|
+
re.IGNORECASE,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _looks_like_error(output: str) -> bool:
|
|
47
|
+
if not output:
|
|
48
|
+
return False
|
|
49
|
+
m = _EXIT_CODE_RE.search(output)
|
|
50
|
+
if m:
|
|
51
|
+
return m.group(1) != "0"
|
|
52
|
+
return bool(_ERROR_MARKER_RE.search(output))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _ts(value: Any) -> float | None:
|
|
56
|
+
if isinstance(value, (int, float)):
|
|
57
|
+
return float(value)
|
|
58
|
+
if isinstance(value, str):
|
|
59
|
+
try:
|
|
60
|
+
return datetime.datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp()
|
|
61
|
+
except (ValueError, AttributeError):
|
|
62
|
+
return None
|
|
63
|
+
return None
|
|
28
64
|
|
|
29
65
|
|
|
30
66
|
class CodexAdapter(BaseAdapter):
|
|
31
67
|
RUNTIME_ID = "codex"
|
|
32
68
|
|
|
33
|
-
# CONFIRMED: path layout from official CLI docs and community tools.
|
|
34
69
|
SESSIONS_DIR = Path.home() / ".codex" / "sessions"
|
|
35
70
|
|
|
36
71
|
def discover(self) -> list[Path]:
|
|
@@ -50,17 +85,17 @@ class CodexAdapter(BaseAdapter):
|
|
|
50
85
|
return sorted(files, key=lambda f: f.stat().st_mtime, reverse=True)
|
|
51
86
|
|
|
52
87
|
def parse(self, path: Path) -> list[dict[str, Any]]:
|
|
53
|
-
"""Parse one Codex session JSONL file
|
|
54
|
-
|
|
55
|
-
STUBBED schema: assumes {"role": "user", "content": "..."} event lines.
|
|
56
|
-
Falls back silently if the schema differs.
|
|
57
|
-
"""
|
|
58
|
-
records: list[dict[str, Any]] = []
|
|
88
|
+
"""Parse one Codex session JSONL file into user-prompt records with
|
|
89
|
+
session-aware error/correction context."""
|
|
59
90
|
try:
|
|
60
91
|
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
61
92
|
except OSError:
|
|
62
93
|
return []
|
|
63
94
|
|
|
95
|
+
# 1) Flatten the file into an ordered timeline of turns.
|
|
96
|
+
timeline: list[dict[str, Any]] = []
|
|
97
|
+
model = ""
|
|
98
|
+
pending_tool = ""
|
|
64
99
|
for line in lines:
|
|
65
100
|
line = line.strip()
|
|
66
101
|
if not line:
|
|
@@ -69,45 +104,103 @@ class CodexAdapter(BaseAdapter):
|
|
|
69
104
|
obj = json.loads(line)
|
|
70
105
|
except json.JSONDecodeError:
|
|
71
106
|
continue
|
|
107
|
+
if not isinstance(obj, dict):
|
|
108
|
+
continue
|
|
72
109
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if
|
|
110
|
+
top_type = obj.get("type")
|
|
111
|
+
payload = obj.get("payload")
|
|
112
|
+
if not isinstance(payload, dict):
|
|
113
|
+
payload = {}
|
|
114
|
+
ts = _ts(obj.get("timestamp"))
|
|
115
|
+
|
|
116
|
+
# Track the active model from session metadata / turn context.
|
|
117
|
+
if top_type in ("session_meta", "turn_context"):
|
|
118
|
+
m = payload.get("model")
|
|
119
|
+
if isinstance(m, str) and m:
|
|
120
|
+
model = m
|
|
76
121
|
continue
|
|
77
122
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
text = content
|
|
89
|
-
else:
|
|
90
|
-
text = ""
|
|
91
|
-
|
|
92
|
-
if not text.strip():
|
|
123
|
+
if top_type == "event_msg":
|
|
124
|
+
p_type = payload.get("type")
|
|
125
|
+
if p_type == "user_message":
|
|
126
|
+
text = payload.get("message", "")
|
|
127
|
+
if isinstance(text, str) and text.strip():
|
|
128
|
+
timeline.append({"kind": "user", "text": text, "ts": ts})
|
|
129
|
+
elif p_type == "agent_message":
|
|
130
|
+
text = payload.get("message", "")
|
|
131
|
+
if isinstance(text, str) and text.strip():
|
|
132
|
+
timeline.append({"kind": "assistant", "text": text, "ts": ts})
|
|
93
133
|
continue
|
|
94
134
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
135
|
+
if top_type == "response_item":
|
|
136
|
+
p_type = payload.get("type")
|
|
137
|
+
if p_type == "function_call":
|
|
138
|
+
name = payload.get("name", "")
|
|
139
|
+
pending_tool = name if isinstance(name, str) else ""
|
|
140
|
+
elif p_type == "function_call_output":
|
|
141
|
+
output = payload.get("output", "")
|
|
142
|
+
output = output if isinstance(output, str) else json.dumps(output)
|
|
143
|
+
timeline.append({
|
|
144
|
+
"kind": "tool_output",
|
|
145
|
+
"tool": pending_tool,
|
|
146
|
+
"is_error": _looks_like_error(output),
|
|
147
|
+
"text": output,
|
|
148
|
+
"ts": ts,
|
|
149
|
+
})
|
|
150
|
+
pending_tool = ""
|
|
151
|
+
# response_item/message (assistant/user/developer) is intentionally
|
|
152
|
+
# ignored: genuine prompts come from event_msg/user_message, and the
|
|
153
|
+
# assistant context comes from event_msg/agent_message.
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
# 2) Walk the timeline; enrich each user prompt with what follows it.
|
|
157
|
+
user_indices = [i for i, t in enumerate(timeline) if t["kind"] == "user"]
|
|
158
|
+
records: list[dict[str, Any]] = []
|
|
159
|
+
for n, idx in enumerate(user_indices):
|
|
160
|
+
turn = timeline[idx]
|
|
161
|
+
next_user = user_indices[n + 1] if n + 1 < len(user_indices) else len(timeline)
|
|
162
|
+
|
|
163
|
+
followed_by_error = False
|
|
164
|
+
error_tool = ""
|
|
165
|
+
error_text = ""
|
|
166
|
+
for j in range(idx + 1, next_user):
|
|
167
|
+
t = timeline[j]
|
|
168
|
+
if t["kind"] == "tool_output" and t.get("is_error"):
|
|
169
|
+
followed_by_error = True
|
|
170
|
+
error_tool = t.get("tool", "")
|
|
171
|
+
error_text = t.get("text", "")
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
followed_by_correction = False
|
|
175
|
+
correction_text = ""
|
|
176
|
+
if next_user < len(timeline):
|
|
177
|
+
nxt = timeline[next_user]["text"]
|
|
178
|
+
if CORRECTION_RE.search(nxt):
|
|
179
|
+
followed_by_correction = True
|
|
180
|
+
correction_text = nxt
|
|
181
|
+
|
|
182
|
+
# context_before: last assistant text strictly before this prompt.
|
|
183
|
+
context_before = ""
|
|
184
|
+
for j in range(idx - 1, -1, -1):
|
|
185
|
+
if timeline[j]["kind"] == "assistant":
|
|
186
|
+
context_before = timeline[j]["text"]
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
error_was_recovered = followed_by_error and not followed_by_correction
|
|
105
190
|
|
|
106
191
|
records.append({
|
|
107
192
|
"runtime": self.RUNTIME_ID,
|
|
108
193
|
"session_file": str(path),
|
|
109
|
-
"prompt_text": text,
|
|
110
|
-
"timestamp": ts,
|
|
194
|
+
"prompt_text": turn["text"],
|
|
195
|
+
"timestamp": turn.get("ts"),
|
|
196
|
+
"model": model,
|
|
197
|
+
"context_before": context_before,
|
|
198
|
+
"followed_by_error": followed_by_error,
|
|
199
|
+
"error_was_recovered": error_was_recovered,
|
|
200
|
+
"followed_by_correction": followed_by_correction,
|
|
201
|
+
"correction_text": correction_text,
|
|
202
|
+
"error_tool": error_tool,
|
|
203
|
+
"error_text": error_text,
|
|
111
204
|
})
|
|
112
205
|
|
|
113
206
|
return records
|
|
@@ -1,50 +1,58 @@
|
|
|
1
1
|
"""Gemini CLI adapter (@google/gemini-cli).
|
|
2
2
|
|
|
3
|
-
Session files: ~/.gemini/tmp/<project_hash>/chats
|
|
4
|
-
Format: JSON
|
|
5
|
-
|
|
6
|
-
CONFIRMED (2026-06,
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Degradation: if ~/.gemini/ does not exist, discover() returns [].
|
|
3
|
+
Session files: ~/.gemini/tmp/<project_hash>/chats/session-*.json
|
|
4
|
+
Format: a SINGLE JSON object per file (not JSONL).
|
|
5
|
+
|
|
6
|
+
CONFIRMED (2026-06, against live ~/.gemini/tmp on macOS):
|
|
7
|
+
- Base path / layout: ~/.gemini/tmp/<project_hash>/chats/*.json
|
|
8
|
+
- File shape: {"sessionId", "projectHash", "startTime", "lastUpdated",
|
|
9
|
+
"messages": [ {"id", "timestamp", "type", "content", ...} ]}
|
|
10
|
+
- messages[].type is "user" or "gemini"; content is a plain string.
|
|
11
|
+
- gemini (assistant) messages also carry "model", "thoughts", "tokens".
|
|
12
|
+
- The format does NOT persist tool calls/errors as messages, so tool-error
|
|
13
|
+
detection is not possible here — followed_by_error stays False (honest);
|
|
14
|
+
correction detection (next user message) still works.
|
|
15
|
+
|
|
16
|
+
Degradation: if ~/.gemini/ does not exist, discover() returns []; a file that
|
|
17
|
+
is not the expected object shape yields no records (parse never raises).
|
|
20
18
|
"""
|
|
21
19
|
|
|
22
20
|
from __future__ import annotations
|
|
23
21
|
|
|
22
|
+
import datetime
|
|
24
23
|
import json
|
|
25
24
|
from pathlib import Path
|
|
26
25
|
from typing import Any
|
|
27
26
|
|
|
28
|
-
from .base import BaseAdapter
|
|
27
|
+
from .base import CORRECTION_RE, BaseAdapter
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _ts(value: Any) -> float | None:
|
|
31
|
+
if isinstance(value, (int, float)):
|
|
32
|
+
return float(value)
|
|
33
|
+
if isinstance(value, str):
|
|
34
|
+
try:
|
|
35
|
+
return datetime.datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp()
|
|
36
|
+
except (ValueError, AttributeError):
|
|
37
|
+
return None
|
|
38
|
+
return None
|
|
29
39
|
|
|
30
40
|
|
|
31
41
|
class GeminiAdapter(BaseAdapter):
|
|
32
42
|
RUNTIME_ID = "gemini"
|
|
33
43
|
|
|
34
|
-
# CONFIRMED: path from official gemini-cli session-management docs.
|
|
35
44
|
GEMINI_TMP_DIR = Path.home() / ".gemini" / "tmp"
|
|
36
45
|
|
|
37
46
|
def discover(self) -> list[Path]:
|
|
38
|
-
"""Find all *.
|
|
47
|
+
"""Find all session *.json chat files under ~/.gemini/tmp/<hash>/chats/."""
|
|
39
48
|
if not self.GEMINI_TMP_DIR.exists():
|
|
40
49
|
return []
|
|
41
50
|
files: list[Path] = []
|
|
42
51
|
try:
|
|
43
|
-
|
|
44
|
-
for jsonl in self.GEMINI_TMP_DIR.rglob("chats/*.jsonl"):
|
|
52
|
+
for jf in self.GEMINI_TMP_DIR.rglob("chats/*.json"):
|
|
45
53
|
try:
|
|
46
|
-
if
|
|
47
|
-
files.append(
|
|
54
|
+
if jf.is_file():
|
|
55
|
+
files.append(jf)
|
|
48
56
|
except OSError:
|
|
49
57
|
continue
|
|
50
58
|
except OSError:
|
|
@@ -52,70 +60,83 @@ class GeminiAdapter(BaseAdapter):
|
|
|
52
60
|
return sorted(files, key=lambda f: f.stat().st_mtime, reverse=True)
|
|
53
61
|
|
|
54
62
|
def parse(self, path: Path) -> list[dict[str, Any]]:
|
|
55
|
-
"""Parse one Gemini
|
|
56
|
-
|
|
57
|
-
STUBBED schema: expected MessageRecord with {role, content: [{text}], ...}.
|
|
58
|
-
Falls back silently if the schema differs.
|
|
59
|
-
"""
|
|
60
|
-
records: list[dict[str, Any]] = []
|
|
63
|
+
"""Parse one Gemini chat JSON file into user-prompt records with
|
|
64
|
+
correction context. Tool errors are not represented in this format."""
|
|
61
65
|
try:
|
|
62
|
-
|
|
63
|
-
except OSError:
|
|
66
|
+
obj = json.loads(path.read_text(encoding="utf-8", errors="replace"))
|
|
67
|
+
except (OSError, json.JSONDecodeError):
|
|
68
|
+
return []
|
|
69
|
+
if not isinstance(obj, dict):
|
|
64
70
|
return []
|
|
65
71
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
continue
|
|
70
|
-
try:
|
|
71
|
-
obj = json.loads(line)
|
|
72
|
-
except json.JSONDecodeError:
|
|
73
|
-
continue
|
|
72
|
+
messages = obj.get("messages")
|
|
73
|
+
if not isinstance(messages, list):
|
|
74
|
+
return []
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
# Normalise into an ordered timeline of {kind, text, model, ts}.
|
|
77
|
+
timeline: list[dict[str, Any]] = []
|
|
78
|
+
for m in messages:
|
|
79
|
+
if not isinstance(m, dict):
|
|
78
80
|
continue
|
|
79
|
-
|
|
80
|
-
content =
|
|
81
|
-
if isinstance(content,
|
|
82
|
-
parts = []
|
|
83
|
-
for block in content:
|
|
84
|
-
if isinstance(block, dict):
|
|
85
|
-
text = block.get("text", "")
|
|
86
|
-
if text:
|
|
87
|
-
parts.append(text)
|
|
88
|
-
text = "\n".join(parts)
|
|
89
|
-
elif isinstance(content, str):
|
|
90
|
-
text = content
|
|
91
|
-
else:
|
|
92
|
-
text = ""
|
|
93
|
-
|
|
94
|
-
if not text.strip():
|
|
81
|
+
mtype = m.get("type")
|
|
82
|
+
content = m.get("content", "")
|
|
83
|
+
if not isinstance(content, str) or not content.strip():
|
|
95
84
|
continue
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
ts
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
85
|
+
ts = _ts(m.get("timestamp"))
|
|
86
|
+
if mtype == "user":
|
|
87
|
+
timeline.append({"kind": "user", "text": content, "ts": ts})
|
|
88
|
+
elif mtype == "gemini":
|
|
89
|
+
model = m.get("model")
|
|
90
|
+
timeline.append({
|
|
91
|
+
"kind": "assistant",
|
|
92
|
+
"text": content,
|
|
93
|
+
"ts": ts,
|
|
94
|
+
"model": str(model) if model else "",
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
user_indices = [i for i, t in enumerate(timeline) if t["kind"] == "user"]
|
|
98
|
+
records: list[dict[str, Any]] = []
|
|
99
|
+
for n, idx in enumerate(user_indices):
|
|
100
|
+
turn = timeline[idx]
|
|
101
|
+
next_user = user_indices[n + 1] if n + 1 < len(user_indices) else len(timeline)
|
|
102
|
+
|
|
103
|
+
# Model that answered this prompt: the first assistant reply after it.
|
|
104
|
+
model = ""
|
|
105
|
+
context_after_assistant = ""
|
|
106
|
+
for j in range(idx + 1, next_user):
|
|
107
|
+
if timeline[j]["kind"] == "assistant":
|
|
108
|
+
model = timeline[j].get("model", "") or model
|
|
109
|
+
if not context_after_assistant:
|
|
110
|
+
context_after_assistant = timeline[j]["text"]
|
|
111
|
+
# context_before: last assistant text strictly before this prompt.
|
|
112
|
+
context_before = ""
|
|
113
|
+
for j in range(idx - 1, -1, -1):
|
|
114
|
+
if timeline[j]["kind"] == "assistant":
|
|
115
|
+
context_before = timeline[j]["text"]
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
followed_by_correction = False
|
|
119
|
+
correction_text = ""
|
|
120
|
+
if next_user < len(timeline):
|
|
121
|
+
nxt = timeline[next_user]["text"]
|
|
122
|
+
if CORRECTION_RE.search(nxt):
|
|
123
|
+
followed_by_correction = True
|
|
124
|
+
correction_text = nxt
|
|
125
|
+
|
|
126
|
+
records.append({
|
|
111
127
|
"runtime": self.RUNTIME_ID,
|
|
112
128
|
"session_file": str(path),
|
|
113
|
-
"prompt_text": text,
|
|
114
|
-
"timestamp": ts,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
record
|
|
118
|
-
|
|
119
|
-
|
|
129
|
+
"prompt_text": turn["text"],
|
|
130
|
+
"timestamp": turn.get("ts"),
|
|
131
|
+
"model": model,
|
|
132
|
+
"context_before": context_before,
|
|
133
|
+
# This format does not record tool errors — be honest, not invent.
|
|
134
|
+
"followed_by_error": False,
|
|
135
|
+
"error_was_recovered": False,
|
|
136
|
+
"followed_by_correction": followed_by_correction,
|
|
137
|
+
"correction_text": correction_text,
|
|
138
|
+
"error_tool": "",
|
|
139
|
+
"error_text": "",
|
|
140
|
+
})
|
|
120
141
|
|
|
121
142
|
return records
|
|
@@ -33,6 +33,7 @@ from typing import Any
|
|
|
33
33
|
# ---------------------------------------------------------------------------
|
|
34
34
|
_TOOLS_DIR = Path(__file__).parent
|
|
35
35
|
sys.path.insert(0, str(_TOOLS_DIR))
|
|
36
|
+
from adapters.base import CORRECTION_RE
|
|
36
37
|
from adapters.registry import get_adapters, list_runtime_ids
|
|
37
38
|
|
|
38
39
|
# ---------------------------------------------------------------------------
|
|
@@ -42,15 +43,11 @@ from adapters.registry import get_adapters, list_runtime_ids
|
|
|
42
43
|
MAX_PROMPTS = 300
|
|
43
44
|
PROMPT_TEXT_LIMIT = 1500
|
|
44
45
|
CORRECTION_TEXT_LIMIT = 500
|
|
46
|
+
ERROR_TEXT_LIMIT = 500
|
|
45
47
|
CONTEXT_BEFORE_LIMIT = 500
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
r"I said|I meant|I asked|that'?s not|please don'?t|why did you|"
|
|
50
|
-
r"you should have|that was wrong|incorrect|try again|redo|"
|
|
51
|
-
r"that broke|you broke|revert|undo)\b",
|
|
52
|
-
re.IGNORECASE,
|
|
53
|
-
)
|
|
49
|
+
# Single source of truth for the correction heuristic — shared with every adapter.
|
|
50
|
+
CORRECTION_PATTERNS = CORRECTION_RE
|
|
54
51
|
|
|
55
52
|
# Model tier classification (provider-neutral labels).
|
|
56
53
|
# Maps substrings found in model IDs to tier names.
|
|
@@ -144,13 +141,15 @@ def normalise_record(raw: dict[str, Any], position: int, total: int) -> dict[str
|
|
|
144
141
|
"has_xml_tags": has_xml_tags,
|
|
145
142
|
"has_file_paths": has_file_paths,
|
|
146
143
|
"has_code_blocks": has_code_blocks,
|
|
147
|
-
#
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
"
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
"
|
|
144
|
+
# Error/correction context: honored from the adapter when it does its own
|
|
145
|
+
# session-aware detection (codex, gemini); defaults to False for adapters
|
|
146
|
+
# that only surface raw prompts. Claude uses process_claude_sessions instead.
|
|
147
|
+
"followed_by_error": bool(raw.get("followed_by_error", False)),
|
|
148
|
+
"error_was_recovered": bool(raw.get("error_was_recovered", False)),
|
|
149
|
+
"followed_by_correction": bool(raw.get("followed_by_correction", False)),
|
|
150
|
+
"correction_text": truncate(raw.get("correction_text", ""), CORRECTION_TEXT_LIMIT),
|
|
151
|
+
"error_tool": raw.get("error_tool", "") or "",
|
|
152
|
+
"error_text": truncate(raw.get("error_text", ""), ERROR_TEXT_LIMIT),
|
|
154
153
|
"context_before": truncate(raw.get("context_before", ""), CONTEXT_BEFORE_LIMIT),
|
|
155
154
|
# Compute fields (best-effort from adapter)
|
|
156
155
|
"model": model_id,
|