fix(self-healing): use slim listTasks in maintenance recover loops
Batch 2 of runMaintenance() runs ~10 recover passes back-to-back, each
calling listTasks({ column: ... }) without slim. On busy boards this
materializes every task's activity log into memory ~10× per cycle,
walking the dashboard heap toward the 8 GB V8 limit until OOM. The
archive pass at line 610 already had this fix; extend it to the in-progress
and in-review recover passes that only read steps / paused / worktree /
mergeDetails / postReviewFixCount — all included in the slim projection.
Triage recovers are left non-slim because hasLatestSpecReviewApproval
scans task.log to find the most recent spec review; the triage column
is small so the memory cost is bounded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { loadAllAppCssBaseOnly } from "../test/cssFixture";
|
||||
|
||||
const css = loadAllAppCssBaseOnly();
|
||||
|
||||
function getSelectorBlock(selector: string): string {
|
||||
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const pattern = new RegExp(`${escaped}\\s*\\{([^}]*)\\}`, "m");
|
||||
const match = css.match(pattern);
|
||||
expect(match, `Expected ${selector} CSS block to exist`).not.toBeNull();
|
||||
return match?.[1] ?? "";
|
||||
}
|
||||
|
||||
function getSelectorZIndex(selector: string): number {
|
||||
const block = getSelectorBlock(selector);
|
||||
const match = block.match(/z-index:\s*(\d+)/);
|
||||
expect(match, `Expected ${selector} to define a z-index`).not.toBeNull();
|
||||
return Number(match?.[1]);
|
||||
}
|
||||
|
||||
describe("onboarding overlay layering contract (FN-2397)", () => {
|
||||
it("keeps modal overlays above sticky top banners", () => {
|
||||
const modalOverlayZ = getSelectorZIndex(".modal-overlay");
|
||||
const sessionBannerZ = getSelectorZIndex(".session-notification-banner");
|
||||
|
||||
expect(modalOverlayZ).toBeGreaterThan(sessionBannerZ);
|
||||
});
|
||||
|
||||
it("keeps the onboarding resume banner in normal document flow", () => {
|
||||
const onboardingResumeBlock = getSelectorBlock(".onboarding-resume-card");
|
||||
|
||||
expect(onboardingResumeBlock).not.toMatch(/position:\s*sticky/);
|
||||
expect(onboardingResumeBlock).not.toMatch(/top:\s*0/);
|
||||
});
|
||||
});
|
||||
@@ -661,7 +661,7 @@ export class SelfHealingManager {
|
||||
if (!recoverFn) return 0;
|
||||
|
||||
try {
|
||||
const tasks = await this.store.listTasks({ column: "in-progress" });
|
||||
const tasks = await this.store.listTasks({ column: "in-progress", slim: true });
|
||||
const executingIds = this.options.getExecutingTaskIds?.() ?? new Set<string>();
|
||||
|
||||
const stuckCompleted = tasks.filter((t) =>
|
||||
@@ -704,7 +704,7 @@ export class SelfHealingManager {
|
||||
*/
|
||||
async recoverMergeableReviewTasks(): Promise<number> {
|
||||
try {
|
||||
const tasks = await this.store.listTasks({ column: "in-review" });
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
|
||||
const mergeable = tasks.filter((t) =>
|
||||
t.column === "in-review" &&
|
||||
@@ -770,7 +770,7 @@ export class SelfHealingManager {
|
||||
const maxFixes = settings.maxPostReviewFixes ?? 1;
|
||||
if (!Number.isFinite(maxFixes) || maxFixes <= 0) return 0;
|
||||
|
||||
const tasks = await this.store.listTasks({ column: "in-review" });
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
const executingIds = this.options.getExecutingTaskIds?.() ?? new Set<string>();
|
||||
|
||||
const candidates = tasks.filter((task) => {
|
||||
@@ -858,7 +858,7 @@ export class SelfHealingManager {
|
||||
if (!timeoutMs || timeoutMs <= 0) return 0;
|
||||
|
||||
const now = Date.now();
|
||||
const tasks = await this.store.listTasks({ column: "in-review" });
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
const staleIncomplete = tasks.filter((task) =>
|
||||
task.column === "in-review" &&
|
||||
!task.paused &&
|
||||
@@ -914,7 +914,7 @@ export class SelfHealingManager {
|
||||
const timeoutMs = settings.taskStuckTimeoutMs;
|
||||
if (!timeoutMs || timeoutMs <= 0) return 0;
|
||||
|
||||
const tasks = await this.store.listTasks({ column: "in-review" });
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
const candidates = tasks.filter((task) =>
|
||||
task.column === "in-review" &&
|
||||
Boolean(task.status && ACTIVE_MERGE_STATUSES.has(task.status)) &&
|
||||
@@ -994,7 +994,7 @@ export class SelfHealingManager {
|
||||
*/
|
||||
async recoverMergedReviewTasks(): Promise<number> {
|
||||
try {
|
||||
const tasks = await this.store.listTasks({ column: "in-review" });
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
|
||||
const mergedButNotDone = tasks.filter((t) =>
|
||||
t.column === "in-review" &&
|
||||
@@ -1047,7 +1047,7 @@ export class SelfHealingManager {
|
||||
*/
|
||||
async recoverMisclassifiedFailures(): Promise<number> {
|
||||
try {
|
||||
const tasks = await this.store.listTasks({ column: "in-review" });
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
|
||||
const misclassified = tasks.filter((t) =>
|
||||
t.column === "in-review" &&
|
||||
@@ -1096,7 +1096,7 @@ export class SelfHealingManager {
|
||||
*/
|
||||
async recoverOrphanedExecutions(): Promise<number> {
|
||||
try {
|
||||
const tasks = await this.store.listTasks({ column: "in-progress" });
|
||||
const tasks = await this.store.listTasks({ column: "in-progress", slim: true });
|
||||
const executingIds = this.options.getExecutingTaskIds?.() ?? new Set<string>();
|
||||
const now = Date.now();
|
||||
|
||||
@@ -1164,7 +1164,7 @@ export class SelfHealingManager {
|
||||
*/
|
||||
async recoverNoProgressNoTaskDoneFailures(): Promise<number> {
|
||||
try {
|
||||
const tasks = await this.store.listTasks({ column: "in-progress" });
|
||||
const tasks = await this.store.listTasks({ column: "in-progress", slim: true });
|
||||
const executingIds = this.options.getExecutingTaskIds?.() ?? new Set<string>();
|
||||
|
||||
const candidates = tasks.filter((task) =>
|
||||
@@ -1237,7 +1237,7 @@ export class SelfHealingManager {
|
||||
*/
|
||||
async recoverPartialProgressNoTaskDoneFailures(): Promise<number> {
|
||||
try {
|
||||
const tasks = await this.store.listTasks({ column: "in-review" });
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
|
||||
const candidates = tasks.filter((task) =>
|
||||
task.column === "in-review" &&
|
||||
|
||||
Reference in New Issue
Block a user