codiedev 0.9.21 → 0.9.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.
@@ -1 +1,2 @@
1
+ export declare function candidateKeys(key: string): string[];
1
2
  export declare function runPull(args: string[]): Promise<void>;
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.candidateKeys = candidateKeys;
36
37
  exports.runPull = runPull;
37
38
  const fs = __importStar(require("fs"));
38
39
  const path = __importStar(require("path"));
@@ -69,21 +70,45 @@ function parseArgs(args) {
69
70
  }
70
71
  return { key, version, out };
71
72
  }
73
+ // The backend stores skills under extension-bearing keys (e.g. "pr-push.md"),
74
+ // and lookup is exact-match, so a bare "pr-push" 404s. Make pull forgiving: try
75
+ // the key as given, then a ".md" fallback when it has no extension, so the
76
+ // intuitive bare name (and the hook's suggested key) both resolve.
77
+ function candidateKeys(key) {
78
+ if (/\.[a-z0-9]+$/i.test(key))
79
+ return [key];
80
+ return [key, `${key}.md`];
81
+ }
72
82
  async function runPull(args) {
73
83
  const { key, version, out } = parseArgs(args);
74
84
  const config = (0, shared_1.requireConfig)();
85
+ const keys = candidateKeys(key);
75
86
  const start = Date.now();
76
87
  try {
77
- const res = await (0, shared_1.apiRequest)("GET", "/api/cli/pull", {
78
- config,
79
- query: { key, version },
80
- });
88
+ let res;
89
+ for (let i = 0; i < keys.length; i++) {
90
+ try {
91
+ res = await (0, shared_1.apiRequest)("GET", "/api/cli/pull", {
92
+ config,
93
+ query: { key: keys[i], version },
94
+ });
95
+ break;
96
+ }
97
+ catch (err) {
98
+ const e = err;
99
+ // A 404 on a non-final candidate is retryable (fall through to ".md");
100
+ // anything else — or the last candidate — propagates to the handler.
101
+ if (e.status === 404 && i < keys.length - 1)
102
+ continue;
103
+ throw err;
104
+ }
105
+ }
106
+ const a = res.artifact;
81
107
  (0, shared_1.recordAgentEvent)(config, {
82
108
  tool: "codiedev_pull",
83
109
  ok: true,
84
110
  latencyMs: Date.now() - start,
85
111
  });
86
- const a = res.artifact;
87
112
  if (out) {
88
113
  const absolute = path.isAbsolute(out) ? out : path.resolve(process.cwd(), out);
89
114
  fs.writeFileSync(absolute, a.markdown, "utf8");
@@ -108,7 +133,8 @@ async function runPull(args) {
108
133
  error: e.message,
109
134
  });
110
135
  if (e.status === 404) {
111
- console.error(`Not found: ${key}${version ? ` v${version}` : ""}`);
136
+ const also = keys.length > 1 ? ` (also tried ${keys.slice(1).join(", ")})` : "";
137
+ console.error(`Not found: ${key}${version ? ` v${version}` : ""}${also}`);
112
138
  }
113
139
  else {
114
140
  console.error(`Pull failed: ${e.message}`);
package/dist/hook.d.ts CHANGED
@@ -1,2 +1,6 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ export declare function buildDiscoveryContext(skills: Array<{
3
+ key: string;
4
+ title: string;
5
+ score: number;
6
+ }>): string | null;
package/dist/hook.js CHANGED
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  };
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.buildDiscoveryContext = buildDiscoveryContext;
37
38
  const fs = __importStar(require("fs"));
38
39
  const os = __importStar(require("os"));
39
40
  const path = __importStar(require("path"));
@@ -74,6 +75,26 @@ function getJson(url, bearerToken) {
74
75
  req.end();
75
76
  });
76
77
  }
