claude-code-session-manager 0.88.0 → 0.89.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-BZ6IY2g1.js → AgentLibrary-tuj-j1kQ.js} +1 -1
- package/dist/assets/{DataModel-dAoGAODX.js → DataModel-DOim5SXW.js} +1 -1
- package/dist/assets/{History-CF157HyX.js → History-DylcasUk.js} +1 -1
- package/dist/assets/{Hooks-CCPPipBk.js → Hooks--2xoag9a.js} +1 -1
- package/dist/assets/{HostBilko-Dlmz2m06.js → HostBilko-oaw8paAh.js} +1 -1
- package/dist/assets/{Library-BaXcboWX.js → Library-DzKA09Iy.js} +1 -1
- package/dist/assets/{ListDetail-CoUCzG5S.js → ListDetail-CEdcO80v.js} +1 -1
- package/dist/assets/{MarkdownEditor-DTg3JlOT.js → MarkdownEditor-BMyo0F7s.js} +1 -1
- package/dist/assets/{McpServers-DdjY1sA3.js → McpServers-BB7ayW7W.js} +1 -1
- package/dist/assets/{Memory-BJ3Jlw0C.js → Memory-BOdYvrL4.js} +1 -1
- package/dist/assets/{Panel-ygRSgc41.js → Panel-Yf3SxsRv.js} +1 -1
- package/dist/assets/{Permissions-m0KAnccF.js → Permissions-BYCpHNy5.js} +1 -1
- package/dist/assets/{Plugins-CJRl6B3Q.js → Plugins-B0lXiYqV.js} +2 -2
- package/dist/assets/{ProvenanceBadge-w8cnlVvW.js → ProvenanceBadge-T7x8LWcV.js} +1 -1
- package/dist/assets/{SaveBar-BpyPcUIk.js → SaveBar-BdVb4DbR.js} +1 -1
- package/dist/assets/{Scheduler-D2ppxSfy.js → Scheduler-DlOCWioV.js} +1 -1
- package/dist/assets/{ScopeSwitcher-EyuIwt-j.js → ScopeSwitcher-WVhmNsL5.js} +1 -1
- package/dist/assets/{Settings-BHSGGFEc.js → Settings-Bt4dVsl6.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-BNaZ2JCF.js → SkillReferenceGraph-R-0YykV8.js} +1 -1
- package/dist/assets/{Skills-Dl3q75QV.js → Skills-DXV_LdJP.js} +1 -1
- package/dist/assets/{SystemPrompt-Bg6F-Cyp.js → SystemPrompt-DiQRo_WF.js} +1 -1
- package/dist/assets/{TagLibrary-BSutflCy.js → TagLibrary-B9OuqU6M.js} +1 -1
- package/dist/assets/{TiptapBody-CXih4xrc.js → TiptapBody-HKKmB0AE.js} +1 -1
- package/dist/assets/{Toggle-2qtUHhA9.js → Toggle-CBggPonH.js} +1 -1
- package/dist/assets/{index-B3emy2oI.js → index-BcJqLDiY.js} +3 -3
- package/dist/assets/{settingsSchema-CdzxaqwM.js → settingsSchema-BYE9DVw_.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/main/__tests__/health-tick-liveness.test.cjs +16 -0
- package/src/main/__tests__/scheduler-integration-failure-stamp.test.cjs +41 -0
- package/src/main/__tests__/scheduler-mechanical-recovery.test.cjs +23 -0
- package/src/main/__tests__/scheduler-stall-per-project.test.cjs +14 -0
- package/src/main/build-info.json +4 -4
- package/src/main/health.cjs +9 -1
- package/src/main/lib/__tests__/gitWorktree.test.cjs +172 -0
- package/src/main/lib/gitWorktree.cjs +195 -3
- package/src/main/lib/rcaReport.cjs +8 -1
- package/src/main/scheduler.cjs +98 -16
package/src/main/scheduler.cjs
CHANGED
|
@@ -1865,7 +1865,12 @@ function computeStallSummary(state) {
|
|
|
1865
1865
|
byProject[key].invalid = (byProject[key].invalid || 0) + 1;
|
|
1866
1866
|
}
|
|
1867
1867
|
const total = jobs.length + invalidJobs.length;
|
|
1868
|
-
|
|
1868
|
+
// A drained queue (every row completed/skipped) is idle, not stalled — a
|
|
1869
|
+
// stall needs at least one parked problem row (failed/needs_review/
|
|
1870
|
+
// quarantined/invalid) that is waiting on someone.
|
|
1871
|
+
const isProblemStatus = (st) => st !== 'completed' && st !== 'skipped';
|
|
1872
|
+
const stalled = total > 0 && running === 0 && pending === 0 && !state?.paused
|
|
1873
|
+
&& (invalidJobs.length > 0 || jobs.some((j) => isProblemStatus(j.status)));
|
|
1869
1874
|
for (const key of Object.keys(byProject)) {
|
|
1870
1875
|
const counts = byProject[key];
|
|
1871
1876
|
const projRunning = counts.running || 0;
|
|
@@ -1873,7 +1878,9 @@ function computeStallSummary(state) {
|
|
|
1873
1878
|
const projTotal = Object.keys(counts)
|
|
1874
1879
|
.filter((k) => k !== 'stalled')
|
|
1875
1880
|
.reduce((sum, k) => sum + counts[k], 0);
|
|
1876
|
-
|
|
1881
|
+
const projProblem = Object.keys(counts)
|
|
1882
|
+
.some((k) => k !== 'stalled' && k !== 'completed' && k !== 'skipped' && counts[k] > 0);
|
|
1883
|
+
counts.stalled = projTotal > 0 && projRunning === 0 && projPending === 0 && !state?.paused && projProblem;
|
|
1877
1884
|
}
|
|
1878
1885
|
return { stalled, total, running, pending, byProject };
|
|
1879
1886
|
}
|
|
@@ -4790,6 +4797,25 @@ As the LAST LINE of your final result text, emit exactly one of:
|
|
|
4790
4797
|
Print PASS only once the commit above has actually landed.`;
|
|
4791
4798
|
}
|
|
4792
4799
|
|
|
4800
|
+
/**
|
|
4801
|
+
* Stamps integrateBranch's failure subtype (`failureKind`) and conflicted
|
|
4802
|
+
* paths onto a job row at finalize; clears both when there is no failure so
|
|
4803
|
+
* a re-run never carries a stale subtype. Plain JSON values only.
|
|
4804
|
+
*/
|
|
4805
|
+
function stampIntegrationFailure(row, integration) {
|
|
4806
|
+
if (integration && integration.failureKind) {
|
|
4807
|
+
row.integrationFailureKind = integration.failureKind;
|
|
4808
|
+
if (Array.isArray(integration.conflictedPaths)) row.integrationConflictPaths = integration.conflictedPaths;
|
|
4809
|
+
else delete row.integrationConflictPaths;
|
|
4810
|
+
if (integration.baseHeadSha) row.integrationBaseHeadSha = integration.baseHeadSha;
|
|
4811
|
+
else delete row.integrationBaseHeadSha;
|
|
4812
|
+
} else {
|
|
4813
|
+
delete row.integrationFailureKind;
|
|
4814
|
+
delete row.integrationConflictPaths;
|
|
4815
|
+
delete row.integrationBaseHeadSha;
|
|
4816
|
+
}
|
|
4817
|
+
}
|
|
4818
|
+
|
|
4793
4819
|
/**
|
|
4794
4820
|
* Mechanical recovery (PRD 1130). isFixPlanBeyondDepthCap (below) is the
|
|
4795
4821
|
* ONLY gate on re-investigating a fix-plan job at investigationDepth >= 2 —
|
|
@@ -4802,14 +4828,21 @@ Print PASS only once the commit above has actually landed.`;
|
|
|
4802
4828
|
* auto-fix it authors no plan and spawns no model; it is pure git.
|
|
4803
4829
|
*
|
|
4804
4830
|
* The closed set of mechanically-resolvable verdicts starts at exactly
|
|
4805
|
-
* 'worktree_integration_failed'
|
|
4806
|
-
*
|
|
4807
|
-
*
|
|
4808
|
-
*
|
|
4809
|
-
*
|
|
4810
|
-
*
|
|
4811
|
-
*
|
|
4812
|
-
*
|
|
4831
|
+
* 'worktree_integration_failed', which covers two subtypes (job.
|
|
4832
|
+
* integrationFailureKind): `blocking_paths` — PRD 1125 taught integrateBranch
|
|
4833
|
+
* to parse git's "would be overwritten by merge" stderr, verify the blocking
|
|
4834
|
+
* paths are byte-identical to the branch, discard the proven duplicates and
|
|
4835
|
+
* retry once, so a re-call can genuinely succeed once the dirty files are
|
|
4836
|
+
* gone — and `content_conflict`, which PRD 1125 explicitly does NOT handle
|
|
4837
|
+
* (parseBlockingMergePaths returns null for it). Only pure-addition text
|
|
4838
|
+
* conflicts are auto-resolved (mc-02, gitWorktree.cjs, kill switch
|
|
4839
|
+
* SM_PURE_ADDITION_MERGE_DISABLE=1) — and that already ran inside the
|
|
4840
|
+
* original failed integration. Re-calling integrateBranch for a content
|
|
4841
|
+
* conflict that survived it, against an unmoved base HEAD, fails
|
|
4842
|
+
* deterministically, so the selector skips it (WITHOUT spending the one
|
|
4843
|
+
* attempt) until HEAD moves — a sibling merge may make the retry succeed.
|
|
4844
|
+
* A job parked with this verdict has its `sm-job/<slug>` branch preserved
|
|
4845
|
+
* (cleanupJobWorktree's `keepBranch: !integration.ok`).
|
|
4813
4846
|
*
|
|
4814
4847
|
* Bounded to exactly one attempt via job.mechanicalRecoveryAttempted,
|
|
4815
4848
|
* stamped in the SAME mutate as the outcome (performMechanicalRecovery,
|
|
@@ -4821,11 +4854,41 @@ Print PASS only once the commit above has actually landed.`;
|
|
|
4821
4854
|
*/
|
|
4822
4855
|
const MECHANICALLY_RESOLVABLE_VERDICTS = new Set(['worktree_integration_failed']);
|
|
4823
4856
|
|
|
4824
|
-
|
|
4857
|
+
/**
|
|
4858
|
+
* True when a retry is provably futile: a content_conflict whose base HEAD is
|
|
4859
|
+
* the same as at failure time. Unknown state (no stamp, null current HEAD)
|
|
4860
|
+
* is NOT futile — falls back to attempting the retry.
|
|
4861
|
+
*/
|
|
4862
|
+
function isMechanicalRecoveryFutile(job, currentHeadSha) {
|
|
4863
|
+
return job.integrationFailureKind === 'content_conflict'
|
|
4864
|
+
&& !!job.integrationBaseHeadSha
|
|
4865
|
+
&& !!currentHeadSha
|
|
4866
|
+
&& job.integrationBaseHeadSha === currentHeadSha;
|
|
4867
|
+
}
|
|
4868
|
+
|
|
4869
|
+
/**
|
|
4870
|
+
* Resolves the current HEAD once per distinct cwd for a pass. Returns a
|
|
4871
|
+
* `(job) => sha|null` lookup over `jobs`; the memo lives only as long as the
|
|
4872
|
+
* returned closure. O(distinct cwds) git calls, not O(jobs).
|
|
4873
|
+
*/
|
|
4874
|
+
async function resolveHeadShasForPass(jobs) {
|
|
4875
|
+
const byCwd = new Map();
|
|
4876
|
+
for (const j of jobs || []) {
|
|
4877
|
+
if (!j || j.status !== 'needs_review' || j.integrationFailureKind !== 'content_conflict') continue;
|
|
4878
|
+
const cwd = j.cwd || DEFAULT_PROJECT_CWD;
|
|
4879
|
+
if (!byCwd.has(cwd)) byCwd.set(cwd, gitHead(cwd));
|
|
4880
|
+
}
|
|
4881
|
+
const resolved = new Map();
|
|
4882
|
+
for (const [cwd, p] of byCwd) resolved.set(cwd, await p);
|
|
4883
|
+
return (j) => (j && resolved.get(j.cwd || DEFAULT_PROJECT_CWD)) || null;
|
|
4884
|
+
}
|
|
4885
|
+
|
|
4886
|
+
function selectMechanicalRecoveryTarget(job, currentHeadSha = null) {
|
|
4825
4887
|
if (process.env.SM_MECHANICAL_RECOVERY_DISABLE === '1') return null;
|
|
4826
4888
|
if (!job || job.status !== 'needs_review') return null;
|
|
4827
4889
|
if (!MECHANICALLY_RESOLVABLE_VERDICTS.has(job.verifierVerdict)) return null;
|
|
4828
4890
|
if (job.mechanicalRecoveryAttempted === true) return null;
|
|
4891
|
+
if (isMechanicalRecoveryFutile(job, currentHeadSha)) return null;
|
|
4829
4892
|
const cwd = job.cwd || DEFAULT_PROJECT_CWD;
|
|
4830
4893
|
return { slug: job.slug, cwd, branch: jobWorktree.branchNameFor(job.slug), carriedPaths: job.carriedPaths || [] };
|
|
4831
4894
|
}
|
|
@@ -6037,7 +6100,7 @@ async function spawnInvestigation(failedJob, runDir, { deadChild = null } = {})
|
|
|
6037
6100
|
// Mechanical recovery (PRD 1130): same first-refusal treatment — a job
|
|
6038
6101
|
// eligible for a pure-git retry must never also get a cold-read fix-plan
|
|
6039
6102
|
// PRD authored in the same pass.
|
|
6040
|
-
if (selectMechanicalRecoveryTarget(failedJob)) {
|
|
6103
|
+
if (selectMechanicalRecoveryTarget(failedJob, failedJob.integrationFailureKind === 'content_conflict' ? await gitHead(failedJob.cwd || DEFAULT_PROJECT_CWD) : null)) {
|
|
6041
6104
|
console.log(`[scheduler] skip investigation: ${failedJob.slug} is mechanical-recovery eligible`);
|
|
6042
6105
|
return { deferred: false };
|
|
6043
6106
|
}
|
|
@@ -6852,6 +6915,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
|
6852
6915
|
let res;
|
|
6853
6916
|
let worktreeLeftoverDirty = [];
|
|
6854
6917
|
let worktreeIntegrationFailure = null;
|
|
6918
|
+
let worktreeIntegrationDetail = null;
|
|
6855
6919
|
// Set only when integrateJobBranch's stderr-parsing auto-resolve fired
|
|
6856
6920
|
// (PRD 1125) — surfaced on the job row so the Queue UI can say the merge
|
|
6857
6921
|
// self-healed rather than silently looking like an ordinary merge.
|
|
@@ -6912,6 +6976,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
|
6912
6976
|
}
|
|
6913
6977
|
if (!integration.ok) {
|
|
6914
6978
|
worktreeIntegrationFailure = integration.reason;
|
|
6979
|
+
worktreeIntegrationDetail = integration;
|
|
6915
6980
|
console.error(`[scheduler] ${job.slug}: worktree branch integration FAILED (${integration.reason}) — branch ${worktree.branch} preserved in ${guardCwd} for manual recovery`);
|
|
6916
6981
|
} else if (integration.integrated) {
|
|
6917
6982
|
console.log(`[scheduler] ${job.slug}: worktree branch ${worktree.branch} integrated into ${guardCwd}${integration.mergeCommit ? ' (merge commit)' : ' (fast-forward)'}`);
|
|
@@ -7198,7 +7263,9 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
|
7198
7263
|
if (worktreeIntegrationFailure) {
|
|
7199
7264
|
verifyResult = {
|
|
7200
7265
|
verdict: 'worktree_integration_failed',
|
|
7201
|
-
reason:
|
|
7266
|
+
reason: worktreeIntegrationDetail && worktreeIntegrationDetail.failureKind === 'content_conflict'
|
|
7267
|
+
? `Integration blocked by a content conflict in ${(worktreeIntegrationDetail.conflictedPaths || []).join(', ') || 'unknown paths'} — branch ${jobWorktree.branchNameFor(job.slug)} preserved; needs a manual merge.`
|
|
7268
|
+
: `worktree branch integration failed: ${worktreeIntegrationFailure} — branch preserved for manual merge`,
|
|
7202
7269
|
downgradeTo: 'needs_review',
|
|
7203
7270
|
};
|
|
7204
7271
|
}
|
|
@@ -7313,6 +7380,9 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
|
7313
7380
|
let mechanicalRecoveryTarget = null;
|
|
7314
7381
|
let terminalNotifySnapshot = null;
|
|
7315
7382
|
const newlyCompletedPrds = [];
|
|
7383
|
+
// Base HEAD for the mechanical-recovery futility check — read here, never inside mutate() (no I/O).
|
|
7384
|
+
const finalizeHeadSha = worktreeIntegrationDetail && worktreeIntegrationDetail.failureKind === 'content_conflict'
|
|
7385
|
+
? await gitHead(guardCwd) : null;
|
|
7316
7386
|
await mutate((s) => {
|
|
7317
7387
|
const i2 = s.jobs.findIndex((x) => x.slug === job.slug);
|
|
7318
7388
|
// A job already moved off 'running' by someone else (namely
|
|
@@ -7504,6 +7574,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
|
7504
7574
|
} else if (!['blocked_by_foreign_wip_streak', 'budget_exceeded'].includes(s.jobs[i2].verifierVerdict)) {
|
|
7505
7575
|
delete s.jobs[i2].verifierVerdict;
|
|
7506
7576
|
}
|
|
7577
|
+
stampIntegrationFailure(s.jobs[i2], worktreeIntegrationFailure ? worktreeIntegrationDetail : null);
|
|
7507
7578
|
transitionJob(s.jobs[i2], effectiveStatus, { reason: finalizeReason, source: 'spawnJob:finalize' });
|
|
7508
7579
|
s.jobs[i2].finishedAt = new Date().toISOString();
|
|
7509
7580
|
s.jobs[i2].exitCode = res.exitCode;
|
|
@@ -7613,7 +7684,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
|
7613
7684
|
// depth-cap check, so it must never fall through to either.
|
|
7614
7685
|
// Snapshot only (no I/O inside mutate()); the actual git retry
|
|
7615
7686
|
// happens outside mutate(), below.
|
|
7616
|
-
const mTarget = selectMechanicalRecoveryTarget(s.jobs[i2]);
|
|
7687
|
+
const mTarget = selectMechanicalRecoveryTarget(s.jobs[i2], finalizeHeadSha);
|
|
7617
7688
|
if (mTarget) {
|
|
7618
7689
|
mechanicalRecoveryJob = { ...s.jobs[i2] };
|
|
7619
7690
|
mechanicalRecoveryTarget = mTarget;
|
|
@@ -10821,9 +10892,17 @@ async function reverifyNeedsReview() {
|
|
|
10821
10892
|
// check in spawnJob missed because the app restarted in between). Depth
|
|
10822
10893
|
// never disqualifies it, so it runs regardless of investigationDepth.
|
|
10823
10894
|
{
|
|
10895
|
+
const headFor = await resolveHeadShasForPass(queueForResumeAndAutofix.jobs);
|
|
10824
10896
|
for (const job of queueForResumeAndAutofix.jobs) {
|
|
10825
|
-
const
|
|
10826
|
-
|
|
10897
|
+
const head = headFor(job);
|
|
10898
|
+
const target = selectMechanicalRecoveryTarget(job, head);
|
|
10899
|
+
if (!target) {
|
|
10900
|
+
if (job.status === 'needs_review' && job.mechanicalRecoveryAttempted !== true
|
|
10901
|
+
&& MECHANICALLY_RESOLVABLE_VERDICTS.has(job.verifierVerdict) && isMechanicalRecoveryFutile(job, head)) {
|
|
10902
|
+
console.log(`[scheduler] mechanical-recovery: skip ${job.slug} — content conflict, base HEAD unchanged since failure`);
|
|
10903
|
+
}
|
|
10904
|
+
continue;
|
|
10905
|
+
}
|
|
10827
10906
|
console.log(`[scheduler] mechanical-recovery: needs_review ${job.slug} → re-integrating ${target.branch}`);
|
|
10828
10907
|
performMechanicalRecovery(job, target).catch((e) => {
|
|
10829
10908
|
console.error('[scheduler] performMechanicalRecovery error', job.slug, e);
|
|
@@ -12591,6 +12670,9 @@ module.exports = {
|
|
|
12591
12670
|
buildClaudeSpawnArgs,
|
|
12592
12671
|
spawnResumeRecovery,
|
|
12593
12672
|
selectMechanicalRecoveryTarget,
|
|
12673
|
+
isMechanicalRecoveryFutile,
|
|
12674
|
+
resolveHeadShasForPass,
|
|
12675
|
+
stampIntegrationFailure,
|
|
12594
12676
|
performMechanicalRecovery,
|
|
12595
12677
|
MECHANICALLY_RESOLVABLE_VERDICTS,
|
|
12596
12678
|
selectLeftoverQuarantineTarget,
|