codiedev 0.9.9 → 0.9.11
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/hook.js +27 -3
- package/dist/utils.d.ts +27 -0
- package/dist/utils.js +205 -15
- package/package.json +1 -1
package/dist/hook.js
CHANGED
|
@@ -168,6 +168,9 @@ async function main() {
|
|
|
168
168
|
post: postJson,
|
|
169
169
|
readFileSync: fs.readFileSync,
|
|
170
170
|
unlinkSync: fs.unlinkSync,
|
|
171
|
+
// Persist a bumped attempt count on failure so a later sweep retries
|
|
172
|
+
// (instead of dropping a session on a transient network blip).
|
|
173
|
+
writeFileSync: fs.writeFileSync,
|
|
171
174
|
});
|
|
172
175
|
}
|
|
173
176
|
process.exit(0);
|
|
@@ -256,9 +259,19 @@ async function main() {
|
|
|
256
259
|
// server-side against a merged MR/PR's source_branch / commits for AI attribution.
|
|
257
260
|
// Capture from the worktree the agent actually worked in (the launch dir stays
|
|
258
261
|
// on main/detached when work happens in a `git worktree`), not workspaceRoot.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
+
// One session can touch several worktrees (→ several PRs); capture EVERY
|
|
263
|
+
// worktree's branch+sha so all the resulting PRs attribute. branch/headSha
|
|
264
|
+
// remain the primary (most-recent) values for back-compat.
|
|
265
|
+
const captureDirs = (0, utils_1.resolveCaptureDirs)(transcriptContent, workspaceRoot);
|
|
266
|
+
const keys = (0, utils_1.collectCaptureKeys)(captureDirs);
|
|
267
|
+
const { branch, branches } = keys;
|
|
268
|
+
// Union the worktree HEAD shas with the SHAs the agent actually committed/
|
|
269
|
+
// pushed (parsed from the transcript's git output, resolved via the shared
|
|
270
|
+
// object store). The authored shas are independent of end-of-session
|
|
271
|
+
// filesystem state, so they rescue attribution when a worktree is stale/gone.
|
|
272
|
+
const authoredShas = (0, utils_1.parseAuthoredShas)(transcriptContent, workspaceRoot);
|
|
273
|
+
const headShas = [...new Set([...keys.headShas, ...authoredShas])];
|
|
274
|
+
const headSha = keys.headSha ?? headShas[0] ?? null;
|
|
262
275
|
// Cost/velocity/steering signals parsed straight from the transcript we're
|
|
263
276
|
// already holding — no extra hooks, no extra git/process calls.
|
|
264
277
|
const usage = (0, utils_1.parseUsage)(transcriptContent, mode);
|
|
@@ -276,6 +289,8 @@ async function main() {
|
|
|
276
289
|
source: mode,
|
|
277
290
|
branch,
|
|
278
291
|
headSha,
|
|
292
|
+
branches,
|
|
293
|
+
headShas,
|
|
279
294
|
inputTokens: usage.inputTokens,
|
|
280
295
|
outputTokens: usage.outputTokens,
|
|
281
296
|
cacheTokens: usage.cacheTokens,
|
|
@@ -293,6 +308,15 @@ async function main() {
|
|
|
293
308
|
const hookScriptPath = process.argv[1];
|
|
294
309
|
const tmpFile = path.join(os.tmpdir(), `codiedev-capture-${session_id ?? process.pid}.json`);
|
|
295
310
|
(0, utils_1.dispatchCaptureDetached)({ url: `${config.backendUrl}/api/captureSession`, token: config.token, body: payload }, hookScriptPath, { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile });
|
|
311
|
+
// Retry any earlier capture jobs that failed to upload (transient network
|
|
312
|
+
// blips), excluding the one we just dispatched. Best-effort, detached.
|
|
313
|
+
(0, utils_1.sweepPendingCaptures)({
|
|
314
|
+
dir: os.tmpdir(),
|
|
315
|
+
readdirSync: fs.readdirSync,
|
|
316
|
+
spawn: child_process_1.spawn,
|
|
317
|
+
hookScriptPath,
|
|
318
|
+
exclude: tmpFile,
|
|
319
|
+
});
|
|
296
320
|
}
|
|
297
321
|
catch (err) {
|
|
298
322
|
process.stderr.write(`[codiedev-hook] Warning: Failed to dispatch session capture — ${err.message}\n`);
|
package/dist/utils.d.ts
CHANGED
|
@@ -28,6 +28,15 @@ export declare function matchRepo(remoteUrl: string, repos: Array<{
|
|
|
28
28
|
fullName: string;
|
|
29
29
|
} | null;
|
|
30
30
|
export declare function resolveCaptureDir(transcriptContent: string, workspaceRoot: string): string;
|
|
31
|
+
export declare function resolveCaptureDirs(transcriptContent: string, workspaceRoot: string): string[];
|
|
32
|
+
export declare function collectCaptureKeys(dirs: string[], getBranch?: (d: string) => string | null, getSha?: (d: string) => string | null): {
|
|
33
|
+
branch: string | null;
|
|
34
|
+
headSha: string | null;
|
|
35
|
+
branches: string[];
|
|
36
|
+
headShas: string[];
|
|
37
|
+
};
|
|
38
|
+
export declare function extractShaTokens(transcriptContent: string): string[];
|
|
39
|
+
export declare function parseAuthoredShas(transcriptContent: string, repoRoot: string, resolveFull?: (sha: string) => string | null): string[];
|
|
31
40
|
export declare function getGitBranch(cwd: string): string | null;
|
|
32
41
|
export declare function getGitHeadSha(cwd: string): string | null;
|
|
33
42
|
export interface CapturePayloadInput {
|
|
@@ -42,6 +51,8 @@ export interface CapturePayloadInput {
|
|
|
42
51
|
source: string;
|
|
43
52
|
branch?: string | null;
|
|
44
53
|
headSha?: string | null;
|
|
54
|
+
branches?: string[];
|
|
55
|
+
headShas?: string[];
|
|
45
56
|
inputTokens?: number;
|
|
46
57
|
outputTokens?: number;
|
|
47
58
|
cacheTokens?: number;
|
|
@@ -149,7 +160,11 @@ export interface CaptureUploadJob {
|
|
|
149
160
|
url: string;
|
|
150
161
|
token: string;
|
|
151
162
|
body: Record<string, unknown>;
|
|
163
|
+
attempts?: number;
|
|
164
|
+
queuedAt?: number;
|
|
152
165
|
}
|
|
166
|
+
export declare const MAX_UPLOAD_ATTEMPTS = 5;
|
|
167
|
+
export declare const UPLOAD_TTL_MS: number;
|
|
153
168
|
interface DispatchDeps {
|
|
154
169
|
spawn: typeof nodeSpawn;
|
|
155
170
|
writeFileSync: typeof fs.writeFileSync;
|
|
@@ -157,10 +172,22 @@ interface DispatchDeps {
|
|
|
157
172
|
nodePath?: string;
|
|
158
173
|
}
|
|
159
174
|
export declare function dispatchCaptureDetached(job: CaptureUploadJob, hookScriptPath: string, deps: DispatchDeps): void;
|
|
175
|
+
interface SweepDeps {
|
|
176
|
+
dir: string;
|
|
177
|
+
readdirSync: (dir: string) => string[];
|
|
178
|
+
spawn: typeof nodeSpawn;
|
|
179
|
+
hookScriptPath: string;
|
|
180
|
+
nodePath?: string;
|
|
181
|
+
exclude?: string;
|
|
182
|
+
max?: number;
|
|
183
|
+
}
|
|
184
|
+
export declare function sweepPendingCaptures(deps: SweepDeps): void;
|
|
160
185
|
interface UploadDeps {
|
|
161
186
|
post: (url: string, body: Record<string, unknown>, token: string) => Promise<void>;
|
|
162
187
|
readFileSync: typeof fs.readFileSync;
|
|
163
188
|
unlinkSync: typeof fs.unlinkSync;
|
|
189
|
+
writeFileSync?: typeof fs.writeFileSync;
|
|
190
|
+
now?: () => number;
|
|
164
191
|
}
|
|
165
192
|
export declare function runCaptureUpload(jobFile: string, deps: UploadDeps): Promise<void>;
|
|
166
193
|
export {};
|
package/dist/utils.js
CHANGED
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.UPLOAD_TTL_MS = exports.MAX_UPLOAD_ATTEMPTS = void 0;
|
|
36
37
|
exports.readConfig = readConfig;
|
|
37
38
|
exports.writeConfig = writeConfig;
|
|
38
39
|
exports.parseGitRemoteFromConfig = parseGitRemoteFromConfig;
|
|
@@ -41,6 +42,10 @@ exports.getGitRemoteUrl = getGitRemoteUrl;
|
|
|
41
42
|
exports.normalizeRepoUrl = normalizeRepoUrl;
|
|
42
43
|
exports.matchRepo = matchRepo;
|
|
43
44
|
exports.resolveCaptureDir = resolveCaptureDir;
|
|
45
|
+
exports.resolveCaptureDirs = resolveCaptureDirs;
|
|
46
|
+
exports.collectCaptureKeys = collectCaptureKeys;
|
|
47
|
+
exports.extractShaTokens = extractShaTokens;
|
|
48
|
+
exports.parseAuthoredShas = parseAuthoredShas;
|
|
44
49
|
exports.getGitBranch = getGitBranch;
|
|
45
50
|
exports.getGitHeadSha = getGitHeadSha;
|
|
46
51
|
exports.buildCapturePayload = buildCapturePayload;
|
|
@@ -75,6 +80,7 @@ exports.parseClaudeCodeStats = parseClaudeCodeStats;
|
|
|
75
80
|
exports.parseCodexStats = parseCodexStats;
|
|
76
81
|
exports.extractCodexRepoUrl = extractCodexRepoUrl;
|
|
77
82
|
exports.dispatchCaptureDetached = dispatchCaptureDetached;
|
|
83
|
+
exports.sweepPendingCaptures = sweepPendingCaptures;
|
|
78
84
|
exports.runCaptureUpload = runCaptureUpload;
|
|
79
85
|
const fs = __importStar(require("fs"));
|
|
80
86
|
const path = __importStar(require("path"));
|
|
@@ -270,28 +276,147 @@ function matchRepo(remoteUrl, repos) {
|
|
|
270
276
|
// root and its subdirs share .git). Fall back to the launch root when no
|
|
271
277
|
// separate worktree was used (work done directly in the repo).
|
|
272
278
|
function resolveCaptureDir(transcriptContent, workspaceRoot) {
|
|
279
|
+
return resolveCaptureDirs(transcriptContent, workspaceRoot)[0] ?? workspaceRoot;
|
|
280
|
+
}
|
|
281
|
+
// Plural: EVERY separate worktree the session touched, most-recent first. One
|
|
282
|
+
// session can produce multiple PRs (work across several worktrees, each on its
|
|
283
|
+
// own feature branch); capturing every worktree's branch+sha lets all of those
|
|
284
|
+
// PRs attribute. Falls back to [workspaceRoot] when no separate worktree was
|
|
285
|
+
// used (work done directly in the repo).
|
|
286
|
+
function resolveCaptureDirs(transcriptContent, workspaceRoot) {
|
|
273
287
|
const rootGitDir = resolveGitDir(workspaceRoot);
|
|
274
|
-
|
|
288
|
+
// Collect EVERY filesystem path the session referenced, in order — not just
|
|
289
|
+
// the session cwd. An agent can touch a worktree without cd-ing into it:
|
|
290
|
+
// - `git -C <path> ...` (operate on a worktree by path)
|
|
291
|
+
// - editing a file inside it ("file_path": "<worktree>/src/x.ts")
|
|
292
|
+
// Catching all of these is what makes worktree detection robust.
|
|
293
|
+
const candidates = [];
|
|
294
|
+
const add = (p) => {
|
|
295
|
+
if (!p)
|
|
296
|
+
return;
|
|
297
|
+
candidates.push(path.isAbsolute(p) ? p : path.resolve(workspaceRoot, p));
|
|
298
|
+
};
|
|
299
|
+
const gitCRe = /git\s+(?:[^\s]+\s+)*?-C\s+(?:"([^"]+)"|'([^']+)'|(\S+))/g;
|
|
300
|
+
const filePathRe = /"file_path"\s*:\s*"((?:[^"\\]|\\.)+)"/g;
|
|
275
301
|
for (const line of transcriptContent.split("\n")) {
|
|
276
302
|
if (!line.trim())
|
|
277
303
|
continue;
|
|
278
304
|
try {
|
|
279
305
|
const entry = JSON.parse(line);
|
|
280
306
|
if (typeof entry?.cwd === "string" && entry.cwd)
|
|
281
|
-
|
|
307
|
+
add(entry.cwd);
|
|
282
308
|
}
|
|
283
309
|
catch {
|
|
284
|
-
// non-JSON / partial line —
|
|
310
|
+
// non-JSON / partial line — still scan the raw text below
|
|
311
|
+
}
|
|
312
|
+
let m;
|
|
313
|
+
gitCRe.lastIndex = 0;
|
|
314
|
+
while ((m = gitCRe.exec(line)))
|
|
315
|
+
add(m[1] ?? m[2] ?? m[3]);
|
|
316
|
+
filePathRe.lastIndex = 0;
|
|
317
|
+
while ((m = filePathRe.exec(line))) {
|
|
318
|
+
try {
|
|
319
|
+
add(path.dirname(JSON.parse(`"${m[1]}"`)));
|
|
320
|
+
}
|
|
321
|
+
catch { /* skip */ }
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Walk most-recent first; one entry per distinct worktree gitdir (a worktree's
|
|
325
|
+
// gitdir is .git/worktrees/<name>; the root and its subdirs share .git, so
|
|
326
|
+
// they're excluded as not-separate). Normalize each to its canonical worktree
|
|
327
|
+
// root so a file path / subdir resolves to the worktree dir itself.
|
|
328
|
+
const out = [];
|
|
329
|
+
const seenGitDirs = new Set();
|
|
330
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
331
|
+
const gitDir = resolveGitDir(candidates[i]);
|
|
332
|
+
if (!gitDir || (rootGitDir && gitDir === rootGitDir))
|
|
333
|
+
continue;
|
|
334
|
+
if (seenGitDirs.has(gitDir))
|
|
335
|
+
continue;
|
|
336
|
+
seenGitDirs.add(gitDir);
|
|
337
|
+
out.push(worktreeRootFromGitDir(gitDir) ?? candidates[i]);
|
|
338
|
+
}
|
|
339
|
+
return out.length > 0 ? out : [workspaceRoot];
|
|
340
|
+
}
|
|
341
|
+
// A linked worktree's gitdir (`.git/worktrees/<name>`) holds a `gitdir` file
|
|
342
|
+
// pointing at the worktree's own `.git` file; its parent is the worktree root.
|
|
343
|
+
function worktreeRootFromGitDir(gitDir) {
|
|
344
|
+
try {
|
|
345
|
+
const p = fs.readFileSync(path.join(gitDir, "gitdir"), "utf8").trim();
|
|
346
|
+
return p ? path.dirname(p) : null;
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Turn the worktrees a session touched into the upload join keys: dedup'd
|
|
353
|
+
// branch + headSha arrays (so one session → many PRs all attribute), plus the
|
|
354
|
+
// primary single values (first entry) for back-compat with servers that read
|
|
355
|
+
// only branch/headSha. getBranch/getSha are injectable for testing.
|
|
356
|
+
function collectCaptureKeys(dirs, getBranch = getGitBranch, getSha = getGitHeadSha) {
|
|
357
|
+
const branches = [];
|
|
358
|
+
const headShas = [];
|
|
359
|
+
for (const d of dirs) {
|
|
360
|
+
const b = getBranch(d);
|
|
361
|
+
if (b && !branches.includes(b))
|
|
362
|
+
branches.push(b);
|
|
363
|
+
const s = getSha(d);
|
|
364
|
+
if (s && !headShas.includes(s))
|
|
365
|
+
headShas.push(s);
|
|
366
|
+
}
|
|
367
|
+
return { branch: branches[0] ?? null, headSha: headShas[0] ?? null, branches, headShas };
|
|
368
|
+
}
|
|
369
|
+
// Pull the commit SHAs the agent actually authored/pushed straight out of the
|
|
370
|
+
// transcript's git command OUTPUT — independent of end-of-session filesystem
|
|
371
|
+
// state. Catches: commit output "[branch 4c4e790]", push ranges
|
|
372
|
+
// "abc1234..def5678", and bare 40-hex (rev-parse). This is the signal that
|
|
373
|
+
// rescues attribution when the worktree's HEAD is stale or the worktree is gone
|
|
374
|
+
// at SessionEnd (pruned worktree, submodule pointer bump).
|
|
375
|
+
function extractShaTokens(transcriptContent) {
|
|
376
|
+
const out = new Set();
|
|
377
|
+
const commitRe = /\[[^\]\n]*?\b([0-9a-f]{7,40})\]/g; // [branch <sha>]
|
|
378
|
+
const rangeRe = /\b([0-9a-f]{7,40})\.\.([0-9a-f]{7,40})\b/g; // <sha>..<sha>
|
|
379
|
+
const fullRe = /\b([0-9a-f]{40})\b/g; // bare full sha (rev-parse)
|
|
380
|
+
let m;
|
|
381
|
+
while ((m = commitRe.exec(transcriptContent)))
|
|
382
|
+
out.add(m[1].toLowerCase());
|
|
383
|
+
while ((m = rangeRe.exec(transcriptContent))) {
|
|
384
|
+
out.add(m[1].toLowerCase());
|
|
385
|
+
out.add(m[2].toLowerCase());
|
|
386
|
+
}
|
|
387
|
+
while ((m = fullRe.exec(transcriptContent)))
|
|
388
|
+
out.add(m[1].toLowerCase());
|
|
389
|
+
return [...out];
|
|
390
|
+
}
|
|
391
|
+
// Resolve the extracted tokens to FULL 40-char SHAs via the repo's shared object
|
|
392
|
+
// store (a worktree's commits live in the main repo's .git/objects, so they
|
|
393
|
+
// resolve even after the worktree dir is removed). Full SHAs pass through; short
|
|
394
|
+
// ones are resolved; unresolvable ones are dropped (a short sha can't exact-
|
|
395
|
+
// match the worker's full commitShas). Keeps the server-side match exact.
|
|
396
|
+
function parseAuthoredShas(transcriptContent, repoRoot, resolveFull = (sha) => defaultResolveFullSha(sha, repoRoot)) {
|
|
397
|
+
const out = new Set();
|
|
398
|
+
for (const tok of extractShaTokens(transcriptContent)) {
|
|
399
|
+
if (tok.length === 40) {
|
|
400
|
+
out.add(tok);
|
|
401
|
+
continue;
|
|
285
402
|
}
|
|
403
|
+
const full = resolveFull(tok);
|
|
404
|
+
if (full && /^[0-9a-f]{40}$/.test(full))
|
|
405
|
+
out.add(full.toLowerCase());
|
|
286
406
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
407
|
+
return [...out];
|
|
408
|
+
}
|
|
409
|
+
function defaultResolveFullSha(sha, repoRoot) {
|
|
410
|
+
try {
|
|
411
|
+
const r = (0, child_process_1.execSync)(`git rev-parse ${sha}^{commit}`, {
|
|
412
|
+
cwd: repoRoot,
|
|
413
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
414
|
+
}).toString().trim();
|
|
415
|
+
return /^[0-9a-f]{40}$/.test(r) ? r : null;
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return null;
|
|
293
419
|
}
|
|
294
|
-
return workspaceRoot;
|
|
295
420
|
}
|
|
296
421
|
// Current branch via direct .git/HEAD read (no git binary required, same
|
|
297
422
|
// robustness as getGitRemoteUrl). Returns null on detached HEAD or when
|
|
@@ -388,6 +513,10 @@ function buildCapturePayload(input) {
|
|
|
388
513
|
companyId: input.companyId,
|
|
389
514
|
branch: input.branch ?? "",
|
|
390
515
|
headSha: input.headSha ?? "",
|
|
516
|
+
// Arrays omitted when empty so older servers (which ignore unknown keys)
|
|
517
|
+
// see no change; newer servers index the session under every key.
|
|
518
|
+
branches: input.branches && input.branches.length ? input.branches : undefined,
|
|
519
|
+
headShas: input.headShas && input.headShas.length ? input.headShas : undefined,
|
|
391
520
|
transcript: input.transcriptBase64,
|
|
392
521
|
messageCount: input.messageCount,
|
|
393
522
|
toolCallCount: input.toolCallCount,
|
|
@@ -1297,6 +1426,10 @@ function extractCodexRepoUrl(content) {
|
|
|
1297
1426
|
}
|
|
1298
1427
|
return null;
|
|
1299
1428
|
}
|
|
1429
|
+
// Give up after this many failed uploads (across sessions) so a permanently
|
|
1430
|
+
// failing job never litters /tmp forever. Paired with UPLOAD_TTL_MS.
|
|
1431
|
+
exports.MAX_UPLOAD_ATTEMPTS = 5;
|
|
1432
|
+
exports.UPLOAD_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
1300
1433
|
// Fire-and-forget the session-capture upload from a fully detached child so the
|
|
1301
1434
|
// SessionEnd hook can exit 0 immediately. Awaiting the upload inline let Claude
|
|
1302
1435
|
// Code's shutdown window elapse and kill the hook ("Hook cancelled") on large
|
|
@@ -1307,24 +1440,81 @@ function dispatchCaptureDetached(job, hookScriptPath, deps) {
|
|
|
1307
1440
|
const child = deps.spawn(deps.nodePath ?? process.execPath, [hookScriptPath, "upload", deps.tmpFile], { detached: true, stdio: "ignore" });
|
|
1308
1441
|
child.unref();
|
|
1309
1442
|
}
|
|
1443
|
+
// Retry kept-on-failure capture jobs: re-dispatch each pending
|
|
1444
|
+
// `codiedev-capture-*.json` to the detached uploader (which bumps attempts /
|
|
1445
|
+
// gives up at the cap). Best-effort — never throws into the hook.
|
|
1446
|
+
function sweepPendingCaptures(deps) {
|
|
1447
|
+
let files;
|
|
1448
|
+
try {
|
|
1449
|
+
files = deps.readdirSync(deps.dir);
|
|
1450
|
+
}
|
|
1451
|
+
catch {
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
const jobs = files
|
|
1455
|
+
.filter((f) => /^codiedev-capture-.*\.json$/.test(f))
|
|
1456
|
+
.map((f) => path.join(deps.dir, f))
|
|
1457
|
+
.filter((p) => p !== deps.exclude)
|
|
1458
|
+
.slice(0, deps.max ?? 10);
|
|
1459
|
+
for (const jobFile of jobs) {
|
|
1460
|
+
try {
|
|
1461
|
+
const child = deps.spawn(deps.nodePath ?? process.execPath, [deps.hookScriptPath, "upload", jobFile], { detached: true, stdio: "ignore" });
|
|
1462
|
+
child.unref();
|
|
1463
|
+
}
|
|
1464
|
+
catch {
|
|
1465
|
+
// ignore — best-effort retry
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1310
1469
|
// Runs inside the detached child spawned by `dispatchCaptureDetached`: read the
|
|
1311
1470
|
// persisted job, POST it, and ALWAYS delete the temp file afterwards (even on
|
|
1312
1471
|
// failure) so we don't litter the tmp dir. Never throws — capture is
|
|
1313
1472
|
// best-effort.
|
|
1314
1473
|
async function runCaptureUpload(jobFile, deps) {
|
|
1474
|
+
let job = null;
|
|
1315
1475
|
try {
|
|
1316
|
-
|
|
1317
|
-
await deps.post(job.url, job.body, job.token);
|
|
1476
|
+
job = JSON.parse(deps.readFileSync(jobFile, "utf8"));
|
|
1318
1477
|
}
|
|
1319
1478
|
catch {
|
|
1320
|
-
//
|
|
1479
|
+
// unreadable/corrupt job — delete it and stop.
|
|
1480
|
+
try {
|
|
1481
|
+
deps.unlinkSync(jobFile);
|
|
1482
|
+
}
|
|
1483
|
+
catch { /* gone */ }
|
|
1484
|
+
return;
|
|
1321
1485
|
}
|
|
1322
|
-
|
|
1486
|
+
try {
|
|
1487
|
+
await deps.post(job.url, job.body, job.token);
|
|
1488
|
+
// success — remove the job.
|
|
1323
1489
|
try {
|
|
1324
1490
|
deps.unlinkSync(jobFile);
|
|
1325
1491
|
}
|
|
1492
|
+
catch { /* gone */ }
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
catch {
|
|
1496
|
+
// Failure: keep the job for a later sweep to retry, UNLESS we've hit the
|
|
1497
|
+
// attempt cap or TTL — then give up so /tmp doesn't accumulate forever.
|
|
1498
|
+
const now = deps.now ? deps.now() : Date.now();
|
|
1499
|
+
const attempts = (job.attempts ?? 0) + 1;
|
|
1500
|
+
const queuedAt = job.queuedAt ?? now;
|
|
1501
|
+
const expired = now - queuedAt > exports.UPLOAD_TTL_MS;
|
|
1502
|
+
if (attempts >= exports.MAX_UPLOAD_ATTEMPTS || expired || !deps.writeFileSync) {
|
|
1503
|
+
try {
|
|
1504
|
+
deps.unlinkSync(jobFile);
|
|
1505
|
+
}
|
|
1506
|
+
catch { /* gone */ }
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
try {
|
|
1510
|
+
deps.writeFileSync(jobFile, JSON.stringify({ ...job, attempts, queuedAt }), { mode: 0o600 });
|
|
1511
|
+
}
|
|
1326
1512
|
catch {
|
|
1327
|
-
//
|
|
1513
|
+
// couldn't persist the bump — drop it rather than risk a stuck file.
|
|
1514
|
+
try {
|
|
1515
|
+
deps.unlinkSync(jobFile);
|
|
1516
|
+
}
|
|
1517
|
+
catch { /* gone */ }
|
|
1328
1518
|
}
|
|
1329
1519
|
}
|
|
1330
1520
|
}
|
package/package.json
CHANGED