@everme/codex 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/package.json +2 -2
- package/src/adapter.js +5 -0
- package/src/store-batches.js +120 -0
- package/src/transcript.js +338 -33
package/README.md
CHANGED
|
@@ -10,8 +10,15 @@ shipped inside the EverMe Codex marketplace plugin and uses the stable
|
|
|
10
10
|
- `SessionStart`: inject the EverMe profile snapshot.
|
|
11
11
|
- `UserPromptSubmit`: sanitize the prompt, search top 10 memories, and inject
|
|
12
12
|
recall without passive profile rows by default.
|
|
13
|
-
- `Stop`: stream the Codex rollout, save
|
|
14
|
-
|
|
13
|
+
- `Stop`: stream the Codex rollout, save the latest real user turn and its
|
|
14
|
+
visible assistant/tool trajectory; when no real user exists, retain genuine
|
|
15
|
+
assistant/tool activity instead of requiring a synthetic user boundary.
|
|
16
|
+
The hook also discovers completed descendant rollouts in the same session
|
|
17
|
+
directory and saves each child assistant/tool trajectory under its own
|
|
18
|
+
conversation id and checkpoint. Flush extraction every five root turns.
|
|
19
|
+
Developer prompts, injected context, UI notifications, reasoning, and
|
|
20
|
+
subagent user/task context are excluded; child execution retains the same
|
|
21
|
+
Codex sender identity without being merged into the root conversation.
|
|
15
22
|
- `PreCompact`: send a flush-only request.
|
|
16
23
|
|
|
17
24
|
All hook failures are fail-open and credentials are redacted from diagnostics.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everme/codex",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Native EverMe lifecycle hooks for Codex.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"registry": "https://registry.npmjs.org"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@everme/agent-sdk": "^0.6.
|
|
43
|
+
"@everme/agent-sdk": "^0.6.2"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"esbuild": "^0.28.1"
|
package/src/adapter.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { readCodexStoreBatches } from "./store-batches.js";
|
|
3
4
|
import { readLastTurn } from "./transcript.js";
|
|
4
5
|
|
|
5
6
|
const CONTEXT_EVENTS = new Set(["SessionStart", "UserPromptSubmit"]);
|
|
@@ -29,6 +30,10 @@ export const codexAdapter = {
|
|
|
29
30
|
return readLastTurn(input?.transcriptPath);
|
|
30
31
|
},
|
|
31
32
|
|
|
33
|
+
readStoreBatches(input, options) {
|
|
34
|
+
return readCodexStoreBatches(input, options);
|
|
35
|
+
},
|
|
36
|
+
|
|
32
37
|
formatOutput(event, { block = "" } = {}) {
|
|
33
38
|
if (!CONTEXT_EVENTS.has(event) || !block) return {};
|
|
34
39
|
return {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readCanonicalTranscript, readLastTurn } from "./transcript.js";
|
|
4
|
+
|
|
5
|
+
export async function readCodexStoreBatches(input, { checkpointStore } = {}) {
|
|
6
|
+
if (!input?.sessionId || !input?.transcriptPath) return [];
|
|
7
|
+
const batches = [];
|
|
8
|
+
const root = await transcriptBatch({
|
|
9
|
+
checkpointStore,
|
|
10
|
+
conversationId: input.sessionId,
|
|
11
|
+
initialTailOnly: true,
|
|
12
|
+
transcriptPath: input.transcriptPath,
|
|
13
|
+
});
|
|
14
|
+
if (root) batches.push(root);
|
|
15
|
+
|
|
16
|
+
for (const child of await completedDescendants(input.transcriptPath, input.sessionId)) {
|
|
17
|
+
const batch = await transcriptBatch({
|
|
18
|
+
checkpointStore,
|
|
19
|
+
conversationId: child.id,
|
|
20
|
+
initialTailOnly: false,
|
|
21
|
+
transcriptPath: child.path,
|
|
22
|
+
});
|
|
23
|
+
if (batch) batches.push(batch);
|
|
24
|
+
}
|
|
25
|
+
return batches;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function transcriptBatch({ checkpointStore, conversationId, initialTailOnly, transcriptPath }) {
|
|
29
|
+
const canonical = await readCanonicalTranscript(transcriptPath);
|
|
30
|
+
const stateId = `codex:${conversationId}`;
|
|
31
|
+
const checkpoint = checkpointStore
|
|
32
|
+
? await checkpointStore.read(stateId)
|
|
33
|
+
: { initialized: false, uploadedCount: 0 };
|
|
34
|
+
let messages;
|
|
35
|
+
if (checkpoint.initialized && checkpoint.uploadedCount <= canonical.length) {
|
|
36
|
+
messages = canonical.slice(checkpoint.uploadedCount);
|
|
37
|
+
} else {
|
|
38
|
+
messages = initialTailOnly ? await readLastTurn(transcriptPath) : canonical;
|
|
39
|
+
}
|
|
40
|
+
if (!messages.length) return null;
|
|
41
|
+
return {
|
|
42
|
+
conversationId,
|
|
43
|
+
messages,
|
|
44
|
+
checkpoint: { stateId, uploadedCount: canonical.length },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function completedDescendants(rootPath, rootId) {
|
|
49
|
+
let names;
|
|
50
|
+
try {
|
|
51
|
+
names = await readdir(path.dirname(rootPath));
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const candidates = [];
|
|
56
|
+
for (const name of names) {
|
|
57
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
58
|
+
const candidatePath = path.join(path.dirname(rootPath), name);
|
|
59
|
+
if (candidatePath === rootPath) continue;
|
|
60
|
+
const metadata = await rolloutMetadata(candidatePath);
|
|
61
|
+
if (metadata?.isSubagent && metadata.complete) {
|
|
62
|
+
candidates.push({ ...metadata, path: candidatePath });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const descendants = [];
|
|
67
|
+
const parents = new Set([rootId]);
|
|
68
|
+
let changed = true;
|
|
69
|
+
while (changed) {
|
|
70
|
+
changed = false;
|
|
71
|
+
for (const candidate of candidates) {
|
|
72
|
+
if (candidate.selected || !parents.has(candidate.parentId)) continue;
|
|
73
|
+
candidate.selected = true;
|
|
74
|
+
parents.add(candidate.id);
|
|
75
|
+
descendants.push(candidate);
|
|
76
|
+
changed = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return descendants;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function rolloutMetadata(transcriptPath) {
|
|
83
|
+
let raw;
|
|
84
|
+
try {
|
|
85
|
+
raw = await readFile(transcriptPath, "utf8");
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
let metadata;
|
|
90
|
+
let complete = false;
|
|
91
|
+
for (const line of raw.split("\n")) {
|
|
92
|
+
if (!line.trim()) continue;
|
|
93
|
+
let event;
|
|
94
|
+
try {
|
|
95
|
+
event = JSON.parse(line);
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!metadata && event?.type === "session_meta") {
|
|
100
|
+
const payload = event.payload || {};
|
|
101
|
+
const id = stringValue(payload.id || payload.session_id);
|
|
102
|
+
const parentId = stringValue(payload.parent_thread_id || payload.forked_from_id);
|
|
103
|
+
metadata = {
|
|
104
|
+
id,
|
|
105
|
+
parentId,
|
|
106
|
+
isSubagent: payload.thread_source === "subagent" || Boolean(parentId),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if ((event?.type === "event_msg" && event?.payload?.type === "task_complete")
|
|
110
|
+
|| event?.type === "task_complete") {
|
|
111
|
+
complete = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!metadata?.id || !metadata.parentId) return null;
|
|
115
|
+
return { ...metadata, complete };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function stringValue(value) {
|
|
119
|
+
return typeof value === "string" ? value.trim() : "";
|
|
120
|
+
}
|
package/src/transcript.js
CHANGED
|
@@ -1,65 +1,351 @@
|
|
|
1
1
|
import { createReadStream } from "node:fs";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
2
3
|
import { createInterface } from "node:readline";
|
|
3
4
|
import { capRunes } from "@everme/agent-sdk";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
const INJECTED_CONTEXT_TAGS = [
|
|
7
|
+
"app-context",
|
|
8
|
+
"apps_instructions",
|
|
9
|
+
"codex_internal_context",
|
|
10
|
+
"environment_context",
|
|
11
|
+
"in-app-browser-context",
|
|
12
|
+
"multi_agent_mode",
|
|
13
|
+
"permissions",
|
|
14
|
+
"plugins_instructions",
|
|
15
|
+
"recommended_plugins",
|
|
16
|
+
"skills_instructions",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const SYNTHETIC_USER_TAGS = [
|
|
20
|
+
"bash-input",
|
|
21
|
+
"bash-stdout",
|
|
22
|
+
"command-name",
|
|
23
|
+
"local-command-stdout",
|
|
24
|
+
"task-notification",
|
|
25
|
+
"turn_aborted",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export function readLastTurn(transcriptPath) {
|
|
29
|
+
return readTranscript(transcriptPath, { lastTurnOnly: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function readCanonicalTranscript(transcriptPath) {
|
|
33
|
+
return readTranscript(transcriptPath, { lastTurnOnly: false });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function readTranscript(transcriptPath, { lastTurnOnly }) {
|
|
6
37
|
if (!transcriptPath) return [];
|
|
38
|
+
const legacyUsersBySegment = await collectLegacyUserMessages(transcriptPath);
|
|
39
|
+
const fallbackTimestampBase = await transcriptFallbackTimestampBase(transcriptPath);
|
|
7
40
|
const lines = createInterface({
|
|
8
41
|
input: createReadStream(transcriptPath, { encoding: "utf8" }),
|
|
9
42
|
crlfDelay: Infinity,
|
|
10
43
|
});
|
|
11
|
-
|
|
12
|
-
let
|
|
44
|
+
const state = newParseState(legacyUsersBySegment);
|
|
45
|
+
let messages = [];
|
|
46
|
+
let lineNumber = 0;
|
|
13
47
|
|
|
14
|
-
for await (const
|
|
48
|
+
for await (const rawLine of lines) {
|
|
49
|
+
const line = rawLine.trim();
|
|
50
|
+
if (!line) continue;
|
|
51
|
+
lineNumber += 1;
|
|
15
52
|
let event;
|
|
16
53
|
try {
|
|
17
54
|
event = JSON.parse(line);
|
|
18
55
|
} catch {
|
|
19
56
|
continue;
|
|
20
57
|
}
|
|
21
|
-
|
|
22
|
-
|
|
58
|
+
const payload = event?.payload;
|
|
59
|
+
if (event?.type === "session_meta") {
|
|
60
|
+
observeSessionMeta(state, payload);
|
|
61
|
+
if (lastTurnOnly) messages = [];
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (skipsInheritedSubagentEvent(state, event)) continue;
|
|
65
|
+
if (event?.type === "event_msg") {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (event?.type !== "response_item" || !payload) continue;
|
|
69
|
+
|
|
70
|
+
const message = mapPayload(
|
|
71
|
+
payload,
|
|
72
|
+
event.timestamp,
|
|
73
|
+
fallbackTimestampBase + lineNumber,
|
|
74
|
+
lineNumber,
|
|
75
|
+
state,
|
|
76
|
+
);
|
|
23
77
|
if (!message) continue;
|
|
78
|
+
if (lastTurnOnly && message.role === "user") {
|
|
79
|
+
messages = [message];
|
|
80
|
+
} else {
|
|
81
|
+
messages.push(message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return messages;
|
|
85
|
+
}
|
|
24
86
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
87
|
+
async function collectLegacyUserMessages(transcriptPath) {
|
|
88
|
+
const usersBySegment = [];
|
|
89
|
+
const lines = createInterface({
|
|
90
|
+
input: createReadStream(transcriptPath, { encoding: "utf8" }),
|
|
91
|
+
crlfDelay: Infinity,
|
|
92
|
+
});
|
|
93
|
+
const state = newParseState([]);
|
|
94
|
+
for await (const rawLine of lines) {
|
|
95
|
+
let event;
|
|
96
|
+
try {
|
|
97
|
+
event = JSON.parse(rawLine);
|
|
98
|
+
} catch {
|
|
99
|
+
continue;
|
|
30
100
|
}
|
|
101
|
+
const payload = event?.payload;
|
|
102
|
+
if (event?.type === "session_meta") {
|
|
103
|
+
observeSessionMeta(state, payload);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (skipsInheritedSubagentEvent(state, event) || state.isSubagent
|
|
107
|
+
|| event?.type !== "event_msg" || payload?.type !== "user_message"
|
|
108
|
+
|| typeof payload.message !== "string") continue;
|
|
109
|
+
const text = payload.message.trim();
|
|
110
|
+
if (!text) continue;
|
|
111
|
+
if (!usersBySegment[state.segmentIndex]) usersBySegment[state.segmentIndex] = new Map();
|
|
112
|
+
const users = usersBySegment[state.segmentIndex];
|
|
113
|
+
users.set(text, (users.get(text) || 0) + 1);
|
|
31
114
|
}
|
|
32
|
-
return
|
|
115
|
+
return usersBySegment;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function newParseState(legacyUsersBySegment) {
|
|
119
|
+
return {
|
|
120
|
+
historyMode: "",
|
|
121
|
+
isSubagent: false,
|
|
122
|
+
segmentIndex: 0,
|
|
123
|
+
seenSessionMeta: false,
|
|
124
|
+
outerSessionIsSubagent: false,
|
|
125
|
+
hasSubagentHistoryStart: false,
|
|
126
|
+
subagentHistoryStart: 0,
|
|
127
|
+
legacyUsersBySegment,
|
|
128
|
+
legacyUserMessages: new Map(legacyUsersBySegment[0] || []),
|
|
129
|
+
pendingLegacyToolCallIds: [],
|
|
130
|
+
};
|
|
33
131
|
}
|
|
34
132
|
|
|
35
|
-
function
|
|
36
|
-
|
|
133
|
+
function observeSessionMeta(state, payload) {
|
|
134
|
+
// Root rollouts can contain multiple sections. Child rollouts instead embed
|
|
135
|
+
// forked root metadata, so their outer subagent identity remains authoritative.
|
|
136
|
+
const firstSessionMeta = !state.seenSessionMeta;
|
|
137
|
+
if (!firstSessionMeta) state.segmentIndex += 1;
|
|
138
|
+
else state.seenSessionMeta = true;
|
|
139
|
+
if (firstSessionMeta) state.outerSessionIsSubagent = sessionMetaIsSubagent(payload);
|
|
140
|
+
if (firstSessionMeta || !state.outerSessionIsSubagent) {
|
|
141
|
+
state.historyMode = typeof payload?.history_mode === "string" ? payload.history_mode : "";
|
|
142
|
+
state.isSubagent = sessionMetaIsSubagent(payload);
|
|
143
|
+
state.hasSubagentHistoryStart = Number.isFinite(payload?.subagent_history_start_ordinal);
|
|
144
|
+
state.subagentHistoryStart = state.hasSubagentHistoryStart
|
|
145
|
+
? Math.trunc(payload.subagent_history_start_ordinal)
|
|
146
|
+
: 0;
|
|
147
|
+
} else {
|
|
148
|
+
state.isSubagent = true;
|
|
149
|
+
}
|
|
150
|
+
state.legacyUserMessages = new Map(state.legacyUsersBySegment[state.segmentIndex] || []);
|
|
151
|
+
state.pendingLegacyToolCallIds = [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function skipsInheritedSubagentEvent(state, event) {
|
|
155
|
+
return state.isSubagent && state.hasSubagentHistoryStart
|
|
156
|
+
&& Number.isFinite(event?.ordinal)
|
|
157
|
+
&& Math.trunc(event.ordinal) < state.subagentHistoryStart;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function sessionMetaIsSubagent(payload) {
|
|
161
|
+
if (payload && payload.thread_source !== undefined && payload.thread_source !== null) {
|
|
162
|
+
return String(payload.thread_source).trim() === "subagent";
|
|
163
|
+
}
|
|
164
|
+
return Boolean(payload?.parent_thread_id);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function mapPayload(payload, timestampValue, fallbackTimestamp, lineNumber, state) {
|
|
168
|
+
const timestamp = normalizeTimestamp(timestampValue, fallbackTimestamp);
|
|
37
169
|
if (payload.type === "message") {
|
|
38
|
-
if (payload.role
|
|
39
|
-
const
|
|
40
|
-
if (!
|
|
41
|
-
|
|
170
|
+
if (payload.role === "developer") return null;
|
|
171
|
+
const rawText = contentText(payload.content);
|
|
172
|
+
if (!rawText) return null;
|
|
173
|
+
if (payload.role === "user") {
|
|
174
|
+
// Subagent user records are inherited parent context or an agent-authored
|
|
175
|
+
// task, not a human utterance.
|
|
176
|
+
if (state.isSubagent) return null;
|
|
177
|
+
const content = normalizeUserMessage(state, rawText);
|
|
178
|
+
if (!content) return null;
|
|
179
|
+
state.pendingLegacyToolCallIds = [];
|
|
180
|
+
return { role: "user", ...stamp(timestamp), content: capText(content) };
|
|
181
|
+
}
|
|
182
|
+
if (payload.role !== "assistant") return null;
|
|
183
|
+
const legacyTool = mapLegacyToolMessage(state, rawText, timestamp, lineNumber);
|
|
184
|
+
if (legacyTool.matched) return legacyTool.message;
|
|
185
|
+
return { role: "assistant", ...stamp(timestamp), content: capText(rawText) };
|
|
42
186
|
}
|
|
43
|
-
if (payload.type === "function_call") {
|
|
187
|
+
if (payload.type === "function_call" || payload.type === "custom_tool_call") {
|
|
188
|
+
const custom = payload.type === "custom_tool_call";
|
|
44
189
|
return {
|
|
45
190
|
role: "assistant",
|
|
46
191
|
...stamp(timestamp),
|
|
47
192
|
toolCalls: [{
|
|
48
|
-
id: payload.call_id ||
|
|
193
|
+
id: payload.call_id || `${custom ? "codex_custom_tool" : "codex_tool"}_${timestamp ?? "untimed"}`,
|
|
49
194
|
type: "function",
|
|
50
195
|
name: payload.name || "unknown",
|
|
51
|
-
arguments: argumentText(payload.arguments),
|
|
196
|
+
arguments: redactText(argumentText(custom ? payload.input : payload.arguments)),
|
|
52
197
|
}],
|
|
53
198
|
};
|
|
54
199
|
}
|
|
55
|
-
if (payload.type
|
|
200
|
+
if (["function_call_output", "custom_tool_call_output"].includes(payload.type) && payload.call_id) {
|
|
56
201
|
return {
|
|
57
202
|
role: "tool",
|
|
58
203
|
...stamp(timestamp),
|
|
59
204
|
toolCallId: payload.call_id,
|
|
60
|
-
content: capText(payload.output || "tool result"),
|
|
205
|
+
content: capText(outputText(payload.output) || "tool result"),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (payload.type === "web_search_call") {
|
|
209
|
+
return {
|
|
210
|
+
role: "assistant",
|
|
211
|
+
...stamp(timestamp),
|
|
212
|
+
toolCalls: [{
|
|
213
|
+
id: `codex_web_search_${timestamp ?? "untimed"}_${lineNumber}`,
|
|
214
|
+
type: "function",
|
|
215
|
+
name: "web_search",
|
|
216
|
+
arguments: redactText(argumentText(payload.action)),
|
|
217
|
+
}],
|
|
61
218
|
};
|
|
62
219
|
}
|
|
220
|
+
if (payload.type === "agent_message") return null;
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function normalizeUserMessage(state, text) {
|
|
225
|
+
if (state.historyMode !== "paginated") {
|
|
226
|
+
const remaining = state.legacyUserMessages.get(text) || 0;
|
|
227
|
+
if (remaining === 0) return "";
|
|
228
|
+
state.legacyUserMessages.set(text, remaining - 1);
|
|
229
|
+
return text;
|
|
230
|
+
}
|
|
231
|
+
return normalizePaginatedUserText(text);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizePaginatedUserText(text) {
|
|
235
|
+
let trimmed = text.trim();
|
|
236
|
+
const tags = [...INJECTED_CONTEXT_TAGS, ...SYNTHETIC_USER_TAGS, "command-args", "command-message"];
|
|
237
|
+
while (trimmed) {
|
|
238
|
+
const command = commandIntent(trimmed);
|
|
239
|
+
if (command) return command;
|
|
240
|
+
|
|
241
|
+
const agentsRemainder = stripLeadingAgentsInstructions(trimmed);
|
|
242
|
+
if (agentsRemainder !== null) {
|
|
243
|
+
trimmed = agentsRemainder;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let stripped = false;
|
|
248
|
+
for (const tag of tags) {
|
|
249
|
+
const remainder = stripLeadingEnvelope(trimmed, tag);
|
|
250
|
+
if (remainder !== null) {
|
|
251
|
+
trimmed = remainder;
|
|
252
|
+
stripped = true;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (!stripped) break;
|
|
257
|
+
}
|
|
258
|
+
return !trimmed || hasEnvelopePrefix(trimmed, "command-message") ? "" : trimmed;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function commandIntent(text) {
|
|
262
|
+
const trimmed = text.trim();
|
|
263
|
+
if (!trimmed.startsWith("<command-message>")) return "";
|
|
264
|
+
const name = envelopeValue(trimmed, "command-name");
|
|
265
|
+
if (!name?.startsWith("/")) return "";
|
|
266
|
+
const args = envelopeValue(trimmed, "command-args");
|
|
267
|
+
return `${name} ${args}`.trim();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function stripLeadingEnvelope(text, tag) {
|
|
271
|
+
if (!hasEnvelopePrefix(text, tag)) return null;
|
|
272
|
+
const openEnd = text.indexOf(">");
|
|
273
|
+
if (openEnd < 0) return null;
|
|
274
|
+
const close = `</${tag}>`;
|
|
275
|
+
const closeStart = text.indexOf(close, openEnd + 1);
|
|
276
|
+
if (closeStart < 0) return null;
|
|
277
|
+
return text.slice(closeStart + close.length).trim();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function hasEnvelopePrefix(text, tag) {
|
|
281
|
+
if (!text.startsWith(`<${tag}`)) return false;
|
|
282
|
+
return [">", " ", "\t", "\n", "\r"].includes(text.at(tag.length + 1));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function stripLeadingAgentsInstructions(text) {
|
|
286
|
+
if (!text.startsWith("# AGENTS.md instructions for ") || !text.includes("<INSTRUCTIONS>")) return null;
|
|
287
|
+
const close = "</INSTRUCTIONS>";
|
|
288
|
+
const closeStart = text.indexOf(close);
|
|
289
|
+
return closeStart < 0 ? "" : text.slice(closeStart + close.length).trim();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function envelopeValue(text, tag) {
|
|
293
|
+
const open = `<${tag}>`;
|
|
294
|
+
const close = `</${tag}>`;
|
|
295
|
+
const start = text.indexOf(open);
|
|
296
|
+
if (start < 0) return "";
|
|
297
|
+
const valueStart = start + open.length;
|
|
298
|
+
const end = text.indexOf(close, valueStart);
|
|
299
|
+
return end < 0 ? "" : text.slice(valueStart, end).trim();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function mapLegacyToolMessage(state, text, timestamp, lineNumber) {
|
|
303
|
+
const envelope = legacyToolEnvelope(text);
|
|
304
|
+
if (!envelope) return { matched: false, message: null };
|
|
305
|
+
if (envelope.kind === "call") {
|
|
306
|
+
const callId = `codex_legacy_tool_${lineNumber}`;
|
|
307
|
+
state.pendingLegacyToolCallIds.push(callId);
|
|
308
|
+
return {
|
|
309
|
+
matched: true,
|
|
310
|
+
message: {
|
|
311
|
+
role: "assistant",
|
|
312
|
+
...stamp(timestamp),
|
|
313
|
+
toolCalls: [{
|
|
314
|
+
id: callId,
|
|
315
|
+
type: "function",
|
|
316
|
+
name: envelope.name,
|
|
317
|
+
arguments: redactText(envelope.body),
|
|
318
|
+
}],
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (state.pendingLegacyToolCallIds.length !== 1) {
|
|
323
|
+
// Legacy wrappers carry no call id; ambiguous parallel calls must not be
|
|
324
|
+
// paired by guesswork and persisted as a false trajectory.
|
|
325
|
+
state.pendingLegacyToolCallIds = [];
|
|
326
|
+
return { matched: true, message: null };
|
|
327
|
+
}
|
|
328
|
+
const [toolCallId] = state.pendingLegacyToolCallIds;
|
|
329
|
+
state.pendingLegacyToolCallIds = [];
|
|
330
|
+
return {
|
|
331
|
+
matched: true,
|
|
332
|
+
message: {
|
|
333
|
+
role: "tool",
|
|
334
|
+
...stamp(timestamp),
|
|
335
|
+
toolCallId,
|
|
336
|
+
content: capText(envelope.body || "tool result"),
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function legacyToolEnvelope(text) {
|
|
342
|
+
const trimmed = text.trim();
|
|
343
|
+
const callMatch = trimmed.match(/^\[external_agent_tool_call:\s*([^\]]+)\]\s*([\s\S]*?)\s*\[\/external_agent_tool_call\]$/);
|
|
344
|
+
if (callMatch) {
|
|
345
|
+
return { kind: "call", name: callMatch[1].trim() || "unknown", body: callMatch[2].trim() };
|
|
346
|
+
}
|
|
347
|
+
const resultMatch = trimmed.match(/^\[external_agent_tool_result\]\s*([\s\S]*?)\s*\[\/external_agent_tool_result\]$/);
|
|
348
|
+
if (resultMatch) return { kind: "result", body: resultMatch[1].trim() };
|
|
63
349
|
return null;
|
|
64
350
|
}
|
|
65
351
|
|
|
@@ -68,7 +354,7 @@ function stamp(timestamp) {
|
|
|
68
354
|
}
|
|
69
355
|
|
|
70
356
|
function contentText(content) {
|
|
71
|
-
if (typeof content === "string") return
|
|
357
|
+
if (typeof content === "string") return content.trim();
|
|
72
358
|
if (!Array.isArray(content)) return "";
|
|
73
359
|
const parts = [];
|
|
74
360
|
for (const item of content) {
|
|
@@ -78,7 +364,11 @@ function contentText(content) {
|
|
|
78
364
|
parts.push(item.text);
|
|
79
365
|
}
|
|
80
366
|
}
|
|
81
|
-
return
|
|
367
|
+
return parts.join("\n").trim();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function outputText(value) {
|
|
371
|
+
return typeof value === "string" ? value.trim() : contentText(value);
|
|
82
372
|
}
|
|
83
373
|
|
|
84
374
|
function argumentText(value) {
|
|
@@ -90,19 +380,34 @@ function argumentText(value) {
|
|
|
90
380
|
}
|
|
91
381
|
}
|
|
92
382
|
|
|
93
|
-
function normalizeTimestamp(value) {
|
|
383
|
+
function normalizeTimestamp(value, fallbackTimestamp) {
|
|
94
384
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
95
385
|
return value > 10_000_000_000 ? Math.trunc(value) : Math.trunc(value * 1000);
|
|
96
386
|
}
|
|
97
387
|
const parsed = Date.parse(value);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
388
|
+
return Number.isFinite(parsed) ? parsed : fallbackTimestamp;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function transcriptFallbackTimestampBase(transcriptPath) {
|
|
392
|
+
try {
|
|
393
|
+
return Math.trunc((await stat(transcriptPath)).mtimeMs);
|
|
394
|
+
} catch {
|
|
395
|
+
return 0;
|
|
396
|
+
}
|
|
102
397
|
}
|
|
103
398
|
|
|
104
|
-
// SDK capRunes keeps head AND tail (0.7 head ratio) so the end of a long
|
|
105
|
-
// tool output — exit status, root-cause line — survives truncation.
|
|
106
399
|
function capText(value) {
|
|
107
|
-
return capRunes(String(value || "").trim());
|
|
400
|
+
return capRunes(redactText(String(value || "").trim()));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function redactText(value) {
|
|
404
|
+
return String(value || "")
|
|
405
|
+
.replace(/sk-[A-Za-z0-9_-]{16,}/g, "[redacted]")
|
|
406
|
+
.replace(/evt_[A-Za-z0-9_-]{8,}/g, "[redacted]")
|
|
407
|
+
.replace(/emk_[A-Za-z0-9_-]{8,}/g, "[redacted]")
|
|
408
|
+
.replace(/ghp_[A-Za-z0-9]{20,}/g, "[redacted]")
|
|
409
|
+
.replace(/AKIA[0-9A-Z]{16}/g, "[redacted]")
|
|
410
|
+
.replace(/bearer\s+[A-Za-z0-9._=-]{10,}/gi, "[redacted]")
|
|
411
|
+
.replace(/X-Amz-Signature=[A-Za-z0-9%]+/g, "[redacted]")
|
|
412
|
+
.replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[redacted]");
|
|
108
413
|
}
|