@tracemarketplace/cli 0.0.17 → 0.0.19

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.
Files changed (38) hide show
  1. package/dist/cli.js +9 -1
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/status.d.ts.map +1 -1
  4. package/dist/commands/status.js +14 -0
  5. package/dist/commands/status.js.map +1 -1
  6. package/dist/commands/submit.d.ts +18 -0
  7. package/dist/commands/submit.d.ts.map +1 -1
  8. package/dist/commands/submit.js +141 -21
  9. package/dist/commands/submit.js.map +1 -1
  10. package/dist/commands/submit.test.d.ts +2 -0
  11. package/dist/commands/submit.test.d.ts.map +1 -0
  12. package/dist/commands/submit.test.js +58 -0
  13. package/dist/commands/submit.test.js.map +1 -0
  14. package/dist/config.d.ts +2 -0
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +6 -0
  17. package/dist/config.js.map +1 -1
  18. package/dist/flush.d.ts +1 -0
  19. package/dist/flush.d.ts.map +1 -1
  20. package/dist/flush.js +18 -5
  21. package/dist/flush.js.map +1 -1
  22. package/dist/submit-worker-state.d.ts +17 -0
  23. package/dist/submit-worker-state.d.ts.map +1 -0
  24. package/dist/submit-worker-state.js +88 -0
  25. package/dist/submit-worker-state.js.map +1 -0
  26. package/dist/submit-worker-state.test.d.ts +2 -0
  27. package/dist/submit-worker-state.test.d.ts.map +1 -0
  28. package/dist/submit-worker-state.test.js +45 -0
  29. package/dist/submit-worker-state.test.js.map +1 -0
  30. package/package.json +2 -2
  31. package/src/cli.ts +10 -1
  32. package/src/commands/status.ts +22 -0
  33. package/src/commands/submit.test.ts +64 -0
  34. package/src/commands/submit.ts +189 -25
  35. package/src/config.ts +8 -0
  36. package/src/flush.ts +19 -4
  37. package/src/submit-worker-state.test.ts +61 -0
  38. package/src/submit-worker-state.ts +127 -0
@@ -1,10 +1,12 @@
1
- import { existsSync } from "fs";
1
+ import { closeSync, existsSync, mkdirSync, openSync, unlinkSync, writeFileSync } from "fs";
2
2
  import { readFile } from "fs/promises";
3
+ import { spawn } from "child_process";
4
+ import { join } from "path";
3
5
  import chalk from "chalk";
4
6
  import ora from "ora";
5
7
  import inquirer from "inquirer";
6
8
  import { extractClaudeCode, extractCodex, extractCursor, type NormalizedTrace } from "@tracemarketplace/shared";
7
- import { loadConfig, loadState, resolveProfile, stateKey } from "../config.js";
9
+ import { getConfigDir, getSubmitLogPath, loadConfig, loadState, resolveProfile, stateKey } from "../config.js";
8
10
  import { loginCommandForProfile } from "../constants.js";
