claude-code-session-manager 0.83.0 → 0.84.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-CCVIpoSz.js} +1 -1
- package/dist/assets/{DataModel-BC6ppcBr.js → DataModel-BltREYde.js} +1 -1
- package/dist/assets/{History-D3Qw2BWn.js → History-BZxFkOp6.js} +2 -2
- package/dist/assets/{Hooks-OxBQ0nIc.js → Hooks-Bznlfaaa.js} +1 -1
- package/dist/assets/{HostBilko-BNFC6bNZ.js → HostBilko-DUG5YHA_.js} +1 -1
- package/dist/assets/{Library-DAMyrUtY.js → Library-Bi2Fn3w9.js} +1 -1
- package/dist/assets/{ListDetail-BwywrpvE.js → ListDetail-4VBvXKrz.js} +1 -1
- package/dist/assets/{MarkdownEditor-HEwjN1bp.js → MarkdownEditor-D2ft_v5j.js} +1 -1
- package/dist/assets/{McpServers-D1VZF7Ix.js → McpServers-FDekKkyE.js} +1 -1
- package/dist/assets/{Memory-CgYJ_QqD.js → Memory-Dkl80uj_.js} +6 -6
- package/dist/assets/{Panel-dTNWC59Q.js → Panel-LurG5VfD.js} +1 -1
- package/dist/assets/{Permissions-qI_NKi1b.js → Permissions-D2wHBCFA.js} +1 -1
- package/dist/assets/{Plugins-DZ0_CHiF.js → Plugins-CTw_wwbI.js} +2 -2
- package/dist/assets/{ProvenanceBadge-B2i_uxCu.js → ProvenanceBadge-CW6HNUv6.js} +1 -1
- package/dist/assets/{SaveBar-4tNMCjUd.js → SaveBar-alDHG6cP.js} +1 -1
- package/dist/assets/{Scheduler-CUen18Gc.js → Scheduler-DxiPcaiW.js} +7 -7
- package/dist/assets/{ScopeSwitcher-Co3tkLTT.js → ScopeSwitcher-DzFXUKLZ.js} +1 -1
- package/dist/assets/Settings-B6H3v2am.js +3 -0
- package/dist/assets/{SkillReferenceGraph-BKcHtMPc.js → SkillReferenceGraph-B8EZalpN.js} +1 -1
- package/dist/assets/{Skills-DHb4INtF.js → Skills-DwuHuA08.js} +2 -2
- package/dist/assets/SystemPrompt-CMMqpGYn.js +1 -0
- package/dist/assets/{TagLibrary-xLP5dJvh.js → TagLibrary-C2N5C1n9.js} +1 -1
- package/dist/assets/{TiptapBody-T-ULTjK4.js → TiptapBody-btlID-dQ.js} +1 -1
- package/dist/assets/{Toggle-B-5M2hGe.js → Toggle-BGOn3DZj.js} +1 -1
- package/dist/assets/{index-CzAxC432.js → index-QLRf0epp.js} +316 -314
- package/dist/assets/{index-CYhdtisq.css → index-mnjNDpb1.css} +1 -1
- package/dist/assets/{settingsSchema-C_nsemcF.js → settingsSchema-DA3N2Up3.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 +143 -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 +66 -0
- package/src/main/__tests__/telemetryContract.test.cjs +883 -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 +97 -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 +620 -0
- package/src/main/lib/__tests__/telemetryBoot.test.cjs +125 -0
- package/src/main/lib/__tests__/telemetryConsent.test.cjs +130 -0
- package/src/main/lib/__tests__/telemetryCounters.test.cjs +57 -0
- package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +89 -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 +233 -20
- 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 +71 -0
- package/src/main/lib/telemetryClient.cjs +31 -0
- 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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* queueStoreMachineStateRecovery.test.cjs — a torn `scheduler-machine.json`
|
|
3
|
+
* must recover instead of poisoning every read with `unreadable` (which
|
|
4
|
+
* halts tickQueue/runDueJobs machine-wide — the user-visible "Run now"
|
|
5
|
+
* hard error "queue.json is unreadable — scheduling halted").
|
|
6
|
+
*
|
|
7
|
+
* fixtures/scheduler-machine.json.corrupt-1789147548 is a byte-for-byte copy
|
|
8
|
+
* of the REAL torn file recovered live on 2026-09-11 — not hand-typed. Its
|
|
9
|
+
* shape: a complete, valid, parseable object (paused: null) followed by the
|
|
10
|
+
* orphaned tail of a longer previous write (an incomplete `resumeAt` string
|
|
11
|
+
* fragment with no reconstructable `"paused"` object around it).
|
|
12
|
+
*
|
|
13
|
+
* Never writes to the real ~/.claude/session-manager/ — HOME is pointed at a
|
|
14
|
+
* fresh temp dir per test and the module is re-required so its module-level
|
|
15
|
+
* MACHINE_STATE_PATH constant is recomputed against it.
|
|
16
|
+
*
|
|
17
|
+
* Run: timeout 120 npx vitest run src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
import { test, expect, beforeEach, afterEach } from 'vitest';
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const fsp = require('node:fs/promises');
|
|
25
|
+
const os = require('node:os');
|
|
26
|
+
const path = require('node:path');
|
|
27
|
+
|
|
28
|
+
const FIXTURE = path.join(__dirname, 'fixtures', 'scheduler-machine.json.corrupt-1789147548');
|
|
29
|
+
|
|
30
|
+
// queueStore.cjs is required via Node's own require() (this is a .cjs file,
|
|
31
|
+
// not transformed ESM) and bakes os.homedir() into a top-level const
|
|
32
|
+
// (MACHINE_STATE_PATH, MACHINE_STATE_LOG_CWD) at first require. vi.resetModules()
|
|
33
|
+
// only resets vitest's ESM module graph, so it does NOT clear Node's own
|
|
34
|
+
// require.cache — purge it directly (same pattern as historyRollup.test.cjs)
|
|
35
|
+
// or every test after the first keeps pointing at the FIRST test's tmpHome.
|
|
36
|
+
function purgeRequireCache() {
|
|
37
|
+
try { delete require.cache[require.resolve('../queueStore.cjs')]; } catch { /* not loaded yet */ }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const realHome = process.env.HOME;
|
|
41
|
+
let tmpHome;
|
|
42
|
+
let queueStore;
|
|
43
|
+
let machineStatePath;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'queueStore-home-'));
|
|
47
|
+
fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager'), { recursive: true });
|
|
48
|
+
fs.mkdirSync(path.join(tmpHome, 'Projects', 'session-manager'), { recursive: true });
|
|
49
|
+
process.env.HOME = tmpHome;
|
|
50
|
+
purgeRequireCache();
|
|
51
|
+
queueStore = require('../queueStore.cjs');
|
|
52
|
+
machineStatePath = queueStore.MACHINE_STATE_PATH;
|
|
53
|
+
expect(machineStatePath.startsWith(tmpHome)).toBe(true); // sanity: never the real file
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(async () => {
|
|
57
|
+
process.env.HOME = realHome;
|
|
58
|
+
purgeRequireCache();
|
|
59
|
+
await fsp.rm(tmpHome, { recursive: true, force: true }).catch(() => {});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('findLongestValidJsonPrefix recovers the complete leading object from the real torn fixture', () => {
|
|
63
|
+
const raw = fs.readFileSync(FIXTURE, 'utf8');
|
|
64
|
+
const result = queueStore.findLongestValidJsonPrefix(raw);
|
|
65
|
+
expect(result).not.toBeNull();
|
|
66
|
+
expect(result.prefixLength).toBeLessThan(raw.length); // there really is an orphaned tail
|
|
67
|
+
expect(result.value.paused).toBeNull();
|
|
68
|
+
expect(result.value.lastRunAt).toBe('2026-09-11T10:00:14.441Z');
|
|
69
|
+
expect(result.value.config.concurrencyCap).toBe(4);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('readMergedSync recovers a torn machine-state file instead of marking it unreadable', () => {
|
|
73
|
+
fs.copyFileSync(FIXTURE, machineStatePath);
|
|
74
|
+
|
|
75
|
+
const state = queueStore.readMergedSync();
|
|
76
|
+
|
|
77
|
+
expect(state.unreadable).toBeUndefined();
|
|
78
|
+
expect(state.machineStateRecovered).toBe(true);
|
|
79
|
+
expect(state.machineStateRecoveryMode).toBe('prefix');
|
|
80
|
+
expect(state.paused).toBeNull();
|
|
81
|
+
expect(state.lastRunAt).toBe('2026-09-11T10:00:14.441Z');
|
|
82
|
+
|
|
83
|
+
// The file is rewritten atomically from the recovered object — a second
|
|
84
|
+
// read must come back clean with no recovery flag.
|
|
85
|
+
const rewritten = fs.readFileSync(machineStatePath, 'utf8');
|
|
86
|
+
expect(() => JSON.parse(rewritten)).not.toThrow();
|
|
87
|
+
const second = queueStore.readMergedSync();
|
|
88
|
+
expect(second.machineStateRecovered).toBeUndefined();
|
|
89
|
+
expect(second.paused).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('a file with no valid JSON prefix at all falls back to defaults, never to unreadable', () => {
|
|
93
|
+
fs.writeFileSync(machineStatePath, 'not json at all {{{ this never balances');
|
|
94
|
+
|
|
95
|
+
const state = queueStore.readMergedSync();
|
|
96
|
+
|
|
97
|
+
expect(state.unreadable).toBeUndefined();
|
|
98
|
+
expect(state.machineStateRecovered).toBe(true);
|
|
99
|
+
expect(state.machineStateRecoveryMode).toBe('default');
|
|
100
|
+
expect(state.config).toEqual({});
|
|
101
|
+
expect(state.paused).toBeNull();
|
|
102
|
+
|
|
103
|
+
// Rewritten to a clean, parseable default doc so the engine keeps ticking.
|
|
104
|
+
const rewritten = fs.readFileSync(machineStatePath, 'utf8');
|
|
105
|
+
expect(() => JSON.parse(rewritten)).not.toThrow();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('a genuinely missing file is a legitimately empty first-boot state, not a recovery', () => {
|
|
109
|
+
const state = queueStore.readMergedSync();
|
|
110
|
+
expect(state.unreadable).toBeUndefined();
|
|
111
|
+
expect(state.machineStateRecovered).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('readMerged (async) recovers the same real fixture', async () => {
|
|
115
|
+
fs.copyFileSync(FIXTURE, machineStatePath);
|
|
116
|
+
|
|
117
|
+
const state = await queueStore.readMerged();
|
|
118
|
+
|
|
119
|
+
expect(state.unreadable).toBeUndefined();
|
|
120
|
+
expect(state.machineStateRecovered).toBe(true);
|
|
121
|
+
expect(state.machineStateRecoveryMode).toBe('prefix');
|
|
122
|
+
expect(state.lastRunAt).toBe('2026-09-11T10:00:14.441Z');
|
|
123
|
+
});
|
|
@@ -63,6 +63,64 @@ test('live pid → skipped regardless of age', () => {
|
|
|
63
63
|
assert.deepStrictEqual(reapable, []);
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
// ---------- dispatchPhase breadcrumb (PRD: dispatch-region diagnostic) ----------
|
|
67
|
+
|
|
68
|
+
test('pidless + dispatchPhase present → reason names the phase and timestamp', () => {
|
|
69
|
+
const jobs = [{
|
|
70
|
+
slug: 'zombie-phase',
|
|
71
|
+
status: 'running',
|
|
72
|
+
startedAt: agoMin(464),
|
|
73
|
+
dispatchPhase: 'baseline-persisted',
|
|
74
|
+
dispatchPhaseAt: '2026-09-12T02:46:10.000Z',
|
|
75
|
+
}];
|
|
76
|
+
const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
|
|
77
|
+
assert.strictEqual(reapable.length, 1);
|
|
78
|
+
assert.strictEqual(
|
|
79
|
+
reapable[0].reason,
|
|
80
|
+
'reaped: no runtime.pid recorded after 10m — spawn never completed '
|
|
81
|
+
+ '(last dispatch phase: baseline-persisted at 2026-09-12T02:46:10.000Z)',
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('pidless + no dispatchPhase (older-build row) → reason byte-identical to the pre-breadcrumb message', () => {
|
|
86
|
+
const jobs = [{ slug: 'zombie-noph', status: 'running', startedAt: agoMin(464) }];
|
|
87
|
+
const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
|
|
88
|
+
assert.strictEqual(reapable.length, 1);
|
|
89
|
+
assert.strictEqual(
|
|
90
|
+
reapable[0].reason,
|
|
91
|
+
'reaped: no runtime.pid recorded after 10m — spawn never completed',
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('pidless + dispatchPhase present but dispatchPhaseAt missing/unparseable → phase named, no "at" clause, never throws', () => {
|
|
96
|
+
const jobs = [
|
|
97
|
+
{ slug: 'zombie-noat', status: 'running', startedAt: agoMin(464), dispatchPhase: 'exec-entered' },
|
|
98
|
+
{ slug: 'zombie-badat', status: 'running', startedAt: agoMin(464), dispatchPhase: 'exec-entered', dispatchPhaseAt: 'not-a-date' },
|
|
99
|
+
];
|
|
100
|
+
const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
|
|
101
|
+
assert.strictEqual(reapable.length, 2);
|
|
102
|
+
for (const r of reapable) {
|
|
103
|
+
assert.strictEqual(
|
|
104
|
+
r.reason,
|
|
105
|
+
'reaped: no runtime.pid recorded after 10m — spawn never completed (last dispatch phase: exec-entered)',
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('pidless + dispatchPhase === "spawned" with no pid → still reapable, breadcrumb changes message only', () => {
|
|
111
|
+
const jobs = [{
|
|
112
|
+
slug: 'torn-write',
|
|
113
|
+
status: 'running',
|
|
114
|
+
startedAt: agoMin(464),
|
|
115
|
+
dispatchPhase: 'spawned',
|
|
116
|
+
dispatchPhaseAt: agoMin(465),
|
|
117
|
+
}];
|
|
118
|
+
const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
|
|
119
|
+
assert.strictEqual(reapable.length, 1);
|
|
120
|
+
assert.strictEqual(reapable[0].pidless, true);
|
|
121
|
+
assert.match(reapable[0].reason, /last dispatch phase: spawned/);
|
|
122
|
+
});
|
|
123
|
+
|
|
66
124
|
test('dead pid → reaped exactly as before (pidless: false, no reason string)', () => {
|
|
67
125
|
const jobs = [{ slug: 'dead', status: 'running', runtime: { pid: 4242 }, startedAt: agoMin(500) }];
|
|
68
126
|
const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysDead, grace: GRACE });
|