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.
Files changed (34) hide show
  1. package/dist/assets/{TiptapBody-D5b7nejd.js → TiptapBody-DXQDiOUx.js} +58 -58
  2. package/dist/assets/index--a8m5_ET.js +3496 -0
  3. package/dist/assets/index-CTTjT08J.css +32 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +2 -1
  6. package/plugins/session-manager-dev/skills/develop/SKILL.md +5 -1
  7. package/scripts/lib/activeSessions.cjs +117 -0
  8. package/scripts/lib/watchdogHelpers.cjs +699 -0
  9. package/src/main/__tests__/broadcastCoalescer.test.cjs +104 -0
  10. package/src/main/__tests__/historyDashboard.test.cjs +163 -0
  11. package/src/main/__tests__/historyRollup.test.cjs +333 -0
  12. package/src/main/__tests__/prdParserHighWater.test.cjs +74 -0
  13. package/src/main/__tests__/queueHistory.test.cjs +233 -0
  14. package/src/main/__tests__/queueOpsAutoArchive.test.cjs +142 -0
  15. package/src/main/__tests__/rcaFeedbackHook.test.cjs +259 -0
  16. package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +51 -0
  17. package/src/main/chatRunner.cjs +46 -12
  18. package/src/main/config.cjs +8 -0
  19. package/src/main/historyAggregator.cjs +391 -50
  20. package/src/main/historyDashboard.cjs +226 -0
  21. package/src/main/index.cjs +35 -1
  22. package/src/main/ipcSchemas.cjs +5 -0
  23. package/src/main/lib/broadcastCoalescer.cjs +46 -0
  24. package/src/main/lib/historyRollup.cjs +148 -0
  25. package/src/main/lib/queueHistory.cjs +216 -0
  26. package/src/main/lib/rcaFeedbackHook.cjs +346 -0
  27. package/src/main/lib/schedulerConfig.cjs +16 -0
  28. package/src/main/queueOps.cjs +92 -0
  29. package/src/main/scheduler/prdParser.cjs +52 -1
  30. package/src/main/scheduler.cjs +237 -29
  31. package/src/preload/api.d.ts +69 -1
  32. package/src/preload/index.cjs +1 -0
  33. package/dist/assets/index-CkiGRskz.css +0 -32
  34. 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
+ });
@@ -54,30 +54,49 @@ const { extractJson } = require('./lib/extractJson.cjs');
54
54
  const STOP_SENTINEL = '<<<SM_NEEDS_INPUT>>>';
55
55
 
56
56
  /**
57
- * Parse the final assistant text for the stop-signal protocol.
57
+ * Split the final assistant text at the stop-signal sentinel.
58
58
  *
59
- * Returns `{ questions: string[] }` when the sentinel is present and a
60
- * balanced JSON object with a `questions` array can be found anywhere in the
61
- * text after it tolerant of leading blank lines, code-fence wrapping,
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 parseStopSignal(finalText) {
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
- return { questions: parsed.questions };
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: If you need clarification from the user before you can continue, ` +
224
- `do NOT guess finish your turn by emitting, as the very last line, exactly:\n` +
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 = parseStopSignal(text);
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,
@@ -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}`);