claude-code-session-manager 0.85.0 → 0.86.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 (52) hide show
  1. package/dist/assets/{AgentLibrary-Bkv-HcP1.js → AgentLibrary-DTFL7y8G.js} +1 -1
  2. package/dist/assets/{DataModel-DRH-Ty20.js → DataModel-Q4jhl24R.js} +1 -1
  3. package/dist/assets/{History-CfRhT1Im.js → History-Cj2FejEo.js} +1 -1
  4. package/dist/assets/{Hooks-wEmh_U6c.js → Hooks-CaelQI6t.js} +1 -1
  5. package/dist/assets/{HostBilko-D_t7Rbi7.js → HostBilko--v7cMR8I.js} +1 -1
  6. package/dist/assets/{Library-CpArQ-OJ.js → Library-DgI9oCCZ.js} +1 -1
  7. package/dist/assets/{ListDetail-pjaKYs84.js → ListDetail-DYUZN-x-.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-Xc141kjj.js → MarkdownEditor-DCIubYWf.js} +1 -1
  9. package/dist/assets/{McpServers-ftqaV3kn.js → McpServers-ypCYURh3.js} +1 -1
  10. package/dist/assets/{Memory-ChMWkNd0.js → Memory-C2qYp-3M.js} +1 -1
  11. package/dist/assets/{Panel-D9Kr40Ai.js → Panel-Cj2kw-Zv.js} +1 -1
  12. package/dist/assets/{Permissions-DKoNVgzj.js → Permissions-BiYZNGYW.js} +1 -1
  13. package/dist/assets/{Plugins-BtChISho.js → Plugins-C1Vj8_dU.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-DBA5EcYy.js → ProvenanceBadge-DczPNM5U.js} +1 -1
  15. package/dist/assets/{SaveBar-I0_dWNTX.js → SaveBar-Cd_7U6Gb.js} +1 -1
  16. package/dist/assets/{Scheduler-CbES7MC8.js → Scheduler-DcLBiJBq.js} +1 -1
  17. package/dist/assets/{ScopeSwitcher-5GTEveb2.js → ScopeSwitcher-DVSyI44-.js} +1 -1
  18. package/dist/assets/{Settings-BX3FElXk.js → Settings-Cv-pRyms.js} +1 -1
  19. package/dist/assets/{SkillReferenceGraph-DNBFGrYE.js → SkillReferenceGraph-CUv1_Q2c.js} +1 -1
  20. package/dist/assets/{Skills-DJB6-bBM.js → Skills-C_YHkAy-.js} +1 -1
  21. package/dist/assets/{SystemPrompt-BiDDrJUA.js → SystemPrompt-B8R7T9xn.js} +1 -1
  22. package/dist/assets/{TagLibrary-_Wrevtop.js → TagLibrary-dj9YHWyy.js} +1 -1
  23. package/dist/assets/{TiptapBody-OWWXdLRy.js → TiptapBody-DnSBUjHE.js} +1 -1
  24. package/dist/assets/{Toggle-B122N0HL.js → Toggle-CjV_BJn6.js} +1 -1
  25. package/dist/assets/{index-DhvuQL4C.js → index-CXFQIPhO.js} +3 -3
  26. package/dist/assets/{settingsSchema-sGoCTd7J.js → settingsSchema-BJVciriw.js} +1 -1
  27. package/dist/index.html +1 -1
  28. package/package.json +3 -2
  29. package/src/main/__tests__/health-starve-escalation.test.cjs +94 -0
  30. package/src/main/__tests__/loadGateDetailTick.test.cjs +31 -0
  31. package/src/main/__tests__/machineProfile.test.cjs +19 -1
  32. package/src/main/__tests__/pty-session-open-telemetry.test.cjs +96 -0
  33. package/src/main/__tests__/queue-starvation-per-project.test.cjs +135 -0
  34. package/src/main/__tests__/scheduler-failed-autoreset.test.cjs +121 -0
  35. package/src/main/__tests__/scheduler-needs-review-autoresolve.test.cjs +189 -0
  36. package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +152 -0
  37. package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +165 -0
  38. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +144 -0
  39. package/src/main/__tests__/telemetryClient.test.cjs +75 -5
  40. package/src/main/__tests__/telemetrySettings.test.cjs +32 -0
  41. package/src/main/health.cjs +75 -2
  42. package/src/main/lib/__tests__/loadGate.test.cjs +103 -2
  43. package/src/main/lib/__tests__/telemetryBoot.test.cjs +11 -0
  44. package/src/main/lib/loadGate.cjs +23 -1
  45. package/src/main/lib/machineProfile.cjs +15 -0
  46. package/src/main/lib/schedulerBatch.cjs +12 -1
  47. package/src/main/lib/schedulerConfig.cjs +13 -0
  48. package/src/main/lib/telemetryBoot.cjs +11 -8
  49. package/src/main/lib/telemetryClient.cjs +44 -2
  50. package/src/main/lib/telemetrySettings.cjs +20 -3
  51. package/src/main/pty.cjs +9 -0
  52. package/src/main/scheduler.cjs +600 -66
