@tracemarketplace/cli 0.0.15 → 0.0.18
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 +7 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +79 -14
- package/dist/api-client.js.map +1 -1
- package/dist/api-client.test.d.ts +2 -0
- package/dist/api-client.test.d.ts.map +1 -0
- package/dist/api-client.test.js +34 -0
- package/dist/api-client.test.js.map +1 -0
- package/dist/cli.js +9 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/daemon.d.ts.map +1 -1
- package/dist/commands/daemon.js +71 -6
- package/dist/commands/daemon.js.map +1 -1
- package/dist/commands/remove-daemon.d.ts +6 -0
- package/dist/commands/remove-daemon.d.ts.map +1 -0
- package/dist/commands/remove-daemon.js +66 -0
- package/dist/commands/remove-daemon.js.map +1 -0
- package/dist/commands/submit.d.ts +1 -0
- package/dist/commands/submit.d.ts.map +1 -1
- package/dist/commands/submit.js +25 -15
- package/dist/commands/submit.js.map +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/flush.d.ts +5 -1
- package/dist/flush.d.ts.map +1 -1
- package/dist/flush.js +107 -27
- package/dist/flush.js.map +1 -1
- package/dist/flush.test.js +162 -7
- package/dist/flush.test.js.map +1 -1
- package/package.json +2 -2
- package/src/api-client.test.ts +47 -0
- package/src/api-client.ts +98 -14
- package/src/cli.ts +10 -9
- package/src/commands/daemon.ts +82 -6
- package/src/commands/remove-daemon.ts +75 -0
- package/src/commands/submit.ts +28 -18
- package/src/config.ts +18 -0
- package/src/flush.test.ts +187 -6
- package/src/flush.ts +140 -39
- package/src/commands/register.ts +0 -8
package/src/flush.ts
CHANGED
|
@@ -7,11 +7,11 @@ import {
|
|
|
7
7
|
extractCursor,
|
|
8
8
|
redactTrace,
|
|
9
9
|
type NormalizedTrace,
|
|
10
|
-
type Turn,
|
|
11
10
|
} from "@tracemarketplace/shared";
|
|
12
11
|
import { ApiClient } from "./api-client.js";
|
|
13
12
|
import {
|
|
14
13
|
loadState,
|
|
14
|
+
migrateSessionUploadState,
|
|
15
15
|
saveState,
|
|
16
16
|
stateKey,
|
|
17
17
|
type Config,
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
import { CURSOR_DB_PATH } from "./sessions.js";
|
|
22
22
|
|
|
23
23
|
const IDLE_FINALIZATION_MS = 2 * 24 * 60 * 60 * 1000;
|
|
24
|
+
const UNCONFIRMED_RESUBMIT_MS = 2 * 60 * 60 * 1000; // re-submit after 2hr with no confirmation
|
|
25
|
+
const INGEST_CONCURRENCY = 12;
|
|
24
26
|
|
|
25
27
|
export interface SessionSource {
|
|
26
28
|
tool: TrackedSessionTool;
|
|
@@ -67,6 +69,12 @@ interface ChunkUploadResult {
|
|
|
67
69
|
error?: string;
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
interface IngestResponse {
|
|
73
|
+
queued?: boolean;
|
|
74
|
+
duplicate?: boolean;
|
|
75
|
+
trace_id?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
export function collectIdleSessionSources(
|
|
71
79
|
sessions: Record<string, SessionUploadState>,
|
|
72
80
|
now = new Date()
|
|
@@ -80,10 +88,60 @@ export function collectIdleSessionSources(
|
|
|
80
88
|
}));
|
|
81
89
|
}
|
|
82
90
|
|
|
91
|
+
interface ChunkExistsResponse {
|
|
92
|
+
exists: boolean;
|
|
93
|
+
trace_id?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function verifyUnconfirmedChunks(
|
|
97
|
+
state: ReturnType<typeof loadState>,
|
|
98
|
+
client: ApiClient,
|
|
99
|
+
now: Date,
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
for (const [key, session] of Object.entries(state.sessions)) {
|
|
102
|
+
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
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check each unconfirmed chunk sequentially — stop at first missing one
|
|
119
|
+
for (let i = session.confirmedChunkIndex; i < session.nextChunkIndex; i++) {
|
|
120
|
+
try {
|
|
121
|
+
const params = new URLSearchParams({
|
|
122
|
+
source_tool: session.sourceTool,
|
|
123
|
+
source_session_id: session.sourceSessionId,
|
|
124
|
+
chunk_index: String(i),
|
|
125
|
+
});
|
|
126
|
+
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
|
+
};
|
|
134
|
+
} catch {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
83
141
|
export async function flushTrackedSessions(
|
|
84
142
|
config: Config,
|
|
85
143
|
sources: SessionSource[],
|
|
86
|
-
opts: { includeIdleTracked?: boolean; now?: Date } = {}
|
|
144
|
+
opts: { includeIdleTracked?: boolean; now?: Date; prefetchedTraces?: Map<string, NormalizedTrace>; sync?: boolean } = {}
|
|
87
145
|
): Promise<FlushResult> {
|
|
88
146
|
const now = opts.now ?? new Date();
|
|
89
147
|
const state = loadState(config.profile);
|
|
@@ -94,12 +152,24 @@ export async function flushTrackedSessions(
|
|
|
94
152
|
...(opts.includeIdleTracked ? collectIdleSessionSources(state.sessions, now) : []),
|
|
95
153
|
]);
|
|
96
154
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
results.push(await processSessionSource(source, state, config, client, now));
|
|
155
|
+
// Migrate any existing sessions that predate the confirmation fields
|
|
156
|
+
for (const [key, session] of Object.entries(state.sessions)) {
|
|
157
|
+
state.sessions[key] = migrateSessionUploadState(session);
|
|
101
158
|
}
|
|
102
159
|
|
|
160
|
+
// Verify previously queued chunks and handle 2hr re-submit timeout
|
|
161
|
+
await verifyUnconfirmedChunks(state, client, now);
|
|
162
|
+
|
|
163
|
+
// Process sessions concurrently — each session's chunks stay sequential internally
|
|
164
|
+
const results = await pLimit(
|
|
165
|
+
allSources.map((source) => () => processSessionSource(
|
|
166
|
+
source, state, config, client, now,
|
|
167
|
+
opts.prefetchedTraces?.get(`${source.tool}:${source.locator}`),
|
|
168
|
+
opts.sync,
|
|
169
|
+
)),
|
|
170
|
+
INGEST_CONCURRENCY,
|
|
171
|
+
);
|
|
172
|
+
|
|
103
173
|
saveState(state, config.profile);
|
|
104
174
|
|
|
105
175
|
return {
|
|
@@ -174,6 +244,9 @@ export function createFreshSessionState(
|
|
|
174
244
|
lastSeenTurnCount: 0,
|
|
175
245
|
lastActivityAt: null,
|
|
176
246
|
lastFlushedTurnId: null,
|
|
247
|
+
confirmedChunkIndex: 0,
|
|
248
|
+
confirmedOpenChunkStartTurn: 0,
|
|
249
|
+
unconfirmedSince: null,
|
|
177
250
|
};
|
|
178
251
|
}
|
|
179
252
|
|
|
@@ -182,15 +255,20 @@ export function migrateLegacySessionState(
|
|
|
182
255
|
trace: NormalizedTrace,
|
|
183
256
|
legacyChunkIndex: number
|
|
184
257
|
): SessionUploadState {
|
|
258
|
+
const nextChunkIndex = legacyChunkIndex + 1;
|
|
259
|
+
const openChunkStartTurn = trace.turn_count;
|
|
185
260
|
return {
|
|
186
261
|
sourceTool: source.tool,
|
|
187
262
|
sourceSessionId: trace.source_session_id,
|
|
188
263
|
locator: source.locator,
|
|
189
|
-
nextChunkIndex
|
|
190
|
-
openChunkStartTurn
|
|
264
|
+
nextChunkIndex,
|
|
265
|
+
openChunkStartTurn,
|
|
191
266
|
lastSeenTurnCount: trace.turn_count,
|
|
192
267
|
lastActivityAt: getLastActivityAt(trace),
|
|
193
268
|
lastFlushedTurnId: trace.turns[trace.turn_count - 1]?.turn_id ?? null,
|
|
269
|
+
confirmedChunkIndex: nextChunkIndex,
|
|
270
|
+
confirmedOpenChunkStartTurn: openChunkStartTurn,
|
|
271
|
+
unconfirmedSince: null,
|
|
194
272
|
};
|
|
195
273
|
}
|
|
196
274
|
|
|
@@ -199,12 +277,14 @@ async function processSessionSource(
|
|
|
199
277
|
state: ReturnType<typeof loadState>,
|
|
200
278
|
config: Config,
|
|
201
279
|
client: ApiClient,
|
|
202
|
-
now: Date
|
|
280
|
+
now: Date,
|
|
281
|
+
prefetchedTrace?: NormalizedTrace,
|
|
282
|
+
sync = false,
|
|
203
283
|
): Promise<SessionFlushResult> {
|
|
204
284
|
let trace: NormalizedTrace;
|
|
205
285
|
|
|
206
286
|
try {
|
|
207
|
-
trace = await extractTraceFromSource(source, config.email);
|
|
287
|
+
trace = prefetchedTrace ?? await extractTraceFromSource(source, config.email);
|
|
208
288
|
} catch (err) {
|
|
209
289
|
return {
|
|
210
290
|
source,
|
|
@@ -250,7 +330,10 @@ async function processSessionSource(
|
|
|
250
330
|
let payoutCents = 0;
|
|
251
331
|
|
|
252
332
|
for (const upload of plan.uploads) {
|
|
253
|
-
const
|
|
333
|
+
const tUpload = Date.now();
|
|
334
|
+
const result = await uploadTraceChunk(upload.trace, client, sync);
|
|
335
|
+
if (!sync) console.error(`[flush] ${key} chunk${upload.trace.chunk_index} queued in ${Date.now()-tUpload}ms`);
|
|
336
|
+
else console.error(`[flush] ${key} chunk${upload.trace.chunk_index} done in ${Date.now()-tUpload}ms err=${result.error?.slice(0,60) ?? 'none'}`);
|
|
254
337
|
if (result.error) {
|
|
255
338
|
state.sessions[key] = workingState;
|
|
256
339
|
if (workingState.nextChunkIndex > 0) {
|
|
@@ -313,44 +396,41 @@ async function extractTraceFromSource(
|
|
|
313
396
|
|
|
314
397
|
async function uploadTraceChunk(
|
|
315
398
|
trace: NormalizedTrace,
|
|
316
|
-
client: ApiClient
|
|
399
|
+
client: ApiClient,
|
|
400
|
+
sync = false,
|
|
317
401
|
): Promise<ChunkUploadResult> {
|
|
402
|
+
// Client-side regex redaction runs before transmission; Presidio runs server-side async.
|
|
403
|
+
const payloadTrace = redactTrace(trace, { homeDir: homedir() });
|
|
404
|
+
const jsonSize = JSON.stringify({ trace: payloadTrace, source_tool: payloadTrace.source_tool }).length;
|
|
405
|
+
console.error(`[upload] ${payloadTrace.source_session_id} payload=${Math.round(jsonSize/1024)}KB turns=${payloadTrace.turn_count}`);
|
|
406
|
+
|
|
407
|
+
if (!sync) {
|
|
408
|
+
// Fire and forget — optimistically treat as queued; verifyUnconfirmedChunks confirms later
|
|
409
|
+
client.post("/api/v1/traces/ingest", {
|
|
410
|
+
trace: payloadTrace,
|
|
411
|
+
source_tool: payloadTrace.source_tool,
|
|
412
|
+
}).catch(() => { /* errors handled by unconfirmed retry mechanism */ });
|
|
413
|
+
return { duplicate: false, payoutCents: 0, traceId: null };
|
|
414
|
+
}
|
|
415
|
+
|
|
318
416
|
try {
|
|
319
|
-
const result = await client.post("/api/v1/traces/
|
|
320
|
-
|
|
321
|
-
source_tool:
|
|
322
|
-
}) as
|
|
323
|
-
accepted: number;
|
|
324
|
-
duplicate: number;
|
|
325
|
-
traces: Array<{
|
|
326
|
-
error?: string;
|
|
327
|
-
is_duplicate?: boolean;
|
|
328
|
-
payout_cents?: number;
|
|
329
|
-
trace_id?: string;
|
|
330
|
-
}>;
|
|
331
|
-
};
|
|
417
|
+
const result = await client.post("/api/v1/traces/ingest", {
|
|
418
|
+
trace: payloadTrace,
|
|
419
|
+
source_tool: payloadTrace.source_tool,
|
|
420
|
+
}) as IngestResponse;
|
|
332
421
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return {
|
|
336
|
-
duplicate: false,
|
|
337
|
-
payoutCents: 0,
|
|
338
|
-
traceId: traceResult.trace_id ?? null,
|
|
339
|
-
error: traceResult.error,
|
|
340
|
-
};
|
|
422
|
+
if (result.duplicate) {
|
|
423
|
+
return { duplicate: true, payoutCents: 0, traceId: result.trace_id ?? null };
|
|
341
424
|
}
|
|
342
425
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
payoutCents: traceResult.payout_cents ?? 0,
|
|
346
|
-
traceId: traceResult.trace_id ?? null,
|
|
347
|
-
};
|
|
426
|
+
// 202 queued — payout credited asynchronously; show $0 until processed
|
|
427
|
+
return { duplicate: false, payoutCents: 0, traceId: null };
|
|
348
428
|
} catch (err) {
|
|
349
429
|
return {
|
|
350
430
|
duplicate: false,
|
|
351
431
|
payoutCents: 0,
|
|
352
432
|
traceId: null,
|
|
353
|
-
error:
|
|
433
|
+
error: formatSubmitFailure(err),
|
|
354
434
|
};
|
|
355
435
|
}
|
|
356
436
|
}
|
|
@@ -379,6 +459,7 @@ function applyUploadedChunk(
|
|
|
379
459
|
const nextOpenChunkStartTurn = (uploadedChunk.chunk_start_turn ?? cursor.openChunkStartTurn)
|
|
380
460
|
+ (uploadedChunk.turn_count ?? uploadedChunk.turns.length);
|
|
381
461
|
|
|
462
|
+
const nowIso = new Date().toISOString();
|
|
382
463
|
return {
|
|
383
464
|
...cursor,
|
|
384
465
|
nextChunkIndex: (uploadedChunk.chunk_index ?? cursor.nextChunkIndex) + 1,
|
|
@@ -386,6 +467,7 @@ function applyUploadedChunk(
|
|
|
386
467
|
lastSeenTurnCount: trace.turn_count,
|
|
387
468
|
lastActivityAt: getLastActivityAt(trace),
|
|
388
469
|
lastFlushedTurnId: trace.turns[nextOpenChunkStartTurn - 1]?.turn_id ?? cursor.lastFlushedTurnId,
|
|
470
|
+
unconfirmedSince: cursor.unconfirmedSince ?? nowIso,
|
|
389
471
|
};
|
|
390
472
|
}
|
|
391
473
|
|
|
@@ -485,6 +567,25 @@ function dedupeSources(sources: SessionSource[]): SessionSource[] {
|
|
|
485
567
|
return [...unique.values()];
|
|
486
568
|
}
|
|
487
569
|
|
|
570
|
+
function formatSubmitFailure(err: unknown): string {
|
|
571
|
+
return err instanceof Error ? `Submit failed: ${err.message}` : `Submit failed: ${String(err)}`;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function pLimit<T>(tasks: Array<() => Promise<T>>, concurrency: number): Promise<T[]> {
|
|
575
|
+
const results: T[] = new Array(tasks.length);
|
|
576
|
+
let index = 0;
|
|
577
|
+
|
|
578
|
+
async function worker() {
|
|
579
|
+
while (index < tasks.length) {
|
|
580
|
+
const i = index++;
|
|
581
|
+
results[i] = await tasks[i]!();
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, tasks.length) }, worker));
|
|
586
|
+
return results;
|
|
587
|
+
}
|
|
588
|
+
|
|
488
589
|
export function buildFileSessionSource(
|
|
489
590
|
tool: "claude_code" | "codex_cli",
|
|
490
591
|
filePath: string
|
package/src/commands/register.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import { CLI_NAME } from "../constants.js";
|
|
3
|
-
import { loginCommand } from "./login.js";
|
|
4
|
-
|
|
5
|
-
export async function registerCommand(opts: { profile?: string; serverUrl?: string }): Promise<void> {
|
|
6
|
-
console.log(chalk.yellow(`\`${CLI_NAME} register\` is now an alias for \`${CLI_NAME} login\`.`));
|
|
7
|
-
await loginCommand({ profile: opts.profile, serverUrl: opts.serverUrl });
|
|
8
|
-
}
|