claude-code-session-manager 0.77.0 → 0.79.0

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 (45) hide show
  1. package/dist/assets/{AgentLibrary-B2ie8bbw.js → AgentLibrary-COtVRqBR.js} +1 -1
  2. package/dist/assets/{DataModel-BIJPYw32.js → DataModel-CSEKw_OR.js} +1 -1
  3. package/dist/assets/{History-CeY6dk9S.js → History-CHHovrAO.js} +1 -1
  4. package/dist/assets/{Hooks-BFH2ocKg.js → Hooks-BZU6C3x6.js} +1 -1
  5. package/dist/assets/{HostBilko-36gj9wLz.js → HostBilko-CqTUoq37.js} +1 -1
  6. package/dist/assets/{Library-C-hBct39.js → Library-BtxdyTLz.js} +1 -1
  7. package/dist/assets/{ListDetail-CNq64VWV.js → ListDetail-qZc7Zm-6.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-Bh3qt5-1.js → MarkdownEditor-BHe_4fJR.js} +1 -1
  9. package/dist/assets/{McpServers-DpGN0oyz.js → McpServers-7Z98HLNo.js} +1 -1
  10. package/dist/assets/{Memory-D59hUjC4.js → Memory-CR72KoyP.js} +1 -1
  11. package/dist/assets/{Panel-DCgbaoci.js → Panel-pL6H3dpQ.js} +1 -1
  12. package/dist/assets/{Permissions-DAmQ0DYV.js → Permissions-CWSWjyXM.js} +1 -1
  13. package/dist/assets/{Plugins-Dyfgn6Is.js → Plugins-CN6lX2lt.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-BiYhPO1U.js → ProvenanceBadge-BXSXwIsk.js} +1 -1
  15. package/dist/assets/{SaveBar-RV7B6sOh.js → SaveBar-BlB5TGpR.js} +1 -1
  16. package/dist/assets/{Scheduler-BPaNqx1b.js → Scheduler-DRciWUmR.js} +1 -1
  17. package/dist/assets/{ScopeSwitcher-P4mdLGNU.js → ScopeSwitcher-kFrXtjpr.js} +1 -1
  18. package/dist/assets/{Settings-BL4vf5aX.js → Settings-BXuyf4lJ.js} +1 -1
  19. package/dist/assets/{SkillReferenceGraph-BRBDyi1_.js → SkillReferenceGraph-Dfacb0PE.js} +1 -1
  20. package/dist/assets/{Skills-BV08gDUH.js → Skills-CHqcpiyt.js} +1 -1
  21. package/dist/assets/{SystemPrompt-CLftSsDw.js → SystemPrompt-fxXm0BZr.js} +1 -1
  22. package/dist/assets/{TagLibrary-Bp8jGsd5.js → TagLibrary-DOz65ZTz.js} +1 -1
  23. package/dist/assets/{TiptapBody-jCpuB6E5.js → TiptapBody-D0bWx_9o.js} +1 -1
  24. package/dist/assets/{Toggle-D2paA1xf.js → Toggle-C9jBwGSx.js} +1 -1
  25. package/dist/assets/{index-BDRSqBl3.js → index-DPYa6jbM.js} +419 -419
  26. package/dist/assets/{settingsSchema-6IOLjZZN.js → settingsSchema-BTPw1bR3.js} +1 -1
  27. package/dist/index.html +1 -1
  28. package/package.json +1 -1
  29. package/src/main/__tests__/runLogRetention.test.cjs +59 -0
  30. package/src/main/__tests__/scheduler-never-stop.test.cjs +157 -0
  31. package/src/main/__tests__/scheduler-no-orphan-run-dir.test.cjs +81 -0
  32. package/src/main/__tests__/scheduler-rate-limit-cooldown-freshness.test.cjs +123 -0
  33. package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +158 -0
  34. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +30 -0
  35. package/src/main/index.cjs +8 -3
  36. package/src/main/ipcSchemas.cjs +2 -0
  37. package/src/main/lib/__tests__/delegationReadiness.test.cjs +45 -0
  38. package/src/main/lib/__tests__/reaperHelpers.test.cjs +90 -1
  39. package/src/main/lib/jobDirtFilter.cjs +54 -0
  40. package/src/main/lib/rateLimitDetect.cjs +35 -0
  41. package/src/main/lib/reaperHelpers.cjs +31 -6
  42. package/src/main/lib/runLogRetention.cjs +82 -4
  43. package/src/main/scheduler.cjs +353 -31
  44. package/src/preload/api.d.ts +8 -4
  45. package/src/preload/index.cjs +1 -0