@@ -81,6 +81,19 @@ module.exports = {
81
81
  // 1086). Escalation only — warn + audit, never dispatch.
82
82
  STARVATION_ESCALATE_MS: 45 * 60_000,
83
83
 
84
+ // A project still meeting findStarvedProjects' STARVED verdict past THIS
85
+ // (later) threshold gets a bounded, automated consequence beyond the
86
+ // repeating project_starved WARN above: a one-shot 'project_starve_escalated'
87
+ // audit event + a user-facing toast naming the project and the last tick's
88
+ // hold reason (scheduler.cjs's `lastTick`). Escalation only — never
89
+ // auto-resets, auto-cancels, or bypasses a gate; the queue's own state is
90
+ // untouched. The 2026-09-12 audit log showed /home/bilko/Projects/Bilko
91
+ // emit project_starved for 19h straight (ageMs 60.5M → 68.9M) with zero
92
+ // consequence and no toast — this is the fix. Latched per-cwd so the SAME
93
+ // starve stretch escalates exactly once; the latch resets the moment that
94
+ // cwd stops appearing in findStarvedProjects.
95
+ STARVE_ESCALATION_MS: 60 * 60_000,
96
+
84
97
  // A RUNNING job that has overrun its own PRD's `estimateMinutes` by this
85
98
  // factor is escalated. Distinct from MAX_JOB_DURATION_MS (4h), which is a
86
99
  // deadman kill: a 20-minute PRD still running at 3h is 9x over estimate but
@@ -38,8 +38,10 @@ function resolveDeps(deps = {}) {
38
38
  * signal that also gates the machine-profile heartbeat below).
39
39
  * 3. install upsert — sent once via telemetryClient.reportInstall(profile)
40
40
  * when telemetrySettings.isMachineReportDue() is true (a version bump, OR
41
- * the 30-day liveness heartbeat), then lastMachineReportAt/Version are
42
- * persisted so a same-version reboot within the window sends nothing.
41
+ * the 30-day liveness heartbeat). lastMachineReportAt/Version are only
42
+ * persisted when reportInstall() reports success — a failed/dropped
43
+ * upsert stays due so the next boot retries, rather than being marked
44
+ * done regardless of outcome.
43
45
  * This lands in bilko.run's app_installs table (the sole source for
44
46
  * every install-shaped number on the site). It replaces the former
45
47
  * track('install.machine', profile) call, which duplicated the same
