claude-code-session-manager 0.124.0 → 0.125.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.
Files changed (46) hide show
  1. package/dist/assets/{AgentLibrary-tsHX4uUD.js → AgentLibrary-CcnnJg8P.js} +1 -1
  2. package/dist/assets/{Button-BC94OZc0.js → Button-sA4lqAao.js} +1 -1
  3. package/dist/assets/{DataModel-DX3sReg5.js → DataModel-BSTejIBU.js} +1 -1
  4. package/dist/assets/{History-DtscQica.js → History-DuwlZsQ5.js} +1 -1
  5. package/dist/assets/{Hooks-CQERyV2Q.js → Hooks-DHDSoDJ_.js} +1 -1
  6. package/dist/assets/{Library-DGZ1bv-H.js → Library-XA9osRfi.js} +1 -1
  7. package/dist/assets/{ListDetail-C6JB9n9x.js → ListDetail-B3goE3Wc.js} +1 -1
  8. package/dist/assets/{McpServers-C5G8m3j2.js → McpServers-cMZTsPS6.js} +1 -1
  9. package/dist/assets/{Memory-KgM5KHiQ.js → Memory-D6qntpZ0.js} +1 -1
  10. package/dist/assets/{Panel-BsOgpYZG.js → Panel-DIyJXHfp.js} +1 -1
  11. package/dist/assets/{Permissions-DruyM6MH.js → Permissions-DJda4Aii.js} +1 -1
  12. package/dist/assets/{Plugins-BBWfU2sh.js → Plugins-DcdRVBnU.js} +2 -2
  13. package/dist/assets/{ProvenanceBadge-CyzjcuYD.js → ProvenanceBadge-BSI_2wAv.js} +1 -1
  14. package/dist/assets/{SaveBar-D5nZclkV.js → SaveBar-CeL4vXRp.js} +1 -1
  15. package/dist/assets/{Scheduler-BuxUAv-X.js → Scheduler-CTd19y5U.js} +1 -1
  16. package/dist/assets/{ScopeSwitcher-BFTsmyXK.js → ScopeSwitcher-BMiHySnz.js} +1 -1
  17. package/dist/assets/{Settings-CsU2wgPl.js → Settings-D-oIEKZy.js} +1 -1
  18. package/dist/assets/{SkillReferenceGraph-WTwaKQBk.js → SkillReferenceGraph-B_7WfFjn.js} +1 -1
  19. package/dist/assets/{Skills-DooJ8MqG.js → Skills-CKpgxhSS.js} +1 -1
  20. package/dist/assets/{SystemPrompt-DjWMf9Rk.js → SystemPrompt-BjJPTx_a.js} +1 -1
  21. package/dist/assets/{TagLibrary-DN8yO-eg.js → TagLibrary-DJBB4gL0.js} +1 -1
  22. package/dist/assets/{TiptapBody-DlbOZsU-.js → TiptapBody-C9Cu2GBL.js} +1 -1
  23. package/dist/assets/{Toggle-BgPR-zkl.js → Toggle-DbIRTOix.js} +1 -1
  24. package/dist/assets/{ViewTabs-Bsqxjjs8.js → ViewTabs-BDpHur6l.js} +1 -1
  25. package/dist/assets/{historyTotals-Dw4jn69k.js → historyTotals-WCJHsBMq.js} +1 -1
  26. package/dist/assets/{index-DCbAgcJt.js → index-BD0ufZqe.js} +3 -3
  27. package/dist/assets/{settingsSchema-Duk46cGB.js → settingsSchema-DmjBj3q7.js} +1 -1
  28. package/dist/assets/{skillFrontmatter-B8F8a18M.js → skillFrontmatter-Tn_XHajE.js} +1 -1
  29. package/dist/assets/{useAgentsDirWatch-0oP0fhBV.js → useAgentsDirWatch-Vz2iXtsQ.js} +1 -1
  30. package/dist/index.html +1 -1
  31. package/dist-web/assets/app.css +32 -0
  32. package/dist-web/assets/app.js +1479 -0
  33. package/dist-web/index.html +16 -0
  34. package/package.json +3 -3
  35. package/plugins/session-manager-dev/skills/builder/3-publish/SKILL.md +28 -10
  36. package/src/main/build-info.json +4 -4
  37. package/src/main/chatRunner.cjs +11 -5
  38. package/src/main/docEdit.cjs +8 -4
  39. package/src/main/ipcSchemas.cjs +4 -1
  40. package/src/main/lib/classifyPromptTicket.cjs +9 -4
  41. package/src/main/lib/claude/runClaudeP.cjs +7 -4
  42. package/src/main/lib/process/childStdin.cjs +39 -0
  43. package/src/main/lib/process/childWithLog.cjs +15 -1
  44. package/src/main/lib/scheduler/launchFailure.cjs +37 -1
  45. package/src/main/scheduler.cjs +111 -30
  46. package/src/main/supervisor.cjs +6 -2
