@everme/codex 0.6.1 → 0.6.3

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
@@ -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 only the latest user turn, and flush
14
- extraction every five turns.
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.1",
3
+ "version": "0.6.3",
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.1"
43
+ "@everme/agent-sdk": "^0.6.3"
44
44
  },
45
45
  "devDependencies": {
46
46
  "esbuild": "^0.28.1"
package/src/adapter.js CHANGED
@@ -1,11 +1,16 @@
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"]);
6
7
 
7
8
  export const codexAdapter = {
8
9
  platform: "codex",
10
+ // One hook invocation is one logical turn and the delivery marker runs on
11
+ // every one of them, so the SDK claims channel="hook" for these writes and
12
+ // they enter L1-2's denominator. Whole-session hosts leave this unset.
13
+ turnBoundary: "stop",
9
14
 
10
15
  envFile() {
11
16
  return process.env.EVERME_ENV_FILE_PATH || path.join(os.homedir(), ".codex", "everme.env");
@@ -29,6 +34,10 @@ export const codexAdapter = {
29
34
  return readLastTurn(input?.transcriptPath);
30
35
  },
31
36
 
37
+ readStoreBatches(input, options) {
38
+ return readCodexStoreBatches(input, options);
39
+ },
40
+
32
41
  formatOutput(event, { block = "" } = {}) {
33
42
  if (!CONTEXT_EVENTS.has(event) || !block) return {};
34
43
  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
- export async function readLastTurn(transcriptPath) {
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
- let delta = [];
12
- let foundUser = false;
44
+ const state = newParseState(legacyUsersBySegment);
45
+ let messages = [];
46
+ let lineNumber = 0;
13
47
 
14
- for await (const line of lines) {
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
- if (event?.type !== "response_item" || !event.payload) continue;
22
- const message = mapPayload(event.payload, event.timestamp);
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
- if (message.role === "user") {
26
- delta = [message];
27
- foundUser = true;
28
- } else if (foundUser) {
29
- delta.push(message);
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 foundUser ? delta : [];
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 mapPayload(payload, timestampValue) {
36
- const timestamp = normalizeTimestamp(timestampValue);
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 !== "user" && payload.role !== "assistant") return null;
39
- const content = contentText(payload.content);
40
- if (!content) return null;
41
- return { role: payload.role, ...stamp(timestamp), content };
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 || `codex_tool_${timestamp ?? "untimed"}`,
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 === "function_call_output" && payload.call_id) {
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 capText(content);
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 capText(parts.join("\n"));
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
- // undefined — never 0: 0 is a finite epoch (1970) that the SDK would ship
99
- // as-is, while a missing timestamp makes the SDK stamp Date.now() instead,
100
- // keeping the epoch-ms wire contract honest.
101
- return Number.isFinite(parsed) ? parsed : undefined;
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
  }