@@ -62,12 +64,13 @@ async function bootSequence({ now = Date.now(), appVersion, installChannel, deps
62
64
 
63
65
  if (telemetrySettings.isMachineReportDue(settings, { now, appVersion })) {
64
66
  const profile = await buildMachineProfile();
65
- await telemetryClient.reportInstall(profile);
66
- await telemetrySettings.save({
67
- ...settings,
68
- lastMachineReportAt: new Date(now).toISOString(),
69
- lastMachineReportVersion: appVersion,
70
- });
67
+ const reportResult = await telemetryClient.reportInstall(profile);
68
+ if (reportResult && reportResult.accepted) {
69
+ await telemetrySettings.save({
70
+ lastMachineReportAt: new Date(now).toISOString(),
71
+ lastMachineReportVersion: appVersion,
72
+ });
73
+ }
71
74
  }
72
75
 
73
76
  telemetryCounters.trackAppLaunch({ installChannel, appVersion });
@@ -91,8 +91,24 @@ const S = {
91
91
  lastError: null,
92
92
  lastFlushAt: null,
93
93
  lastFlushReason: null,
94
+ logger: null,
94
95
  };
95
96
 
97
+ /**
98
+ * Best-effort warn logger for failures this module would otherwise swallow
99
+ * (e.g. reportInstall()'s bare catch). Lazily requires logs.cjs — which
100
+ * requires('electron') at module scope — so this stays a no-op under plain
101
+ * vitest with no Electron runtime, exactly like machineProfile.cjs's
102
+ * resolveElectronApp(). Injectable via _setLogger so tests can observe it
103
+ * without an Electron runtime.
104
+ */
105
+ function logWarn(message, meta) {
106
+ try {
107
+ const fn = S.logger || require('../logs.cjs').writeLine;
108
+ fn({ scope: 'telemetry', level: 'warn', message, meta });
109
+ } catch { /* logs unavailable outside an Electron runtime */ }
110
+ }
111
+
96
112
  /**
97
113
  * SM_TELEMETRY_SPOOL, when set, overrides the spool directory in place of
98
114
  * `~/.config/session-manager` — the explicit opt-in a test that genuinely
@@ -130,6 +146,18 @@ function getBeaconToken() {
130
146
  return process.env.SM_BEACON_KEY || FALLBACK_BEACON_KEY;
131
147
  }
132
148
 
149
+ /**
150
+ * The wire-level `env` discriminator for the currently-running process,
151
+ * resolved from real signals (never from an appVersion string — see
152
+ * machineProfile.cjs's resolveEnv header comment for why that matters).
153
+ */
154
+ function currentEnv() {
155
+ return machineProfile.resolveEnv({
156
+ isTestRunner: isTestEnvironment(),
157
+ installChannel: S.profile && S.profile.installChannel,
158
+ });
159
+ }
160
+
133
161
  // ─── safe coercion helpers (never throw, regardless of input shape) ───────
134
162
 
135
163
  function safeObj(v) {
@@ -436,6 +464,7 @@ async function track(name, props) {
436
464
  props: meta,
437
465
  session_id: S.sessionId,
438
466
  visitor_id: S.settings.installId,
467
+ env: currentEnv(),
439
468
  };
440
469
  return await appendRecord('event', wire, recordId);
441
470
  } catch {
@@ -465,6 +494,7 @@ async function logLine(opts) {
465
494
  session_id: S.sessionId,
466
495
  fields: meta,
467
496
  ts: Date.now(),
497
+ env: currentEnv(),
468
498
  };
469
499
  return await appendRecord('log', wire, recordId);
470
500
  } catch {
@@ -501,6 +531,7 @@ async function reportError(opts) {
501
531
  session_id: S.sessionId,
502
532
  context: meta,
503
533
  ts: Date.now(),
534
+ env: currentEnv(),
504
535
  };
505
536
  return await appendRecord('error', wire, recordId);
506
537
  } catch {
@@ -527,6 +558,11 @@ function buildInstallWire(profile, installId) {
527
558
  // the numeric UTC offset (see machineProfile.cjs) — so that is what maps
528
559
  // onto the server's `timezone` column.
529
560
  timezone: safeStr(p.timezoneOffsetMinutes, 20),
561
+ // Resolved from this passed-in profile's own installChannel (never
562
+ // S.profile — reportInstall() is always given a freshly-built profile,
563
+ // not the module-singleton one) plus the current process's test-runner
564
+ // signal, same three-value contract as the other three channels.
565
+ env: machineProfile.resolveEnv({ isTestRunner: isTestEnvironment(), installChannel: p.installChannel }),
530
566
  };
531
567
  }
532
568
 
@@ -544,7 +580,8 @@ async function reportInstall(profile) {
544
580
  const recordId = crypto.randomUUID();
545
581
  const wire = buildInstallWire(profile, S.settings.installId);
546
582
  return await appendRecord('install', wire, recordId);
547
- } catch {
583
+ } catch (e) {
584
+ logWarn('reportInstall failed', { error: e && e.message ? e.message : String(e) });
548
585
  return { accepted: false, reason: 'error' };
549
586
  }
550
587
  }
@@ -684,7 +721,7 @@ async function flushImpl(reason) {
684
721
  await persistSent();
685
722
 
686
723
  if (safeReason === 'daily' && result.failed.length === 0) {
687
- S.settings = await telemetrySettings.save({ ...S.settings, lastDailyFlushAt: new Date().toISOString() });
724
+ S.settings = await telemetrySettings.save({ lastDailyFlushAt: new Date().toISOString() });
688
725
  }
689
726
  } catch { /* fail-inert */ }
690
727
  S.lastFlushAt = Date.now();
@@ -749,6 +786,10 @@ function _setMachineProfileBuilder(fn) {
749
786
  S.profileBuilder = typeof fn === 'function' ? fn : machineProfile.buildMachineProfile;
750
787
  }
751
788
 
789
+ function _setLogger(fn) {
790
+ S.logger = typeof fn === 'function' ? fn : null;
791
+ }
792
+
752
793
  module.exports = {
753
794
  track,
754
795
  logLine,
@@ -764,4 +805,5 @@ module.exports = {
764
805
  sentPath,
765
806
  _setFetchImpl,
766
807
  _setMachineProfileBuilder,
808
+ _setLogger,
767
809
  };
@@ -78,11 +78,28 @@ function normalize(cfg) {
78
78
  };
79
79
  }
80
80
 
81
+ /**
82
+ * Persists `patch` merged onto a FRESHLY-READ copy of the on-disk config —
83
+ * never onto a caller-held snapshot. Two independent modules (telemetryBoot's
84
+ * install-report stamp, telemetryClient's own daily-flush stamp) each keep
85
+ * their own in-memory copy of settings loaded at different times; if save()
86
+ * blindly wrote a caller's full snapshot, whichever call landed last would
87
+ * silently revert every field the OTHER caller had just written (a lost
88
+ * update — the root cause behind lastMachineReportAt never sticking). Calls
89
+ * are still serialized through the existing writeQueue chain, so the fresh
90
+ * read for call N+1 always observes call N's write.
91
+ */
81
92
  let writeQueue = Promise.resolve();
82
- async function save(cfg) {
83
- if (!isValid(cfg)) throw new Error('Invalid telemetry config');
84
- const next = normalize(cfg);
93
+ async function save(patch) {
94
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch)) throw new Error('Invalid telemetry config');
95
+ for (const k of Object.keys(patch)) {
96
+ if (!KNOWN_KEYS.has(k)) throw new Error('Invalid telemetry config');
97
+ }
85
98
  const run = async () => {
99
+ const current = await readRaw();
100
+ const merged = { ...current, ...patch };
101
+ if (!isValid(merged)) throw new Error('Invalid telemetry config');
102
+ const next = normalize(merged);
86
103
  await config.writeTextAtomic(storePath(), JSON.stringify(next, null, 2) + '\n', { mode: 0o600 });
87
104
  return next;
88
105
  };
package/src/main/pty.cjs CHANGED
@@ -109,6 +109,15 @@ class PtyManager {
109
109
  // renderer will re-register its data/exit listeners on the same IPC
110
110
  // channels. The data stream is live; pre-reattach output is lost, which
111
111
  // is acceptable for a dev reload.
112
+ //
113
+ // Telemetry: this branch deliberately does NOT call
114
+ // telemetryCounters.trackSessionOpen() (see below, after the real spawn).
115
+ // Tab = claudeSessionId is a 1:1 mapping (CLAUDE.md domain model) — a
116
+ // reattach is the SAME session process still running, not a new one, so
117
+ // counting it here would double-count every renderer reload and every
118
+ // switch back to an already-open Epic's Terminal pane within the same
119
+ // Electron process. `session.open` counts fresh PTY spawns only; see
120
+ // telemetry.md for the full rationale and its trade-off.
112
121
  const existing = this.sessions.get(tabId);
113
122
  if (existing) {
114
123
  console.log('[pty] reattach to existing session tabId=', tabId, 'pid=', existing.proc.pid);