@@ -9,6 +9,7 @@
9
9
 
10
10
  const fs = require('node:fs');
11
11
  const { readTail } = require('./fileTail.cjs');
12
+ const { detectRateLimitInLog } = require('./rateLimitDetect.cjs');
12
13
 
13
14
  /**
14
15
  * Return true if pid is alive AND its cmdline looks like a claude process.
@@ -38,11 +39,16 @@ function claudePidAlive(pid) {
38
39
  * of its log file and scanning for the LAST `{"type":"result"}` JSONL event.
39
40
  *
40
41
  * Returns:
41
- * 'success' — last result event has subtype=success and is_error !== true
42
- * 'failed' — last result event exists but indicates an error
43
- * 'no_result' — no result event found in the tail (process may have been killed
44
- * before emitting one, or the log is absent/empty)
45
- * 'unknown' — unexpected error reading/parsing (outer catch)
42
+ * 'success' — last result event has subtype=success and is_error !== true
43
+ * 'rate_limited' — the log tail shows the same rate-limit signal spawnJob's own
44
+ * live-process check uses (detectRateLimitInLog, the shared
45
+ * single source of truth) — a NEW, distinct outcome from
46
+ * 'failed' (PRD 1117): a rate-limited death is retryable, not
47
+ * a genuine gate failure, and must never collapse into 'failed'
48
+ * 'failed' — last result event exists but indicates a genuine error
49
+ * 'no_result' — no result event found in the tail (process may have been killed
50
+ * before emitting one, or the log is absent/empty)
51
+ * 'unknown' — unexpected error reading/parsing (outer catch)
46
52
  */
47
53
  function classifyRunOutcome(logPath) {
48
54
  try {
@@ -56,8 +62,27 @@ function classifyRunOutcome(logPath) {
56
62
  if (obj && obj.type === 'result') lastResult = obj;
57
63
  } catch { /* partial line at tail boundary or non-JSON scheduler log line */ }
58
64
  }
65
+ // ORDER IS LOAD-BEARING. The rate-limit check must come AFTER the success
66
+ // determination, never before it. The CLI emits an informational
67
+ // `rate_limit_event` with status:"allowed_warning" on essentially every
68
+ // run once utilization is non-zero, and detectRateLimitInLog matches its
69
+ // "rateLimitType" field — so checking first classified genuinely
70
+ // SUCCESSFUL runs as rate_limited. Measured on 2026-09-05 against the
71
+ // eight most recent runs whose own meta.json recorded exitCode:0 and
72
+ // rateLimited:false, four came back 'rate_limited' (e.g.
73
+ // 200-campaign-toolkit-weak-points-and-stuns: 55 turns, is_error:false,
74
+ // terminalReasonFromHarness:'completed', landed commit 7fd05f7 — matched
75
+ // purely on an allowed_warning five_hour event). In reapDeadRunningJobs
76
+ // that resets a finished job to 'pending' to re-run shipped work AND
77
+ // engages setPaused('rate_limit') with no rate limit in effect — strictly
78
+ // worse than the terminal-'failed' bug the rate_limit branch was added to
79
+ // fix. See the guard test in this file's __tests__ sibling.
80
+ if (lastResult && lastResult.subtype === 'success' && lastResult.is_error !== true) return 'success';
81
+ // Still checked ahead of 'no_result': a run killed mid-flight by a rate
82
+ // limit may never emit a result event at all, and that is a rate-limited
83
+ // death, not silence.
84
+ if (detectRateLimitInLog(logPath)) return 'rate_limited';
59
85
  if (!lastResult) return 'no_result';
60
- if (lastResult.subtype === 'success' && lastResult.is_error !== true) return 'success';
61
86
  return 'failed';
62
87
  } catch {
63
88
  return 'unknown';
@@ -296,11 +296,84 @@ function resolveLiveKeysForApply(runsDir, opts, enabled) {
296
296
  }
297
297
  }
298
298
 
