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.
- package/dist/assets/{AgentLibrary-B2ie8bbw.js → AgentLibrary-COtVRqBR.js} +1 -1
- package/dist/assets/{DataModel-BIJPYw32.js → DataModel-CSEKw_OR.js} +1 -1
- package/dist/assets/{History-CeY6dk9S.js → History-CHHovrAO.js} +1 -1
- package/dist/assets/{Hooks-BFH2ocKg.js → Hooks-BZU6C3x6.js} +1 -1
- package/dist/assets/{HostBilko-36gj9wLz.js → HostBilko-CqTUoq37.js} +1 -1
- package/dist/assets/{Library-C-hBct39.js → Library-BtxdyTLz.js} +1 -1
- package/dist/assets/{ListDetail-CNq64VWV.js → ListDetail-qZc7Zm-6.js} +1 -1
- package/dist/assets/{MarkdownEditor-Bh3qt5-1.js → MarkdownEditor-BHe_4fJR.js} +1 -1
- package/dist/assets/{McpServers-DpGN0oyz.js → McpServers-7Z98HLNo.js} +1 -1
- package/dist/assets/{Memory-D59hUjC4.js → Memory-CR72KoyP.js} +1 -1
- package/dist/assets/{Panel-DCgbaoci.js → Panel-pL6H3dpQ.js} +1 -1
- package/dist/assets/{Permissions-DAmQ0DYV.js → Permissions-CWSWjyXM.js} +1 -1
- package/dist/assets/{Plugins-Dyfgn6Is.js → Plugins-CN6lX2lt.js} +2 -2
- package/dist/assets/{ProvenanceBadge-BiYhPO1U.js → ProvenanceBadge-BXSXwIsk.js} +1 -1
- package/dist/assets/{SaveBar-RV7B6sOh.js → SaveBar-BlB5TGpR.js} +1 -1
- package/dist/assets/{Scheduler-BPaNqx1b.js → Scheduler-DRciWUmR.js} +1 -1
- package/dist/assets/{ScopeSwitcher-P4mdLGNU.js → ScopeSwitcher-kFrXtjpr.js} +1 -1
- package/dist/assets/{Settings-BL4vf5aX.js → Settings-BXuyf4lJ.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-BRBDyi1_.js → SkillReferenceGraph-Dfacb0PE.js} +1 -1
- package/dist/assets/{Skills-BV08gDUH.js → Skills-CHqcpiyt.js} +1 -1
- package/dist/assets/{SystemPrompt-CLftSsDw.js → SystemPrompt-fxXm0BZr.js} +1 -1
- package/dist/assets/{TagLibrary-Bp8jGsd5.js → TagLibrary-DOz65ZTz.js} +1 -1
- package/dist/assets/{TiptapBody-jCpuB6E5.js → TiptapBody-D0bWx_9o.js} +1 -1
- package/dist/assets/{Toggle-D2paA1xf.js → Toggle-C9jBwGSx.js} +1 -1
- package/dist/assets/{index-BDRSqBl3.js → index-DPYa6jbM.js} +419 -419
- package/dist/assets/{settingsSchema-6IOLjZZN.js → settingsSchema-BTPw1bR3.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/main/__tests__/runLogRetention.test.cjs +59 -0
- package/src/main/__tests__/scheduler-never-stop.test.cjs +157 -0
- package/src/main/__tests__/scheduler-no-orphan-run-dir.test.cjs +81 -0
- package/src/main/__tests__/scheduler-rate-limit-cooldown-freshness.test.cjs +123 -0
- package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +158 -0
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +30 -0
- package/src/main/index.cjs +8 -3
- package/src/main/ipcSchemas.cjs +2 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +45 -0
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +90 -1
- package/src/main/lib/jobDirtFilter.cjs +54 -0
- package/src/main/lib/rateLimitDetect.cjs +35 -0
- package/src/main/lib/reaperHelpers.cjs +31 -6
- package/src/main/lib/runLogRetention.cjs +82 -4
- package/src/main/scheduler.cjs +353 -31
- package/src/preload/api.d.ts +8 -4
- 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'
|
|
42
|
-
* '
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
|
302
|
-
*
|
|
303
|
-
* `deleted: false`,
|
|
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
|
-
|
|
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,
|