@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/dist/api-client.d.ts +2 -1
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +47 -13
- package/dist/api-client.js.map +1 -1
- package/dist/api-client.test.js +36 -1
- package/dist/api-client.test.js.map +1 -1
- package/dist/commands/submit-worker.test.d.ts +2 -0
- package/dist/commands/submit-worker.test.d.ts.map +1 -0
- package/dist/commands/submit-worker.test.js +94 -0
- package/dist/commands/submit-worker.test.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -2
- package/dist/config.js.map +1 -1
- package/dist/config.test.d.ts +2 -0
- package/dist/config.test.d.ts.map +1 -0
- package/dist/config.test.js +65 -0
- package/dist/config.test.js.map +1 -0
- package/dist/flush.d.ts +3 -0
- package/dist/flush.d.ts.map +1 -1
- package/dist/flush.js +43 -21
- package/dist/flush.js.map +1 -1
- package/dist/flush.test.js +71 -1
- package/dist/flush.test.js.map +1 -1
- package/package.json +2 -2
- package/src/api-client.test.ts +46 -1
- package/src/api-client.ts +54 -13
- package/src/commands/submit-worker.test.ts +112 -0
- package/src/config.test.ts +75 -0
- package/src/config.ts +8 -2
- package/src/flush.test.ts +89 -1
- package/src/flush.ts +63 -22
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(
|
|
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
|
-
|
|
105
|
-
|
|
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 (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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 =
|
|
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
|
|