@@ -3474,7 +3474,9 @@ const TRANSIENT_RETRY_CAP = 2;
3474
3474
  * transient (PRD 543/545 incident) — same bounded-retry treatment, not a
3475
3475
  * second parallel classifier.
3476
3476
  */
3477
- function classifyFailureOutcome({ exitCode, networkError, durationMs, transientRetries, newlyDirtyCount }) {
3477
+ function classifyFailureOutcome({ exitCode, networkError, durationMs, transientRetries, newlyDirtyCount, launchError }) {
3478
+ // The run never started (argv too large): not transient, not investigable.
3479
+ if (launchError) return { action: 'fail-launch', launchError };
3478
3480
  const signalTransient = (exitCode === 143 || exitCode === 137) && durationMs < IDLE_OUTPUT_KILL_MS;
3479
3481
  const networkTransient = networkError === true;
3480
3482
  const transient = signalTransient || networkTransient;
@@ -3677,37 +3679,87 @@ function capDirtyPaths(paths, cap = PRE_RUN_DIRTY_PATHS_CAP) {
3677
3679
  const FOREIGN_WIP_DELIMITER = '--- FOREIGN WORKING-TREE STATE (not your work) ---';
3678
3680
  const FOREIGN_WIP_END_DELIMITER = '--- END FOREIGN WORKING-TREE STATE ---';
3679
3681
 
3682
+ const FOREIGN_WIP_SECTION_MAX_BYTES = 16 * 1024;
3683
+ const FOREIGN_WIP_SUMMARY_RESERVE_BYTES = 1024;
3684
+ const FOREIGN_WIP_OMITTED_SENTENCE = 'The omitted paths are just as foreign: do not stage, commit, revert, or stash them either.';
3685
+
3686
+ /**
3687
+ * Pure: bound a foreign-WIP path list by count (PRE_RUN_DIRTY_PATHS_CAP) and
3688
+ * bytes. A trailing capDirtyPaths `+N more` marker is never listed; its N is
3689
+ * added to omittedCount. Omitted real paths are grouped by parent directory.
3690
+ * `fixedBytes` = byte length of the section's fixed sentences/delimiters.
3691
+ */
3692
+ function limitForeignWipPaths(paths, fixedBytes = 0) {
3693
+ const all = Array.isArray(paths) ? paths.filter(Boolean) : [];
3694
+ let omittedCount = 0;
3695
+ if (all.length && /^\+\d+ more$/.test(all[all.length - 1])) {
3696
+ omittedCount += Number(all.pop().slice(1).split(' ')[0]);
3697
+ }
3698
+ const budget = FOREIGN_WIP_SECTION_MAX_BYTES - fixedBytes - FOREIGN_WIP_SUMMARY_RESERVE_BYTES;
3699
+ const listed = [];
3700
+ let used = 0;
3701
+ let i = 0;
3702
+ for (; i < all.length && listed.length < PRE_RUN_DIRTY_PATHS_CAP; i++) {
3703
+ const lineBytes = Buffer.byteLength(` ${all[i]}\n`, 'utf8');
3704
+ if (used + lineBytes > budget) break;
3705
+ used += lineBytes;
3706
+ listed.push(all[i]);
3707
+ }
3708
+ const byDir = new Map();
3709
+ for (; i < all.length; i++) {
3710
+ const slash = all[i].lastIndexOf('/');
3711
+ const dir = slash >= 0 ? all[i].slice(0, slash + 1) : '(repo root)';
3712
+ byDir.set(dir, (byDir.get(dir) || 0) + 1);
3713
+ omittedCount += 1;
3714
+ }
3715
+ const omittedByDir = [...byDir.entries()].sort((x, y) => y[1] - x[1]);
3716
+ return { listed, omittedCount, omittedByDir };
3717
+ }
3718
+
3719
+ function foreignWipSummaryLine(omittedCount, omittedByDir) {
3720
+ const fmt = (n) => n.toLocaleString('en-US');
3721
+ const clip = (d) => (d.length > 120 ? `${d.slice(0, 117)}...` : d);
3722
+ const top = omittedByDir.slice(0, 5);
3723
+ const parts = top.map(([d, n]) => `${fmt(n)} under ${clip(d)}`);
3724
+ const rest = omittedByDir.slice(5).reduce((sum, [, n]) => sum + n, 0);
3725
+ if (rest > 0) parts.push(`${fmt(rest)} elsewhere`);
3726
+ const tail = parts.length ? `: ${parts.join(', ')}` : '';
3727
+ return ` ... and ${fmt(omittedCount)} more path(s) not listed${tail}`;
3728
+ }
3729
+
3680
3730
  /**
3681
3731
  * Pure: build the executor-prompt section warning about pre-existing dirty
3682
3732
  * paths this job does not own — either base WIP carried into an isolated
3683
3733
  * worktree (PRD 1094's carriedPaths, checked first since it's the more
3684
3734
  * specific/authoritative case) or the raw pre-run dirty snapshot of a shared
3685
3735
  * (non-isolated) tree. Returns '' when both lists are empty so a clean spawn
3686
- * produces a byte-identical prompt to before this section existed.
3736
+ * produces a byte-identical prompt to before this section existed. Both
3737
+ * branches are bounded by count and bytes (FOREIGN_WIP_SECTION_MAX_BYTES).
3687
3738
  */
3688
3739
  function buildForeignWipSection({ preRunDirtyPaths, carriedPaths } = {}) {
3689
3740
  const carried = Array.isArray(carriedPaths) ? carriedPaths.filter(Boolean) : [];
3690
- if (carried.length) {
3691
- return [
3692
- FOREIGN_WIP_DELIMITER,
3693
- 'This job is running in an isolated git worktree, but the following paths carry uncommitted base-tree work-in-progress that was carried into this checkout so the tree is self-consistent. The authoritative copy of these files lives in the MAIN tree, not this worktree.',
3694
- 'These files were already modified before this job started. They are NOT this job\'s work:',
3695
- ...carried.map((p) => ` ${p}`),
3696
- 'Do not stage, commit, revert, or stash these paths. A test failure confined to these paths is not this job\'s regression.',
3697
- FOREIGN_WIP_END_DELIMITER,
3698
- ].join('\n');
3699
- }
3700
3741
  const dirty = Array.isArray(preRunDirtyPaths) ? preRunDirtyPaths.filter(Boolean) : [];
3701
- if (dirty.length) {
3702
- return [
3703
- FOREIGN_WIP_DELIMITER,
3704
- 'This job is running in a SHARED working tree (not isolated in its own worktree). The following paths were already modified when this job started:',
3705
- ...dirty.map((p) => ` ${p}`),
3706
- 'These files are NOT this job\'s work. Do not stage, commit, revert, or stash them. A test failure confined to these paths is not this job\'s regression.',
3707
- FOREIGN_WIP_END_DELIMITER,
3708
- ].join('\n');
3709
- }
3710
- return '';
3742
+ if (!carried.length && !dirty.length) return '';
3743
+ const isCarried = carried.length > 0;
3744
+ const head = isCarried
3745
+ ? [
3746
+ FOREIGN_WIP_DELIMITER,
3747
+ 'This job is running in an isolated git worktree, but the following paths carry uncommitted base-tree work-in-progress that was carried into this checkout so the tree is self-consistent. The authoritative copy of these files lives in the MAIN tree, not this worktree.',
3748
+ 'These files were already modified before this job started. They are NOT this job\'s work:',
3749
+ ]
3750
+ : [
3751
+ FOREIGN_WIP_DELIMITER,
3752
+ 'This job is running in a SHARED working tree (not isolated in its own worktree). The following paths were already modified when this job started:',
3753
+ ];
3754
+ const tailLines = isCarried
3755
+ ? ['Do not stage, commit, revert, or stash these paths. A test failure confined to these paths is not this job\'s regression.', FOREIGN_WIP_END_DELIMITER]
3756
+ : ['These files are NOT this job\'s work. Do not stage, commit, revert, or stash them. A test failure confined to these paths is not this job\'s regression.', FOREIGN_WIP_END_DELIMITER];
3757
+ const fixedBytes = Buffer.byteLength([...head, ...tailLines, FOREIGN_WIP_OMITTED_SENTENCE].join('\n'), 'utf8') + 8;
3758
+ const { listed, omittedCount, omittedByDir } = limitForeignWipPaths(isCarried ? carried : dirty, fixedBytes);
3759
+ const omitted = omittedCount > 0
3760
+ ? [foreignWipSummaryLine(omittedCount, omittedByDir), FOREIGN_WIP_OMITTED_SENTENCE]
3761
+ : [];
3762
+ return [...head, ...listed.map((p) => ` ${p}`), ...omitted, ...tailLines].join('\n');
3711
3763
  }
3712
3764
 
3713
3765
  // A job whose validated BLOCKED_BY_FOREIGN_WIP claim keeps recurring against
@@ -4157,11 +4209,15 @@ const HEADLESS_DISALLOWED_TOOLS = Object.freeze([
4157
4209
  * inserted BEFORE that flag, never between `--disallowedTools` and its value.
4158
4210
  * `--disallowedTools` takes a variadic list, so another flag must always
4159
4211
  * follow it to end that list — `--output-format` does, right after. Never
4160
- * put `--disallowedTools` last or right before the prompt.
4212
+ * put `--disallowedTools` last.
4213
+ * The prompt is NOT in argv: callers send it on the child's stdin
4214
+ * (withChildAndLog's `stdinText`). Linux caps a single argv element at 128 KB,
4215
+ * and a large prompt failed with spawn E2BIG before the job started (the
4216
+ * 2026-10-26 incident). `claude -p` with no positional prompt reads stdin.
4161
4217
  */
4162
- function buildClaudeSpawnArgs({ prompt, model, effort, sessionId, resume, systemPrompt, toolScope }) {
4218
+ function buildClaudeSpawnArgs({ model, effort, sessionId, resume, systemPrompt, toolScope }) {
4163
4219
  return [
4164
- '-p', prompt,
4220
+ '-p',
4165
4221
  '--model', model,
4166
4222
  ...effortArgs(effort),
4167
4223
  ...(systemPrompt ? ['--append-system-prompt', systemPrompt] : []),
@@ -4685,7 +4741,6 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
4685
4741
  // Distinct comm (`sm-claude-job`) + slug-labelled argv0 for System Monitor.
4686
4742
  // Both keep the word `claude`, which the /\bclaude\b/ reaper gates need.
4687
4743
  const jobArgs = buildClaudeSpawnArgs({
4688
- prompt,
4689
4744
  model: personaResolution.model,
4690
4745
  effort: personaEffort,
4691
4746
  sessionId,
@@ -4695,6 +4750,17 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
4695
4750
  });
4696
4751
  const jobSpawn = claudeSpawnTarget('job', job.slug, claudeBin, jobArgs);
4697
4752
 
4753
+ // Pre-spawn argv guard: one oversized argument makes spawn throw E2BIG.
4754
+ const oversized = launchFailure.findOversizedArg(jobSpawn.args || jobArgs);
4755
+ if (oversized) {
4756
+ const errMsg = `argv too large: ${oversized.flag} is ${oversized.bytes} bytes, over the ${launchFailure.MAX_ARG_BYTES}-byte per-argument limit; not spawned`;
4757
+ safeLog(`[scheduler] ${errMsg}\n`);
4758
+ closeFd();
4759
+ config.writeJsonSync(metaPath, { slug: job.slug, cwd, sessionId, exitCode: -1, error: errMsg, startedAt, finishedAt: Date.now(), durationMs: 0, ...SCHEDULER_META_IDENTITY, ...setupMeta });
4760
+ resolve({ exitCode: -1, durationMs: 0, error: errMsg, launchError: 'argv_too_large', sessionId });
4761
+ return;
4762
+ }
4763
+
4698
4764
  const { child } = withChildAndLog({
4699
4765
  fd,
4700
4766
  logPath,
@@ -4702,6 +4768,7 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
4702
4768
  closeFd,
4703
4769
  spawn: {
4704
4770
  command: jobSpawn.command,
4771
+ stdinText: prompt,
4705
4772
  // Resume mode passes `--resume <sessionId>` (reconnect to the SAME
4706
4773
  // session) INSTEAD of `--session-id <sessionId>` (mint a new one) —
4707
4774
  // never both, see buildClaudeSpawnArgs.
@@ -4737,7 +4804,8 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
4737
4804
  // Sync write: inside a Promise executor callback; must flush meta
4738
4805
  // before resolve() so the spawnJob mutate() that follows sees it.
4739
4806
  config.writeJsonSync(metaPath, { slug: job.slug, cwd, sessionId, exitCode: -1, error: errMsg, startedAt, finishedAt: Date.now(), durationMs, leakedDescendants: leaked, ...SCHEDULER_META_IDENTITY, ...setupMeta, originSessionId, contextDigestApplied });
4740
- resolve({ exitCode: -1, durationMs, error: errMsg, leakedDescendants: leaked, sessionId });
4807
+ resolve({ exitCode: -1, durationMs, error: errMsg, leakedDescendants: leaked, sessionId,
4808
+ ...(launchFailure.isArgvTooLargeSpawnError(error?.message) ? { launchError: 'argv_too_large' } : {}) });
4741
4809
  return;
4742
4810
  }
4743
4811
 
@@ -5277,8 +5345,9 @@ async function spawnInvestigation(failedJob, runDir, { deadChild = null } = {})
5277
5345
  closeFd,
5278
5346
  spawn: {
5279
5347
  command: probeSpawn.command,
5348
+ stdinText: prompt,
5280
5349
  args: [
5281
- '-p', prompt,
5350
+ '-p',
5282
5351
  '--model', 'opus',
5283
5352
  '--dangerously-skip-permissions',
5284
5353
  ...headlessPermissionArgs(),
@@ -6790,7 +6859,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
6790
6859
  // says WHY it ended without anyone opening the transcript.
6791
6860
  s.jobs[i2].terminalReason = launchFailure.deriveTerminalReason({
6792
6861
  effectiveStatus, exitCode: res.exitCode, verifyResult, sigtermOverride, worktreeIntegrationFailure,
6793
- budgetKill: !!budgetKill,
6862
+ budgetKill: !!budgetKill, launchError: res.launchError,
6794
6863
  });
6795
6864
  delete s.jobs[i2].launchFailure;
6796
6865
  delete s.jobs[i2].heldReason;
@@ -7100,9 +7169,19 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
7100
7169
  durationMs: res.durationMs,
7101
7170
  transientRetries: retries,
7102
7171
  newlyDirtyCount,
7172
+ launchError: res.launchError,
7103
7173
  });
7104
7174
 
7105
- if (decision.action === 'retry') {
7175
+ if (decision.action === 'fail-launch') {
7176
+ console.log(`[scheduler] launch failure (${decision.launchError}) for ${job.slug} — run never started; no investigation`);
7177
+ await mutate((s) => {
7178
+ const i = s.jobs.findIndex((x) => x.slug === job.slug);
7179
+ if (i >= 0) {
7180
+ s.jobs[i].error = `launch failure (${decision.launchError}): ${res.error}; the run never started, so no investigation was spawned`;
7181
+ }
7182
+ });
7183
+ await broadcast({ flush: true });
7184
+ } else if (decision.action === 'retry') {
7106
7185
  console.log(`[scheduler] transient failure (${decision.transientKind} dur=${res.durationMs}ms) — auto-retry ${decision.retries + 1}/${TRANSIENT_RETRY_CAP} for ${job.slug}`);
7107
7186
  await mutate((s) => {
7108
7187
  const i = s.jobs.findIndex((x) => x.slug === job.slug);
@@ -11243,6 +11322,8 @@ module.exports = {
11243
11322
  LEFTOVER_PATHS_CAP,
11244
11323
  capDirtyPaths,
11245
11324
  buildForeignWipSection,
11325
+ limitForeignWipPaths,
11326
+ FOREIGN_WIP_SECTION_MAX_BYTES,
11246
11327
  PRE_RUN_DIRTY_PATHS_CAP,
11247
11328
  FOREIGN_WIP_DELIMITER,
11248
11329
  FOREIGN_WIP_END_DELIMITER,
@@ -19,6 +19,7 @@ const { ipcMain } = require('electron');
19
19
  const { resolveClaudeBin, claudeSpawnTarget } = require('./lib/claude/claudeBin.cjs');
20
20
  const { headlessPermissionArgs, ensureCliCapsProbed } = require('./lib/claude/claudeCliCaps.cjs');
21
21
  const { withProcRole } = require('./lib/process/cleanEnv.cjs');
22
+ const { feedChildStdin } = require('./lib/process/childStdin.cjs');
22
23
  const schedulerPaths = require('./lib/scheduler/schedulerPaths.cjs');
23
24
 
24
25
  const HOME = os.homedir();
@@ -247,7 +248,7 @@ async function runProbe(claudeBin, prompt) {
247
248
  try {
248
249
  const target = claudeSpawnTarget('aux', 'supervisor', claudeBin);
249
250
  child = spawn(target.command, [
250
- '-p', prompt,
251
+ '-p',
251
252
  '--model', 'claude-haiku-5-5',
252
253
  '--no-session-persistence',
253
254
  '--output-format', 'json',
@@ -256,7 +257,9 @@ async function runProbe(claudeBin, prompt) {
256
257
  ...headlessPermissionArgs(),
257
258
  '--strict-mcp-config',
258
259
  '--allowedTools', 'Bash',
259
- ], { env: withProcRole(process.env, 'aux'), stdio: ['ignore', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
260
+ ], { env: withProcRole(process.env, 'aux'), stdio: ['pipe', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
261
+ // Prompt on stdin, not argv: a >128 KB argv element fails spawn with E2BIG.
262
+ feedChildStdin(child, prompt);
260
263
  } catch (e) {
261
264
  console.error('[supervisor] probe spawn failed:', e?.message);
262
265
  resolve({ verdict: 'ok', action: 'none', targetPid: null, reason: `spawn failed: ${e?.message}`, costUsd: null });
@@ -530,4 +533,5 @@ module.exports = {
530
533
  applyAction,
531
534
  registerHandlers,
532
535
  parseProbeOutput,
536
+ runProbe,
533
537
  };