@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 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 saves each child assistant/tool trajectory under its own
18
- conversation id and checkpoint. Flush extraction every five root turns.
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; child execution retains the same
21
- Codex sender identity without being merged into the root conversation.
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.4",
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.4"
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 = "" } = {}) {
@@ -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
+ }
@@ -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
- const stateId = `codex:${conversationId}`;
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
- export function readLastTurn(transcriptPath) {
29
- return readTranscript(transcriptPath, { lastTurnOnly: true });
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)) continue;
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
- pendingLegacyToolCallIds: [],
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.pendingLegacyToolCallIds = [];
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 === "developer") return null;
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) return null;
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) return null;
266
+ if (state.isSubagent) {
267
+ countDiagnostic(state, "subagent_user");
268
+ return null;
269
+ }
177
270
  const content = normalizeUserMessage(state, rawText);
178
- if (!content) return null;
179
- state.pendingLegacyToolCallIds = [];
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") 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) };
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: payload.call_id || `${custom ? "codex_custom_tool" : "codex_tool"}_${timestamp ?? "untimed"}`,
304
+ id: callId,
194
305
  type: "function",
195
306
  name: payload.name || "unknown",
196
- arguments: redactText(argumentText(custom ? payload.input : payload.arguments)),
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) && payload.call_id) {
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: payload.call_id,
205
- content: capText(outputText(payload.output) || "tool result"),
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
- toolCalls: [{
213
- id: `codex_web_search_${timestamp ?? "untimed"}_${lineNumber}`,
214
- type: "function",
215
- name: "web_search",
216
- arguments: redactText(argumentText(payload.action)),
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 === "agent_message") return null;
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 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
- };
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") return value;
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);