@everme/codex 0.6.4 → 0.6.5
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 +47 -4
- package/package.json +2 -2
- package/src/adapter.js +3 -2
- package/src/fragment.js +31 -0
- package/src/store-batches.js +10 -4
- package/src/transcript.js +185 -70
package/README.md
CHANGED
|
@@ -14,13 +14,56 @@ shipped inside the EverMe Codex marketplace plugin and uses the stable
|
|
|
14
14
|
visible assistant/tool trajectory; when no real user exists, retain genuine
|
|
15
15
|
assistant/tool activity instead of requiring a synthetic user boundary.
|
|
16
16
|
The hook also discovers completed descendant rollouts in the same session
|
|
17
|
-
directory and
|
|
18
|
-
|
|
17
|
+
directory and skips children without a real user after filtering, without
|
|
18
|
+
advancing their checkpoints. Flush extraction every five root turns.
|
|
19
19
|
Developer prompts, injected context, UI notifications, reasoning, and
|
|
20
|
-
subagent user/task context are excluded
|
|
21
|
-
|
|
20
|
+
subagent user/task context are excluded. Goal continuations retain only the
|
|
21
|
+
explicitly user-provided objective, not scheduler instructions or budgets.
|
|
22
22
|
- `PreCompact`: send a flush-only request.
|
|
23
23
|
|
|
24
|
+
Native paginated rollouts isolate transcript checkpoints by the filename's
|
|
25
|
+
thread UUID (`codex-fragment-<uuid>`), even when multiple files share the hook
|
|
26
|
+
session ID. The same scope accompanies every write and matches cold-start
|
|
27
|
+
fragment coordinates; logical conversation attribution stays unchanged.
|
|
28
|
+
Unscoped legacy checkpoints are not copied into fragment state. A newly seen
|
|
29
|
+
root fragment still captures only its last turn, then captures appended messages
|
|
30
|
+
on later Stops; this is not automatic historical replay. Before writing, the SDK
|
|
31
|
+
requires `hookSyncScopeSupported` from the server's memory-state endpoint.
|
|
32
|
+
Deploy the server before the plugin; unsupported servers leave the checkpoint
|
|
33
|
+
unchanged. Separate session flush requests retain their existing contract.
|
|
34
|
+
|
|
35
|
+
Both realtime capture and Codex cold-start import first plan user tasks. Fitting
|
|
36
|
+
tasks keep their tool results unchanged. Tasks exceeding the 280 KiB message
|
|
37
|
+
budget try 4, 3, then 2 KiB UTF-8 tool-result budgets from original content,
|
|
38
|
+
retaining 70% head and 30% tail around a byte-count marker. Compression changes
|
|
39
|
+
no IDs, arguments, timestamps or ordering. Count-only overflow is not compressed.
|
|
40
|
+
The 64 KiB soft target cannot split a fitting task; remaining byte or 500-message
|
|
41
|
+
overflow prefers text edges, then complete tool pairs, then hard cuts. Hard
|
|
42
|
+
fallbacks cannot guarantee a user or text edges in every batch. Existing
|
|
43
|
+
redaction and ordinary-text budgets remain unchanged, as do checkpoint and
|
|
44
|
+
flush semantics. The shared SDK strategy is opt-in, currently enabled by Codex.
|
|
45
|
+
|
|
46
|
+
Both paths retain recorded empty tool results as empty content with their
|
|
47
|
+
original call id. Legacy wrappers without native IDs are dropped with bounded
|
|
48
|
+
count diagnostics; neither line-number IDs nor inferred pairing are used.
|
|
49
|
+
A standalone `web_search_call` has no paired result:
|
|
50
|
+
its source payload is retained as JSON text in an assistant message, subject to
|
|
51
|
+
the normal redaction and length limits. It does not create a synthetic tool call
|
|
52
|
+
or result, and an absent source status is not replaced with a success claim.
|
|
53
|
+
|
|
54
|
+
Tool arguments are JSON-encoded strings: existing JSON is retained, empty
|
|
55
|
+
arguments become `{}`, and unstructured legacy/function input is preserved as
|
|
56
|
+
`{"input":"original text"}`. Local `turnId` and tri-state `turnComplete`
|
|
57
|
+
metadata guide SDK batching without adding fields to the BFF message contract.
|
|
58
|
+
Native function/custom calls and results must carry a non-blank string
|
|
59
|
+
`call_id`. Invalid records are dropped with bounded count diagnostics; no
|
|
60
|
+
fallback ID is generated and no missing ID is inferred from pending calls.
|
|
61
|
+
Valid source IDs are preserved unchanged.
|
|
62
|
+
Explicit final answers and completion events identify completed turns;
|
|
63
|
+
commentary and standalone search records do not close a turn. Unknown or
|
|
64
|
+
malformed records and filtering reasons produce one bounded count summary per
|
|
65
|
+
transcript, without source text or credentials.
|
|
66
|
+
|
|
24
67
|
All hook failures are fail-open and credentials are redacted from diagnostics.
|
|
25
68
|
`evercli plugin install codex` writes credentials to `~/.codex/everme.env`
|
|
26
69
|
with mode `0600`; the user reviews and trusts the commands in `/hooks`. Hook
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everme/codex",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
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.5"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"esbuild": "^0.28.1"
|
package/src/adapter.js
CHANGED
|
@@ -11,6 +11,7 @@ export const codexAdapter = {
|
|
|
11
11
|
// every one of them, so the SDK claims channel="hook" for these writes and
|
|
12
12
|
// they enter L1-2's denominator. Whole-session hosts leave this unset.
|
|
13
13
|
turnBoundary: "stop",
|
|
14
|
+
taskBatching: true,
|
|
14
15
|
|
|
15
16
|
envFile() {
|
|
16
17
|
return process.env.EVERME_ENV_FILE_PATH || path.join(os.homedir(), ".codex", "everme.env");
|
|
@@ -31,11 +32,11 @@ export const codexAdapter = {
|
|
|
31
32
|
},
|
|
32
33
|
|
|
33
34
|
readLastTurn(input) {
|
|
34
|
-
return readLastTurn(input?.transcriptPath);
|
|
35
|
+
return readLastTurn(input?.transcriptPath, { diagnostic: (line) => console.error(line) });
|
|
35
36
|
},
|
|
36
37
|
|
|
37
38
|
readStoreBatches(input, options) {
|
|
38
|
-
return readCodexStoreBatches(input, options);
|
|
39
|
+
return readCodexStoreBatches(input, { ...options, diagnostic: (line) => console.error(line) });
|
|
39
40
|
},
|
|
40
41
|
|
|
41
42
|
formatOutput(event, { block = "" } = {}) {
|
package/src/fragment.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
|
|
5
|
+
const UUID = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
|
|
6
|
+
const ROLLOUT = new RegExp(`^rollout-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-(${UUID})(?:_(${UUID}))?\\.jsonl$`);
|
|
7
|
+
|
|
8
|
+
export async function readFragmentScope(transcriptPath) {
|
|
9
|
+
const stream = createReadStream(transcriptPath, { encoding: "utf8" });
|
|
10
|
+
const lines = createInterface({ input: stream, crlfDelay: Infinity });
|
|
11
|
+
try {
|
|
12
|
+
for await (const line of lines) {
|
|
13
|
+
let event;
|
|
14
|
+
try { event = JSON.parse(line); } catch { continue; }
|
|
15
|
+
if (event?.type !== "session_meta") continue;
|
|
16
|
+
if (event.payload?.history_mode !== "paginated") return "";
|
|
17
|
+
const match = path.basename(transcriptPath).match(ROLLOUT);
|
|
18
|
+
if (!match) {
|
|
19
|
+
if (event.payload?.history_base?.thread_id) {
|
|
20
|
+
throw new Error("codex paginated fragment has no native rollout filename identity");
|
|
21
|
+
}
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
return `codex-fragment-${match[2] || match[1]}`;
|
|
25
|
+
}
|
|
26
|
+
return "";
|
|
27
|
+
} finally {
|
|
28
|
+
lines.close();
|
|
29
|
+
stream.destroy();
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/store-batches.js
CHANGED
|
@@ -2,12 +2,14 @@ import { readFile, readdir } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { readCanonicalTranscript, readLastTurn } from "./transcript.js";
|
|
4
4
|
import { turnOrdinals } from "@everme/agent-sdk";
|
|
5
|
+
import { readFragmentScope } from "./fragment.js";
|
|
5
6
|
|
|
6
|
-
export async function readCodexStoreBatches(input, { checkpointStore } = {}) {
|
|
7
|
+
export async function readCodexStoreBatches(input, { checkpointStore, diagnostic = () => {} } = {}) {
|
|
7
8
|
if (!input?.sessionId || !input?.transcriptPath) return [];
|
|
8
9
|
const batches = [];
|
|
9
10
|
const root = await transcriptBatch({
|
|
10
11
|
checkpointStore,
|
|
12
|
+
diagnostic,
|
|
11
13
|
conversationId: input.sessionId,
|
|
12
14
|
initialTailOnly: true,
|
|
13
15
|
transcriptPath: input.transcriptPath,
|
|
@@ -17,6 +19,7 @@ export async function readCodexStoreBatches(input, { checkpointStore } = {}) {
|
|
|
17
19
|
for (const child of await completedDescendants(input.transcriptPath, input.sessionId)) {
|
|
18
20
|
const batch = await transcriptBatch({
|
|
19
21
|
checkpointStore,
|
|
22
|
+
diagnostic,
|
|
20
23
|
conversationId: child.id,
|
|
21
24
|
initialTailOnly: false,
|
|
22
25
|
transcriptPath: child.path,
|
|
@@ -26,9 +29,11 @@ export async function readCodexStoreBatches(input, { checkpointStore } = {}) {
|
|
|
26
29
|
return batches;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
async function transcriptBatch({ checkpointStore, conversationId, initialTailOnly, transcriptPath }) {
|
|
30
|
-
const canonical = await readCanonicalTranscript(transcriptPath);
|
|
31
|
-
|
|
32
|
+
async function transcriptBatch({ checkpointStore, conversationId, initialTailOnly, transcriptPath, diagnostic }) {
|
|
33
|
+
const canonical = await readCanonicalTranscript(transcriptPath, { diagnostic });
|
|
34
|
+
if (!canonical.length) return null;
|
|
35
|
+
const syncScope = await readFragmentScope(transcriptPath);
|
|
36
|
+
const stateId = syncScope ? `codex:${conversationId}:${syncScope}` : `codex:${conversationId}`;
|
|
32
37
|
const checkpoint = checkpointStore
|
|
33
38
|
? await checkpointStore.read(stateId)
|
|
34
39
|
: { initialized: false, uploadedCount: 0 };
|
|
@@ -39,6 +44,7 @@ async function transcriptBatch({ checkpointStore, conversationId, initialTailOnl
|
|
|
39
44
|
if (!messages.length) return null;
|
|
40
45
|
return {
|
|
41
46
|
conversationId,
|
|
47
|
+
...(syncScope ? { syncScope } : {}),
|
|
42
48
|
messages,
|
|
43
49
|
// Address by absolute turn only when everything before the slice is
|
|
44
50
|
// known to be covered: a checkpoint continuation, or the whole canonical
|
package/src/transcript.js
CHANGED
|
@@ -25,15 +25,23 @@ const SYNTHETIC_USER_TAGS = [
|
|
|
25
25
|
"turn_aborted",
|
|
26
26
|
];
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const KNOWN_EVENT_MESSAGES = new Set([
|
|
29
|
+
"agent_message", "agent_reasoning", "context_compacted", "item_completed",
|
|
30
|
+
"mcp_tool_call_begin", "mcp_tool_call_end", "patch_apply_begin", "patch_apply_end",
|
|
31
|
+
"reasoning", "task_complete", "task_started", "sub_agent_activity", "thread_goal_updated",
|
|
32
|
+
"thread_rolled_back", "thread_settings_applied", "token_count", "turn_aborted",
|
|
33
|
+
"turn_complete", "user_message", "web_search_begin", "web_search_end",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
export function readLastTurn(transcriptPath, options = {}) {
|
|
37
|
+
return readTranscript(transcriptPath, { ...options, lastTurnOnly: true });
|
|
30
38
|
}
|
|
31
39
|
|
|
32
|
-
export function readCanonicalTranscript(transcriptPath) {
|
|
33
|
-
return readTranscript(transcriptPath, { lastTurnOnly: false });
|
|
40
|
+
export function readCanonicalTranscript(transcriptPath, options = {}) {
|
|
41
|
+
return readTranscript(transcriptPath, { ...options, lastTurnOnly: false });
|
|
34
42
|
}
|
|
35
43
|
|
|
36
|
-
async function readTranscript(transcriptPath, { lastTurnOnly }) {
|
|
44
|
+
async function readTranscript(transcriptPath, { lastTurnOnly, diagnostic = () => {} }) {
|
|
37
45
|
if (!transcriptPath) return [];
|
|
38
46
|
const legacyUsersBySegment = await collectLegacyUserMessages(transcriptPath);
|
|
39
47
|
const fallbackTimestampBase = await transcriptFallbackTimestampBase(transcriptPath);
|
|
@@ -53,6 +61,7 @@ async function readTranscript(transcriptPath, { lastTurnOnly }) {
|
|
|
53
61
|
try {
|
|
54
62
|
event = JSON.parse(line);
|
|
55
63
|
} catch {
|
|
64
|
+
countDiagnostic(state, "malformed_json");
|
|
56
65
|
continue;
|
|
57
66
|
}
|
|
58
67
|
const payload = event?.payload;
|
|
@@ -61,11 +70,34 @@ async function readTranscript(transcriptPath, { lastTurnOnly }) {
|
|
|
61
70
|
if (lastTurnOnly) messages = [];
|
|
62
71
|
continue;
|
|
63
72
|
}
|
|
64
|
-
if (skipsInheritedSubagentEvent(state, event))
|
|
73
|
+
if (skipsInheritedSubagentEvent(state, event)) {
|
|
74
|
+
countDiagnostic(state, "inherited_context");
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
observeTurn(state, payload);
|
|
78
|
+
if (event?.type === "turn_context") {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (["task_complete", "turn_complete"].includes(event?.type) || (event?.type === "event_msg"
|
|
82
|
+
&& ["task_complete", "turn_complete"].includes(payload?.type))) {
|
|
83
|
+
if (event.type !== "event_msg" && typeof event.turn_id === "string" && event.turn_id) {
|
|
84
|
+
observeTurn(state, event);
|
|
85
|
+
}
|
|
86
|
+
const last = messages.at(-1);
|
|
87
|
+
if (last && last === state.lastAssistant && last.turnComplete !== false
|
|
88
|
+
&& (last.turnId || "") === state.turnId) {
|
|
89
|
+
setBoundaryMetadata(last, "turnComplete", true);
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
65
93
|
if (event?.type === "event_msg") {
|
|
94
|
+
if (!KNOWN_EVENT_MESSAGES.has(payload?.type)) countDiagnostic(state, "unknown_event_message");
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (event?.type !== "response_item" || !payload) {
|
|
98
|
+
if (!["compacted"].includes(event?.type)) countDiagnostic(state, "unknown_event");
|
|
66
99
|
continue;
|
|
67
100
|
}
|
|
68
|
-
if (event?.type !== "response_item" || !payload) continue;
|
|
69
101
|
|
|
70
102
|
const message = mapPayload(
|
|
71
103
|
payload,
|
|
@@ -75,15 +107,58 @@ async function readTranscript(transcriptPath, { lastTurnOnly }) {
|
|
|
75
107
|
state,
|
|
76
108
|
);
|
|
77
109
|
if (!message) continue;
|
|
110
|
+
if (state.turnId) setBoundaryMetadata(message, "turnId", state.turnId);
|
|
78
111
|
if (lastTurnOnly && message.role === "user") {
|
|
79
112
|
messages = [message];
|
|
80
113
|
} else {
|
|
81
114
|
messages.push(message);
|
|
82
115
|
}
|
|
83
116
|
}
|
|
117
|
+
if (state.isSubagent && !messages.some((message) => message.role === "user")) {
|
|
118
|
+
countDiagnostic(state, "subagent_without_user");
|
|
119
|
+
messages = [];
|
|
120
|
+
}
|
|
121
|
+
diagnoseToolPairs(messages, state);
|
|
122
|
+
if (Object.keys(state.diagnostics).length) {
|
|
123
|
+
const counts = Object.entries(state.diagnostics).map(([reason, count]) => `${reason}=${count}`).join(" ");
|
|
124
|
+
diagnostic(`[everme] codex parse lines=${lineNumber} messages=${messages.length} ${counts}`.slice(0, 1024));
|
|
125
|
+
}
|
|
84
126
|
return messages;
|
|
85
127
|
}
|
|
86
128
|
|
|
129
|
+
function countDiagnostic(state, reason) {
|
|
130
|
+
state.diagnostics[reason] = (state.diagnostics[reason] || 0) + 1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function sourceCallId(payload) {
|
|
134
|
+
return typeof payload?.call_id === "string" && payload.call_id.trim() ? payload.call_id : "";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function diagnoseToolPairs(messages, state) {
|
|
138
|
+
const seen = new Set();
|
|
139
|
+
const pending = new Map();
|
|
140
|
+
for (const message of messages) {
|
|
141
|
+
for (const call of message.toolCalls || []) {
|
|
142
|
+
if (seen.has(call.id)) countDiagnostic(state, "duplicate_call_id");
|
|
143
|
+
seen.add(call.id);
|
|
144
|
+
pending.set(call.id, (pending.get(call.id) || 0) + 1);
|
|
145
|
+
}
|
|
146
|
+
if (message.role !== "tool") continue;
|
|
147
|
+
const count = pending.get(message.toolCallId) || 0;
|
|
148
|
+
if (!count) countDiagnostic(state, "orphan_result");
|
|
149
|
+
else if (count === 1) pending.delete(message.toolCallId);
|
|
150
|
+
else pending.set(message.toolCallId, count - 1);
|
|
151
|
+
}
|
|
152
|
+
for (const count of pending.values()) {
|
|
153
|
+
state.diagnostics.missing_result = (state.diagnostics.missing_result || 0) + count;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function setBoundaryMetadata(message, key, value) {
|
|
158
|
+
Object.defineProperty(message, key, { value, writable: true, configurable: true });
|
|
159
|
+
return message;
|
|
160
|
+
}
|
|
161
|
+
|
|
87
162
|
async function collectLegacyUserMessages(transcriptPath) {
|
|
88
163
|
const usersBySegment = [];
|
|
89
164
|
const lines = createInterface({
|
|
@@ -115,6 +190,12 @@ async function collectLegacyUserMessages(transcriptPath) {
|
|
|
115
190
|
return usersBySegment;
|
|
116
191
|
}
|
|
117
192
|
|
|
193
|
+
function observeTurn(state, payload) {
|
|
194
|
+
const turnId = payload?.turn_id || payload?.internal_chat_message_metadata_passthrough?.turn_id;
|
|
195
|
+
if (typeof turnId !== "string" || !turnId) return;
|
|
196
|
+
state.turnId = turnId;
|
|
197
|
+
}
|
|
198
|
+
|
|
118
199
|
function newParseState(legacyUsersBySegment) {
|
|
119
200
|
return {
|
|
120
201
|
historyMode: "",
|
|
@@ -126,7 +207,9 @@ function newParseState(legacyUsersBySegment) {
|
|
|
126
207
|
subagentHistoryStart: 0,
|
|
127
208
|
legacyUsersBySegment,
|
|
128
209
|
legacyUserMessages: new Map(legacyUsersBySegment[0] || []),
|
|
129
|
-
|
|
210
|
+
diagnostics: {},
|
|
211
|
+
turnId: "",
|
|
212
|
+
lastAssistant: null,
|
|
130
213
|
};
|
|
131
214
|
}
|
|
132
215
|
|
|
@@ -148,7 +231,8 @@ function observeSessionMeta(state, payload) {
|
|
|
148
231
|
state.isSubagent = true;
|
|
149
232
|
}
|
|
150
233
|
state.legacyUserMessages = new Map(state.legacyUsersBySegment[state.segmentIndex] || []);
|
|
151
|
-
state.
|
|
234
|
+
state.turnId = "";
|
|
235
|
+
state.lastAssistant = null;
|
|
152
236
|
}
|
|
153
237
|
|
|
154
238
|
function skipsInheritedSubagentEvent(state, event) {
|
|
@@ -167,57 +251,94 @@ function sessionMetaIsSubagent(payload) {
|
|
|
167
251
|
function mapPayload(payload, timestampValue, fallbackTimestamp, lineNumber, state) {
|
|
168
252
|
const timestamp = normalizeTimestamp(timestampValue, fallbackTimestamp);
|
|
169
253
|
if (payload.type === "message") {
|
|
170
|
-
if (payload.role
|
|
254
|
+
if (["developer", "system"].includes(payload.role)) {
|
|
255
|
+
countDiagnostic(state, "system_message");
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
171
258
|
const rawText = contentText(payload.content);
|
|
172
|
-
if (!rawText)
|
|
259
|
+
if (!rawText) {
|
|
260
|
+
countDiagnostic(state, "empty_or_nontext_message");
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
173
263
|
if (payload.role === "user") {
|
|
174
264
|
// Subagent user records are inherited parent context or an agent-authored
|
|
175
265
|
// task, not a human utterance.
|
|
176
|
-
if (state.isSubagent)
|
|
266
|
+
if (state.isSubagent) {
|
|
267
|
+
countDiagnostic(state, "subagent_user");
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
177
270
|
const content = normalizeUserMessage(state, rawText);
|
|
178
|
-
if (!content)
|
|
179
|
-
|
|
271
|
+
if (!content) {
|
|
272
|
+
countDiagnostic(state, "injected_user");
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
if (content !== rawText) countDiagnostic(state, "stripped_user_wrapper");
|
|
180
276
|
return { role: "user", ...stamp(timestamp), content: capText(content) };
|
|
181
277
|
}
|
|
182
|
-
if (payload.role !== "assistant")
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
278
|
+
if (payload.role !== "assistant") {
|
|
279
|
+
countDiagnostic(state, "unknown_role");
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
if (legacyToolEnvelope(rawText)) {
|
|
283
|
+
countDiagnostic(state, "dropped_legacy_tool_without_id");
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const message = { role: "assistant", ...stamp(timestamp), content: capText(rawText) };
|
|
287
|
+
if (typeof payload.phase === "string" && payload.phase) {
|
|
288
|
+
setBoundaryMetadata(message, "turnComplete", payload.phase === "final_answer");
|
|
289
|
+
}
|
|
290
|
+
state.lastAssistant = message;
|
|
291
|
+
return message;
|
|
186
292
|
}
|
|
187
293
|
if (payload.type === "function_call" || payload.type === "custom_tool_call") {
|
|
188
294
|
const custom = payload.type === "custom_tool_call";
|
|
295
|
+
const callId = sourceCallId(payload);
|
|
296
|
+
if (!callId) {
|
|
297
|
+
countDiagnostic(state, "dropped_missing_call_id");
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
189
300
|
return {
|
|
190
301
|
role: "assistant",
|
|
191
302
|
...stamp(timestamp),
|
|
192
303
|
toolCalls: [{
|
|
193
|
-
id:
|
|
304
|
+
id: callId,
|
|
194
305
|
type: "function",
|
|
195
306
|
name: payload.name || "unknown",
|
|
196
|
-
arguments: redactText(
|
|
307
|
+
arguments: redactText(custom ? customToolArguments(payload.input) : argumentText(payload.arguments)),
|
|
197
308
|
}],
|
|
198
309
|
};
|
|
199
310
|
}
|
|
200
|
-
if (["function_call_output", "custom_tool_call_output"].includes(payload.type)
|
|
311
|
+
if (["function_call_output", "custom_tool_call_output"].includes(payload.type)) {
|
|
312
|
+
const toolCallId = sourceCallId(payload);
|
|
313
|
+
if (!toolCallId) {
|
|
314
|
+
countDiagnostic(state, "dropped_missing_result_id");
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
if (!Object.hasOwn(payload, "output")) countDiagnostic(state, "missing_output_field");
|
|
201
318
|
return {
|
|
202
319
|
role: "tool",
|
|
203
320
|
...stamp(timestamp),
|
|
204
|
-
toolCallId
|
|
205
|
-
content:
|
|
321
|
+
toolCallId,
|
|
322
|
+
content: redactText(outputText(payload.output)),
|
|
206
323
|
};
|
|
207
324
|
}
|
|
208
325
|
if (payload.type === "web_search_call") {
|
|
209
|
-
return {
|
|
326
|
+
return setBoundaryMetadata({
|
|
210
327
|
role: "assistant",
|
|
211
328
|
...stamp(timestamp),
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
329
|
+
// This source record has no paired result; preserve it without inventing one.
|
|
330
|
+
content: capText(JSON.stringify(payload)),
|
|
331
|
+
}, "turnComplete", false);
|
|
332
|
+
}
|
|
333
|
+
if (payload.type === "agent_message") {
|
|
334
|
+
countDiagnostic(state, "internal_agent_message");
|
|
335
|
+
return null;
|
|
219
336
|
}
|
|
220
|
-
if (payload.type === "
|
|
337
|
+
if (payload.type === "reasoning") {
|
|
338
|
+
countDiagnostic(state, "internal_reasoning");
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
countDiagnostic(state, "unknown_payload");
|
|
221
342
|
return null;
|
|
222
343
|
}
|
|
223
344
|
|
|
@@ -235,6 +356,8 @@ function normalizePaginatedUserText(text) {
|
|
|
235
356
|
let trimmed = text.trim();
|
|
236
357
|
const tags = [...INJECTED_CONTEXT_TAGS, ...SYNTHETIC_USER_TAGS, "command-args", "command-message"];
|
|
237
358
|
while (trimmed) {
|
|
359
|
+
const objective = goalObjective(trimmed);
|
|
360
|
+
if (objective) return objective;
|
|
238
361
|
const command = commandIntent(trimmed);
|
|
239
362
|
if (command) return command;
|
|
240
363
|
|
|
@@ -299,43 +422,18 @@ function envelopeValue(text, tag) {
|
|
|
299
422
|
return end < 0 ? "" : text.slice(valueStart, end).trim();
|
|
300
423
|
}
|
|
301
424
|
|
|
302
|
-
function
|
|
303
|
-
const
|
|
304
|
-
if (!
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
};
|
|
425
|
+
function goalObjective(text) {
|
|
426
|
+
const prefix = '<codex_internal_context source="goal">';
|
|
427
|
+
if (!text.startsWith(prefix)) return "";
|
|
428
|
+
const close = "</codex_internal_context>";
|
|
429
|
+
const end = text.indexOf(close, prefix.length);
|
|
430
|
+
if (end < 0) return "";
|
|
431
|
+
const body = text.slice(prefix.length, end);
|
|
432
|
+
if (!body.includes("The objective below is user-provided data.")) return "";
|
|
433
|
+
const objective = envelopeValue(body, "objective");
|
|
434
|
+
if (!objective) return "";
|
|
435
|
+
const suffix = normalizePaginatedUserText(text.slice(end + close.length));
|
|
436
|
+
return suffix ? `${objective}\n\n${suffix}` : objective;
|
|
339
437
|
}
|
|
340
438
|
|
|
341
439
|
function legacyToolEnvelope(text) {
|
|
@@ -372,7 +470,15 @@ function outputText(value) {
|
|
|
372
470
|
}
|
|
373
471
|
|
|
374
472
|
function argumentText(value) {
|
|
375
|
-
if (typeof value === "string")
|
|
473
|
+
if (typeof value === "string") {
|
|
474
|
+
if (!value.trim()) return "{}";
|
|
475
|
+
try {
|
|
476
|
+
JSON.parse(value);
|
|
477
|
+
return value;
|
|
478
|
+
} catch {
|
|
479
|
+
return JSON.stringify({ input: value });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
376
482
|
try {
|
|
377
483
|
return JSON.stringify(value ?? {});
|
|
378
484
|
} catch {
|
|
@@ -380,6 +486,15 @@ function argumentText(value) {
|
|
|
380
486
|
}
|
|
381
487
|
}
|
|
382
488
|
|
|
489
|
+
function customToolArguments(value) {
|
|
490
|
+
if (value == null || value === "") return "{}";
|
|
491
|
+
try {
|
|
492
|
+
return JSON.stringify({ input: value });
|
|
493
|
+
} catch {
|
|
494
|
+
return "{}";
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
383
498
|
function normalizeTimestamp(value, fallbackTimestamp) {
|
|
384
499
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
385
500
|
return value > 10_000_000_000 ? Math.trunc(value) : Math.trunc(value * 1000);
|