claude-code-session-manager 0.75.1 → 0.75.3

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 (50) hide show
  1. package/dist/assets/{AgentLibrary-CGplUKxY.js → AgentLibrary-CzQqcObq.js} +1 -1
  2. package/dist/assets/{DataModel-DQ15KxVb.js → DataModel-Bj_WlLz8.js} +1 -1
  3. package/dist/assets/{History-v2TSC_6A.js → History-DnSi_OHm.js} +1 -1
  4. package/dist/assets/{Hooks-Co7s6BNB.js → Hooks-0BB0dp3S.js} +1 -1
  5. package/dist/assets/{HostBilko-DJj8FWPK.js → HostBilko-DHpwwsLQ.js} +1 -1
  6. package/dist/assets/{Library-DQaC9VQ1.js → Library-CaJVqVvi.js} +1 -1
  7. package/dist/assets/{ListDetail-BwFRfAZm.js → ListDetail-C1W2HmC2.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-C155gNpc.js → MarkdownEditor-5Ob9FW3z.js} +1 -1
  9. package/dist/assets/{McpServers-BmLgE_gS.js → McpServers-JxCSfm1S.js} +1 -1
  10. package/dist/assets/{Memory-ISwNcS5j.js → Memory-BDeqlqwH.js} +1 -1
  11. package/dist/assets/{Panel-1adjEpj-.js → Panel-Dh9ZHuEj.js} +1 -1
  12. package/dist/assets/{Permissions-FrCz5cGa.js → Permissions-DXy-CbEY.js} +1 -1
  13. package/dist/assets/{Plugins-zUssKp_b.js → Plugins-_n1Iuc8T.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-CAXqA_HX.js → ProvenanceBadge-BP_evfxE.js} +1 -1
  15. package/dist/assets/{SaveBar-D9cqms3Y.js → SaveBar-D-gCUx4n.js} +1 -1
  16. package/dist/assets/{Scheduler-dNNc9S11.js → Scheduler-Bpd4OGju.js} +3 -3
  17. package/dist/assets/{ScopeSwitcher-1wBTne7S.js → ScopeSwitcher-CAWzM6RI.js} +1 -1
  18. package/dist/assets/{Settings-BT_expIg.js → Settings-DRRozLyT.js} +1 -1
  19. package/dist/assets/{SkillReferenceGraph-CVccq4Cu.js → SkillReferenceGraph-DGHDWlz4.js} +1 -1
  20. package/dist/assets/{Skills-xFpRY0ld.js → Skills-D8L66eiX.js} +1 -1
  21. package/dist/assets/{SystemPrompt-CEk_71th.js → SystemPrompt-CYtUsonD.js} +1 -1
  22. package/dist/assets/{TagLibrary-C2zhihQG.js → TagLibrary-E5CLeuVk.js} +1 -1
  23. package/dist/assets/{TiptapBody-CQKcHKaD.js → TiptapBody-B2hRgbPE.js} +1 -1
  24. package/dist/assets/{Toggle-CQ-IBb8m.js → Toggle-BTwsbxam.js} +1 -1
  25. package/dist/assets/{index-Dbo9R4e0.css → index-CMLnzdZC.css} +1 -1
  26. package/dist/assets/{index-BgbV3sKX.js → index-DijufvkJ.js} +4 -4
  27. package/dist/assets/{settingsSchema-D6ccSKqm.js → settingsSchema-D6wzxAi6.js} +1 -1
  28. package/dist/index.html +2 -2
  29. package/package.json +1 -1
  30. package/scripts/lib/activeSessions.cjs +35 -3
  31. package/scripts/scheduler-mcp-server.cjs +16 -1
  32. package/src/main/__tests__/prdAdminRoutes.test.cjs +100 -0
  33. package/src/main/__tests__/queueHistory.test.cjs +7 -0
  34. package/src/main/__tests__/scheduleJobSchema.test.cjs +1 -1
  35. package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +1 -1
  36. package/src/main/__tests__/scheduleJobTransitions.test.cjs +24 -0
  37. package/src/main/__tests__/scheduler-clear-queue-history.test.cjs +134 -0
  38. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +5 -1
  39. package/src/main/ipcSchemas.cjs +10 -0
  40. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +51 -0
  41. package/src/main/lib/__tests__/opsRootNestedWrite.test.cjs +51 -0
  42. package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +26 -0
  43. package/src/main/lib/prdAdminRoutes.cjs +12 -2
  44. package/src/main/lib/queueHistory.cjs +6 -4
  45. package/src/main/lib/queueStore.cjs +42 -6
  46. package/src/main/lib/scheduleJobSchema.cjs +1 -1
  47. package/src/main/lib/scheduleJobTransitions.cjs +19 -6
  48. package/src/main/lib/schedulerBatch.cjs +17 -1
  49. package/src/main/scheduler.cjs +89 -16
  50. package/src/preload/api.d.ts +1 -1
