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.
Files changed (78) hide show
  1. package/dist/assets/{DataModel-DDGx3qrn.js → DataModel-DZK9QZpV.js} +1 -1
  2. package/dist/assets/{History-CHBjYZyD.js → History-BDyJUGtR.js} +2 -2
  3. package/dist/assets/{Hooks-DG4c-Rzz.js → Hooks-CmK6l0uf.js} +3 -3
  4. package/dist/assets/HostBilko-D8TIx-Bx.js +1 -0
  5. package/dist/assets/{Library-CN1h5rAE.js → Library-DTxpOjx8.js} +25 -25
  6. package/dist/assets/{MarkdownEditor-DmBxrhdn.js → MarkdownEditor-BPJyrevq.js} +1 -1
  7. package/dist/assets/McpServers-B_rvIYDs.js +2 -0
  8. package/dist/assets/{Memory-Dk4uDCjg.js → Memory-B-9GlHqe.js} +6 -6
  9. package/dist/assets/{Permissions-BovobeZw.js → Permissions-DhMMp27n.js} +3 -3
  10. package/dist/assets/{Plugins-DvOf_B62.js → Plugins-DKfAlmMY.js} +2 -2
  11. package/dist/assets/{ProvenanceBadge-Dutz_4OY.js → ProvenanceBadge-ptlItdCK.js} +1 -1
  12. package/dist/assets/{SaveBar-CT5u6ZCJ.js → SaveBar-CSbNSl2W.js} +1 -1
  13. package/dist/assets/Scheduler--rn1QBYc.js +16 -0
  14. package/dist/assets/{ScopeSwitcher-BUYDpz2v.js → ScopeSwitcher-BJof-GWH.js} +1 -1
  15. package/dist/assets/{Settings-DIRmr3tL.js → Settings-Dsx6XZP5.js} +3 -3
  16. package/dist/assets/{SkillReferenceGraph-CdGr6d3_.js → SkillReferenceGraph-CGl4B89-.js} +2 -2
  17. package/dist/assets/{Skills-BQOheQv4.js → Skills-D522Zfol.js} +2 -2
  18. package/dist/assets/{SystemPrompt-5LsimKbX.js → SystemPrompt-DKj-q39J.js} +1 -1
  19. package/dist/assets/{TagLibrary-DreLsT3n.js → TagLibrary-BH7CG7-6.js} +1 -1
  20. package/dist/assets/{TiptapBody-C9kmlBnI.js → TiptapBody-BbcJL9az.js} +1 -1
  21. package/dist/assets/{Toggle-BKLCBu9A.js → Toggle-DAslvnnv.js} +1 -1
  22. package/dist/assets/{index-Db_SK9rW.js → index-Yf8e7VCB.js} +821 -821
  23. package/dist/assets/{settingsSchema-B5jBhSUP.js → settingsSchema-pzp0TgvN.js} +1 -1
  24. package/dist/index.html +1 -1
  25. package/package.json +8 -3
  26. package/scripts/README.md +6 -0
  27. package/src/main/__tests__/chat-cancel-terminal.test.cjs +5 -2
  28. package/src/main/__tests__/chat-exit-close-race.test.cjs +8 -2
  29. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +5 -2
  30. package/src/main/__tests__/health-tick-liveness.test.cjs +12 -0
  31. package/src/main/__tests__/intradayRefresh.test.cjs +39 -0
  32. package/src/main/__tests__/openExternalApp-spawn-error.test.cjs +25 -0
  33. package/src/main/__tests__/opsErrorLog.test.cjs +22 -0
  34. package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +21 -0
  35. package/src/main/__tests__/rateLimitPollerStreak.test.cjs +14 -0
  36. package/src/main/__tests__/runVerify-atomic-verdicts.test.cjs +26 -0
  37. package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +11 -1
  38. package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +11 -1
  39. package/src/main/__tests__/runVerify.test.cjs +12 -1
  40. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +15 -0
  41. package/src/main/__tests__/scheduler-utilization-hold.test.cjs +89 -0
  42. package/src/main/__tests__/transcriptsUsageFor.test.cjs +112 -1
  43. package/src/main/build-info.json +4 -4
  44. package/src/main/chatRunner.cjs +45 -10
  45. package/src/main/crashDiagnostics.cjs +9 -0
  46. package/src/main/git.cjs +9 -2
  47. package/src/main/health.cjs +25 -1
  48. package/src/main/historyAggregator.cjs +2 -19
  49. package/src/main/index.cjs +7 -8
  50. package/src/main/ipcSchemas.cjs +13 -3
  51. package/src/main/lib/__tests__/childWithLog.test.cjs +180 -0
  52. package/src/main/lib/__tests__/delegationReadiness.test.cjs +19 -0
  53. package/src/main/lib/__tests__/gitCacheBound.test.cjs +69 -0
  54. package/src/main/lib/__tests__/loopDelay.test.cjs +68 -0
  55. package/src/main/lib/__tests__/usageCircuit.test.cjs +86 -17
  56. package/src/main/lib/agentPersonaSchema.cjs +2 -0
  57. package/src/main/lib/childWithLog.cjs +92 -51
  58. package/src/main/lib/delegationReadiness.cjs +3 -1
  59. package/src/main/lib/epicSpawnPlan.cjs +27 -8
  60. package/src/main/lib/epicTranscriptPath.cjs +5 -1
  61. package/src/main/lib/headTailBuffer.cjs +43 -0
  62. package/src/main/lib/intradayRefresh.cjs +33 -0
  63. package/src/main/lib/loopDelay.cjs +44 -0
  64. package/src/main/lib/lruCache.cjs +39 -0
  65. package/src/main/lib/openExternalApp.cjs +27 -9
  66. package/src/main/lib/opsErrorLog.cjs +22 -0
  67. package/src/main/lib/promptSessionSchema.cjs +2 -0
  68. package/src/main/lib/rendererRecovery.cjs +141 -0
  69. package/src/main/lib/scheduleJobSchema.cjs +3 -0
  70. package/src/main/lib/usageCircuit.cjs +40 -12
  71. package/src/main/runVerify.cjs +3 -1
  72. package/src/main/scheduler.cjs +409 -260
  73. package/src/main/transcripts.cjs +54 -11
  74. package/src/main/usage.cjs +9 -1
  75. package/src/preload/api.d.ts +20 -3
  76. package/dist/assets/HostBilko-DHXcpV2c.js +0 -1
  77. package/dist/assets/McpServers-Beb_HD6I.js +0 -2
  78. 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. Prefers `payload.limits[]`'s
55
- * `is_active` entry (the richer per-window shape); falls back to the flat
56
- * `five_hour` field when `limits[]` is absent, matching usage.cjs's shape.
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) && payload.limits.length) {
60
- const active = payload.limits.find((l) => l && l.is_active);
61
- if (active) {
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: active.type || active.name || 'unknown',
64
- utilization: active.utilization,
65
- resets_at: active.resets_at ?? null,
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 — never 0, which
92
- * would read as "plenty of headroom" instead of "we don't know." A fresh
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) && window.utilization > 0
127
+ let utilization = lastGoodPayload && Number.isFinite(window.utilization)
100
128
  ? window.utilization
101
129
  : 100;
102
130
 
@@ -898,7 +898,9 @@ async function verifyRun({
898
898
  scannedAt: new Date().toISOString(),
899
899
  ...(extras ?? {}),
900
900
  };
901
- try { fs.writeFileSync(verdictsPath, JSON.stringify(record, null, 2)); } catch { /* best-effort */ }
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