9
11
  import {
10
12
  buildCursorSessionSource,
@@ -16,6 +18,7 @@ import {
16
18
  type SessionSource,
17
19
  } from "../flush.js";
18
20
  import { CURSOR_DB_PATH, findFiles } from "../sessions.js";
21
+ import { createSubmitWorkerState, removeSubmitWorkerState, writeSubmitWorkerState } from "../submit-worker-state.js";
19
22
 
20
23
  interface SubmitOptions {
21
24
  profile?: string;
@@ -23,6 +26,7 @@ interface SubmitOptions {
23
26
  session?: string;
24
27
  dryRun?: boolean;
25
28
  since?: string;
29
+ sync?: boolean;
26
30
  }
27
31
 
28
32
  interface DiscoveredSession {
@@ -35,6 +39,18 @@ interface PlannedSession extends DiscoveredSession {
35
39
  pending: boolean;
36
40
  }
37
41
 
42
+ interface SubmitWorkerOptions {
43
+ profile?: string;
44
+ manifest: string;
45
+ }
46
+
47
+ interface SubmitWorkerManifest {
48
+ createdAt: string;
49
+ readyChunkCount: number;
50
+ readySessionCount: number;
51
+ sources: SessionSource[];
52
+ }
53
+
38
54
  function pushSessionIfNonEmpty(
39
55
  sessions: DiscoveredSession[],
40
56
  source: SessionSource,
@@ -268,41 +284,128 @@ export async function submitCommand(opts: SubmitOptions): Promise<void> {
268
284
  return;
269
285
  }
270
286
 
271
- const uploadSpinner = ora(`Submitting ${readyChunkCount} finalized chunk(s) to ${config.profile}...`).start();
287
+ const readySessions = plannedSessions.filter((session) => session.readyChunks > 0);
272
288
 
273
- try {
274
- const readySessions = plannedSessions.filter((session) => session.readyChunks > 0);
289
+ if (opts.sync) {
290
+ const uploadSpinner = ora(`Submitting ${readyChunkCount} finalized chunk(s) to ${config.profile}...`).start();
275
291
  const prefetchedTraces = new Map(readySessions.map((s) => [`${s.source.tool}:${s.source.locator}`, s.trace]));
276
- const result = await flushTrackedSessions(
277
- config,
278
- readySessions.map((session) => session.source),
279
- { includeIdleTracked: false, prefetchedTraces }
280
- );
292
+ try {
293
+ const result = await flushTrackedSessions(
294
+ config,
295
+ readySessions.map((session) => session.source),
296
+ { includeIdleTracked: false, prefetchedTraces, sync: true }
297
+ );
298
+
299
+ uploadSpinner.stop();
300
+ const failedSessions = result.results.filter((session) => session.error && session.error !== "Empty session");
301
+ console.log(chalk.green("\nSubmission complete!"));
302
+ console.log(` Uploaded chunks: ${chalk.bold(result.uploadedChunks)}`);
303
+ console.log(` Duplicate chunks: ${chalk.gray(result.duplicateChunks)}`);
304
+ console.log(` Pending sessions: ${result.pendingSessions}`);
305
+ console.log(` Payout: ${chalk.green("$" + (result.payoutCents / 100).toFixed(2))}`);
306
+ if (failedSessions.length > 0) {
307
+ console.log(chalk.yellow(`\n${failedSessions.length} session(s) failed during submit:`));
308
+ failedSessions.slice(0, 3).forEach((session) => {
309
+ console.log(chalk.gray(` ${session.source.label}: ${session.error}`));
310
+ });
311
+ if (failedSessions.length > 3) {
312
+ console.log(chalk.gray(` ...and ${failedSessions.length - 3} more`));
313
+ }
314
+ }
315
+ } catch (e) {
316
+ uploadSpinner.fail("Submission failed");
317
+ console.error(chalk.red(String(e)));
318
+ process.exit(1);
319
+ }
320
+ return;
321
+ }
322
+
323
+ const launchSpinner = ora(`Starting background submit for ${readyChunkCount} finalized chunk(s)...`).start();
281
324
 
282
- uploadSpinner.stop();
325
+ try {
326
+ const { logPath, pid } = spawnDetachedSubmitWorker(config.profile, {
327
+ createdAt: new Date().toISOString(),
328
+ readyChunkCount,
329
+ readySessionCount,
330
+ sources: readySessions.map((session) => session.source),
331
+ });
332
+
333
+ launchSpinner.stop();
334
+ console.log(chalk.green(`\nStarted background submit for ${readyChunkCount} chunk(s).`));
335
+ if (pid) {
336
+ console.log(chalk.gray(` Worker PID: ${pid}`));
337
+ }
338
+ console.log(chalk.gray(` Log: ${logPath}`));
339
+ console.log(chalk.gray(" Run `tracemp status` later to check confirmation status."));
340
+ } catch (e) {
341
+ launchSpinner.fail("Could not start background submit");
342
+ console.error(chalk.red(String(e)));
343
+ process.exit(1);
344
+ }
345
+ }
283
346
 
347
+ export async function submitWorkerCommand(opts: SubmitWorkerOptions): Promise<void> {
348
+ const profile = resolveProfile(opts.profile);
349
+ try {
350
+ const manifest = parseSubmitWorkerManifest(await readFile(opts.manifest, "utf-8"));
351
+ const config = loadConfig(profile);
352
+ if (!config) {
353
+ throw new Error(`Not authenticated for profile '${profile}'. Run: ${loginCommandForProfile(profile)}`);
354
+ }
355
+
356
+ console.log(
357
+ `[${new Date().toISOString()}] submit worker starting profile=${profile} ready_chunks=${manifest.readyChunkCount} sessions=${manifest.readySessionCount}`
358
+ );
359
+
360
+ const result = await flushTrackedSessions(config, manifest.sources, { includeIdleTracked: false, sync: false });
284
361
  const failedSessions = result.results.filter((session) => session.error && session.error !== "Empty session");
285
362
 
286
- console.log(chalk.green("\nSubmission complete!"));
287
- console.log(` Uploaded chunks: ${chalk.bold(result.uploadedChunks)}`);
288
- console.log(` Duplicate chunks: ${chalk.gray(result.duplicateChunks)}`);
289
- console.log(` Pending sessions: ${result.pendingSessions}`);
290
- console.log(` Payout: ${chalk.green("$" + (result.payoutCents / 100).toFixed(2))}`);
363
+ console.log(
364
+ `[${new Date().toISOString()}] submit worker queued chunks=${result.uploadedChunks} pending_sessions=${result.pendingSessions}`
365
+ );
291
366
 
292
367
  if (failedSessions.length > 0) {
293
- console.log(chalk.yellow(`\n${failedSessions.length} session(s) failed during submit:`));
294
- failedSessions.slice(0, 3).forEach((session) => {
295
- console.log(chalk.gray(` ${session.source.label}: ${session.error}`));
368
+ console.log(`[${new Date().toISOString()}] ${failedSessions.length} session(s) failed during submit:`);
369
+ failedSessions.slice(0, 10).forEach((session) => {
370
+ console.log(` ${session.source.label}: ${session.error}`);
296
371
  });
297
- if (failedSessions.length > 3) {
298
- console.log(chalk.gray(` ...and ${failedSessions.length - 3} more`));
372
+ if (failedSessions.length > 10) {
373
+ console.log(` ...and ${failedSessions.length - 10} more`);
299
374
  }
300
375
  }
301
- } catch (e) {
302
- uploadSpinner.fail("Submission failed");
303
- console.error(chalk.red(String(e)));
304
- process.exit(1);
376
+ } finally {
377
+ removeSubmitWorkerState(profile, process.pid);
378
+ try {
379
+ unlinkSync(opts.manifest);
380
+ } catch {}
381
+ }
382
+ }
383
+
384
+ export function buildSubmitWorkerRuntime(profile: string, manifestPath: string): { command: string; args: string[] } {
385
+ const cliEntrypoint = process.argv[1];
386
+ if (!cliEntrypoint) {
387
+ throw new Error("Unable to resolve tracemp CLI entrypoint for background submit.");
305
388
  }
389
+
390
+ const execArgv = process.execArgv.filter((arg) => !arg.startsWith("--inspect"));
391
+ return {
392
+ command: process.execPath,
393
+ args: [...execArgv, cliEntrypoint, "submit-worker", "--profile", profile, "--manifest", manifestPath],
394
+ };
395
+ }
396
+
397
+ export function parseSubmitWorkerManifest(raw: string): SubmitWorkerManifest {
398
+ const parsed = JSON.parse(raw) as Partial<SubmitWorkerManifest>;
399
+ if (!Array.isArray(parsed.sources) || parsed.sources.some((source) => !isSessionSource(source))) {
400
+ throw new Error("Invalid submit worker manifest.");
401
+ }
402
+
403
+ return {
404
+ createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : new Date(0).toISOString(),
405
+ readyChunkCount: typeof parsed.readyChunkCount === "number" ? parsed.readyChunkCount : parsed.sources.length,
406
+ readySessionCount: typeof parsed.readySessionCount === "number" ? parsed.readySessionCount : parsed.sources.length,
407
+ sources: parsed.sources,
408
+ };
306
409
  }
307
410
 
308
411
  function describeSessionStatus(session: PlannedSession): string {
@@ -311,3 +414,64 @@ function describeSessionStatus(session: PlannedSession): string {
311
414
  if (session.pending) return "pending";
312
415
  return "up-to-date";
313
416
  }
417
+
418
+ function isSessionSource(value: unknown): value is SessionSource {
419
+ if (!value || typeof value !== "object") {
420
+ return false;
421
+ }
422
+
423
+ const source = value as Record<string, unknown>;
424
+ return (
425
+ (source.tool === "claude_code" || source.tool === "codex_cli" || source.tool === "cursor")
426
+ && typeof source.locator === "string"
427
+ && typeof source.label === "string"
428
+ );
429
+ }
430
+
431
+ function spawnDetachedSubmitWorker(
432
+ profile: string,
433
+ manifest: SubmitWorkerManifest
434
+ ): { logPath: string; pid: number | null } {
435
+ mkdirSync(getConfigDir(), { recursive: true });
436
+ const manifestPath = join(
437
+ getConfigDir(),
438
+ `submit-job-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}.json`
439
+ );
440
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
441
+
442
+ const logPath = getSubmitLogPath(profile);
443
+ const logFd = openSync(logPath, "a");
444
+
445
+ try {
446
+ const { command, args } = buildSubmitWorkerRuntime(profile, manifestPath);
447
+ const child = spawn(command, args, {
448
+ cwd: process.cwd(),
449
+ detached: true,
450
+ env: process.env,
451
+ stdio: ["ignore", logFd, logFd],
452
+ });
453
+ child.unref();
454
+
455
+ if (child.pid) {
456
+ writeSubmitWorkerState(
457
+ profile,
458
+ createSubmitWorkerState(
459
+ profile,
460
+ child.pid,
461
+ manifest.createdAt,
462
+ manifest.readyChunkCount,
463
+ manifest.readySessionCount,
464
+ ),
465
+ );
466
+ }
467
+
468
+ return { logPath, pid: child.pid ?? null };
469
+ } catch (err) {
470
+ try {
471
+ unlinkSync(manifestPath);
472
+ } catch {}
473
+ throw err;
474
+ } finally {
475
+ closeSync(logFd);
476
+ }
477
+ }
package/src/config.ts CHANGED
@@ -60,6 +60,14 @@ export function getAutoSubmitLogPath(profile?: string): string {
60
60
  return join(getConfigDir(), `auto-submit${profileSuffix(profile)}.log`);
61
61
  }
62
62
 
63
+ export function getSubmitLogPath(profile?: string): string {
64
+ return join(getConfigDir(), `submit${profileSuffix(profile)}.log`);
65
+ }
66
+
67
+ export function getSubmitWorkerStateDir(profile?: string): string {
68
+ return join(getConfigDir(), `submit-workers${profileSuffix(profile)}`);
69
+ }
70
+
63
71
  export function getDaemonStatePath(profile?: string): string {
64
72
  return join(getConfigDir(), `daemon-state${profileSuffix(profile)}.json`);
65
73
  }
package/src/flush.ts CHANGED
@@ -141,7 +141,7 @@ export async function verifyUnconfirmedChunks(
141
141
  export async function flushTrackedSessions(
142
142
  config: Config,
143
143
  sources: SessionSource[],
144
- opts: { includeIdleTracked?: boolean; now?: Date; prefetchedTraces?: Map<string, NormalizedTrace> } = {}
144
+ opts: { includeIdleTracked?: boolean; now?: Date; prefetchedTraces?: Map<string, NormalizedTrace>; sync?: boolean } = {}
145
145
  ): Promise<FlushResult> {
146
146
  const now = opts.now ?? new Date();
147
147
  const state = loadState(config.profile);
@@ -165,6 +165,7 @@ export async function flushTrackedSessions(
165
165
  allSources.map((source) => () => processSessionSource(
166
166
  source, state, config, client, now,
167
167
  opts.prefetchedTraces?.get(`${source.tool}:${source.locator}`),
168
+ opts.sync,
168
169
  )),
169
170
  INGEST_CONCURRENCY,
170
171
  );
@@ -278,6 +279,7 @@ async function processSessionSource(
278
279
  client: ApiClient,
279
280
  now: Date,
280
281
  prefetchedTrace?: NormalizedTrace,
282
+ sync = false,
281
283
  ): Promise<SessionFlushResult> {
282
284
  let trace: NormalizedTrace;
283
285
 
@@ -329,8 +331,9 @@ async function processSessionSource(
329
331
 
330
332
  for (const upload of plan.uploads) {
331
333
  const tUpload = Date.now();
332
- const result = await uploadTraceChunk(upload.trace, client);
333
- console.error(`[flush] ${key} chunk${upload.trace.chunk_index} done in ${Date.now()-tUpload}ms err=${result.error?.slice(0,60) ?? 'none'}`);
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'}`);
334
337
  if (result.error) {
335
338
  state.sessions[key] = workingState;
336
339
  if (workingState.nextChunkIndex > 0) {
@@ -393,10 +396,22 @@ async function extractTraceFromSource(
393
396
 
394
397
  async function uploadTraceChunk(
395
398
  trace: NormalizedTrace,
396
- client: ApiClient
399
+ client: ApiClient,
400
+ sync = false,
397
401
  ): Promise<ChunkUploadResult> {
398
402
  // Client-side regex redaction runs before transmission; Presidio runs server-side async.
399
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
+ }
400
415
 
401
416
  try {
402
417
  const result = await client.post("/api/v1/traces/ingest", {
@@ -0,0 +1,61 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { mkdtempSync, readFileSync, writeFileSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ import {
6
+ createSubmitWorkerState,
7
+ listActiveSubmitWorkers,
8
+ removeSubmitWorkerState,
9
+ writeSubmitWorkerState,
10
+ } from "./submit-worker-state.js";
11
+
12
+ describe("submit worker state", () => {
13
+ const tempDirs: string[] = [];
14
+
15
+ afterEach(() => {
16
+ vi.restoreAllMocks();
17
+ });
18
+
19
+ it("lists active workers and prunes stale ones", () => {
20
+ const dir = mkdtempSync(join(tmpdir(), "tracemp-submit-workers-"));
21
+ tempDirs.push(dir);
22
+
23
+ writeSubmitWorkerState("prod", createSubmitWorkerState("prod", 111, "2026-03-21T00:00:00.000Z", 3, 2), { dir });
24
+ writeSubmitWorkerState("prod", createSubmitWorkerState("prod", 222, "2026-03-21T01:00:00.000Z", 1, 1), { dir });
25
+
26
+ vi.spyOn(process, "kill").mockImplementation((pid: number | NodeJS.Signals) => {
27
+ if (pid === 111) {
28
+ return true as never;
29
+ }
30
+ const err = new Error("missing") as NodeJS.ErrnoException;
31
+ err.code = "ESRCH";
32
+ throw err;
33
+ });
34
+
35
+ const workers = listActiveSubmitWorkers("prod", { dir });
36
+
37
+ expect(workers).toEqual([
38
+ createSubmitWorkerState("prod", 111, "2026-03-21T00:00:00.000Z", 3, 2),
39
+ ]);
40
+ expect(() => readFileSync(join(dir, "222.json"), "utf-8")).toThrow();
41
+ });
42
+
43
+ it("removes worker state files", () => {
44
+ const dir = mkdtempSync(join(tmpdir(), "tracemp-submit-workers-"));
45
+ tempDirs.push(dir);
46
+
47
+ writeSubmitWorkerState("prod", createSubmitWorkerState("prod", 333, "2026-03-21T00:00:00.000Z", 2, 1), { dir });
48
+ removeSubmitWorkerState("prod", 333, { dir });
49
+
50
+ expect(() => readFileSync(join(dir, "333.json"), "utf-8")).toThrow();
51
+ });
52
+
53
+ it("prunes invalid state payloads", () => {
54
+ const dir = mkdtempSync(join(tmpdir(), "tracemp-submit-workers-"));
55
+ tempDirs.push(dir);
56
+ writeFileSync(join(dir, "bad.json"), JSON.stringify({ nope: true }), "utf-8");
57
+
58
+ expect(listActiveSubmitWorkers("prod", { dir })).toEqual([]);
59
+ expect(() => readFileSync(join(dir, "bad.json"), "utf-8")).toThrow();
60
+ });
61
+ });
@@ -0,0 +1,127 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { getSubmitLogPath, getSubmitWorkerStateDir } from "./config.js";
4
+
5
+ export interface SubmitWorkerState {
6
+ pid: number;
7
+ startedAt: string;
8
+ readyChunkCount: number;
9
+ readySessionCount: number;
10
+ logPath: string;
11
+ }
12
+
13
+ interface SubmitWorkerStateOptions {
14
+ dir?: string;
15
+ }
16
+
17
+ export function isProcessRunning(pid: number): boolean {
18
+ try {
19
+ process.kill(pid, 0);
20
+ return true;
21
+ } catch (err) {
22
+ return (err as NodeJS.ErrnoException).code === "EPERM";
23
+ }
24
+ }
25
+
26
+ export function writeSubmitWorkerState(
27
+ profile: string,
28
+ state: SubmitWorkerState,
29
+ opts: SubmitWorkerStateOptions = {},
30
+ ): void {
31
+ const dir = resolveStateDir(profile, opts.dir);
32
+ mkdirSync(dir, { recursive: true });
33
+ writeFileSync(join(dir, `${state.pid}.json`), JSON.stringify(state, null, 2) + "\n", "utf-8");
34
+ }
35
+
36
+ export function removeSubmitWorkerState(
37
+ profile: string,
38
+ pid: number,
39
+ opts: SubmitWorkerStateOptions = {},
40
+ ): void {
41
+ const dir = resolveStateDir(profile, opts.dir);
42
+ const path = join(dir, `${pid}.json`);
43
+ if (!existsSync(path)) {
44
+ return;
45
+ }
46
+
47
+ try {
48
+ unlinkSync(path);
49
+ } catch {}
50
+ }
51
+
52
+ export function listActiveSubmitWorkers(
53
+ profile: string,
54
+ opts: SubmitWorkerStateOptions = {},
55
+ ): SubmitWorkerState[] {
56
+ const dir = resolveStateDir(profile, opts.dir);
57
+ if (!existsSync(dir)) {
58
+ return [];
59
+ }
60
+
61
+ const workers: SubmitWorkerState[] = [];
62
+ for (const entry of readdirSync(dir)) {
63
+ if (!entry.endsWith(".json")) {
64
+ continue;
65
+ }
66
+
67
+ const path = join(dir, entry);
68
+ const parsed = readSubmitWorkerStateFile(path);
69
+ if (!parsed || !isProcessRunning(parsed.pid)) {
70
+ try {
71
+ unlinkSync(path);
72
+ } catch {}
73
+ continue;
74
+ }
75
+
76
+ workers.push(parsed);
77
+ }
78
+
79
+ return workers.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
80
+ }
81
+
82
+ export function createSubmitWorkerState(
83
+ profile: string,
84
+ pid: number,
85
+ startedAt: string,
86
+ readyChunkCount: number,
87
+ readySessionCount: number,
88
+ ): SubmitWorkerState {
89
+ return {
90
+ pid,
91
+ startedAt,
92
+ readyChunkCount,
93
+ readySessionCount,
94
+ logPath: getSubmitLogPath(profile),
95
+ };
96
+ }
97
+
98
+ function resolveStateDir(profile: string, dir?: string): string {
99
+ return dir ?? getSubmitWorkerStateDir(profile);
100
+ }
101
+
102
+ function readSubmitWorkerStateFile(path: string): SubmitWorkerState | null {
103
+ try {
104
+ const value = JSON.parse(readFileSync(path, "utf-8")) as Partial<SubmitWorkerState>;
105
+ if (
106
+ typeof value.pid !== "number"
107
+ || !Number.isInteger(value.pid)
108
+ || value.pid <= 0
109
+ || typeof value.startedAt !== "string"
110
+ || typeof value.readyChunkCount !== "number"
111
+ || typeof value.readySessionCount !== "number"
112
+ || typeof value.logPath !== "string"
113
+ ) {
114
+ return null;
115
+ }
116
+
117
+ return {
118
+ pid: value.pid,
119
+ startedAt: value.startedAt,
120
+ readyChunkCount: value.readyChunkCount,
121
+ readySessionCount: value.readySessionCount,
122
+ logPath: value.logPath,
123
+ };
124
+ } catch {
125
+ return null;
126
+ }
127
+ }