@tracemarketplace/cli 0.0.21 → 0.0.23

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/src/flush.test.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  createFreshSessionState,
8
8
  migrateLegacySessionState,
9
9
  planSessionUploads,
10
+ prepareTraceForUpload,
10
11
  verifyUnconfirmedChunks,
11
12
  } from "./flush.js";
12
13
  import { migrateSessionUploadState } from "./config.js";
@@ -185,6 +186,35 @@ describe("planSessionUploads", () => {
185
186
  });
186
187
  });
187
188
 
189
+ describe("prepareTraceForUpload", () => {
190
+ it("always omits raw session payloads before submit", () => {
191
+ const trace = {
192
+ ...makeTrace("session-raw", [
193
+ makeTurn("u1", "user", "2026-03-21T00:00:00.000Z"),
194
+ makeTurn("a1", "assistant", "2026-03-21T00:01:00.000Z", 10),
195
+ ]),
196
+ submitted_by: "user@example.com",
197
+ raw_json: {
198
+ events: [
199
+ {
200
+ type: "event_msg",
201
+ text: "/Users/fleet/project/secrets.txt",
202
+ },
203
+ ],
204
+ },
205
+ raw_json_format: "codex_cli.jsonl",
206
+ } satisfies NormalizedTrace;
207
+
208
+ const prepared = prepareTraceForUpload(trace, { homeDir: "/Users/fleet" });
209
+
210
+ expect(prepared.raw_json).toBeNull();
211
+ expect(prepared.raw_json_format).toBeNull();
212
+ expect(prepared.submitted_by).toBe("[redacted]");
213
+ expect(prepared.turn_count).toBe(trace.turn_count);
214
+ expect(prepared.turns).toHaveLength(trace.turns.length);
215
+ });
216
+ });
217
+
188
218
  describe("migrateSessionUploadState", () => {
189
219
  it("fills in missing confirmation fields from next/openChunk values", () => {
190
220
  const legacy = {
@@ -231,7 +261,13 @@ describe("verifyUnconfirmedChunks", () => {
231
261
  return { version: 2 as const, chunks: {}, sessions };
232
262
  }
233
263
 
234
- function makeMockClient(responses: Record<string, { exists: boolean }>) {
264
+ function makeMockClient(
265
+ responses: Record<string, {
266
+ exists: boolean;
267
+ pending?: boolean;
268
+ status?: "queued" | "running" | "retry_wait" | "completed" | "duplicate" | "failed_terminal" | "payload_expired" | "missing";
269
+ }>,
270
+ ) {
235
271
  return {
236
272
  async get(path: string) {
237
273
  const url = new URL(`http://x${path}`);
@@ -326,6 +362,58 @@ describe("verifyUnconfirmedChunks", () => {
326
362
  expect(s.unconfirmedSince).toBeNull();
327
363
  });
328
364
 
365
+ it("does not reset timed-out chunks that are still pending on the server", async () => {
366
+ const state = makeSubmitState({
367
+ "codex_cli:sess_pending": makeSessionState({
368
+ sourceTool: "codex_cli",
369
+ sourceSessionId: "sess_pending",
370
+ locator: "/tmp/sess_pending.jsonl",
371
+ nextChunkIndex: 3,
372
+ openChunkStartTurn: 12,
373
+ confirmedChunkIndex: 1,
374
+ confirmedOpenChunkStartTurn: 4,
375
+ unconfirmedSince: "2026-03-21T00:00:00.000Z",
376
+ }),
377
+ });
378
+
379
+ const client = makeMockClient({
380
+ "codex_cli:sess_pending:1": { exists: false, pending: true, status: "retry_wait" },
381
+ });
382
+
383
+ await verifyUnconfirmedChunks(state, client, new Date("2026-03-21T02:01:00.000Z"));
384
+
385
+ const s = state.sessions["codex_cli:sess_pending"]!;
386
+ expect(s.nextChunkIndex).toBe(3);
387
+ expect(s.openChunkStartTurn).toBe(12);
388
+ expect(s.unconfirmedSince).toBe("2026-03-21T00:00:00.000Z");
389
+ });
390
+
391
+ it("immediately resets when the server reports a terminal ingest failure", async () => {
392
+ const state = makeSubmitState({
393
+ "codex_cli:sess_failed": makeSessionState({
394
+ sourceTool: "codex_cli",
395
+ sourceSessionId: "sess_failed",
396
+ locator: "/tmp/sess_failed.jsonl",
397
+ nextChunkIndex: 3,
398
+ openChunkStartTurn: 12,
399
+ confirmedChunkIndex: 1,
400
+ confirmedOpenChunkStartTurn: 4,
401
+ unconfirmedSince: "2026-03-21T00:00:00.000Z",
402
+ }),
403
+ });
404
+
405
+ const client = makeMockClient({
406
+ "codex_cli:sess_failed:1": { exists: false, pending: false, status: "failed_terminal" },
407
+ });
408
+
409
+ await verifyUnconfirmedChunks(state, client, new Date("2026-03-21T00:05:00.000Z"));
410
+
411
+ const s = state.sessions["codex_cli:sess_failed"]!;
412
+ expect(s.nextChunkIndex).toBe(1);
413
+ expect(s.openChunkStartTurn).toBe(4);
414
+ expect(s.unconfirmedSince).toBeNull();
415
+ });
416
+
329
417
  it("skips sessions that are already fully confirmed", async () => {
330
418
  const state = makeSubmitState({
331
419
  "codex_cli:sess4": makeSessionState({
package/src/flush.ts CHANGED
@@ -75,6 +75,23 @@ interface IngestResponse {
75
75
  trace_id?: string;
76
76
  }
77
77
 
78
+ export function prepareTraceForUpload(
79
+ trace: NormalizedTrace,
80
+ options: { homeDir?: string } = {},
81
+ ): NormalizedTrace {
82
+ const { homeDir = homedir() } = options;
83
+
84
+ // Keep session-sized raw payloads local until they have a separate upload path.
85
+ return redactTrace(
86
+ {
87
+ ...trace,
88
+ raw_json: null,
89
+ raw_json_format: null,
90
+ },
91
+ { homeDir },
92
+ );
93
+ }
94
+
78
95
  export function collectIdleSessionSources(
79
96
  sessions: Record<string, SessionUploadState>,
80
97
  now = new Date()
@@ -90,7 +107,10 @@ export function collectIdleSessionSources(
90
107
 
91
108
  interface ChunkExistsResponse {
92
109
  exists: boolean;
110
+ pending?: boolean;
111
+ status?: "queued" | "running" | "retry_wait" | "completed" | "duplicate" | "failed_terminal" | "payload_expired" | "missing";
93
112
  trace_id?: string;
113
+ retry_after_ms?: number;
94
114
  }
95
115
 
96
116
  export async function verifyUnconfirmedChunks(
@@ -100,20 +120,9 @@ export async function verifyUnconfirmedChunks(
100
120
  ): Promise<void> {
101
121
  for (const [key, session] of Object.entries(state.sessions)) {
102
122
  if (session.confirmedChunkIndex >= session.nextChunkIndex) continue;
103
-
104
- // Check if timed out — reset to re-submit from last confirmed point
105
- if (session.unconfirmedSince) {
106
- const age = now.getTime() - Date.parse(session.unconfirmedSince);
107
- if (age >= UNCONFIRMED_RESUBMIT_MS) {
108
- state.sessions[key] = {
109
- ...session,
110
- nextChunkIndex: session.confirmedChunkIndex,
111
- openChunkStartTurn: session.confirmedOpenChunkStartTurn,
112
- unconfirmedSince: null,
113
- };
114
- continue;
115
- }
116
- }
123
+ const timedOut = session.unconfirmedSince
124
+ ? now.getTime() - Date.parse(session.unconfirmedSince) >= UNCONFIRMED_RESUBMIT_MS
125
+ : false;
117
126
 
118
127
  // Check each unconfirmed chunk sequentially — stop at first missing one
119
128
  for (let i = session.confirmedChunkIndex; i < session.nextChunkIndex; i++) {
@@ -124,20 +133,52 @@ export async function verifyUnconfirmedChunks(
124
133
  chunk_index: String(i),
125
134
  });
126
135
  const result = await client.get(`/api/v1/traces/exists?${params}`) as ChunkExistsResponse;
127
- if (!result.exists) break;
128
- state.sessions[key] = {
129
- ...state.sessions[key]!,
130
- confirmedChunkIndex: i + 1,
131
- confirmedOpenChunkStartTurn: state.sessions[key]!.openChunkStartTurn,
132
- unconfirmedSince: i + 1 >= session.nextChunkIndex ? null : state.sessions[key]!.unconfirmedSince,
133
- };
136
+ if (result.exists) {
137
+ state.sessions[key] = {
138
+ ...state.sessions[key]!,
139
+ confirmedChunkIndex: i + 1,
140
+ confirmedOpenChunkStartTurn: state.sessions[key]!.openChunkStartTurn,
141
+ unconfirmedSince: i + 1 >= session.nextChunkIndex ? null : state.sessions[key]!.unconfirmedSince,
142
+ };
143
+ continue;
144
+ }
145
+
146
+ if (result.pending) {
147
+ break;
148
+ }
149
+
150
+ if (result.status === "failed_terminal" || result.status === "payload_expired") {
151
+ resetSessionToConfirmedBaseline(state, key, session);
152
+ break;
153
+ }
154
+
155
+ if (timedOut) {
156
+ resetSessionToConfirmedBaseline(state, key, session);
157
+ }
158
+ break;
134
159
  } catch {
160
+ if (timedOut) {
161
+ resetSessionToConfirmedBaseline(state, key, session);
162
+ }
135
163
  break;
136
164
  }
137
165
  }
138
166
  }
139
167
  }
140
168
 
169
+ function resetSessionToConfirmedBaseline(
170
+ state: ReturnType<typeof loadState>,
171
+ key: string,
172
+ session: SessionUploadState,
173
+ ): void {
174
+ state.sessions[key] = {
175
+ ...session,
176
+ nextChunkIndex: session.confirmedChunkIndex,
177
+ openChunkStartTurn: session.confirmedOpenChunkStartTurn,
178
+ unconfirmedSince: null,
179
+ };
180
+ }
181
+
141
182
  export async function flushTrackedSessions(
142
183
  config: Config,
143
184
  sources: SessionSource[],
@@ -400,7 +441,7 @@ async function uploadTraceChunk(
400
441
  sync = false,
401
442
  ): Promise<ChunkUploadResult> {
402
443
  // Client-side regex redaction runs before transmission; Presidio runs server-side async.
403
- const payloadTrace = redactTrace(trace, { homeDir: homedir() });
444
+ const payloadTrace = prepareTraceForUpload(trace);
404
445
  const jsonSize = JSON.stringify({ trace: payloadTrace, source_tool: payloadTrace.source_tool }).length;
405
446
  console.error(`[upload] ${payloadTrace.source_session_id} payload=${Math.round(jsonSize/1024)}KB turns=${payloadTrace.turn_count}`);
406
447