claude-code-session-manager 0.83.0 → 0.85.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-B-OM-4gk.js → AgentLibrary-Bkv-HcP1.js} +1 -1
- package/dist/assets/{DataModel-BC6ppcBr.js → DataModel-DRH-Ty20.js} +1 -1
- package/dist/assets/{History-D3Qw2BWn.js → History-CfRhT1Im.js} +2 -2
- package/dist/assets/{Hooks-OxBQ0nIc.js → Hooks-wEmh_U6c.js} +1 -1
- package/dist/assets/{HostBilko-BNFC6bNZ.js → HostBilko-D_t7Rbi7.js} +1 -1
- package/dist/assets/{Library-DAMyrUtY.js → Library-CpArQ-OJ.js} +1 -1
- package/dist/assets/{ListDetail-BwywrpvE.js → ListDetail-pjaKYs84.js} +1 -1
- package/dist/assets/{MarkdownEditor-HEwjN1bp.js → MarkdownEditor-Xc141kjj.js} +1 -1
- package/dist/assets/{McpServers-D1VZF7Ix.js → McpServers-ftqaV3kn.js} +1 -1
- package/dist/assets/{Memory-CgYJ_QqD.js → Memory-ChMWkNd0.js} +6 -6
- package/dist/assets/{Panel-dTNWC59Q.js → Panel-D9Kr40Ai.js} +1 -1
- package/dist/assets/{Permissions-qI_NKi1b.js → Permissions-DKoNVgzj.js} +1 -1
- package/dist/assets/{Plugins-DZ0_CHiF.js → Plugins-BtChISho.js} +2 -2
- package/dist/assets/{ProvenanceBadge-B2i_uxCu.js → ProvenanceBadge-DBA5EcYy.js} +1 -1
- package/dist/assets/{SaveBar-4tNMCjUd.js → SaveBar-I0_dWNTX.js} +1 -1
- package/dist/assets/{Scheduler-CUen18Gc.js → Scheduler-CbES7MC8.js} +7 -7
- package/dist/assets/{ScopeSwitcher-Co3tkLTT.js → ScopeSwitcher-5GTEveb2.js} +1 -1
- package/dist/assets/Settings-BX3FElXk.js +3 -0
- package/dist/assets/{SkillReferenceGraph-BKcHtMPc.js → SkillReferenceGraph-DNBFGrYE.js} +1 -1
- package/dist/assets/{Skills-DHb4INtF.js → Skills-DJB6-bBM.js} +2 -2
- package/dist/assets/SystemPrompt-BiDDrJUA.js +1 -0
- package/dist/assets/{TagLibrary-xLP5dJvh.js → TagLibrary-_Wrevtop.js} +1 -1
- package/dist/assets/{TiptapBody-T-ULTjK4.js → TiptapBody-OWWXdLRy.js} +1 -1
- package/dist/assets/{Toggle-B-5M2hGe.js → Toggle-B122N0HL.js} +1 -1
- package/dist/assets/{index-CYhdtisq.css → index-CDo9xBR9.css} +1 -1
- package/dist/assets/{index-CzAxC432.js → index-DhvuQL4C.js} +316 -314
- package/dist/assets/{settingsSchema-C_nsemcF.js → settingsSchema-sGoCTd7J.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +4 -1
- package/scripts/hooks/guard-destructive-git.cjs +514 -0
- package/scripts/hooks/guard-inline-implementation.cjs +219 -0
- package/scripts/hooks/guard-prd-writes.cjs +200 -0
- package/src/main/__tests__/epicMintTelemetryTap.test.cjs +64 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +2 -1
- package/src/main/__tests__/health-queue-dispatch.test.cjs +84 -0
- package/src/main/__tests__/health-usage-poller.test.cjs +97 -0
- package/src/main/__tests__/health-worktree-cap-blocked.test.cjs +65 -0
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +149 -0
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +120 -0
- package/src/main/__tests__/promptSessionTranscript.test.cjs +0 -0
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +143 -0
- package/src/main/__tests__/rateLimitPollerStreak.test.cjs +79 -0
- package/src/main/__tests__/scheduleJobTransitionsTelemetryTap.test.cjs +72 -0
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +74 -0
- package/src/main/__tests__/scheduler-job-overrun.test.cjs +58 -0
- package/src/main/__tests__/scheduler-notify-originating-tab-transcript.test.cjs +1 -0
- package/src/main/__tests__/scheduler-periodic-reverify-guard.test.cjs +134 -2
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +33 -0
- package/src/main/__tests__/scheduler-stuck-failed-escalation.test.cjs +136 -0
- package/src/main/__tests__/telemetryClient.test.cjs +242 -1
- package/src/main/__tests__/telemetryContract.test.cjs +930 -0
- package/src/main/crashDiagnostics.cjs +29 -1
- package/src/main/health.cjs +197 -3
- package/src/main/index.cjs +65 -6
- package/src/main/ipcSchemas.cjs +19 -2
- package/src/main/lib/__tests__/crashTelemetry.test.cjs +103 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +302 -4
- package/src/main/lib/__tests__/fixtures/scheduler-machine.json.corrupt-1789147548 +34 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +413 -1
- package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +71 -0
- package/src/main/lib/__tests__/queueStoreAtomicWrite.test.cjs +88 -0
- package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +123 -0
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +58 -0
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +626 -0
- package/src/main/lib/__tests__/telemetryBoot.test.cjs +123 -0
- package/src/main/lib/__tests__/telemetryConsent.test.cjs +136 -0
- package/src/main/lib/__tests__/telemetryCounters.test.cjs +57 -0
- package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +95 -0
- package/src/main/lib/crashTelemetry.cjs +37 -0
- package/src/main/lib/delegationReadiness.cjs +119 -1
- package/src/main/lib/epicMint.cjs +2 -0
- package/src/main/lib/gitWorktree.cjs +427 -17
- package/src/main/lib/jobWorktree.cjs +2 -1
- package/src/main/lib/jobWorktreeBootLive.cjs +51 -0
- package/src/main/lib/jobWorktreeTerminalOrphanLive.cjs +68 -0
- package/src/main/lib/opsErrorLog.cjs +78 -25
- package/src/main/lib/queueStore.cjs +241 -22
- package/src/main/lib/reaperHelpers.cjs +23 -1
- package/src/main/lib/scheduleJobSchema.cjs +7 -0
- package/src/main/lib/scheduleJobTransitions.cjs +12 -0
- package/src/main/lib/telemetryBacklog.cjs +601 -0
- package/src/main/lib/telemetryBoot.cjs +76 -0
- package/src/main/lib/telemetryClient.cjs +150 -5
- package/src/main/lib/telemetryConsent.cjs +34 -0
- package/src/main/lib/telemetryCounters.cjs +45 -0
- package/src/main/promptSessionTranscript.cjs +0 -0
- package/src/main/pty.cjs +2 -0
- package/src/main/scheduler.cjs +481 -33
- package/src/preload/api.d.ts +84 -4
- package/src/preload/index.cjs +9 -0
- package/dist/assets/Settings-DXVgKLUx.js +0 -3
- package/dist/assets/SystemPrompt-BjqFOHSk.js +0 -1
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
const { app, powerSaveBlocker, powerMonitor, crashReporter } = require('electron');
|
|
27
27
|
const fs = require('node:fs');
|
|
28
28
|
const path = require('node:path');
|
|
29
|
+
const { reportCrash } = require('./lib/crashTelemetry.cjs');
|
|
29
30
|
|
|
30
31
|
let logs = null;
|
|
31
32
|
let getPowerBlockerId = () => -1;
|
|
@@ -139,6 +140,7 @@ function checkPreviousRun() {
|
|
|
139
140
|
try { prev = JSON.parse(fs.readFileSync(sentinelPath, 'utf8')); } catch { return; }
|
|
140
141
|
if (!prev || prev.open !== true) return;
|
|
141
142
|
const s = prev.lastSample;
|
|
143
|
+
const lastTop = s && Array.isArray(s.procs) ? [...s.procs].sort((a, b) => b.mb - a.mb).slice(0, 4).map((p) => `${p.type}:${p.mb}MB`) : undefined;
|
|
142
144
|
logs?.writeLine({
|
|
143
145
|
scope: 'crash-diag',
|
|
144
146
|
level: 'error',
|
|
@@ -149,11 +151,24 @@ function checkPreviousRun() {
|
|
|
149
151
|
lastSampleTs: s?.ts,
|
|
150
152
|
lastTotalMb: s?.totalMb,
|
|
151
153
|
lastMainRssMb: s?.main?.rssMb,
|
|
152
|
-
lastTop
|
|
154
|
+
lastTop,
|
|
153
155
|
powerBlockerHeldAtDeath: s?.powerBlockerHeld,
|
|
154
156
|
hint: 'Check `journalctl -k | grep -i oom` around lastSampleTs to confirm the kernel OOM-killer.',
|
|
155
157
|
},
|
|
156
158
|
});
|
|
159
|
+
// The unclean-shutdown postmortem is the report most likely to be the ONLY
|
|
160
|
+
// thing ever received from a machine that just OOM-killed the app — the
|
|
161
|
+
// dying process couldn't write it itself. See crashTelemetry.cjs header.
|
|
162
|
+
reportCrash({
|
|
163
|
+
reason: 'unclean-shutdown',
|
|
164
|
+
meta: {
|
|
165
|
+
lastSampleTs: s?.ts,
|
|
166
|
+
lastTotalMb: s?.totalMb,
|
|
167
|
+
lastMainRssMb: s?.main?.rssMb,
|
|
168
|
+
lastTop,
|
|
169
|
+
powerBlockerHeldAtDeath: s?.powerBlockerHeld,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
157
172
|
}
|
|
158
173
|
|
|
159
174
|
function registerCrashHooks() {
|
|
@@ -167,6 +182,10 @@ function registerCrashHooks() {
|
|
|
167
182
|
message: 'render-process-gone',
|
|
168
183
|
meta: { reason: details?.reason, exitCode: details?.exitCode, lastTotalMb: lastSample?.totalMb },
|
|
169
184
|
});
|
|
185
|
+
reportCrash({
|
|
186
|
+
reason: details?.reason || 'unknown',
|
|
187
|
+
meta: { type: 'render', exitCode: details?.exitCode, lastTotalMb: lastSample?.totalMb },
|
|
188
|
+
});
|
|
170
189
|
});
|
|
171
190
|
|
|
172
191
|
// GPU / Utility / Pepper / network-service child death.
|
|
@@ -183,6 +202,15 @@ function registerCrashHooks() {
|
|
|
183
202
|
lastTotalMb: lastSample?.totalMb,
|
|
184
203
|
},
|
|
185
204
|
});
|
|
205
|
+
reportCrash({
|
|
206
|
+
reason: details?.reason || 'unknown',
|
|
207
|
+
meta: {
|
|
208
|
+
type: details?.type,
|
|
209
|
+
name: details?.name || details?.serviceName,
|
|
210
|
+
exitCode: details?.exitCode,
|
|
211
|
+
lastTotalMb: lastSample?.totalMb,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
186
214
|
});
|
|
187
215
|
|
|
188
216
|
// powerMonitor ties the suspend story to the crash timeline. If we ever log
|
package/src/main/health.cjs
CHANGED
|
@@ -16,7 +16,7 @@ const { checkDelegationReadiness } = require('./lib/delegationReadiness.cjs');
|
|
|
16
16
|
const { resolvePrdsDirs } = require('./lib/prdLocations.cjs');
|
|
17
17
|
const { migratePrds } = require('./lib/prdMigration.cjs');
|
|
18
18
|
const queueStore = require('./lib/queueStore.cjs');
|
|
19
|
-
const { computeStallSummary } = require('./scheduler.cjs');
|
|
19
|
+
const { computeStallSummary, SCHEDULER_STATE_PATH, FAILURE_STREAK_WARN_THRESHOLD, classifyQueueStarvation } = require('./scheduler.cjs');
|
|
20
20
|
const { DEFAULT_RUNS_DIR, computeReport, isRetentionEnabled, liveKeysFromJobs } = require('./lib/runLogRetention.cjs');
|
|
21
21
|
const { allProjectCwds } = require('../../scripts/lib/activeSessions.cjs');
|
|
22
22
|
|
|
@@ -154,6 +154,37 @@ function computeProjectProblemCounts(jobs) {
|
|
|
154
154
|
return byProject;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
// evaluateWorktreeCapBlocked(jobs, runningCount) → { ok, blocked, capBlockedSlugs? }
|
|
158
|
+
//
|
|
159
|
+
// The worktree cap (gitWorktree.cjs) can genuinely, correctly block every
|
|
160
|
+
// pending job at once — a real out-of-capacity condition, not a bug — but
|
|
161
|
+
// before this check existed that state was invisible to `npm run health`:
|
|
162
|
+
// nothing but a console.log line and a `heldReason` field on the row (see
|
|
163
|
+
// scheduler.cjs's spawnJob preflight). Rows in four separate project queues
|
|
164
|
+
// carried heldReason 'worktree cap reached (5 concurrent)' for 16 hours with
|
|
165
|
+
// zero entries in session-manager-operations/logs/ and nothing non-GREEN
|
|
166
|
+
// here. `runningCount === 0` is the load-bearing condition: while at least
|
|
167
|
+
// one job is running, the cap is doing its job as designed (bounding
|
|
168
|
+
// concurrency), not starving the queue.
|
|
169
|
+
function evaluateWorktreeCapBlocked(jobs, runningCount) {
|
|
170
|
+
// queueStore.readMergedSync() hands back jobs as an object map (merged
|
|
171
|
+
// across projects); other callers (tests, scheduler.cjs's own in-memory
|
|
172
|
+
// state) pass a plain array — accept either rather than forcing every
|
|
173
|
+
// caller to know this module's federated-storage detail.
|
|
174
|
+
const jobList = Array.isArray(jobs) ? jobs : Object.values(jobs || {});
|
|
175
|
+
const capBlocked = jobList.filter(
|
|
176
|
+
(j) => j.status === 'pending' && /^worktree cap reached\b/.test(j.heldReason || '')
|
|
177
|
+
);
|
|
178
|
+
if ((runningCount ?? 0) === 0 && capBlocked.length > 0) {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
blocked: true,
|
|
182
|
+
capBlockedSlugs: capBlocked.map((j) => j.slug),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return { ok: true, blocked: false };
|
|
186
|
+
}
|
|
187
|
+
|
|
157
188
|
// evaluatePerProjectStall(stallSummary, lastRunAtIso, now, thresholdMs) →
|
|
158
189
|
// { [cwd]: { stalled, pastThreshold?, ageMs?, caveat? } }
|
|
159
190
|
//
|
|
@@ -335,6 +366,113 @@ function evaluateEpicIndexHealth(cwds) {
|
|
|
335
366
|
return { component: { ok: !anyOrphanRows, byProject }, issues };
|
|
336
367
|
}
|
|
337
368
|
|
|
369
|
+
// Pure evaluator over ~/.claude/session-manager/scheduler-state.json's parsed
|
|
370
|
+
// content (the sidecar scheduler.cjs's pollLoop maintains for the billing
|
|
371
|
+
// usage-meter / rate-limit-window poller — see rateLimitPollerStreak tests).
|
|
372
|
+
// `state` null/missing means the poller has never run yet (fresh install) —
|
|
373
|
+
// not a failure, matches evaluatePrdMigrationHealth's fail-open-on-absence
|
|
374
|
+
// spirit. Kept separate from the fs-touching check() call site, same pattern
|
|
375
|
+
// as every other evaluate* helper in this file.
|
|
376
|
+
function evaluateUsagePollerHealth(state, threshold = FAILURE_STREAK_WARN_THRESHOLD) {
|
|
377
|
+
if (!state || typeof state !== 'object') return { ok: true, applicable: false };
|
|
378
|
+
const consecutiveFailures = typeof state.consecutiveFailures === 'number' ? state.consecutiveFailures : 0;
|
|
379
|
+
const backoffMs = typeof state.backoffMs === 'number' ? state.backoffMs : null;
|
|
380
|
+
const lastPollAt = typeof state.lastPollAt === 'number' ? state.lastPollAt : null;
|
|
381
|
+
// Trips at the SAME count as scheduler.cjs's shouldWarnFailureStreak
|
|
382
|
+
// (`consecutiveFailures >= threshold`) — the WARN and this non-GREEN must
|
|
383
|
+
// fire together, not one failure apart, or an operator sees the log line
|
|
384
|
+
// while the health dashboard still reads green.
|
|
385
|
+
const ok = consecutiveFailures < threshold;
|
|
386
|
+
return {
|
|
387
|
+
ok,
|
|
388
|
+
applicable: true,
|
|
389
|
+
consecutiveFailures,
|
|
390
|
+
backoffMs,
|
|
391
|
+
lastPollAt,
|
|
392
|
+
threshold,
|
|
393
|
+
...(ok ? {} : {
|
|
394
|
+
message: `usage/rate-limit poller has ${consecutiveFailures} consecutive failures `
|
|
395
|
+
+ `(threshold ${threshold}), backoffMs=${backoffMs} — see logs scope=scheduler`,
|
|
396
|
+
}),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Reads + parses a scheduler-state.json-shaped sidecar at `statePath`,
|
|
401
|
+
// classifying the outcome into exactly one of three shapes so the caller
|
|
402
|
+
// never has to distinguish "absent" from "corrupt" itself:
|
|
403
|
+
// { missing: true } — ENOENT: never run yet, not a failure
|
|
404
|
+
// { errorMessage: string } — unreadable OR unparseable (corrupt)
|
|
405
|
+
// { state: object } — parsed successfully
|
|
406
|
+
// Takes an explicit path (rather than reading SCHEDULER_STATE_PATH directly)
|
|
407
|
+
// so it's unit-testable against a real temp file instead of mocking global
|
|
408
|
+
// fs or exercising the full (slow, machine-coupled) check().
|
|
409
|
+
// A DISPATCHABLE pending job (not merely pending) with 0 running and no
|
|
410
|
+
// dispatch attempt in this long is the exact shape of the 2026-09-11
|
|
411
|
+
// incident (27 pending across two projects, 0 running, for days, every
|
|
412
|
+
// other surface reporting healthy). Deliberately much longer than
|
|
413
|
+
// scheduler.cjs's own QUEUE_STARVATION_MS (10 min) — that's the watchdog's
|
|
414
|
+
// OWN forcing threshold; this is the "even the watchdog isn't helping
|
|
415
|
+
// anymore" signal a human needs to see.
|
|
416
|
+
const DISPATCH_STALL_THRESHOLD_MS = 2 * 60 * 60_000;
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* evaluateQueueDispatchHealth(queueState, runningCount, now, thresholdMs) →
|
|
420
|
+
* { ok, blocked?, starved?, pending?, dispatchable?, idleMs?, message? }
|
|
421
|
+
*
|
|
422
|
+
* Reuses classifyQueueStarvation's own verdict rather than re-deriving a
|
|
423
|
+
* second "is the queue stuck" heuristic — evaluateTickLiveness above already
|
|
424
|
+
* has one, but it does not distinguish a genuinely blocked dependsOn chain
|
|
425
|
+
* (which no amount of ticking can fix) from real dispatchable starvation, so
|
|
426
|
+
* reusing it here would report a dependency-blocked project as unhealthy
|
|
427
|
+
* dispatch even though nothing is actually wrong with dispatch itself. A
|
|
428
|
+
* `blocked` verdict therefore stays `ok: true` (never trips the health gate)
|
|
429
|
+
* but is still reported by name so it isn't silently invisible either.
|
|
430
|
+
*/
|
|
431
|
+
function evaluateQueueDispatchHealth(queueState, runningCount, now, thresholdMs = DISPATCH_STALL_THRESHOLD_MS) {
|
|
432
|
+
const verdict = classifyQueueStarvation({
|
|
433
|
+
jobs: queueState?.jobs,
|
|
434
|
+
paused: queueState?.paused,
|
|
435
|
+
runningCount,
|
|
436
|
+
lastRunAtMs: Date.parse(queueState?.lastDispatchAttemptAt ?? ''),
|
|
437
|
+
now,
|
|
438
|
+
thresholdMs,
|
|
439
|
+
});
|
|
440
|
+
if (!verdict) return { ok: true };
|
|
441
|
+
if (verdict.kind === 'blocked') {
|
|
442
|
+
return {
|
|
443
|
+
ok: true,
|
|
444
|
+
blocked: true,
|
|
445
|
+
pending: verdict.pending,
|
|
446
|
+
message: `${verdict.pending} pending job(s) behind a blocked dependsOn chain — dispatch cannot help, needs a human`,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const ageMin = Math.round(verdict.idleMs / 60_000);
|
|
450
|
+
return {
|
|
451
|
+
ok: false,
|
|
452
|
+
starved: true,
|
|
453
|
+
pending: verdict.pending,
|
|
454
|
+
dispatchable: verdict.dispatchable,
|
|
455
|
+
idleMs: verdict.idleMs,
|
|
456
|
+
message: `${verdict.dispatchable} dispatchable pending job(s), 0 running, no dispatch attempt in ~${ageMin}m `
|
|
457
|
+
+ `(threshold ${Math.round(thresholdMs / 60_000)}m) — dispatch appears stuck`,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function loadUsagePollerState(statePath) {
|
|
462
|
+
let raw;
|
|
463
|
+
try {
|
|
464
|
+
raw = fs.readFileSync(statePath, 'utf8');
|
|
465
|
+
} catch (e) {
|
|
466
|
+
if (e.code === 'ENOENT') return { missing: true };
|
|
467
|
+
return { errorMessage: `cannot read scheduler-state.json: ${e.message}` };
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
return { state: JSON.parse(raw) };
|
|
471
|
+
} catch (parseErr) {
|
|
472
|
+
return { errorMessage: `scheduler-state.json is corrupt: ${parseErr.message}` };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
338
476
|
async function check() {
|
|
339
477
|
const start = Date.now();
|
|
340
478
|
const status = {
|
|
@@ -407,6 +545,16 @@ async function check() {
|
|
|
407
545
|
try {
|
|
408
546
|
queueState = queueStore.readMergedSync();
|
|
409
547
|
if (queueState.unreadable) throw new Error(queueState.unreadable);
|
|
548
|
+
// A torn scheduler-machine.json is now recovered rather than halting
|
|
549
|
+
// dispatch (queueStore.cjs's findLongestValidJsonPrefix) — but recovery
|
|
550
|
+
// happening at all means the file failed to parse moments ago, which is
|
|
551
|
+
// exactly the "machine-runtime file fails to parse" signal health must
|
|
552
|
+
// surface as non-GREEN even though scheduling itself kept running.
|
|
553
|
+
if (queueState.machineStateRecovered) {
|
|
554
|
+
status.issues.push(
|
|
555
|
+
`scheduler-machine.json failed to parse and was recovered (mode=${queueState.machineStateRecoveryMode}) — see logs scope=scheduler`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
410
558
|
const runningCount = Object.values(queueState.jobs || {}).filter(
|
|
411
559
|
(j) => j.status === 'running'
|
|
412
560
|
).length;
|
|
@@ -440,7 +588,7 @@ async function check() {
|
|
|
440
588
|
.map(([cwd]) => cwd);
|
|
441
589
|
|
|
442
590
|
status.components.scheduler_queue = {
|
|
443
|
-
ok: !liveness.stalled && projectsPastThreshold.length === 0,
|
|
591
|
+
ok: !liveness.stalled && projectsPastThreshold.length === 0 && !queueState.machineStateRecovered,
|
|
444
592
|
path: queuePath,
|
|
445
593
|
jobs: Object.keys(queueState.jobs || {}).length,
|
|
446
594
|
running: runningCount,
|
|
@@ -450,6 +598,8 @@ async function check() {
|
|
|
450
598
|
byProject: computeProjectProblemCounts(queueState.jobs),
|
|
451
599
|
perProjectStall,
|
|
452
600
|
tickLiveness: liveness.reason,
|
|
601
|
+
machineStateRecovered: queueState.machineStateRecovered ?? false,
|
|
602
|
+
machineStateRecoveryMode: queueState.machineStateRecoveryMode ?? null,
|
|
453
603
|
// Informational only (PRD 1085): current 1-min loadavg per core vs the
|
|
454
604
|
// launch-gate threshold, so a "nothing is launching" report can be
|
|
455
605
|
// read next to the reason without opening the app.
|
|
@@ -485,6 +635,25 @@ async function check() {
|
|
|
485
635
|
);
|
|
486
636
|
}
|
|
487
637
|
}
|
|
638
|
+
status.components.queue_dispatch = evaluateQueueDispatchHealth(queueState, runningCount, now);
|
|
639
|
+
if (!status.components.queue_dispatch.ok || status.components.queue_dispatch.blocked) {
|
|
640
|
+
status.issues.push(`Queue dispatch: ${status.components.queue_dispatch.message}`);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Worktree cap blocking every dispatchable pending job with nothing
|
|
644
|
+
// running (see evaluateWorktreeCapBlocked's header) — a distinct
|
|
645
|
+
// condition from generic tick/dispatch staleness above, since a leaked
|
|
646
|
+
// cap count can hold a row `pending` with a fresh heldReason on every
|
|
647
|
+
// single tick (tickQueue keeps reaching spawnJob, per PRD 1141/1144's
|
|
648
|
+
// live evidence), never tripping evaluateTickLiveness's staleness check.
|
|
649
|
+
const worktreeCapBlocked = evaluateWorktreeCapBlocked(queueState.jobs, runningCount);
|
|
650
|
+
status.components.scheduler_queue.worktreeCapBlocked = worktreeCapBlocked;
|
|
651
|
+
if (!worktreeCapBlocked.ok) {
|
|
652
|
+
status.components.scheduler_queue.ok = false;
|
|
653
|
+
status.issues.push(
|
|
654
|
+
`Worktree cap is blocking dispatch with 0 jobs running: ${worktreeCapBlocked.capBlockedSlugs.join(', ')}`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
488
657
|
} catch (e) {
|
|
489
658
|
if (e.code !== 'ENOENT') {
|
|
490
659
|
status.issues.push(`Scheduler queue unreadable: ${e.message}`);
|
|
@@ -495,6 +664,26 @@ async function check() {
|
|
|
495
664
|
exists: false,
|
|
496
665
|
error: e.code === 'ENOENT' ? 'not yet created' : e.message,
|
|
497
666
|
};
|
|
667
|
+
status.components.queue_dispatch = { ok: true };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// 3.5. Usage/rate-limit poller (scheduler.cjs pollLoop) — a distinct
|
|
671
|
+
// subsystem from scheduler_queue above: it can be alive (lastPollAt keeps
|
|
672
|
+
// advancing) while failing every single poll, silently, with nothing else
|
|
673
|
+
// surfacing it (the 57-consecutive-failure incident this check exists to
|
|
674
|
+
// catch). Missing state file means the poller hasn't run yet — not a
|
|
675
|
+
// failure. A CORRUPT file (the torn-write class fixed for the sibling
|
|
676
|
+
// scheduler-machine.json in f56bdc0) is NOT the same as missing — surfaced
|
|
677
|
+
// as non-GREEN rather than fail-open, via loadUsagePollerState below.
|
|
678
|
+
const loaded = loadUsagePollerState(SCHEDULER_STATE_PATH);
|
|
679
|
+
if (loaded.missing) {
|
|
680
|
+
status.components.usage_poller = { ok: true, applicable: false };
|
|
681
|
+
} else if (loaded.errorMessage) {
|
|
682
|
+
status.components.usage_poller = { ok: false, applicable: true, error: loaded.errorMessage };
|
|
683
|
+
status.issues.push(`Usage/rate-limit poller: ${loaded.errorMessage}`);
|
|
684
|
+
} else {
|
|
685
|
+
status.components.usage_poller = evaluateUsagePollerHealth(loaded.state);
|
|
686
|
+
if (status.components.usage_poller.message) status.issues.push(status.components.usage_poller.message);
|
|
498
687
|
}
|
|
499
688
|
|
|
500
689
|
// 4. Check PRDs directories — one per active project
|
|
@@ -688,7 +877,7 @@ async function check() {
|
|
|
688
877
|
// Critical: nodejs, config dir, typescript, build artifact, test infrastructure.
|
|
689
878
|
// Non-fatal: scheduler/transcripts dirs may not exist on fresh install.
|
|
690
879
|
// Informational: app log age (shows if app is running, but not blocking).
|
|
691
|
-
const criticalComponents = ['nodejs', 'config_dir', 'typescript', 'build_artifact', 'test_infrastructure', 'scheduler_queue', 'prd_migration', 'claude_md_budget', 'delegation_chain'];
|
|
880
|
+
const criticalComponents = ['nodejs', 'config_dir', 'typescript', 'build_artifact', 'test_infrastructure', 'scheduler_queue', 'queue_dispatch', 'usage_poller', 'prd_migration', 'claude_md_budget', 'delegation_chain'];
|
|
692
881
|
status.ok = criticalComponents.every((c) => status.components[c]?.ok !== false);
|
|
693
882
|
|
|
694
883
|
status.elapsedMs = Date.now() - start;
|
|
@@ -713,9 +902,14 @@ module.exports = {
|
|
|
713
902
|
computeEpicIndexDrift,
|
|
714
903
|
evaluateEpicIndexHealth,
|
|
715
904
|
computeProjectProblemCounts,
|
|
905
|
+
evaluateWorktreeCapBlocked,
|
|
716
906
|
evaluatePerProjectStall,
|
|
717
907
|
parseClaudeMdBudget,
|
|
718
908
|
evaluateClaudeMdBudget,
|
|
909
|
+
evaluateUsagePollerHealth,
|
|
910
|
+
loadUsagePollerState,
|
|
911
|
+
evaluateQueueDispatchHealth,
|
|
912
|
+
DISPATCH_STALL_THRESHOLD_MS,
|
|
719
913
|
TICK_STALL_THRESHOLD_MS,
|
|
720
914
|
HEARTBEAT_STALE_MS,
|
|
721
915
|
};
|
package/src/main/index.cjs
CHANGED
|
@@ -37,7 +37,7 @@ const epicWorktreeMint = require('./lib/epicWorktreeMint.cjs');
|
|
|
37
37
|
const epicWorktreeMerge = require('./lib/epicWorktreeMerge.cjs');
|
|
38
38
|
const epicWorktreeProjectConfig = require('./lib/epicWorktreeProjectConfig.cjs');
|
|
39
39
|
const agentLibrary = require('./agentLibrary.cjs');
|
|
40
|
-
const { checkDelegationReadiness, installPrdWriteGuard, installDestructiveGitGuard } = require('./lib/delegationReadiness.cjs');
|
|
40
|
+
const { checkDelegationReadiness, installPrdWriteGuard, installDestructiveGitGuard, installInlineImplementationGuard } = require('./lib/delegationReadiness.cjs');
|
|
41
41
|
const { MCP_TOOL_CATALOG, MCP_RECIPES } = require('./lib/mcpToolCatalog.cjs');
|
|
42
42
|
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
43
43
|
const { resolveBuildTarget } = require('./lib/buildTarget.cjs');
|
|
@@ -63,6 +63,10 @@ const { seedSchedulerMcp } = require('./seedSchedulerMcp.cjs');
|
|
|
63
63
|
const { getSeedStatus } = require('./seedStatus.cjs');
|
|
64
64
|
const otel = require('./otel.cjs');
|
|
65
65
|
const otelSettings = require('./otelSettings.cjs');
|
|
66
|
+
const telemetrySettings = require('./lib/telemetrySettings.cjs');
|
|
67
|
+
const telemetryClient = require('./lib/telemetryClient.cjs');
|
|
68
|
+
const telemetryBacklog = require('./lib/telemetryBacklog.cjs');
|
|
69
|
+
const telemetryConsent = require('./lib/telemetryConsent.cjs');
|
|
66
70
|
const { registerHistoryAggregatorHandlers, finalizeClosedDays, refreshIntradayToday } = require('./historyAggregator.cjs');
|
|
67
71
|
const runLogRetention = require('./lib/runLogRetention.cjs');
|
|
68
72
|
const { registerHistoryDashboardHandlers } = require('./historyDashboard.cjs');
|
|
@@ -391,6 +395,16 @@ function createWindow() {
|
|
|
391
395
|
if (firstPaintTimer) { clearTimeout(firstPaintTimer); firstPaintTimer = null; }
|
|
392
396
|
mainWindow.maximize();
|
|
393
397
|
mainWindow.show();
|
|
398
|
+
|
|
399
|
+
// Backlog drain: ships the pre-existing errors-<date>.jsonl backlog every
|
|
400
|
+
// known project has already accumulated, marking exactly what's
|
|
401
|
+
// CONFIRMED delivered. Fired after the window is shown (never blocks
|
|
402
|
+
// startup) and yields between files internally. See telemetryBacklog.cjs.
|
|
403
|
+
require('./lib/telemetryBacklog.cjs')
|
|
404
|
+
.bootDrain({ appVersion: app.getVersion() })
|
|
405
|
+
.catch((e) => {
|
|
406
|
+
logs.writeLine({ scope: 'telemetry', level: 'warn', message: 'telemetryBacklog.bootDrain failed', meta: { error: e?.message } });
|
|
407
|
+
});
|
|
394
408
|
});
|
|
395
409
|
|
|
396
410
|
// Native right-click menu — Copy / Paste / Select All everywhere. Roles
|
|
@@ -513,17 +527,23 @@ ipcMain.handle('app:delegation-readiness', validated(schemas.delegationReadiness
|
|
|
513
527
|
checkDelegationReadiness(payload)
|
|
514
528
|
));
|
|
515
529
|
|
|
516
|
-
// The
|
|
517
|
-
// standardizes on the REFERENCE approach (absolute path to this
|
|
518
|
-
// script, never a vendored copy) — see installPrdWriteGuard's
|
|
519
|
-
//
|
|
520
|
-
//
|
|
530
|
+
// The three readiness checks with a sanctioned one-press install. Session
|
|
531
|
+
// Manager standardizes on the REFERENCE approach (absolute path to this
|
|
532
|
+
// repo's guard script, never a vendored copy) — see installPrdWriteGuard's
|
|
533
|
+
// header. prd-write-guard and destructive-git-guard stay separate handlers
|
|
534
|
+
// (and separate PreToolUse entries) because those two hooks have different
|
|
535
|
+
// matchers: Write|Edit|NotebookEdit vs Bash — installDestructiveGitGuard's
|
|
536
|
+
// header. inline-implementation-guard shares prd-write-guard's matcher, so
|
|
537
|
+
// its installer appends into that SAME matcher's `hooks` array instead.
|
|
521
538
|
ipcMain.handle('app:install-prd-write-guard', validated(schemas.delegationReadinessCwd, (payload) =>
|
|
522
539
|
installPrdWriteGuard(payload)
|
|
523
540
|
));
|
|
524
541
|
ipcMain.handle('app:install-destructive-git-guard', validated(schemas.delegationReadinessCwd, (payload) =>
|
|
525
542
|
installDestructiveGitGuard(payload)
|
|
526
543
|
));
|
|
544
|
+
ipcMain.handle('app:install-inline-implementation-guard', validated(schemas.delegationReadinessCwd, (payload) =>
|
|
545
|
+
installInlineImplementationGuard(payload)
|
|
546
|
+
));
|
|
527
547
|
|
|
528
548
|
ipcMain.handle('app:engage-rules-path', () => process.env.SESSION_MANAGER_ENGAGE_RULES || null);
|
|
529
549
|
|
|
@@ -896,6 +916,23 @@ ipcMain.handle('otel:set-config', async (_e, cfg) => {
|
|
|
896
916
|
ipcMain.handle('otel:status', () => otel.status());
|
|
897
917
|
ipcMain.handle('otel:config-path', () => otelSettings.storePath());
|
|
898
918
|
|
|
919
|
+
// Product telemetry (bilko.run) — anonymous, on-by-default, opt-out consent
|
|
920
|
+
// UX + inspector over the client PRDs above wired. Settings > Telemetry's
|
|
921
|
+
// "Product telemetry" section is the sole UI surface for this; see
|
|
922
|
+
// session-manager-operations/architecture/telemetry.md for the data model.
|
|
923
|
+
ipcMain.handle('telemetry:get-config', async () => telemetrySettings.load());
|
|
924
|
+
ipcMain.handle('telemetry:set-config', validated(schemas.telemetrySetConfig, async (cfg) => {
|
|
925
|
+
const saved = await telemetryConsent.applyConsentUpdate(cfg, { telemetrySettings, telemetryClient });
|
|
926
|
+
return { ok: true, config: saved, status: telemetryClient.status() };
|
|
927
|
+
}));
|
|
928
|
+
ipcMain.handle('telemetry:status', () => ({
|
|
929
|
+
...telemetryClient.status(),
|
|
930
|
+
backlog: telemetryBacklog.lastRunSummary(),
|
|
931
|
+
}));
|
|
932
|
+
ipcMain.handle('telemetry:config-path', () => telemetrySettings.storePath());
|
|
933
|
+
ipcMain.handle('telemetry:recent-records', () => telemetryClient.recentRecords());
|
|
934
|
+
ipcMain.handle('telemetry:flush-now', () => telemetryClient.flush('manual'));
|
|
935
|
+
|
|
899
936
|
// Diagnostic-only renderer heap snapshot. No-op unless SM_HEAP_SNAPSHOT=1 —
|
|
900
937
|
// see heapSnapshot.cjs for why this stays off by default.
|
|
901
938
|
heapSnapshot.registerIpc({ ipcMain, getWindow: () => mainWindow });
|
|
@@ -1019,13 +1056,31 @@ app.whenReady().then(async () => {
|
|
|
1019
1056
|
}
|
|
1020
1057
|
|
|
1021
1058
|
process.on('uncaughtException', (err) => {
|
|
1059
|
+
try {
|
|
1060
|
+
require('./lib/telemetryClient.cjs').reportError({ name: 'uncaughtException', msg: err?.message, stack: err?.stack });
|
|
1061
|
+
} catch { /* telemetry must never break the existing handling below */ }
|
|
1022
1062
|
logs.writeLine({ scope: 'main', level: 'error', message: 'uncaughtException', meta: { error: err?.message, stack: err?.stack } });
|
|
1023
1063
|
});
|
|
1024
1064
|
process.on('unhandledRejection', (reason) => {
|
|
1025
1065
|
const r = reason instanceof Error ? { error: reason.message, stack: reason.stack } : { reason: String(reason) };
|
|
1066
|
+
try {
|
|
1067
|
+
require('./lib/telemetryClient.cjs').reportError({ name: 'unhandledRejection', msg: r.error || r.reason, stack: r.stack });
|
|
1068
|
+
} catch { /* telemetry must never break the existing handling below */ }
|
|
1026
1069
|
logs.writeLine({ scope: 'main', level: 'error', message: 'unhandledRejection', meta: r });
|
|
1027
1070
|
});
|
|
1028
1071
|
|
|
1072
|
+
// Telemetry boot sequence: flush('boot') always, flush('version-change')
|
|
1073
|
+
// when detected, and the once-per-machine install upsert (app_installs) when
|
|
1074
|
+
// due (version bump or 30-day liveness heartbeat). See telemetryBoot.cjs.
|
|
1075
|
+
require('./lib/telemetryBoot.cjs')
|
|
1076
|
+
.bootSequence({
|
|
1077
|
+
appVersion: app.getVersion(),
|
|
1078
|
+
installChannel: require('./lib/machineProfile.cjs').resolveInstallChannel({ appPath: app.getAppPath() }),
|
|
1079
|
+
})
|
|
1080
|
+
.catch((e) => {
|
|
1081
|
+
logs.writeLine({ scope: 'telemetry', level: 'warn', message: 'bootSequence failed', meta: { error: e?.message } });
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1029
1084
|
// Inject Content-Security-Policy for all renderer responses.
|
|
1030
1085
|
// frame-src / frame-ancestors locked to 'none' — the app is a top-level
|
|
1031
1086
|
// Electron BrowserWindow; iframes/embedding have no legitimate use.
|
|
@@ -1376,4 +1431,8 @@ app.on('before-quit', () => {
|
|
|
1376
1431
|
// Best-effort flush of any pending OTEL spans. shutdown() has its own 2s
|
|
1377
1432
|
// ceiling so a wedged exporter can't hold quit.
|
|
1378
1433
|
otel.shutdown().catch(() => {});
|
|
1434
|
+
// Best-effort delivery attempt for whatever telemetry accumulated this run.
|
|
1435
|
+
// flush() only removes records it actually delivered — an interrupted or
|
|
1436
|
+
// failed attempt leaves the queue file intact for the next boot's flush.
|
|
1437
|
+
require('./lib/telemetryClient.cjs').flush('quit').catch(() => {});
|
|
1379
1438
|
});
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -610,8 +610,9 @@ const projectBriefCwd = z.object({
|
|
|
610
610
|
}).strict();
|
|
611
611
|
|
|
612
612
|
// ──────────────────────────────────────────── Delegation readiness
|
|
613
|
-
// Shared by app:delegation-readiness, app:install-prd-write-guard
|
|
614
|
-
// app:install-destructive-git-guard
|
|
613
|
+
// Shared by app:delegation-readiness, app:install-prd-write-guard,
|
|
614
|
+
// app:install-destructive-git-guard and app:install-inline-implementation-guard
|
|
615
|
+
// — all four take exactly { cwd }.
|
|
615
616
|
const delegationReadinessCwd = z.object({
|
|
616
617
|
cwd: z.string().min(1).max(4096),
|
|
617
618
|
}).strict();
|
|
@@ -975,6 +976,21 @@ const pluginsAbort = z.object({
|
|
|
975
976
|
slug: z.string().regex(PLUGIN_SLUG_RE).min(1).max(128),
|
|
976
977
|
}).passthrough();
|
|
977
978
|
|
|
979
|
+
// ──────────────────────────────────────────── Product telemetry (bilko.run)
|
|
980
|
+
// Mirrors telemetrySettings.cjs's own isValid() shape — the renderer always
|
|
981
|
+
// round-trips a full config object it got from telemetry:get-config, same
|
|
982
|
+
// pattern as otel:set-config / OtelConfig.
|
|
983
|
+
const telemetrySetConfig = z.object({
|
|
984
|
+
enabled: z.boolean(),
|
|
985
|
+
installId: z.string().max(200),
|
|
986
|
+
endpoint: z.string().min(1).max(2048),
|
|
987
|
+
noticeAckedAt: z.string().max(64).nullable(),
|
|
988
|
+
lastMachineReportAt: z.string().max(64).nullable(),
|
|
989
|
+
lastMachineReportVersion: z.string().max(64),
|
|
990
|
+
lastDailyFlushAt: z.string().max(64).nullable(),
|
|
991
|
+
schemaVersion: z.literal(1),
|
|
992
|
+
});
|
|
993
|
+
|
|
978
994
|
/**
|
|
979
995
|
* Wrap an IPC handler with schema validation. Returns a new handler that
|
|
980
996
|
* parses the payload before calling the original. On invalid payload throws
|
|
@@ -1093,6 +1109,7 @@ module.exports = {
|
|
|
1093
1109
|
agentsSavePersona,
|
|
1094
1110
|
agentsGetPersonaBody,
|
|
1095
1111
|
delegationReadinessCwd,
|
|
1112
|
+
telemetrySetConfig,
|
|
1096
1113
|
},
|
|
1097
1114
|
validated,
|
|
1098
1115
|
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crashTelemetry.test.cjs — unit tests for the electron-free crash-report tap
|
|
3
|
+
* crashDiagnostics.cjs's render-process-gone / child-process-gone hooks and
|
|
4
|
+
* unclean-shutdown postmortem call into.
|
|
5
|
+
*
|
|
6
|
+
* Run: timeout 120 npx vitest run src/main/lib/__tests__/crashTelemetry.test.cjs
|
|
7
|
+
*/
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
import { test, expect, afterEach } from 'vitest';
|
|
11
|
+
const fsp = require('node:fs/promises');
|
|
12
|
+
const os = require('node:os');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
|
|
15
|
+
const { reportCrash } = require('../crashTelemetry.cjs');
|
|
16
|
+
|
|
17
|
+
const tmpDirs = [];
|
|
18
|
+
let originalHome;
|
|
19
|
+
let originalSmTelemetrySpool;
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
if (originalHome !== undefined) process.env.HOME = originalHome;
|
|
23
|
+
if (originalSmTelemetrySpool === undefined) delete process.env.SM_TELEMETRY_SPOOL; else process.env.SM_TELEMETRY_SPOOL = originalSmTelemetrySpool;
|
|
24
|
+
while (tmpDirs.length) {
|
|
25
|
+
const d = tmpDirs.pop();
|
|
26
|
+
await fsp.rm(d, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
async function mkHome() {
|
|
31
|
+
originalHome = process.env.HOME;
|
|
32
|
+
originalSmTelemetrySpool = process.env.SM_TELEMETRY_SPOOL;
|
|
33
|
+
const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-crash-telemetry-home-'));
|
|
34
|
+
tmpDirs.push(dir);
|
|
35
|
+
process.env.HOME = dir;
|
|
36
|
+
// Explicit opt-in: overrides the test-environment no-op guard so this
|
|
37
|
+
// suite's real telemetryClient calls actually persist into an isolated dir.
|
|
38
|
+
process.env.SM_TELEMETRY_SPOOL = path.join(dir, '.config', 'session-manager');
|
|
39
|
+
return dir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function freshClient() {
|
|
43
|
+
for (const p of ['../telemetryClient.cjs', '../../config.cjs', '../telemetrySettings.cjs', '../machineProfile.cjs']) {
|
|
44
|
+
const resolved = require.resolve(p);
|
|
45
|
+
delete require.cache[resolved];
|
|
46
|
+
}
|
|
47
|
+
const client = require('../telemetryClient.cjs');
|
|
48
|
+
client._setMachineProfileBuilder(async () => ({
|
|
49
|
+
appVersion: '0.83.0', platform: 'linux', arch: 'x64', machineDigest: 'deadbeefcafe',
|
|
50
|
+
}));
|
|
51
|
+
return client;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function readQueueLines(client) {
|
|
55
|
+
const fs = require('node:fs/promises');
|
|
56
|
+
try {
|
|
57
|
+
const raw = await fs.readFile(client.queuePath(), 'utf8');
|
|
58
|
+
return raw.split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
59
|
+
} catch (e) {
|
|
60
|
+
if (e.code === 'ENOENT') return [];
|
|
61
|
+
throw e;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
test('reportCrash names the record "crash.<reason>"', async () => {
|
|
66
|
+
await mkHome();
|
|
67
|
+
const client = freshClient();
|
|
68
|
+
await reportCrash({ reason: 'oom', meta: { type: 'render' }, deps: { telemetryClient: client } });
|
|
69
|
+
const [rec] = await readQueueLines(client);
|
|
70
|
+
expect(rec.wire.name).toBe('crash.oom');
|
|
71
|
+
client.shutdown();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('a crash record carries the full attribution stamp: appVersion, platform, arch, machineDigest', async () => {
|
|
75
|
+
await mkHome();
|
|
76
|
+
const client = freshClient();
|
|
77
|
+
await reportCrash({ reason: 'killed', meta: { type: 'child', name: 'gpu' }, deps: { telemetryClient: client } });
|
|
78
|
+
const [rec] = await readQueueLines(client);
|
|
79
|
+
expect(rec.wire.version).toBe('0.83.0');
|
|
80
|
+
expect(rec.wire.context.platform).toBeTruthy();
|
|
81
|
+
expect(rec.wire.context.arch).toBeTruthy();
|
|
82
|
+
expect(rec.wire.context.machineDigest).toBeTruthy();
|
|
83
|
+
client.shutdown();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('the unclean-shutdown postmortem is reported as crash.unclean-shutdown with the last memory sample', async () => {
|
|
87
|
+
await mkHome();
|
|
88
|
+
const client = freshClient();
|
|
89
|
+
await reportCrash({
|
|
90
|
+
reason: 'unclean-shutdown',
|
|
91
|
+
meta: { lastTotalMb: 1800, lastMainRssMb: 900, lastTop: ['renderer:900MB'] },
|
|
92
|
+
deps: { telemetryClient: client },
|
|
93
|
+
});
|
|
94
|
+
const [rec] = await readQueueLines(client);
|
|
95
|
+
expect(rec.wire.name).toBe('crash.unclean-shutdown');
|
|
96
|
+
expect(rec.wire.context.lastTotalMb).toBe(1800);
|
|
97
|
+
client.shutdown();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('reportCrash never throws even when the injected telemetryClient throws', () => {
|
|
101
|
+
const throwingClient = { reportError: () => { throw new Error('telemetry boom'); } };
|
|
102
|
+
expect(() => reportCrash({ reason: 'oom', meta: {}, deps: { telemetryClient: throwingClient } })).not.toThrow();
|
|
103
|
+
});
|