78
+ // Pure builder for the discovery additionalContext. Returns null when there are
79
+ // no matches. The suggested `codiedev pull` command carries the EXACT top key
80
+ // (e.g. "pr-push.md") rather than a "<key>" placeholder — otherwise agents
81
+ // paraphrase the key (pr-push) and the extension-sensitive `pull` 404s.
82
+ function buildDiscoveryContext(skills) {
83
+ if (skills.length === 0)
84
+ return null;
85
+ // Don't trust the API's ordering — rank by score so the suggested pull key is
86
+ // always the highest-confidence match.
87
+ const top = [...skills].sort((a, b) => b.score - a.score).slice(0, 3);
88
+ const lines = top
89
+ .map((s) => `- ${s.key} — ${(s.score * 100).toFixed(0)}% match`)
90
+ .join("\n");
91
+ return ("Your team has a CodieDev skill that fits this task — prefer it over improvising:\n" +
92
+ lines +
93
+ "\nPull and follow it now: `codiedev pull " +
94
+ top[0].key +
95
+ "`. It encodes the team's vetted " +
96
+ "approach. Skip it only if it clearly doesn't apply.");
97
+ }
77
98
  // UserPromptSubmit: query skill discovery for the prompt and, if a team skill
78
99
  // clears the confidence threshold, inject it as additionalContext so the agent
79
100
  // can offer to pull+run it. Fully silent on miss/error — never blocks input.
@@ -86,15 +107,8 @@ async function runDiscover(prompt) {
86
107
  try {
87
108
  const res = await getJson(`${config.backendUrl}/api/cli/skill-discovery?q=${encodeURIComponent(prompt)}`, config.token);
88
109
  const skills = res?.skills ?? [];
89
- if (skills.length > 0) {
90
- const lines = skills
91
- .slice(0, 3)
92
- .map((s) => `- ${s.key} — ${(s.score * 100).toFixed(0)}% match`)
93
- .join("\n");
94
- const additionalContext = "Your team has a CodieDev skill that fits this task — prefer it over improvising:\n" +
95
- lines +
96
- "\nPull and follow it now: `codiedev pull <key>`. It encodes the team's vetted " +
97
- "approach. Skip it only if it clearly doesn't apply.";
110
+ const additionalContext = buildDiscoveryContext(skills);
111
+ if (additionalContext) {
98
112
  process.stdout.write(JSON.stringify({
99
113
  hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext },
100
114
  }));
@@ -144,6 +158,20 @@ async function runStopFailure(hookInput) {
144
158
  }
145
159
  process.exit(0);
146
160
  }
161
+ // Skip-reason ping: this session is ending WITHOUT a capture, and here's why.
162
+ // Turns silent coverage loss into per-reason buckets on the admin Capture
163
+ // Health panel (patches the session's aiSessionStarts row server-side). Tiny
164
+ // like the sessionStart ping. Best-effort, never throws.
165
+ async function postSessionSkip(config, sessionId, reason) {
166
+ if (!sessionId)
167
+ return;
168
+ try {
169
+ await postJson(`${config.backendUrl}/api/cli/sessionSkip`, { sessionId, reason, at: Date.now() }, config.token, PING_TIMEOUT_MS);
170
+ }
171
+ catch {
172
+ /* best-effort */
173
+ }
174
+ }
147
175
  // SessionStart: the capture-coverage denominator. Record that a session opened
148
176
  // so the backend can later compute captured ÷ started. Deliberately tiny — no
149
177
  // transcript read, no git — so it never slows session startup. Best-effort +
@@ -158,7 +186,7 @@ async function runSessionStart(hookInput) {
158
186
  sessionId: hookInput.session_id,
159
187
  startedAt: Date.now(),
160
188
  source: "claude-code",
161
- }, config.token);
189
+ }, config.token, PING_TIMEOUT_MS);
162
190
  }
163
191
  catch {
164
192
  /* best-effort — never block session startup */
@@ -166,7 +194,13 @@ async function runSessionStart(hookInput) {
166
194
  }
167
195
  process.exit(0);
168
196
  }
169
- function postJson(url, body, bearerToken) {
197
+ function postJson(url, body, bearerToken,
198
+ // Socket timeout. Pass a SMALL value for tiny pings that run on (or near)
199
+ // Claude Code's shutdown clock — a black-holed connection (proxy dropping
200
+ // packets without RST) otherwise hangs the hook until CC kills it. Leave
201
+ // unset for transcript uploads, which legitimately take a while on slow
202
+ // corporate links and already run in detached children with retry.
203
+ timeoutMs) {
170
204
  return new Promise((resolve, reject) => {
171
205
  const parsed = new URL(url);
172
206
  const data = JSON.stringify(body);
@@ -198,10 +232,19 @@ function postJson(url, body, bearerToken) {
198
232
  });
199
233
  });
200
234
  req.on("error", reject);
235
+ if (timeoutMs) {
236
+ req.setTimeout(timeoutMs, () => {
237
+ req.destroy(new Error(`timeout after ${timeoutMs}ms`));
238
+ });
239
+ }
201
240
  req.write(data);
202
241
  req.end();
203
242
  });