299
+ /**
300
+ * Build the set of runIds currently claimed by a job in status 'running' —
301
+ * used by the orphan pass below to skip a directory whose spawn may be
302
+ * mid-flight right now (scheduler.cjs's pickRunDir mints the runId/dir pair
303
+ * before the child process is spawned; the directory itself is only
304
+ * mkdir'd, and files only start landing in it, once execution actually
305
+ * commits — see executeJob). Deliberately keyed by runId alone, not
306
+ * `${slug}|${runId}` like liveKeysFromJobs: a shared batch dir (tickQueue
307
+ * hands ONE runId to several jobs at once) is in flight if ANY job dispatched
308
+ * into it is still running, regardless of which slug.
309
+ */
310
+ function runningRunIdsFromJobs(jobs) {
311
+ const ids = new Set();
312
+ for (const job of jobs || []) {
313
+ if (job && job.status === 'running' && job.runId) ids.add(job.runId);
314
+ }
315
+ return ids;
316
+ }
317
+
318
+ /**
319
+ * Resolve the running-runId protection set for applyRetention's orphan pass,
320
+ * mirroring resolveLiveKeysForApply's precedence: explicit opts win, then
321
+ * opts.jobs, then (only when deletion is actually about to happen) a direct
322
+ * read of the real scheduler queue.
323
+ */
324
+ function resolveRunningRunIdsForApply(opts, enabled) {
325
+ if (opts && opts.runningRunIds) return opts.runningRunIds;
326
+ if (opts && opts.jobs) return runningRunIdsFromJobs(opts.jobs);
327
+ if (!enabled) return new Set();
328
+ try {
329
+ const queueStore = require('./queueStore.cjs');
330
+ const state = queueStore.readMergedSync();
331
+ return runningRunIdsFromJobs(state.jobs || []);
332
+ } catch {
333
+ return new Set();
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Remove every EMPTY directory directly under runsDir, at any age, except
339
+ * one whose runId is in runningRunIds. These are orphans left behind by a
340
+ * dispatch that minted a runId/dir pair (pickRunDir) but aborted before
341
+ * spawnJob's executeJob ever wrote into it (slot-acquire miss, worktree-cap
342
+ * deferral, launch-gate block, ...) — scanRunEntries never sees them (no
343
+ * meta.json), so the age/count policy above can't reach them either.
344
+ * Returns the count of directories removed.
345
+ */
346
+ function sweepOrphanDirs(runsDir, runningRunIds, errors) {
347
+ let dirNames;
348
+ try {
349
+ dirNames = fs.readdirSync(runsDir, { withFileTypes: true })
350
+ .filter((d) => d.isDirectory())
351
+ .map((d) => d.name);
352
+ } catch {
353
+ return 0;
354
+ }
355
+
356
+ let removed = 0;
357
+ for (const runId of dirNames) {
358
+ if (runningRunIds.has(runId)) continue;
359
+ const dir = path.join(runsDir, runId);
360
+ try {
361
+ if (fs.readdirSync(dir).length !== 0) continue;
362
+ fs.rmdirSync(dir);
363
+ removed += 1;
364
+ } catch (e) {
365
+ if (e && e.code !== 'ENOENT') errors.push({ path: dir, error: e.message });
366
+ }
367
+ }
368
+ return removed;
369
+ }
370
+
299
371
  /**
300
372
  * Compute the report, and — ONLY when isRetentionEnabled(settings) — delete
301
- * the eligible files and rmdir any directory left fully empty. With no
302
- * opt-in (the default), this is exactly computeReport(): read-only,
303
- * `deleted: false`, nothing removed.
373
+ * the eligible files, rmdir any directory left fully empty, and sweep any
374
+ * orphan empty run directory (see sweepOrphanDirs). With no opt-in (the
375
+ * default), this is exactly computeReport(): read-only, `deleted: false`,
376
+ * nothing removed.
304
377
  */
305
378
  function applyRetention(runsDir, settings, opts) {
306
379
  const cfg = (settings && settings.schedulerRunLogRetention) || null;
@@ -342,7 +415,10 @@ function applyRetention(runsDir, settings, opts) {
342
415
  }
343
416
  }
344
417
 
345
- return { deleted: true, removedFiles, freedBytes, errors, report };
418
+ const runningRunIds = resolveRunningRunIdsForApply(opts, enabled);
419
+ const orphanDirsRemoved = sweepOrphanDirs(runsDir, runningRunIds, errors);
420
+
421
+ return { deleted: true, removedFiles, freedBytes, orphanDirsRemoved, errors, report };
346
422
  }
347
423
 
348
424
  /**
@@ -370,6 +446,8 @@ module.exports = {
370
446
  LIVE_STATUSES,
371
447
  isLiveJob,
372
448
  liveKeysFromJobs,
449
+ runningRunIdsFromJobs,
450
+ sweepOrphanDirs,
373
451
  scanRunEntries,
374
452
  computeEligibility,
375
453
  computeReport,