codiedev 0.9.23 → 0.9.24
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 +91 -6
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +37 -2
- package/package.json +1 -1
package/dist/hook.js
CHANGED
|
@@ -241,6 +241,84 @@ timeoutMs) {
|
|
|
241
241
|
req.end();
|
|
242
242
|
});
|
|
243
243
|
}
|
|
244
|
+
// postJson variant that returns the parsed response body — the upload-URL
|
|
245
|
+
// handshake needs the { uploadUrl } the server hands back. No timeout: runs
|
|
246
|
+
// only in detached uploader children, never on the shutdown clock.
|
|
247
|
+
function postJsonForResponse(url, body, bearerToken) {
|
|
248
|
+
return new Promise((resolve, reject) => {
|
|
249
|
+
const parsed = new URL(url);
|
|
250
|
+
const data = JSON.stringify(body);
|
|
251
|
+
const lib = parsed.protocol === "https:" ? https : http;
|
|
252
|
+
const req = lib.request({
|
|
253
|
+
hostname: parsed.hostname,
|
|
254
|
+
port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
|
|
255
|
+
path: parsed.pathname + parsed.search,
|
|
256
|
+
method: "POST",
|
|
257
|
+
headers: {
|
|
258
|
+
"Content-Type": "application/json",
|
|
259
|
+
"Content-Length": Buffer.byteLength(data),
|
|
260
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
261
|
+
"X-Codiedev-Cli-Version": version_1.CLI_VERSION,
|
|
262
|
+
},
|
|
263
|
+
}, (res) => {
|
|
264
|
+
let responseData = "";
|
|
265
|
+
res.on("data", (chunk) => (responseData += chunk));
|
|
266
|
+
res.on("end", () => {
|
|
267
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
268
|
+
try {
|
|
269
|
+
resolve(JSON.parse(responseData));
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
reject(new Error(`HTTP ${res.statusCode}: unparseable response`));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
reject(new Error(`HTTP ${res.statusCode}: ${responseData}`));
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
req.on("error", reject);
|
|
281
|
+
req.write(data);
|
|
282
|
+
req.end();
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
// Raw-bytes POST to a Convex storage upload URL (no auth header — the URL
|
|
286
|
+
// itself is the one-time credential). Returns the parsed { storageId }.
|
|
287
|
+
function postBytes(url, bytes, contentType) {
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
289
|
+
const parsed = new URL(url);
|
|
290
|
+
const lib = parsed.protocol === "https:" ? https : http;
|
|
291
|
+
const req = lib.request({
|
|
292
|
+
hostname: parsed.hostname,
|
|
293
|
+
port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
|
|
294
|
+
path: parsed.pathname + parsed.search,
|
|
295
|
+
method: "POST",
|
|
296
|
+
headers: {
|
|
297
|
+
"Content-Type": contentType,
|
|
298
|
+
"Content-Length": bytes.length,
|
|
299
|
+
},
|
|
300
|
+
}, (res) => {
|
|
301
|
+
let responseData = "";
|
|
302
|
+
res.on("data", (chunk) => (responseData += chunk));
|
|
303
|
+
res.on("end", () => {
|
|
304
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
305
|
+
try {
|
|
306
|
+
resolve(JSON.parse(responseData));
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
reject(new Error(`HTTP ${res.statusCode}: unparseable response`));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
reject(new Error(`HTTP ${res.statusCode}: ${responseData}`));
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
req.on("error", reject);
|
|
318
|
+
req.write(bytes);
|
|
319
|
+
req.end();
|
|
320
|
+
});
|
|
321
|
+
}
|
|
244
322
|
// Short leash for the tiny coverage pings (sessionStart / sessionSkip): they
|
|
245
323
|
// carry a few bytes and must never stall a hook. 3s is generous for a ping
|
|
246
324
|
// and a hard bound for the shutdown clock.
|
|
@@ -270,6 +348,12 @@ async function main() {
|
|
|
270
348
|
function makeUploadDeps() {
|
|
271
349
|
return {
|
|
272
350
|
post: postJson,
|
|
351
|
+
// Direct-to-storage flow for big transcripts (see utils.ts
|
|
352
|
+
// DIRECT_UPLOAD_THRESHOLD_CHARS): fetch a one-time upload URL, then
|
|
353
|
+
// POST the raw bytes to it. Keeps ~7MB+ transcripts out of the
|
|
354
|
+
// backend's 64MB captureSession isolate.
|
|
355
|
+
requestJson: postJsonForResponse,
|
|
356
|
+
uploadBytes: postBytes,
|
|
273
357
|
readFileSync: fs.readFileSync,
|
|
274
358
|
unlinkSync: fs.unlinkSync,
|
|
275
359
|
// Persist a bumped attempt count on failure so a later sweep retries
|
|
@@ -508,17 +592,18 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
508
592
|
try {
|
|
509
593
|
transcriptContent = fs.readFileSync(transcript_path, "utf8");
|
|
510
594
|
}
|
|
511
|
-
catch {
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
//
|
|
595
|
+
catch (err) {
|
|
596
|
+
// ENOENT → "no-transcript" (phantom desktop-app helper session, excluded
|
|
597
|
+
// from coverage server-side); anything else → "transcript-unreadable:<code>"
|
|
598
|
+
// (real transcript, read failed — a genuine, diagnosable loss).
|
|
599
|
+
const reason = (0, utils_1.transcriptReadSkipReason)(err);
|
|
515
600
|
(0, captureLog_1.appendCaptureLog)({
|
|
516
601
|
stage: "capture-finish",
|
|
517
602
|
sessionId: session_id,
|
|
518
603
|
outcome: "skip",
|
|
519
|
-
detail:
|
|
604
|
+
detail: reason,
|
|
520
605
|
});
|
|
521
|
-
await postSessionSkip(config, session_id,
|
|
606
|
+
await postSessionSkip(config, session_id, reason);
|
|
522
607
|
return;
|
|
523
608
|
}
|
|
524
609
|
// Resolve repo URL. Codex embeds it in session_meta; CC and Cursor both
|
package/dist/utils.d.ts
CHANGED
|
@@ -248,8 +248,11 @@ export declare function pruneCaptureStateDir(deps?: {
|
|
|
248
248
|
};
|
|
249
249
|
unlinkSync: (p: string) => void;
|
|
250
250
|
}, dir?: string, now?: number): void;
|
|
251
|
+
export declare const DIRECT_UPLOAD_THRESHOLD_CHARS = 2000000;
|
|
251
252
|
interface UploadDeps {
|
|
252
253
|
post: (url: string, body: Record<string, unknown>, token: string) => Promise<void>;
|
|
254
|
+
requestJson?: (url: string, body: Record<string, unknown>, token: string) => Promise<any>;
|
|
255
|
+
uploadBytes?: (url: string, bytes: Buffer, contentType: string) => Promise<any>;
|
|
253
256
|
readFileSync: typeof fs.readFileSync;
|
|
254
257
|
unlinkSync: typeof fs.unlinkSync;
|
|
255
258
|
writeFileSync?: typeof fs.writeFileSync;
|
|
@@ -286,4 +289,5 @@ interface DeferredUploadDeps extends UploadDeps {
|
|
|
286
289
|
sleep?: (ms: number) => Promise<void>;
|
|
287
290
|
}
|
|
288
291
|
export declare function runDeferredUpload(jobFile: string, delayMs: number, deps: DeferredUploadDeps): Promise<void>;
|
|
292
|
+
export declare function transcriptReadSkipReason(err: unknown): string;
|
|
289
293
|
export {};
|
package/dist/utils.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.SWEEP_MIN_AGE_MS = exports.CAPTURE_THROTTLE_MS = exports.FINISH_ORPHAN_AGE_MS = exports.CLAIM_STALE_MS = exports.UPLOAD_TTL_MS = exports.MAX_UPLOAD_ATTEMPTS = void 0;
|
|
36
|
+
exports.DIRECT_UPLOAD_THRESHOLD_CHARS = exports.SWEEP_MIN_AGE_MS = exports.CAPTURE_THROTTLE_MS = exports.FINISH_ORPHAN_AGE_MS = exports.CLAIM_STALE_MS = exports.UPLOAD_TTL_MS = exports.MAX_UPLOAD_ATTEMPTS = void 0;
|
|
37
37
|
exports.readConfig = readConfig;
|
|
38
38
|
exports.writeConfig = writeConfig;
|
|
39
39
|
exports.parseGitRemoteFromConfig = parseGitRemoteFromConfig;
|
|
@@ -99,6 +99,7 @@ exports.writeLastCapture = writeLastCapture;
|
|
|
99
99
|
exports.readLastCapture = readLastCapture;
|
|
100
100
|
exports.runCaptureUpload = runCaptureUpload;
|
|
101
101
|
exports.runDeferredUpload = runDeferredUpload;
|
|
102
|
+
exports.transcriptReadSkipReason = transcriptReadSkipReason;
|
|
102
103
|
const fs = __importStar(require("fs"));
|
|
103
104
|
const path = __importStar(require("path"));
|
|
104
105
|
const os = __importStar(require("os"));
|
|
@@ -2028,6 +2029,13 @@ function pruneCaptureStateDir(deps = fs, dir = CAPTURE_STATE_DIR, now = Date.now
|
|
|
2028
2029
|
// dir missing/unreadable — nothing to prune
|
|
2029
2030
|
}
|
|
2030
2031
|
}
|
|
2032
|
+
// Base64 length above which the transcript goes straight to storage instead
|
|
2033
|
+
// of inline in the captureSession JSON body. The backend's HTTP-action
|
|
2034
|
+
// isolate has a 64MB cap and inline base64 amplifies ~8x in memory (request
|
|
2035
|
+
// text + parsed JSON + decode output all resident) — ~7MB transcripts OOM it
|
|
2036
|
+
// (observed live 2026-07-02). 2M base64 chars ≈ 1.5MB raw: far below the
|
|
2037
|
+
// cliff, while per-turn snapshots (typically KBs) skip the extra roundtrips.
|
|
2038
|
+
exports.DIRECT_UPLOAD_THRESHOLD_CHARS = 2_000_000;
|
|
2031
2039
|
// "Last session captured" breadcrumb. Written next to config.json on a
|
|
2032
2040
|
// successful capture upload; read by `codiedev doctor` so a user can confirm
|
|
2033
2041
|
// their most recent session actually landed (the single hardest thing to
|
|
@@ -2086,7 +2094,22 @@ async function runCaptureUpload(jobFile, deps) {
|
|
|
2086
2094
|
}
|
|
2087
2095
|
const sessionId = job.body?.sessionId;
|
|
2088
2096
|
try {
|
|
2089
|
-
|
|
2097
|
+
// Big transcript + capable deps → direct-to-storage: fetch a one-time
|
|
2098
|
+
// upload URL, PUT the raw bytes, send captureSession the storageId. Any
|
|
2099
|
+
// step failing throws into the retry path below, same as a failed POST.
|
|
2100
|
+
let body = job.body;
|
|
2101
|
+
const transcript = body?.transcript;
|
|
2102
|
+
if (transcript &&
|
|
2103
|
+
transcript.length > exports.DIRECT_UPLOAD_THRESHOLD_CHARS &&
|
|
2104
|
+
deps.requestJson &&
|
|
2105
|
+
deps.uploadBytes) {
|
|
2106
|
+
const uploadUrlEndpoint = job.url.replace(/\/api\/captureSession$/, "/api/captureUploadUrl");
|
|
2107
|
+
const { uploadUrl } = await deps.requestJson(uploadUrlEndpoint, {}, job.token);
|
|
2108
|
+
const { storageId } = await deps.uploadBytes(uploadUrl, Buffer.from(transcript, "base64"), "application/json");
|
|
2109
|
+
body = { ...body, transcriptStorageId: storageId };
|
|
2110
|
+
delete body.transcript;
|
|
2111
|
+
}
|
|
2112
|
+
await deps.post(job.url, body, job.token);
|
|
2090
2113
|
// success — drop a "last session captured" breadcrumb (before removing the
|
|
2091
2114
|
// job) so doctor can confirm the most recent session actually landed.
|
|
2092
2115
|
if (deps.recordSuccess) {
|
|
@@ -2225,3 +2248,15 @@ async function runDeferredUpload(jobFile, delayMs, deps) {
|
|
|
2225
2248
|
}
|
|
2226
2249
|
await runCaptureUpload(claimed, deps);
|
|
2227
2250
|
}
|
|
2251
|
+
// Classify a transcript read failure at capture time. ENOENT = the transcript
|
|
2252
|
+
// never existed: a desktop-app phantom helper session (SessionStart/End hooks
|
|
2253
|
+
// fire, no conversation ever happens), not lost work — the backend excludes
|
|
2254
|
+
// "no-transcript" from coverage. Anything else means a real transcript was
|
|
2255
|
+
// there but the read failed: a genuine loss, tagged with the errno so it stays
|
|
2256
|
+
// diagnosable from the server side.
|
|
2257
|
+
function transcriptReadSkipReason(err) {
|
|
2258
|
+
const code = err?.code;
|
|
2259
|
+
if (code === "ENOENT")
|
|
2260
|
+
return "no-transcript";
|
|
2261
|
+
return `transcript-unreadable:${code || "unknown"}`;
|
|
2262
|
+
}
|
package/package.json
CHANGED