@@ -100,7 +100,7 @@ const queueOps = require('./queueOps.cjs');
100
100
  // home-dir layout.
101
101
  const { resolvePrdsDirs, resolveArchivedPrdsDirs, resolvePrdWriteDir, listEpicPrdDirs, listArchivedPrdDirs } = require('./lib/prdLocations.cjs');
102
102
  const { ensureEpic, appendPrdCreatedEvent, readActiveIndex } = require('./lib/epicMint.cjs');
103
- const { transitionJob } = require('./lib/scheduleJobTransitions.cjs');
103
+ const { transitionJob, STATUS_HISTORY_CAP } = require('./lib/scheduleJobTransitions.cjs');
104
104
  const { buildContextDigest, composeExecutorPrompt } = require('./lib/epicContextDigest.cjs');
105
105
  const { JOB_STATUSES } = require('./lib/scheduleJobSchema.cjs');
106
106
  const { appendAuditEvent } = require('./lib/auditLog.cjs');
@@ -1424,7 +1424,7 @@ async function reconcile(state) {
1424
1424
  seen.add(job.slug);
1425
1425
  next.push({ ...job });
1426
1426
  console.warn(`[scheduler] reconcile: keeping ${job.status} job ${job.slug} — PRD source not visible in any candidate dir`);
1427
- } else if (job.status === 'completed' || job.status === 'failed') {
1427
+ } else if (job.status === 'completed' || job.status === 'failed' || job.status === 'skipped') {
1428
1428
  terminalDroppedNeedingHistoryCheck.push(job);
1429
1429
  }
1430
1430
  continue;
