claude-code-session-manager 0.36.0 → 0.37.1
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/{TiptapBody-D5b7nejd.js → TiptapBody-DXQDiOUx.js} +58 -58
- package/dist/assets/index--a8m5_ET.js +3496 -0
- package/dist/assets/index-CTTjT08J.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +5 -1
- package/scripts/lib/activeSessions.cjs +117 -0
- package/scripts/lib/watchdogHelpers.cjs +699 -0
- package/src/main/__tests__/broadcastCoalescer.test.cjs +104 -0
- package/src/main/__tests__/historyDashboard.test.cjs +163 -0
- package/src/main/__tests__/historyRollup.test.cjs +333 -0
- package/src/main/__tests__/prdParserHighWater.test.cjs +74 -0
- package/src/main/__tests__/queueHistory.test.cjs +233 -0
- package/src/main/__tests__/queueOpsAutoArchive.test.cjs +142 -0
- package/src/main/__tests__/rcaFeedbackHook.test.cjs +259 -0
- package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +51 -0
- package/src/main/chatRunner.cjs +46 -12
- package/src/main/config.cjs +8 -0
- package/src/main/historyAggregator.cjs +391 -50
- package/src/main/historyDashboard.cjs +226 -0
- package/src/main/index.cjs +35 -1
- package/src/main/ipcSchemas.cjs +5 -0
- package/src/main/lib/broadcastCoalescer.cjs +46 -0
- package/src/main/lib/historyRollup.cjs +148 -0
- package/src/main/lib/queueHistory.cjs +216 -0
- package/src/main/lib/rcaFeedbackHook.cjs +346 -0
- package/src/main/lib/schedulerConfig.cjs +16 -0
- package/src/main/queueOps.cjs +92 -0
- package/src/main/scheduler/prdParser.cjs +52 -1
- package/src/main/scheduler.cjs +237 -29
- package/src/preload/api.d.ts +69 -1
- package/src/preload/index.cjs +1 -0
- package/dist/assets/index-CkiGRskz.css +0 -32
- package/dist/assets/index-DrzirIUy.js +0 -3562
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-effective-concurrency.test.cjs — unit tests for the
|
|
3
|
+
* effectiveConcurrency field on buildScheduleStatePayload.
|
|
4
|
+
*
|
|
5
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/scheduler-effective-concurrency.test.cjs
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
import { test, expect, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
const { buildScheduleStatePayload } = require('../scheduler.cjs');
|
|
12
|
+
|
|
13
|
+
const ENV_KEY = 'SM_SCHEDULER_MAX_CONCURRENCY';
|
|
14
|
+
let savedEnv;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
savedEnv = process.env[ENV_KEY];
|
|
18
|
+
delete process.env[ENV_KEY];
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
if (savedEnv === undefined) delete process.env[ENV_KEY];
|
|
23
|
+
else process.env[ENV_KEY] = savedEnv;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function fakeState(concurrencyCap) {
|
|
27
|
+
return {
|
|
28
|
+
config: { concurrencyCap },
|
|
29
|
+
jobs: [],
|
|
30
|
+
scheduledFor: null,
|
|
31
|
+
lastRunAt: null,
|
|
32
|
+
paused: null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test('effectiveConcurrency is config-sourced when env var is unset', () => {
|
|
37
|
+
const payload = buildScheduleStatePayload(fakeState(5));
|
|
38
|
+
expect(payload.effectiveConcurrency).toEqual({ cap: 5, source: 'config' });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('effectiveConcurrency is env-sourced and wins over config when env var is set', () => {
|
|
42
|
+
process.env[ENV_KEY] = '7';
|
|
43
|
+
const payload = buildScheduleStatePayload(fakeState(5));
|
|
44
|
+
expect(payload.effectiveConcurrency).toEqual({ cap: 7, source: 'env' });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('effectiveConcurrency clamps env var into [1, 20]', () => {
|
|
48
|
+
process.env[ENV_KEY] = '999';
|
|
49
|
+
const payload = buildScheduleStatePayload(fakeState(5));
|
|
50
|
+
expect(payload.effectiveConcurrency).toEqual({ cap: 20, source: 'env' });
|
|
51
|
+
});
|
package/src/main/chatRunner.cjs
CHANGED
|
@@ -54,30 +54,49 @@ const { extractJson } = require('./lib/extractJson.cjs');
|
|
|
54
54
|
const STOP_SENTINEL = '<<<SM_NEEDS_INPUT>>>';
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
*
|
|
57
|
+
* Split the final assistant text at the stop-signal sentinel.
|
|
58
58
|
*
|
|
59
|
-
* Returns `{ questions: string[] }` when the sentinel is
|
|
60
|
-
* balanced JSON object with a `questions` array can be found
|
|
61
|
-
* text after it
|
|
62
|
-
* multi-line pretty-printing, and trailing prose after the closing
|
|
59
|
+
* Returns `{ answerBody: string, questions: string[] }` when the sentinel is
|
|
60
|
+
* present and a balanced JSON object with a `questions` array can be found
|
|
61
|
+
* anywhere in the text after it (tolerant of leading blank lines, code-fence
|
|
62
|
+
* wrapping, multi-line pretty-printing, and trailing prose after the closing
|
|
63
|
+
* `}`). `answerBody` is everything before the LAST sentinel occurrence, with
|
|
64
|
+
* the trailing sentinel line and JSON line removed and whitespace trimmed —
|
|
65
|
+
* an earlier literal sentinel string embedded in the agent's own answer text
|
|
66
|
+
* is preserved as part of `answerBody` (lastIndexOf semantics).
|
|
63
67
|
* Returns `null` when the sentinel is absent OR when no such JSON object is
|
|
64
|
-
* found — both cases are treated as a completed run with no crash.
|
|
68
|
+
* found after it — both cases are treated as a completed run with no crash.
|
|
65
69
|
*
|
|
66
70
|
* @param {string} finalText
|
|
67
|
-
* @returns {{ questions: string[] } | null}
|
|
71
|
+
* @returns {{ answerBody: string, questions: string[] } | null}
|
|
68
72
|
*/
|
|
69
|
-
function
|
|
73
|
+
function splitStopSignal(finalText) {
|
|
70
74
|
if (typeof finalText !== 'string') return null;
|
|
71
75
|
const idx = finalText.lastIndexOf(STOP_SENTINEL);
|
|
72
76
|
if (idx === -1) return null;
|
|
73
77
|
const after = finalText.slice(idx + STOP_SENTINEL.length);
|
|
74
78
|
const parsed = extractJson(after);
|
|
75
79
|
if (parsed && Array.isArray(parsed.questions)) {
|
|
76
|
-
|
|
80
|
+
const answerBody = finalText.slice(0, idx).trim();
|
|
81
|
+
return { answerBody, questions: parsed.questions };
|
|
77
82
|
}
|
|
78
83
|
return null;
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Parse the final assistant text for the stop-signal protocol.
|
|
88
|
+
*
|
|
89
|
+
* Thin wrapper over `splitStopSignal` — kept for existing callers that only
|
|
90
|
+
* need the questions, not the pre-sentinel answer body.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} finalText
|
|
93
|
+
* @returns {{ questions: string[] } | null}
|
|
94
|
+
*/
|
|
95
|
+
function parseStopSignal(finalText) {
|
|
96
|
+
const split = splitStopSignal(finalText);
|
|
97
|
+
return split ? { questions: split.questions } : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
81
100
|
// ─── `/context` probe (PRD 470) ────────────────────────────────────────────
|
|
82
101
|
// `claude -p "/context"` is answered entirely locally by the CLI (zero-cost,
|
|
83
102
|
// no real API call) with markdown containing a summary line and a per-category
|
|
@@ -220,10 +239,14 @@ function hasMcpConsentDenial(text) {
|
|
|
220
239
|
// Instruction prepended to every prompt. Tells the agent how to signal that
|
|
221
240
|
// it needs clarification vs. having completed the task.
|
|
222
241
|
const STOP_SIGNAL_INSTRUCTION =
|
|
223
|
-
`IMPORTANT:
|
|
224
|
-
`
|
|
242
|
+
`IMPORTANT: First deliver everything you CAN answer or produce right now — findings, ` +
|
|
243
|
+
`the requested content, work already completed — as normal output. Only THEN, if ` +
|
|
244
|
+
`something still blocks you from finishing, end your turn by emitting, as the very ` +
|
|
245
|
+
`last line, exactly:\n` +
|
|
225
246
|
`${STOP_SENTINEL}\n` +
|
|
226
247
|
`followed on the next line by a single-line JSON object {"questions":["..."]}. ` +
|
|
248
|
+
`Never make the question your entire reply when the user asked for content — do NOT ` +
|
|
249
|
+
`guess on what's genuinely blocked, but always answer what you can first. ` +
|
|
227
250
|
`Otherwise complete the task and end with a concise summary of what you did.\n\n`;
|
|
228
251
|
|
|
229
252
|
// ─── Serial run queue (v0.34) ───────────────────────────────────────────────
|
|
@@ -507,14 +530,24 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
507
530
|
emitTerminal('chat:run:complete', { tabId, sessionId, finalMessage: text });
|
|
508
531
|
if (typeof onSilentResult === 'function') onSilentResult(text);
|
|
509
532
|
} else {
|
|
510
|
-
const signal =
|
|
533
|
+
const signal = splitStopSignal(text);
|
|
511
534
|
if (signal) {
|
|
512
535
|
emitTerminal('chat:run:needs-input', {
|
|
513
536
|
tabId,
|
|
514
537
|
sessionId,
|
|
515
538
|
questions: signal.questions,
|
|
539
|
+
answerBody: signal.answerBody,
|
|
516
540
|
raw: text,
|
|
517
541
|
});
|
|
542
|
+
// Record durable exchange off the hot path — UI must not wait on Haiku
|
|
543
|
+
recordExchange({
|
|
544
|
+
sessionId,
|
|
545
|
+
cwd,
|
|
546
|
+
prompt,
|
|
547
|
+
result: `${signal.answerBody}\n\n[asked: ${signal.questions.join(' | ')}]`,
|
|
548
|
+
}).catch((err) => {
|
|
549
|
+
console.error('[chatRunner] recordExchange failed:', err?.message ?? err);
|
|
550
|
+
});
|
|
518
551
|
} else {
|
|
519
552
|
emitTerminal('chat:run:complete', { tabId, sessionId, finalMessage: text });
|
|
520
553
|
// Record durable exchange off the hot path — UI must not wait on Haiku
|
|
@@ -640,6 +673,7 @@ module.exports = {
|
|
|
640
673
|
attachWindow,
|
|
641
674
|
registerChatHandlers,
|
|
642
675
|
parseStopSignal,
|
|
676
|
+
splitStopSignal,
|
|
643
677
|
parseContextUsageMarkdown,
|
|
644
678
|
probeContextUsage,
|
|
645
679
|
STOP_SENTINEL,
|
package/src/main/config.cjs
CHANGED
|
@@ -134,6 +134,14 @@ function validateWrite(realAbs) {
|
|
|
134
134
|
if (realAbs === browserSub || realAbs.startsWith(browserSub + path.sep)) {
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
|
+
// Feedback inbox writes (scheduler RCA hook — rcaFeedbackHook.cjs — and
|
|
138
|
+
// any interactive feedback filing): narrowly scoped to
|
|
139
|
+
// session-manager-operations/feedback/, this repo's existing
|
|
140
|
+
// per-project intake convention.
|
|
141
|
+
const feedbackSub = path.join(realRoot, 'session-manager-operations', 'feedback');
|
|
142
|
+
if (realAbs === feedbackSub || realAbs.startsWith(feedbackSub + path.sep)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
137
145
|
}
|
|
138
146
|
}
|
|
139
147
|
throw new Error(`Write outside allowed write boundaries: ${realAbs}`);
|