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.
- package/dist/assets/{AgentLibrary-Bkv-HcP1.js → AgentLibrary-DTFL7y8G.js} +1 -1
- package/dist/assets/{DataModel-DRH-Ty20.js → DataModel-Q4jhl24R.js} +1 -1
- package/dist/assets/{History-CfRhT1Im.js → History-Cj2FejEo.js} +1 -1
- package/dist/assets/{Hooks-wEmh_U6c.js → Hooks-CaelQI6t.js} +1 -1
- package/dist/assets/{HostBilko-D_t7Rbi7.js → HostBilko--v7cMR8I.js} +1 -1
- package/dist/assets/{Library-CpArQ-OJ.js → Library-DgI9oCCZ.js} +1 -1
- package/dist/assets/{ListDetail-pjaKYs84.js → ListDetail-DYUZN-x-.js} +1 -1
- package/dist/assets/{MarkdownEditor-Xc141kjj.js → MarkdownEditor-DCIubYWf.js} +1 -1
- package/dist/assets/{McpServers-ftqaV3kn.js → McpServers-ypCYURh3.js} +1 -1
- package/dist/assets/{Memory-ChMWkNd0.js → Memory-C2qYp-3M.js} +1 -1
- package/dist/assets/{Panel-D9Kr40Ai.js → Panel-Cj2kw-Zv.js} +1 -1
- package/dist/assets/{Permissions-DKoNVgzj.js → Permissions-BiYZNGYW.js} +1 -1
- package/dist/assets/{Plugins-BtChISho.js → Plugins-C1Vj8_dU.js} +2 -2
- package/dist/assets/{ProvenanceBadge-DBA5EcYy.js → ProvenanceBadge-DczPNM5U.js} +1 -1
- package/dist/assets/{SaveBar-I0_dWNTX.js → SaveBar-Cd_7U6Gb.js} +1 -1
- package/dist/assets/{Scheduler-CbES7MC8.js → Scheduler-DcLBiJBq.js} +1 -1
- package/dist/assets/{ScopeSwitcher-5GTEveb2.js → ScopeSwitcher-DVSyI44-.js} +1 -1
- package/dist/assets/{Settings-BX3FElXk.js → Settings-Cv-pRyms.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-DNBFGrYE.js → SkillReferenceGraph-CUv1_Q2c.js} +1 -1
- package/dist/assets/{Skills-DJB6-bBM.js → Skills-C_YHkAy-.js} +1 -1
- package/dist/assets/{SystemPrompt-BiDDrJUA.js → SystemPrompt-B8R7T9xn.js} +1 -1
- package/dist/assets/{TagLibrary-_Wrevtop.js → TagLibrary-dj9YHWyy.js} +1 -1
- package/dist/assets/{TiptapBody-OWWXdLRy.js → TiptapBody-DnSBUjHE.js} +1 -1
- package/dist/assets/{Toggle-B122N0HL.js → Toggle-CjV_BJn6.js} +1 -1
- package/dist/assets/{index-DhvuQL4C.js → index-CXFQIPhO.js} +3 -3
- package/dist/assets/{settingsSchema-sGoCTd7J.js → settingsSchema-BJVciriw.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +3 -2
- package/src/main/__tests__/health-starve-escalation.test.cjs +94 -0
- package/src/main/__tests__/loadGateDetailTick.test.cjs +31 -0
- package/src/main/__tests__/machineProfile.test.cjs +19 -1
- package/src/main/__tests__/pty-session-open-telemetry.test.cjs +96 -0
- package/src/main/__tests__/queue-starvation-per-project.test.cjs +135 -0
- package/src/main/__tests__/scheduler-failed-autoreset.test.cjs +121 -0
- package/src/main/__tests__/scheduler-needs-review-autoresolve.test.cjs +189 -0
- package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +152 -0
- package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +165 -0
- package/src/main/__tests__/scheduler-starve-escalation.test.cjs +144 -0
- package/src/main/__tests__/telemetryClient.test.cjs +75 -5
- package/src/main/__tests__/telemetrySettings.test.cjs +32 -0
- package/src/main/health.cjs +75 -2
- package/src/main/lib/__tests__/loadGate.test.cjs +103 -2
- package/src/main/lib/__tests__/telemetryBoot.test.cjs +11 -0
- package/src/main/lib/loadGate.cjs +23 -1
- package/src/main/lib/machineProfile.cjs +15 -0
- package/src/main/lib/schedulerBatch.cjs +12 -1
- package/src/main/lib/schedulerConfig.cjs +13 -0
- package/src/main/lib/telemetryBoot.cjs +11 -8
- package/src/main/lib/telemetryClient.cjs +44 -2
- package/src/main/lib/telemetrySettings.cjs +20 -3
- package/src/main/pty.cjs +9 -0
- 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)
|
|
42
|
-
* persisted
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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({
|
|
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(
|
|
83
|
-
if (!
|
|
84
|
-
const
|
|
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);
|