claude-code-session-manager 0.93.0 → 0.95.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/{DataModel-DDGx3qrn.js → DataModel-DZK9QZpV.js} +1 -1
- package/dist/assets/{History-CHBjYZyD.js → History-BDyJUGtR.js} +2 -2
- package/dist/assets/{Hooks-DG4c-Rzz.js → Hooks-CmK6l0uf.js} +3 -3
- package/dist/assets/HostBilko-D8TIx-Bx.js +1 -0
- package/dist/assets/{Library-CN1h5rAE.js → Library-DTxpOjx8.js} +25 -25
- package/dist/assets/{MarkdownEditor-DmBxrhdn.js → MarkdownEditor-BPJyrevq.js} +1 -1
- package/dist/assets/McpServers-B_rvIYDs.js +2 -0
- package/dist/assets/{Memory-Dk4uDCjg.js → Memory-B-9GlHqe.js} +6 -6
- package/dist/assets/{Permissions-BovobeZw.js → Permissions-DhMMp27n.js} +3 -3
- package/dist/assets/{Plugins-DvOf_B62.js → Plugins-DKfAlmMY.js} +2 -2
- package/dist/assets/{ProvenanceBadge-Dutz_4OY.js → ProvenanceBadge-ptlItdCK.js} +1 -1
- package/dist/assets/{SaveBar-CT5u6ZCJ.js → SaveBar-CSbNSl2W.js} +1 -1
- package/dist/assets/Scheduler--rn1QBYc.js +16 -0
- package/dist/assets/{ScopeSwitcher-BUYDpz2v.js → ScopeSwitcher-BJof-GWH.js} +1 -1
- package/dist/assets/{Settings-DIRmr3tL.js → Settings-Dsx6XZP5.js} +3 -3
- package/dist/assets/{SkillReferenceGraph-CdGr6d3_.js → SkillReferenceGraph-CGl4B89-.js} +2 -2
- package/dist/assets/{Skills-BQOheQv4.js → Skills-D522Zfol.js} +2 -2
- package/dist/assets/{SystemPrompt-5LsimKbX.js → SystemPrompt-DKj-q39J.js} +1 -1
- package/dist/assets/{TagLibrary-DreLsT3n.js → TagLibrary-BH7CG7-6.js} +1 -1
- package/dist/assets/{TiptapBody-C9kmlBnI.js → TiptapBody-BbcJL9az.js} +1 -1
- package/dist/assets/{Toggle-BKLCBu9A.js → Toggle-DAslvnnv.js} +1 -1
- package/dist/assets/{index-Db_SK9rW.js → index-Yf8e7VCB.js} +821 -821
- package/dist/assets/{settingsSchema-B5jBhSUP.js → settingsSchema-pzp0TgvN.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +8 -3
- package/scripts/README.md +6 -0
- package/src/main/__tests__/chat-cancel-terminal.test.cjs +5 -2
- package/src/main/__tests__/chat-exit-close-race.test.cjs +8 -2
- package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +5 -2
- package/src/main/__tests__/health-tick-liveness.test.cjs +12 -0
- package/src/main/__tests__/intradayRefresh.test.cjs +39 -0
- package/src/main/__tests__/openExternalApp-spawn-error.test.cjs +25 -0
- package/src/main/__tests__/opsErrorLog.test.cjs +22 -0
- package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +21 -0
- package/src/main/__tests__/rateLimitPollerStreak.test.cjs +14 -0
- package/src/main/__tests__/runVerify-atomic-verdicts.test.cjs +26 -0
- package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +11 -1
- package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +11 -1
- package/src/main/__tests__/runVerify.test.cjs +12 -1
- package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +15 -0
- package/src/main/__tests__/scheduler-utilization-hold.test.cjs +89 -0
- package/src/main/__tests__/transcriptsUsageFor.test.cjs +112 -1
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +45 -10
- package/src/main/crashDiagnostics.cjs +9 -0
- package/src/main/git.cjs +9 -2
- package/src/main/health.cjs +25 -1
- package/src/main/historyAggregator.cjs +2 -19
- package/src/main/index.cjs +7 -8
- package/src/main/ipcSchemas.cjs +13 -3
- package/src/main/lib/__tests__/childWithLog.test.cjs +180 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +19 -0
- package/src/main/lib/__tests__/gitCacheBound.test.cjs +69 -0
- package/src/main/lib/__tests__/loopDelay.test.cjs +68 -0
- package/src/main/lib/__tests__/usageCircuit.test.cjs +86 -17
- package/src/main/lib/agentPersonaSchema.cjs +2 -0
- package/src/main/lib/childWithLog.cjs +92 -51
- package/src/main/lib/delegationReadiness.cjs +3 -1
- package/src/main/lib/epicSpawnPlan.cjs +27 -8
- package/src/main/lib/epicTranscriptPath.cjs +5 -1
- package/src/main/lib/headTailBuffer.cjs +43 -0
- package/src/main/lib/intradayRefresh.cjs +33 -0
- package/src/main/lib/loopDelay.cjs +44 -0
- package/src/main/lib/lruCache.cjs +39 -0
- package/src/main/lib/openExternalApp.cjs +27 -9
- package/src/main/lib/opsErrorLog.cjs +22 -0
- package/src/main/lib/promptSessionSchema.cjs +2 -0
- package/src/main/lib/rendererRecovery.cjs +141 -0
- package/src/main/lib/scheduleJobSchema.cjs +3 -0
- package/src/main/lib/usageCircuit.cjs +40 -12
- package/src/main/runVerify.cjs +3 -1
- package/src/main/scheduler.cjs +409 -260
- package/src/main/transcripts.cjs +54 -11
- package/src/main/usage.cjs +9 -1
- package/src/preload/api.d.ts +20 -3
- package/dist/assets/HostBilko-DHXcpV2c.js +0 -1
- package/dist/assets/McpServers-Beb_HD6I.js +0 -2
- package/dist/assets/Scheduler-B7GSnZj5.js +0 -16
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Bounded automatic recovery for a dead / hung / failed-to-load renderer.
|
|
4
|
+
// Without it an OOM-killed renderer leaves a permanently white window while
|
|
5
|
+
// scheduler jobs keep running headless with no UI to pause them.
|
|
6
|
+
//
|
|
7
|
+
// `createReloadPolicy` is the pure cap/timing unit (injected clock);
|
|
8
|
+
// `attachRendererRecovery` wires it to one webContents (= one window).
|
|
9
|
+
|
|
10
|
+
const MAX_RELOADS = 3;
|
|
11
|
+
const WINDOW_MS = 10 * 60 * 1000;
|
|
12
|
+
const UNRESPONSIVE_GRACE_MS = 30 * 1000;
|
|
13
|
+
const LOAD_RETRY_DELAY_MS = 2000;
|
|
14
|
+
const ERR_ABORTED = -3;
|
|
15
|
+
|
|
16
|
+
/** Sliding-window cap: at most `max` reloads per `windowMs`. O(max) per call. */
|
|
17
|
+
function createReloadPolicy({ now = Date.now, max = MAX_RELOADS, windowMs = WINDOW_MS } = {}) {
|
|
18
|
+
let stamps = [];
|
|
19
|
+
return {
|
|
20
|
+
/** Record a reload if the cap allows it. Returns true when allowed. */
|
|
21
|
+
tryReload() {
|
|
22
|
+
const t = now();
|
|
23
|
+
stamps = stamps.filter((s) => t - s < windowMs);
|
|
24
|
+
if (stamps.length >= max) return false;
|
|
25
|
+
stamps.push(t);
|
|
26
|
+
return true;
|
|
27
|
+
},
|
|
28
|
+
/** Would tryReload() succeed right now? Pure — consumes no slot. */
|
|
29
|
+
canReload() {
|
|
30
|
+
const t = now();
|
|
31
|
+
return stamps.filter((s) => t - s < windowMs).length < max;
|
|
32
|
+
},
|
|
33
|
+
/** Should a render-process-gone reason trigger recovery? */
|
|
34
|
+
shouldRecoverFromGone(reason) {
|
|
35
|
+
return reason !== 'clean-exit';
|
|
36
|
+
},
|
|
37
|
+
/** Should a did-fail-load trigger a retry? Main frame only, not ERR_ABORTED. */
|
|
38
|
+
shouldRetryLoad(errorCode, isMainFrame) {
|
|
39
|
+
return isMainFrame !== false && errorCode !== ERR_ABORTED;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function attachRendererRecovery(wc, {
|
|
45
|
+
logs,
|
|
46
|
+
policy = createReloadPolicy(),
|
|
47
|
+
setTimer = setTimeout,
|
|
48
|
+
clearTimer = clearTimeout,
|
|
49
|
+
} = {}) {
|
|
50
|
+
let unresponsiveTimer = null;
|
|
51
|
+
let loadRetryTimer = null;
|
|
52
|
+
let capLogged = false;
|
|
53
|
+
|
|
54
|
+
const log = (level, message, meta) => {
|
|
55
|
+
try { logs?.writeLine({ scope: 'crash-diag', level, message, meta }); } catch { /* logging must never break recovery */ }
|
|
56
|
+
};
|
|
57
|
+
const alive = () => !wc.isDestroyed();
|
|
58
|
+
|
|
59
|
+
function logCapReached(reason) {
|
|
60
|
+
if (capLogged) return;
|
|
61
|
+
capLogged = true;
|
|
62
|
+
log('error', 'renderer auto-reload cap reached — giving up', { reason, max: MAX_RELOADS, windowMs: WINDOW_MS });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function reload(reason) {
|
|
66
|
+
if (!alive()) return;
|
|
67
|
+
if (!policy.tryReload()) {
|
|
68
|
+
logCapReached(reason);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
capLogged = false;
|
|
72
|
+
log('warn', 'renderer auto-reload', { reason });
|
|
73
|
+
try { wc.reload(); } catch (e) { log('error', 'renderer reload threw', { reason, error: e?.message }); }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// A hung main thread cannot commit a same-process reload, so kill the renderer
|
|
77
|
+
// and let the render-process-gone handler do the single cap-counted reload
|
|
78
|
+
// (in a new process). No slot is consumed here.
|
|
79
|
+
function recoverFromHang() {
|
|
80
|
+
if (!alive()) return;
|
|
81
|
+
if (!policy.canReload()) {
|
|
82
|
+
// A hung-but-visible window beats a dead white one.
|
|
83
|
+
logCapReached('unresponsive');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
if (typeof wc.forcefullyCrashRenderer !== 'function') throw new Error('forcefullyCrashRenderer unavailable');
|
|
88
|
+
log('warn', 'renderer unresponsive — forcefully crashing for recovery');
|
|
89
|
+
wc.forcefullyCrashRenderer();
|
|
90
|
+
} catch (e) {
|
|
91
|
+
log('warn', 'forcefullyCrashRenderer failed — falling back to reload', { error: e?.message });
|
|
92
|
+
reload('unresponsive');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
wc.on('render-process-gone', (_e, details) => {
|
|
97
|
+
const reason = details?.reason;
|
|
98
|
+
if (!policy.shouldRecoverFromGone(reason)) return;
|
|
99
|
+
reload(`render-process-gone:${reason}`);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
wc.on('unresponsive', () => {
|
|
103
|
+
log('warn', 'renderer unresponsive');
|
|
104
|
+
if (unresponsiveTimer) return;
|
|
105
|
+
unresponsiveTimer = setTimer(() => {
|
|
106
|
+
unresponsiveTimer = null;
|
|
107
|
+
recoverFromHang();
|
|
108
|
+
}, UNRESPONSIVE_GRACE_MS);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
wc.on('responsive', () => {
|
|
112
|
+
if (unresponsiveTimer) { clearTimer(unresponsiveTimer); unresponsiveTimer = null; }
|
|
113
|
+
log('info', 'renderer responsive again');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
wc.on('did-fail-load', (_e, errorCode, errorDescription, _url, isMainFrame) => {
|
|
117
|
+
if (!policy.shouldRetryLoad(errorCode, isMainFrame)) return;
|
|
118
|
+
log('warn', 'main-frame did-fail-load', { errorCode, errorDescription });
|
|
119
|
+
if (loadRetryTimer) return;
|
|
120
|
+
loadRetryTimer = setTimer(() => {
|
|
121
|
+
loadRetryTimer = null;
|
|
122
|
+
reload(`did-fail-load:${errorCode}`);
|
|
123
|
+
}, LOAD_RETRY_DELAY_MS);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
wc.once('destroyed', () => {
|
|
127
|
+
if (unresponsiveTimer) clearTimer(unresponsiveTimer);
|
|
128
|
+
if (loadRetryTimer) clearTimer(loadRetryTimer);
|
|
129
|
+
unresponsiveTimer = loadRetryTimer = null;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
createReloadPolicy,
|
|
135
|
+
attachRendererRecovery,
|
|
136
|
+
MAX_RELOADS,
|
|
137
|
+
WINDOW_MS,
|
|
138
|
+
UNRESPONSIVE_GRACE_MS,
|
|
139
|
+
LOAD_RETRY_DELAY_MS,
|
|
140
|
+
ERR_ABORTED,
|
|
141
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
/**
|
|
2
3
|
* Canonical runtime schema for a ScheduleJob record, mirroring the TS
|
|
3
4
|
* `ScheduleJob` interface (`ScheduleJob` in src/preload/api.d.ts) field-for-field.
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
|
|
25
26
|
const { z } = require('zod');
|
|
26
27
|
|
|
28
|
+
/** @type {[string, ...string[]]} */
|
|
27
29
|
const JOB_STATUSES = ['pending', 'running', 'investigating', 'completed', 'skipped', 'failed', 'needs_review', 'quarantined'];
|
|
28
30
|
|
|
29
31
|
const ScheduleJobStatusSchema = z.enum(JOB_STATUSES);
|
|
@@ -138,6 +140,7 @@ const ScheduleJobSchema = z
|
|
|
138
140
|
/**
|
|
139
141
|
* Throws a clear, descriptive error (not a raw ZodError dump) when `job`
|
|
140
142
|
* doesn't match ScheduleJobSchema. Mirrors assertValidPromptSession's shape.
|
|
143
|
+
* @param {unknown} job
|
|
141
144
|
*/
|
|
142
145
|
function assertValidScheduleJob(job) {
|
|
143
146
|
const result = ScheduleJobSchema.safeParse(job);
|
|
@@ -50,19 +50,44 @@ function isResetFresh(iso, now) {
|
|
|
50
50
|
return resetMs > nowMs;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/** Flat sibling window that mirrors a `limits[]` entry's `kind`, for resets_at backfill. */
|
|
54
|
+
const FLAT_WINDOW_BY_KIND = { session: 'five_hour', weekly_all: 'seven_day' };
|
|
55
|
+
|
|
56
|
+
/** An entry's utilization: real `percent`, else the flat-shape `utilization`. */
|
|
57
|
+
function entryPercent(l) {
|
|
58
|
+
return Number.isFinite(l.percent) ? l.percent : l.utilization;
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
/**
|
|
54
|
-
* Which window is actually binding dispatch.
|
|
55
|
-
* `
|
|
56
|
-
* `
|
|
62
|
+
* Which window is actually binding dispatch. The real /api/oauth/usage
|
|
63
|
+
* `limits[]` entries carry `kind` / `group` / `percent` / `severity` /
|
|
64
|
+
* `resets_at` / `scope` / `is_active` (percent is 0-100, may exceed 100).
|
|
65
|
+
* Only UNSCOPED entries (`scope == null`) are candidates — a scoped entry
|
|
66
|
+
* (e.g. `weekly_scoped` for one model) does not bind dispatch generally. Among
|
|
67
|
+
* them the HIGHEST finite percent wins ("binding" = closest to blocking us);
|
|
68
|
+
* `is_active` only breaks ties, because the API sets it on group precedence
|
|
69
|
+
* and trusting it alone would pick weekly_all 64 over session 95. With no
|
|
70
|
+
* usable unscoped entry, falls back to the flat `five_hour`. Pure, no I/O.
|
|
57
71
|
*/
|
|
58
72
|
function bindingWindow(payload) {
|
|
59
|
-
if (payload && Array.isArray(payload.limits)
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
if (payload && Array.isArray(payload.limits)) {
|
|
74
|
+
let best = null;
|
|
75
|
+
let bestPct = -Infinity;
|
|
76
|
+
for (const l of payload.limits) {
|
|
77
|
+
if (!l || l.scope != null) continue;
|
|
78
|
+
const pct = entryPercent(l);
|
|
79
|
+
if (!Number.isFinite(pct)) continue;
|
|
80
|
+
if (pct > bestPct || (pct === bestPct && l.is_active === true && !(best && best.is_active === true))) {
|
|
81
|
+
best = l;
|
|
82
|
+
bestPct = pct;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (best) {
|
|
86
|
+
const flat = payload[FLAT_WINDOW_BY_KIND[best.kind]];
|
|
62
87
|
return {
|
|
63
|
-
name:
|
|
64
|
-
utilization:
|
|
65
|
-
resets_at:
|
|
88
|
+
name: best.kind || best.type || best.name || 'unknown',
|
|
89
|
+
utilization: bestPct,
|
|
90
|
+
resets_at: best.resets_at ?? (flat ? flat.resets_at ?? null : null),
|
|
66
91
|
};
|
|
67
92
|
}
|
|
68
93
|
}
|
|
@@ -88,15 +113,18 @@ function degradedConcurrencyCap(configuredCap) {
|
|
|
88
113
|
|
|
89
114
|
/**
|
|
90
115
|
* Conservative budget to run on while the meter is down. utilization is
|
|
91
|
-
* carried forward from the last known-good BINDING window
|
|
92
|
-
*
|
|
116
|
+
* carried forward from the last known-good BINDING window (a genuine 0% stays
|
|
117
|
+
* 0); with no payload ever received it is 100, never a blind 0 that would read
|
|
118
|
+
* as "plenty of headroom" instead of "we don't know." A fresh
|
|
93
119
|
* executor-observed 429 (its own window not yet passed) pins utilization to
|
|
94
120
|
* 100 regardless of the stale cached value. concurrencyCap never exceeds 2
|
|
95
121
|
* (or SM_USAGE_DEGRADED_CAP, if set).
|
|
96
122
|
*/
|
|
97
123
|
function degradedBudget(lastGoodPayload, executorEvidence = {}) {
|
|
124
|
+
// "Never a blind 0" means NO payload ever received -> 100. A payload whose
|
|
125
|
+
// binding window genuinely reads 0% carries forward as 0.
|
|
98
126
|
const window = bindingWindow(lastGoodPayload);
|
|
99
|
-
let utilization = Number.isFinite(window.utilization)
|
|
127
|
+
let utilization = lastGoodPayload && Number.isFinite(window.utilization)
|
|
100
128
|
? window.utilization
|
|
101
129
|
: 100;
|
|
102
130
|
|
package/src/main/runVerify.cjs
CHANGED
|
@@ -898,7 +898,9 @@ async function verifyRun({
|
|
|
898
898
|
scannedAt: new Date().toISOString(),
|
|
899
899
|
...(extras ?? {}),
|
|
900
900
|
};
|
|
901
|
-
|
|
901
|
+
// Atomic (tmp+rename) — scheduler.cjs reads this sidecar to decide completed-equivalence,
|
|
902
|
+
// so a torn write must never be observable. Lazy require keeps this module electron-free at load.
|
|
903
|
+
try { require('./config.cjs').writeJsonSync(verdictsPath, record); } catch { /* best-effort */ }
|
|
902
904
|
return { verdict, reason, downgradeTo, ...(extras ?? {}) };
|
|
903
905
|
}
|
|
904
906
|
|