204
243
  }
244
+ // Short leash for the tiny coverage pings (sessionStart / sessionSkip): they
245
+ // carry a few bytes and must never stall a hook. 3s is generous for a ping
246
+ // and a hard bound for the shutdown clock.
247
+ const PING_TIMEOUT_MS = 3000;
205
248
  async function readStdin() {
206
249
  return new Promise((resolve, reject) => {
207
250
  let data = "";
@@ -329,14 +372,12 @@ async function main() {
329
372
  await runSessionStart(hookInput);
330
373
  return;
331
374
  }
332
- // Self-heal: a user who upgraded the CLI but hasn't re-run `connect` gets the
333
- // discovery + auto-backup + limit hooks wired on their next SessionEnd, without
334
- // re-connecting. Cheap (settings.json read, writes only when missing), so it
335
- // stays inline; the costly transcript/git work is what moves to the child.
336
- (0, utils_1.ensureDiscoveryHookInstalled)();
337
- (0, utils_1.ensureBackupHookInstalled)();
338
- (0, utils_1.ensureLimitHookInstalled)();
339
- (0, utils_1.ensureSessionStartHookInstalled)();
375
+ // Self-heal: a user who upgraded the CLI but hasn't re-run `connect` gets
376
+ // every codiedev hook — INCLUDING SessionEnd→capture, which previously had
377
+ // no self-heal and could stay stripped forever while SessionStart kept
378
+ // pinging (the Sonifi "starts arrive, captures never" loss) — re-wired on
379
+ // their next Stop/SessionEnd. One read + one write, only when missing.
380
+ (0, utils_1.ensureAllCodiedevHooksInstalled)();
340
381
  const { session_id, transcript_path, cwd, workspace_roots } = hookInput;
341
382
  const workspaceRoot = workspace_roots && workspace_roots.length > 0
342
383
  ? workspace_roots[0]
@@ -393,9 +434,14 @@ async function main() {
393
434
  // finished and the upload went through, but CC had already given up waiting.
394
435
  const config = (0, utils_1.readConfig)();
395
436
  if (!config) {
437
+ // No config = no token; nothing can be sent, not even a skip ping.
396
438
  process.exit(0);
397
439
  }
398
440
  if (!transcript_path) {
441
+ // Nothing to capture — but say so, instead of leaving a start ping with
442
+ // no matching capture (silent coverage loss). One tiny POST; the rare
443
+ // no-transcript case has no other work on the shutdown clock.
444
+ await postSessionSkip(config, session_id, "no-transcript-path");
399
445
  process.exit(0);
400
446
  }
401
447
  try {
@@ -434,8 +480,10 @@ async function runCaptureFinish(input) {
434
480
  if (!config)
435
481
  return;
436
482
  const { session_id, transcript_path, cwd, workspace_roots } = input;
437
- if (!transcript_path)
483
+ if (!transcript_path) {
484
+ await postSessionSkip(config, session_id, "no-transcript-path");
438
485
  return;
486
+ }
439
487
  // Log that this child has started processing — the first diagnostic proof
440
488
  // that the detached capture-finish child actually ran (visible in doctor).
441
489
  (0, captureLog_1.appendCaptureLog)({ stage: "capture-finish", sessionId: session_id, outcome: "start" });
@@ -461,6 +509,16 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
461
509
  transcriptContent = fs.readFileSync(transcript_path, "utf8");
462
510
  }
463
511
  catch {
512
+ // Previously the one fully-silent gap in the detached child: no log, no
513
+ // server signal. Diagnose it — deleted/rotated transcript, permissions,
514
+ // unmounted path.
515
+ (0, captureLog_1.appendCaptureLog)({
516
+ stage: "capture-finish",
517
+ sessionId: session_id,
518
+ outcome: "skip",
519
+ detail: "transcript-unreadable",
520
+ });
521
+ await postSessionSkip(config, session_id, "transcript-unreadable");
464
522
  return;
465
523
  }
466
524
  // Resolve repo URL. Codex embeds it in session_meta; CC and Cursor both
@@ -579,6 +637,14 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
579
637
  // Retry any earlier capture jobs that failed to upload (transient network
580
638
  // blips), excluding the one we just dispatched. Best-effort, detached.
581
639
  // Skip the sweep on deferred path — the deferred child will handle uploading.
640
+ //
641
+ // Orphan pings are COLLECTED and awaited below: this child process calls
642
+ // process.exit(0) as soon as runCaptureFinish resolves, so a
643
+ // fire-and-forget POST here would be killed mid-flight — after the
644
+ // orphan file was already unlinked, i.e. the loss would stay invisible
645
+ // forever. The await is off the shutdown clock (detached child) and
646
+ // bounded by PING_TIMEOUT_MS.
647
+ const orphanPings = [];
582
648
  (0, utils_1.sweepPendingCaptures)({
583
649
  dir: os.tmpdir(),
584
650
  readdirSync: fs.readdirSync,
@@ -589,7 +655,15 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
589
655
  exclude: tmpFile,
590
656
  openSync: fs.openSync,
591
657
  closeSync: fs.closeSync,
658
+ // Orphaned finish-inputs (capture-finish child died before writing an
659
+ // upload job): unrecoverable payload, but report the loss so it lands
660
+ // as a skip-reason instead of a silent coverage hole.
661
+ unlinkSync: fs.unlinkSync,
662
+ reportOrphan: (sid) => {
663
+ orphanPings.push(postSessionSkip(config, sid, "finish-orphaned"));
664
+ },
592
665
  });
666
+ await Promise.all(orphanPings);
593
667
  }
594
668
  else {
595
669
  // Deferred: write the job file (newer turns overwrite = supersede), then
package/dist/utils.d.ts CHANGED
@@ -81,6 +81,7 @@ export declare function installHook(): void;
81
81
  export declare function ensureDiscoveryHookInstalled(): void;
82
82
  export declare function ensureBackupHookInstalled(): void;
83
83
  export declare function ensureLimitHookInstalled(): void;
84
+ export declare function ensureAllCodiedevHooksInstalled(): void;
84
85
  export declare function ensureSessionStartHookInstalled(): void;
85
86
  export declare function installClaudeCodeInstructions(): void;
86
87
  export declare function installCodexInstructions(): void;
@@ -206,8 +207,11 @@ interface SweepDeps {
206
207
  openSync?: typeof fs.openSync;
207
208
  closeSync?: typeof fs.closeSync;
208
209
  pruneState?: () => void;
210
+ unlinkSync?: (p: string) => void;
211
+ reportOrphan?: (sessionId: string | undefined) => void;
209
212
  }
210
213
  export declare const CLAIM_STALE_MS: number;
214
+ export declare const FINISH_ORPHAN_AGE_MS: number;
211
215
  export declare function sweepPendingCaptures(deps: SweepDeps): void;
212
216
  export declare const CAPTURE_THROTTLE_MS: number;
213
217
  export declare const SWEEP_MIN_AGE_MS: number;
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.CLAIM_STALE_MS = exports.UPLOAD_TTL_MS = exports.MAX_UPLOAD_ATTEMPTS = void 0;
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;
37
37
  exports.readConfig = readConfig;
38
38
  exports.writeConfig = writeConfig;
39
39
  exports.parseGitRemoteFromConfig = parseGitRemoteFromConfig;
@@ -61,6 +61,7 @@ exports.installHook = installHook;
61
61
  exports.ensureDiscoveryHookInstalled = ensureDiscoveryHookInstalled;
62
62
  exports.ensureBackupHookInstalled = ensureBackupHookInstalled;
63
63
  exports.ensureLimitHookInstalled = ensureLimitHookInstalled;
64
+ exports.ensureAllCodiedevHooksInstalled = ensureAllCodiedevHooksInstalled;
64
65
  exports.ensureSessionStartHookInstalled = ensureSessionStartHookInstalled;
65
66
  exports.installClaudeCodeInstructions = installClaudeCodeInstructions;
66
67
  exports.installCodexInstructions = installCodexInstructions;
@@ -913,6 +914,61 @@ function ensureLimitHookInstalled() {
913
914
  }
914
915
  catch { /* best-effort */ }
915
916
  }
917
+ // Self-heal ALL codiedev hooks in one read → one write. This replaces calling
918
+ // the four per-hook ensure* functions in sequence, and fixes two real losses
919
+ // seen in the field (Sonifi, 2026-07):
920
+ // 1. The SessionEnd → capture hook — the one that actually uploads sessions
921
+ // — had NO self-heal at all: it was written once at `connect` and never
922
+ // repaired. Anything that stripped it (IT-managed dotfiles baseline, the
923
+ // race below) left SessionStart pinging forever while captures silently
924
+ // stopped — the exact "start pings arrive, captures never" signature.
925
+ // 2. Each per-hook ensure* did its own read-modify-write of settings.json
926
+ // with no locking; two concurrent Claude Code sessions repairing
927
+ // different hooks could lose each other's writes. One read + one write
928
+ // shrinks that window and removes the intra-process version entirely.
929
+ // The per-hook ensure* functions below are kept for tests/back-compat but are
930
+ // no longer called from the hook entrypoint.
931
+ function ensureAllCodiedevHooksInstalled() {
932
+ try {
933
+ if (!fs.existsSync(CLAUDE_SETTINGS_PATH))
934
+ return; // not a Claude Code user
935
+ const settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf8"));
936
+ const hooks = (settings.hooks ?? {});
937
+ const wanted = [
938
+ ["SessionEnd", "capture"],
939
+ ["UserPromptSubmit", "discover"],
940
+ ["Stop", "backup"],
941
+ ["StopFailure", "limit", "rate_limit"],
942
+ ["SessionStart", "session-start"],
943
+ ];
944
+ const missing = wanted.filter(([eventName]) => !hasCodiedevHook(hooks, eventName));
945
+ if (missing.length === 0)
946
+ return; // steady state: no write, no churn
947
+ settings.hooks = hooks;
948
+ for (const [eventName, subcommand, matcher] of missing) {
949
+ upsertCodiedevHook(hooks, eventName, subcommand, matcher);
950
+ }
951
+ fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf8");
952
+ }
953
+ catch {
954
+ // Best-effort — never block a session on self-heal.
955
+ }
956
+ }
957
+ function hasCodiedevHook(hooks, eventName) {
958
+ const entries = hooks[eventName] ?? [];
959
+ // A malformed (non-array) event value would make `.some` throw and — under
960
+ // ensureAll's single try/catch — abort EVERY heal, including the capture
961
+ // heal. Report "present" instead: leave the foreign/corrupt key alone and
962
+ // keep healing the other events (the per-hook fault isolation the old
963
+ // ensure* functions had).
964
+ if (!Array.isArray(entries))
965
+ return true;
966
+ return entries.some((hook) => {
967
+ const inner = hook.hooks;
968
+ return (Array.isArray(inner) &&
969
+ inner.some((h) => (0, detection_1.isCodiedevHookCommand)(h.command)));
970
+ });
971
+ }
916
972
  // Self-heal: ensure the SessionStart → session-start hook is present without a
917
973
  // full `connect` re-run, mirroring ensureLimitHookInstalled. Called from the
918
974
  // capture hook so an upgraded-but-not-reconnected user starts emitting the
@@ -1733,6 +1789,11 @@ function dispatchDeferredUpload(job, hookScriptPath, deps, delayMs) {
1733
1789
  // A claimed job whose uploader hasn't finished after this long is presumed
1734
1790
  // crashed and gets renamed back to its canonical name for a later retry.
1735
1791
  exports.CLAIM_STALE_MS = 15 * 60 * 1000;
1792
+ // A finish-input file older than this is an orphan: its capture-finish child
1793
+ // normally consumes + unlinks it within seconds, so an hour-old file means the
1794
+ // child died (spawn failure, OOM/AV kill, machine sleep). Generous so we never
1795
+ // race a legitimately slow child.
1796
+ exports.FINISH_ORPHAN_AGE_MS = 60 * 60 * 1000;
1736
1797
  // Retry kept-on-failure capture jobs: re-dispatch each pending
1737
1798
  // `codiedev-capture-*.json` to the detached uploader (which bumps attempts /
1738
1799
  // gives up at the cap). Best-effort — never throws into the hook.
@@ -1770,6 +1831,37 @@ function sweepPendingCaptures(deps) {
1770
1831
  // raced another sweeper or the uploader just finished — fine
1771
1832
  }
1772
1833
  }
1834
+ // Orphaned finish-inputs: the FIRST detached child (capture-finish) died
1835
+ // before it could build an upload job. That path has no retry queue — the
1836
+ // payload is gone — but the loss must not stay invisible: report the
1837
+ // sessionId (filename embeds it: codiedev-finish-<sid>-<pid>-<ts>.json) so
1838
+ // it lands as a skip-reason, then clean the file up.
1839
+ if (deps.unlinkSync) {
1840
+ for (const f of files) {
1841
+ const m = /^codiedev-finish-(.+)-\d+-\d+\.json$/.exec(f);
1842
+ if (!m)
1843
+ continue;
1844
+ const orphanPath = path.join(deps.dir, f);
1845
+ try {
1846
+ if (now - deps.statSync(orphanPath).mtimeMs < exports.FINISH_ORPHAN_AGE_MS)
1847
+ continue;
1848
+ deps.unlinkSync(orphanPath);
1849
+ // finishInputTmpFile falls back to the PID when the hook had no
1850
+ // session_id (codiedev-finish-<pid>-<pid>-<ts>.json). A pure-digit
1851
+ // capture is that fallback, not a session id — reporting it would
1852
+ // create a phantom session row server-side. Real session ids are
1853
+ // UUIDs and never all-digits.
1854
+ const sid = /^\d+$/.test(m[1]) ? undefined : m[1];
1855
+ try {
1856
+ deps.reportOrphan?.(sid);
1857
+ }
1858
+ catch { /* report is best-effort */ }
1859
+ }
1860
+ catch {
1861
+ // raced its owner or another sweeper — fine
1862
+ }
1863
+ }
1864
+ }
1773
1865
  const jobs = files
1774
1866
  .filter((f) => /^codiedev-capture-.*\.json$/.test(f))
1775
1867
  .map((f) => path.join(deps.dir, f))
@@ -2046,10 +2138,30 @@ async function runCaptureUpload(jobFile, deps) {
2046
2138
  }
2047
2139
  catch { /* log is best-effort */ }
2048
2140
  }
2141
+ // Unlink FIRST: if the skip ping below hangs (no timeout on injected
2142
+ // post impls), a still-present job file would be reclaimed by the
2143
+ // 15-min stale-claim sweep and re-enter this give-up path forever.
2049
2144
  try {
2050
2145
  deps.unlinkSync(jobFile);
2051
2146
  }
2052
2147
  catch { /* gone */ }
2148
+ // Tell the server this capture is permanently lost and why. The full
2149
+ // transcript POST may be what's blocked (proxy/DLP body inspection) —
2150
+ // this tiny ping often still passes, turning an invisible loss into a
2151
+ // per-reason bucket on the admin Capture Health panel.
2152
+ if (sessionId) {
2153
+ try {
2154
+ const skipUrl = job.url.replace(/\/api\/captureSession$/, "/api/cli/sessionSkip");
2155
+ if (skipUrl !== job.url) {
2156
+ await deps.post(skipUrl, {
2157
+ sessionId,
2158
+ reason: expired ? "upload-gave-up:ttl" : "upload-gave-up:attempts",
2159
+ at: now,
2160
+ }, job.token);
2161
+ }
2162
+ }
2163
+ catch { /* best-effort — the ping may be blocked too */ }
2164
+ }
2053
2165
  return;
2054
2166
  }
2055
2167
  // Log the error before persisting the retry bump.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.21",
3
+ "version": "0.9.23",
4
4
  "description": "Connect Claude Code, Codex, Cursor, or VS Code Copilot to Codiedev for org-wide session capture and artifact collaboration",
5
5
  "bin": {
6
6
  "codiedev": "dist/cli.js",