@@ -2104,7 +2104,7 @@ async function clearPause(source) {
2104
2104
  * already-'completed' job, and that path is exactly what this guards.
2105
2105
  */
2106
2106
  function resetJobFields(job, errorMsg, opts = {}) {
2107
- if (job.status === 'completed' && opts.force !== true) return false;
2107
+ if ((job.status === 'completed' || job.status === 'skipped') && opts.force !== true) return false;
2108
2108
  if (!transitionJob(job, 'pending', { reason: errorMsg ?? 'reset to pending', source: opts.source ?? 'resetJobFields' })) return false;
2109
2109
  job.runId = null;
2110
2110
  job.startedAt = null;
@@ -3486,13 +3486,18 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
3486
3486
  // Stale queue entry: the PRD was archived (already shipped) or is gone
3487
3487
  // from disk entirely (deleted before dispatch). Both are retire-not-fail
3488
3488
  // results from prdArchivedSkipResult with exitCode 0 and no transcript —
3489
- // treat any truthy skip reason as a plain completion: no verify pass, no
3490
- // commit guard, no RCA feedback item.
3489
+ // no verify pass, no commit guard, no RCA feedback item — but only
3490
+ // 'prd-archived' actually means the work shipped (archivedTwinExists
3491
+ // found the file under a prds-archived/ dir). 'prd-missing' means no
3492
+ // executor ever ran at all, so it must NOT land on the same 'completed'
3493
+ // status a genuine successful run produces — that conflation is what let
3494
+ // 20 never-executed sigma PRDs read as shipped (2026-08-30 incident).
3491
3495
  if (res.skipped) {
3496
+ const targetStatus = res.skipped === 'prd-missing' ? 'skipped' : 'completed';
3492
3497
  await mutate((s) => {
3493
3498
  const idx = s.jobs.findIndex((x) => x.slug === job.slug);
3494
3499
  if (idx >= 0) {
3495
- transitionJob(s.jobs[idx], 'completed', { reason: res.note ?? 'PRD archived or missing — treated as already-shipped', source: 'spawnJob:skip-archived' });
3500
+ transitionJob(s.jobs[idx], targetStatus, { reason: res.note ?? 'PRD archived or missing — treated as already-shipped', source: 'spawnJob:skip-archived' });
3496
3501
  s.jobs[idx].finishedAt = new Date().toISOString();
3497
3502
  s.jobs[idx].exitCode = 0;
3498
3503
  s.jobs[idx].error = null;
@@ -4297,7 +4302,7 @@ async function pollLoop() {
4297
4302
  // callers/tests that only pass `jobs` are unaffected.
4298
4303
  function selectHistoryJobs(jobs, limit, historyEntries = []) {
4299
4304
  const cap = Math.max(1, Math.min(500, Number.isFinite(limit) ? Math.floor(limit) : 50));
4300
- const hot = (Array.isArray(jobs) ? jobs : []).filter((j) => j && (j.status === 'completed' || j.status === 'failed'));
4305
+ const hot = (Array.isArray(jobs) ? jobs : []).filter((j) => j && (j.status === 'completed' || j.status === 'failed' || j.status === 'skipped'));
4301
4306
  const seen = new Set(hot.map((j) => `${j.slug}|${j.runId ?? ''}`));
4302
4307
  const archived = (Array.isArray(historyEntries) ? historyEntries : []).filter((j) => {
4303
4308
  if (!j) return false;
@@ -4743,6 +4748,43 @@ async function reverifyNeedsReview() {
4743
4748
  return { rescanned: candidates.length, healed, leftForReview };
4744
4749
  }
4745
4750
 
4751
+ /**
4752
+ * schedule:clear-queue's core state mutation, split out so it's testable
4753
+ * without ipcMain/electron: every victim (non-running job whose slug is in
4754
+ * `victimSlugs`) must leave a durable history.jsonl record before it
4755
+ * disappears from queue.json — a manual clear used to just filter the row
4756
+ * out with no trace (a job could vanish with zero audit trail; see the
4757
+ * 2026-08-30 sigma `788-pr-sweep-final-gate` incident). Non-terminal victims
4758
+ * (pending/investigating/needs_review/quarantined) are force-transitioned to
4759
+ * 'skipped' — never 'completed', since a manually-cleared job did not ship.
4760
+ * Victims already terminal (completed/failed/skipped) keep their real
4761
+ * status; they just get a statusHistory note + a history row so they don't
4762
+ * silently drop before aging past HISTORY_RETENTION_MS.
4763
+ *
4764
+ * Mutates `state.jobs` in place (drops the victims, same contract as
4765
+ * reconcile()) and returns the job snapshots to append to history.jsonl.
4766
+ */
4767
+ function applyClearQueueVictims(state, victimSlugs, archiveNoteBySlug, archiveDir) {
4768
+ const historyEntries = [];
4769
+ for (const job of state.jobs) {
4770
+ if (!victimSlugs.has(job.slug)) continue;
4771
+ const reason = archiveNoteBySlug?.get(job.slug) ?? `PRD archived to ${archiveDir}`;
4772
+ if (job.status === 'completed' || job.status === 'failed' || job.status === 'skipped') {
4773
+ // Already terminal — keep the real status, just record the clear.
4774
+ const history = Array.isArray(job.statusHistory) ? job.statusHistory : [];
4775
+ history.push({ from: job.status, to: job.status, reason, source: 'clear-queue', at: new Date().toISOString() });
4776
+ while (history.length > STATUS_HISTORY_CAP) history.shift();
4777
+ job.statusHistory = history;
4778
+ } else {
4779
+ transitionJob(job, 'skipped', { reason, source: 'clear-queue' });
4780
+ }
4781
+ if (!job.finishedAt) job.finishedAt = new Date().toISOString();
4782
+ historyEntries.push({ ...job });
4783
+ }
4784
+ state.jobs = state.jobs.filter((j) => !victimSlugs.has(j.slug));
4785
+ return historyEntries;
4786
+ }
4787
+
4746
4788
  function registerScheduleHandlers() {
4747
4789
  ensureDirs();
4748
4790
  supervisor.registerHandlers();
@@ -4905,10 +4947,6 @@ function registerScheduleHandlers() {
4905
4947
  return { ok: true, kind: 'info', message };
4906
4948
  });
4907
4949
 
4908
- // Archive every non-running PRD and drop its entry from queue.json.
4909
- // Running entries are kept (would orphan an in-flight job). PRD files are
4910
- // moved (not deleted) to prds-archived/<ISO>/ so the user can recover them.
4911
- // Path containment is enforced — only files inside PRDS_DIR are moved.
4912
4950
  ipcMain.handle('schedule:clear-queue', async () => {
4913
4951
  ensureDirs();
4914
4952
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
@@ -4920,6 +4958,7 @@ function registerScheduleHandlers() {
4920
4958
  }
4921
4959
  await fsp.mkdir(archiveDir, { recursive: true });
4922
4960
  let archived = 0;
4961
+ const archiveNoteBySlug = new Map();
4923
4962
  for (const job of victims) {
4924
4963
  const srcDir = await findPrdDir(job.slug) ?? prdDirForCwd(job.cwd);
4925
4964
  const src = path.resolve(path.join(srcDir, `${job.slug}.md`));
@@ -4928,17 +4967,27 @@ function registerScheduleHandlers() {
4928
4967
  try {
4929
4968
  await fsp.rename(src, dst);
4930
4969
  archived++;
4970
+ archiveNoteBySlug.set(job.slug, `PRD file archived to ${archiveDir}`);
4931
4971
  } catch (e) {
4932
4972
  // ENOENT: the .md is already gone (reconcile would drop it on next
4933
4973
  // read anyway). Either way, fall through and remove from queue.
4934
4974
  if (e?.code !== 'ENOENT') {
4935
4975
  logs.writeLine({ level: 'warn', scope: 'scheduler', message: 'clear-queue: rename failed', meta: { slug: job.slug, error: e?.message } });
4936
4976
  }
4977
+ archiveNoteBySlug.set(job.slug, `PRD file already gone (ENOENT) when archiving to ${archiveDir}`);
4937
4978
  }
4938
4979
  }
4939
4980
  await mutate(async (s) => {
4940
4981
  const victimSlugs = new Set(victims.map((j) => j.slug));
4941
- s.jobs = s.jobs.filter((j) => !victimSlugs.has(j.slug));
4982
+ const historyEntries = applyClearQueueVictims(s, victimSlugs, archiveNoteBySlug, archiveDir);
4983
+ // Append BEFORE this mutate's writeQueue persists s.jobs without the
4984
+ // victims (mutate() only writes after this callback resolves) — same
4985
+ // append-before-drop ordering queueHistory.cjs's reconcile() callers
4986
+ // use, so a crash right after this handler returns can never leave a
4987
+ // row gone from queue.json with nothing in history.jsonl yet.
4988
+ if (historyEntries.length > 0) {
4989
+ await queueHistory.appendHistory(historyEntries);
4990
+ }
4942
4991
  await reconcile(s);
4943
4992
  return null;
4944
4993
  });
@@ -5353,6 +5402,20 @@ async function init() {
5353
5402
  }
5354
5403
  }
5355
5404
 
5405
+ // Measured against this repo's own live+archived PRD set (868 entries
5406
+ // across every local project — the admin route has no cwd filter by
5407
+ // default): no single field dominates a listPrdsInternal() entry: title is
5408
+ // the largest at ~30% of payload bytes, followed by slug/cwd/sourcePromptId/
5409
+ // epicId at 12-14% each — the 353KB the route was shipping came from
5410
+ // returning the full unbounded row count, not from one oversized field.
5411
+ // This trims the fields an agent doesn't need to answer "what PRDs are
5412
+ // there" (parallelGroup/estimateMinutes/sourcePromptId/epicId/
5413
+ // archivedStatus); `fields=full` restores them.
5414
+ function toCompactPrdEntry(entry) {
5415
+ const { slug, title, cwd, mtimeMs, archived, status } = entry;
5416
+ return { slug, title, cwd, mtimeMs, archived, status };
5417
+ }
5418
+
5356
5419
  /**
5357
5420
  * listPrdsInternal() → every live + archived PRD across every project,
5358
5421
  * with each entry's real job status folded in (`status`: the live queue
@@ -5612,15 +5675,25 @@ const remote = {
5612
5675
 
5613
5676
  // Every live+archived PRD across every project (listPrdsInternal, shared
5614
5677
  // with the renderer's schedule:list-prds IPC handler), filtered by the
5615
- // admin route's cwd/epicId/status query params.
5678
+ // admin route's cwd/epicId/status query params and paged by
5679
+ // limit/offset. Sort order is listPrdsInternal's own slug.localeCompare
5680
+ // (numeric) — stable across calls since it's a pure string sort, so
5681
+ // paging by offset can't skip or duplicate an entry between requests as
5682
+ // long as the underlying PRD set doesn't change mid-page.
5616
5683
  async listPrds(filter = {}) {
5617
5684
  const all = await listPrdsInternal();
5618
- return all.filter((entry) => {
5685
+ const filtered = all.filter((entry) => {
5619
5686
  if (filter.cwd && entry.cwd !== filter.cwd) return false;
5620
5687
  if (filter.epicId && entry.epicId !== filter.epicId) return false;
5621
5688
  if (filter.status && entry.status !== filter.status) return false;
5622
5689
  return true;
5623
5690
  });
5691
+ const total = filtered.length;
5692
+ const limit = filter.limit ?? 100;
5693
+ const offset = filter.offset ?? 0;
5694
+ const page = filtered.slice(offset, offset + limit);
5695
+ const prds = filter.fields === 'full' ? page : page.map(toCompactPrdEntry);
5696
+ return { prds, total, limit, offset, hasMore: offset + page.length < total };
5624
5697
  },
5625
5698
 
5626
5699
  // Full body + parsed frontmatter for one PRD, live or archived. Mirrors
@@ -5759,7 +5832,7 @@ const remote = {
5759
5832
  const state = await readQueue();
5760
5833
  const job = state.jobs.find((j) => j.slug === slug);
5761
5834
  if (!job) return { ok: false, error: 'not found' };
5762
- if (job.status === 'completed' || job.status === 'failed' || job.status === 'needs_review') {
5835
+ if (job.status === 'completed' || job.status === 'failed' || job.status === 'needs_review' || job.status === 'skipped') {
5763
5836
  return { ok: false, error: `job already terminal (status: "${job.status}") — nothing to cancel` };
5764
5837
  }
5765
5838
  const wasRunning = job.status === 'running';
@@ -5821,4 +5894,4 @@ function registerAdminRoutes(adminHttp, remoteObj = remote) {
5821
5894
  });
5822
5895
  }
5823
5896
 
5824
- module.exports = { findOverrunningJobs, JOB_OVERRUN_FACTOR, JOB_OVERRUN_FLOOR_MS, registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, healRefusalReason, writeQueue, reconcile, reconcileSourcePromptId, allocateParallelGroup, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, isEligibleForImmediateAutoFix, resolveRunId, isUnresolvableNeedsReview, isExhaustedAutoFix, isPlanUnqueued, fixSlugFor, healTargetForFix, buildInvestigationPrompt, isGitRepoSync, committedInWindow, computeCommittedDuringRun, classifySigtermWithCommit, isFixPlanSlug, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH, forceTickOutcome, applyPauseCleared, detectNetworkErrorInLog, detectRateLimitInLog, classifyFailureOutcome, commitGuardVerdict, TRANSIENT_RETRY_CAP, buildScheduleStatePayload, partitionBootOrphans, applyOrphanOutcome, BOOT_ORPHAN_KILL_GRACE_MS, registerAdminRoutes, notifyOriginatingTab, notifyNeedsReview, isNotifiableTerminalStatus, extractResultTextFromLog, candidatePrdsDirs, candidateArchivedPrdsDirs, resolveArchivedPrdStatus, prdDirForCwd, prdPathForJob, archivedPrdPathForJob, archivedTwinExists, findPrdDir, resolveVerifyPrdPath, resolveFixPlanPath, resolveNotifyPrd, runPrdMigration, consolidateAllFlatPrds, shouldSkipInvestigationForCleanRun, archiveCompletedPrd, retireCompletedSlugs, SCHEDULER_BOOTED_AT, SCHEDULER_CODE_SHA, resetJobFields, executeJob, prdArchivedSkipResult, spawnJob, listPrdsInternal, computeStallSummary, findStaleQuarantinedJobs, QUARANTINE_ESCALATE_MS };
5897
+ module.exports = { findOverrunningJobs, JOB_OVERRUN_FACTOR, JOB_OVERRUN_FLOOR_MS, registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, healRefusalReason, writeQueue, reconcile, reconcileSourcePromptId, allocateParallelGroup, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, isEligibleForImmediateAutoFix, resolveRunId, isUnresolvableNeedsReview, isExhaustedAutoFix, isPlanUnqueued, fixSlugFor, healTargetForFix, buildInvestigationPrompt, isGitRepoSync, committedInWindow, computeCommittedDuringRun, classifySigtermWithCommit, isFixPlanSlug, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH, forceTickOutcome, applyPauseCleared, detectNetworkErrorInLog, detectRateLimitInLog, classifyFailureOutcome, commitGuardVerdict, TRANSIENT_RETRY_CAP, buildScheduleStatePayload, partitionBootOrphans, applyOrphanOutcome, BOOT_ORPHAN_KILL_GRACE_MS, registerAdminRoutes, notifyOriginatingTab, notifyNeedsReview, isNotifiableTerminalStatus, extractResultTextFromLog, candidatePrdsDirs, candidateArchivedPrdsDirs, resolveArchivedPrdStatus, prdDirForCwd, prdPathForJob, archivedPrdPathForJob, archivedTwinExists, findPrdDir, resolveVerifyPrdPath, resolveFixPlanPath, resolveNotifyPrd, runPrdMigration, consolidateAllFlatPrds, shouldSkipInvestigationForCleanRun, archiveCompletedPrd, retireCompletedSlugs, SCHEDULER_BOOTED_AT, SCHEDULER_CODE_SHA, resetJobFields, executeJob, prdArchivedSkipResult, spawnJob, listPrdsInternal, computeStallSummary, findStaleQuarantinedJobs, QUARANTINE_ESCALATE_MS, applyClearQueueVictims };
@@ -391,7 +391,7 @@ export interface ScheduleConfig {
391
391
  * so this union is a manually-kept copy; drift from JOB_STATUSES (and from
392
392
  * the renderer's own mirrors in StatusBadge.tsx/SchedulePanel.tsx) is
393
393
  * caught by src/main/__tests__/scheduleJobStatusDrift.test.cjs. */
394
- export type ScheduleJobStatus = 'pending' | 'running' | 'investigating' | 'completed' | 'failed' | 'needs_review' | 'quarantined';
394
+ export type ScheduleJobStatus = 'pending' | 'running' | 'investigating' | 'completed' | 'skipped' | 'failed' | 'needs_review' | 'quarantined';
395
395
 
396
396
  export interface ScheduleJobRuntime {
397
397
  pid: number;