* refactor: encapsulate auto.ts state into AutoSession class (#898) Follow-up to PR #906 (7 module extractions). All ~40 mutable module-level variables in auto.ts are replaced with properties on a single AutoSession class instance (s). Changes: - auto/session.ts: 200-line AutoSession class with typed properties, clearTimers(), resetDispatchCounters(), completeCurrentUnit(), reset(), and toJSON() for diagnostics. - auto.ts: ~700 variable references renamed from bare names to s.xxx. All module-level let/const state declarations removed. Constants (MAX_UNIT_DISPATCHES, etc.) re-exported from session.ts. - Tests updated: milestone-transition-worktree.test.ts and triage-dispatch.test.ts source-grep patterns updated for s.xxx names. Benefits: - 40 scattered declarations → 1 class with typed properties - Manual reset of 25+ variables in stopAuto → s.reset() - s.toJSON() for state snapshots and diagnostics - grep 's.' shows every state access No behavioral changes. 1224 tests pass. * fix: import constants locally for tsconfig.extensions.json compatibility The extensions tsconfig couldn't resolve re-exported constants from auto/session.js. Fix: import them explicitly in addition to re-exporting. Also remove leftover DISPATCH_GAP_TIMEOUT_MS local declaration.
This commit is contained in:
parent
1e979ff626
commit
1b1df58749
4 changed files with 857 additions and 690 deletions
File diff suppressed because it is too large
Load diff
224
src/resources/extensions/gsd/auto/session.ts
Normal file
224
src/resources/extensions/gsd/auto/session.ts
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
/**
|
||||
* AutoSession — encapsulates all mutable auto-mode state into a single instance.
|
||||
*
|
||||
* Replaces ~40 module-level variables scattered across auto.ts with typed
|
||||
* properties on a class instance. Benefits:
|
||||
*
|
||||
* - reset() clears everything in one call (was 25+ manual resets in stopAuto)
|
||||
* - toJSON() provides diagnostic snapshots
|
||||
* - grep `s.` shows every state access
|
||||
* - Constructable for testing
|
||||
*/
|
||||
|
||||
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
||||
import type { GitServiceImpl } from "../git-service.js";
|
||||
import type { CaptureEntry } from "../captures.js";
|
||||
import type { BudgetAlertLevel } from "../auto-budget.js";
|
||||
|
||||
// ─── Exported Types ──────────────────────────────────────────────────────────
|
||||
|
||||
export interface CompletedUnit {
|
||||
type: string;
|
||||
id: string;
|
||||
startedAt: number;
|
||||
finishedAt: number;
|
||||
}
|
||||
|
||||
export interface CurrentUnit {
|
||||
type: string;
|
||||
id: string;
|
||||
startedAt: number;
|
||||
}
|
||||
|
||||
export interface UnitRouting {
|
||||
tier: string;
|
||||
modelDowngraded: boolean;
|
||||
}
|
||||
|
||||
export interface StartModel {
|
||||
provider: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface PendingVerificationRetry {
|
||||
unitId: string;
|
||||
failureContext: string;
|
||||
attempt: number;
|
||||
}
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
export const MAX_UNIT_DISPATCHES = 3;
|
||||
export const STUB_RECOVERY_THRESHOLD = 2;
|
||||
export const MAX_LIFETIME_DISPATCHES = 6;
|
||||
export const MAX_CONSECUTIVE_SKIPS = 3;
|
||||
export const DISPATCH_GAP_TIMEOUT_MS = 5_000;
|
||||
export const MAX_SKIP_DEPTH = 20;
|
||||
|
||||
// ─── AutoSession ─────────────────────────────────────────────────────────────
|
||||
|
||||
export class AutoSession {
|
||||
// ── Lifecycle ────────────────────────────────────────────────────────────
|
||||
active = false;
|
||||
paused = false;
|
||||
stepMode = false;
|
||||
verbose = false;
|
||||
cmdCtx: ExtensionCommandContext | null = null;
|
||||
|
||||
// ── Paths ────────────────────────────────────────────────────────────────
|
||||
basePath = "";
|
||||
originalBasePath = "";
|
||||
gitService: GitServiceImpl | null = null;
|
||||
|
||||
// ── Dispatch counters ────────────────────────────────────────────────────
|
||||
readonly unitDispatchCount = new Map<string, number>();
|
||||
readonly unitLifetimeDispatches = new Map<string, number>();
|
||||
readonly unitRecoveryCount = new Map<string, number>();
|
||||
readonly unitConsecutiveSkips = new Map<string, number>();
|
||||
readonly completedKeySet = new Set<string>();
|
||||
|
||||
// ── Timers ───────────────────────────────────────────────────────────────
|
||||
unitTimeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
||||
wrapupWarningHandle: ReturnType<typeof setTimeout> | null = null;
|
||||
idleWatchdogHandle: ReturnType<typeof setInterval> | null = null;
|
||||
continueHereHandle: ReturnType<typeof setInterval> | null = null;
|
||||
dispatchGapHandle: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// ── Current unit ─────────────────────────────────────────────────────────
|
||||
currentUnit: CurrentUnit | null = null;
|
||||
currentUnitRouting: UnitRouting | null = null;
|
||||
completedUnits: CompletedUnit[] = [];
|
||||
currentMilestoneId: string | null = null;
|
||||
|
||||
// ── Model state ──────────────────────────────────────────────────────────
|
||||
autoModeStartModel: StartModel | null = null;
|
||||
originalModelId: string | null = null;
|
||||
originalModelProvider: string | null = null;
|
||||
lastBudgetAlertLevel: BudgetAlertLevel = 0;
|
||||
|
||||
// ── Recovery ─────────────────────────────────────────────────────────────
|
||||
pendingCrashRecovery: string | null = null;
|
||||
pendingVerificationRetry: PendingVerificationRetry | null = null;
|
||||
readonly verificationRetryCount = new Map<string, number>();
|
||||
pausedSessionFile: string | null = null;
|
||||
resourceVersionOnStart: string | null = null;
|
||||
lastStateRebuildAt = 0;
|
||||
|
||||
// ── Guards ───────────────────────────────────────────────────────────────
|
||||
handlingAgentEnd = false;
|
||||
dispatching = false;
|
||||
skipDepth = 0;
|
||||
readonly recentlyEvictedKeys = new Set<string>();
|
||||
|
||||
// ── Metrics ──────────────────────────────────────────────────────────────
|
||||
autoStartTime = 0;
|
||||
lastPromptCharCount: number | undefined;
|
||||
lastBaselineCharCount: number | undefined;
|
||||
pendingQuickTasks: CaptureEntry[] = [];
|
||||
|
||||
// ── Signal handler ───────────────────────────────────────────────────────
|
||||
sigtermHandler: (() => void) | null = null;
|
||||
|
||||
// ── Methods ──────────────────────────────────────────────────────────────
|
||||
|
||||
clearTimers(): void {
|
||||
if (this.unitTimeoutHandle) { clearTimeout(this.unitTimeoutHandle); this.unitTimeoutHandle = null; }
|
||||
if (this.wrapupWarningHandle) { clearTimeout(this.wrapupWarningHandle); this.wrapupWarningHandle = null; }
|
||||
if (this.idleWatchdogHandle) { clearInterval(this.idleWatchdogHandle); this.idleWatchdogHandle = null; }
|
||||
if (this.continueHereHandle) { clearInterval(this.continueHereHandle); this.continueHereHandle = null; }
|
||||
if (this.dispatchGapHandle) { clearTimeout(this.dispatchGapHandle); this.dispatchGapHandle = null; }
|
||||
}
|
||||
|
||||
resetDispatchCounters(): void {
|
||||
this.unitDispatchCount.clear();
|
||||
this.unitLifetimeDispatches.clear();
|
||||
this.unitConsecutiveSkips.clear();
|
||||
}
|
||||
|
||||
get lockBasePath(): string {
|
||||
return this.originalBasePath || this.basePath;
|
||||
}
|
||||
|
||||
completeCurrentUnit(): CompletedUnit | null {
|
||||
if (!this.currentUnit) return null;
|
||||
const done: CompletedUnit = { ...this.currentUnit, finishedAt: Date.now() };
|
||||
this.completedUnits.push(done);
|
||||
this.currentUnit = null;
|
||||
return done;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.clearTimers();
|
||||
|
||||
// Lifecycle
|
||||
this.active = false;
|
||||
this.paused = false;
|
||||
this.stepMode = false;
|
||||
this.verbose = false;
|
||||
this.cmdCtx = null;
|
||||
|
||||
// Paths
|
||||
this.basePath = "";
|
||||
this.originalBasePath = "";
|
||||
this.gitService = null;
|
||||
|
||||
// Dispatch
|
||||
this.unitDispatchCount.clear();
|
||||
this.unitLifetimeDispatches.clear();
|
||||
this.unitRecoveryCount.clear();
|
||||
this.unitConsecutiveSkips.clear();
|
||||
// Note: completedKeySet is intentionally NOT cleared — it persists
|
||||
// across restarts to prevent re-dispatching completed units.
|
||||
|
||||
// Unit
|
||||
this.currentUnit = null;
|
||||
this.currentUnitRouting = null;
|
||||
this.completedUnits = [];
|
||||
this.currentMilestoneId = null;
|
||||
|
||||
// Model
|
||||
this.autoModeStartModel = null;
|
||||
this.originalModelId = null;
|
||||
this.originalModelProvider = null;
|
||||
this.lastBudgetAlertLevel = 0;
|
||||
|
||||
// Recovery
|
||||
this.pendingCrashRecovery = null;
|
||||
this.pendingVerificationRetry = null;
|
||||
this.verificationRetryCount.clear();
|
||||
this.pausedSessionFile = null;
|
||||
this.resourceVersionOnStart = null;
|
||||
this.lastStateRebuildAt = 0;
|
||||
|
||||
// Guards
|
||||
this.handlingAgentEnd = false;
|
||||
this.dispatching = false;
|
||||
this.skipDepth = 0;
|
||||
this.recentlyEvictedKeys.clear();
|
||||
|
||||
// Metrics
|
||||
this.autoStartTime = 0;
|
||||
this.lastPromptCharCount = undefined;
|
||||
this.lastBaselineCharCount = undefined;
|
||||
this.pendingQuickTasks = [];
|
||||
|
||||
// Signal handler
|
||||
this.sigtermHandler = null;
|
||||
}
|
||||
|
||||
toJSON(): Record<string, unknown> {
|
||||
return {
|
||||
active: this.active,
|
||||
paused: this.paused,
|
||||
stepMode: this.stepMode,
|
||||
basePath: this.basePath,
|
||||
currentMilestoneId: this.currentMilestoneId,
|
||||
currentUnit: this.currentUnit,
|
||||
completedUnits: this.completedUnits.length,
|
||||
completedKeySet: this.completedKeySet.size,
|
||||
unitDispatchCount: Object.fromEntries(this.unitDispatchCount),
|
||||
dispatching: this.dispatching,
|
||||
skipDepth: this.skipDepth,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -134,7 +134,7 @@ test("auto.ts milestone transition block contains worktree lifecycle", () => {
|
|||
"auto.ts should contain the worktree lifecycle comment marker",
|
||||
);
|
||||
assert.ok(
|
||||
autoSrc.includes("mergeMilestoneToMain") && autoSrc.includes("mid !== currentMilestoneId"),
|
||||
autoSrc.includes("mergeMilestoneToMain") && autoSrc.includes("mid !== s.currentMilestoneId"),
|
||||
"auto.ts should call mergeMilestoneToMain during milestone transition",
|
||||
);
|
||||
assert.ok(
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ test("dispatch: triage check guards against step mode", () => {
|
|||
autoSrc.indexOf("In step mode, pause and show a wizard"),
|
||||
);
|
||||
assert.ok(
|
||||
triageBlock.includes("!stepMode"),
|
||||
triageBlock.includes("!s.stepMode"),
|
||||
"triage block should guard against step mode",
|
||||
);
|
||||
});
|
||||
|
|
@ -77,7 +77,7 @@ test("dispatch: triage check guards against hook unit types", () => {
|
|||
autoSrc.indexOf("In step mode, pause and show a wizard"),
|
||||
);
|
||||
assert.ok(
|
||||
triageBlock.includes('!currentUnit.type.startsWith("hook/")'),
|
||||
triageBlock.includes('!s.currentUnit.type.startsWith("hook/")'),
|
||||
"triage block should not fire for hook units",
|
||||
);
|
||||
});
|
||||
|
|
@ -88,7 +88,7 @@ test("dispatch: triage check guards against triage-on-triage", () => {
|
|||
autoSrc.indexOf("In step mode, pause and show a wizard"),
|
||||
);
|
||||
assert.ok(
|
||||
triageBlock.includes('currentUnit.type !== "triage-captures"'),
|
||||
triageBlock.includes('s.currentUnit.type !== "triage-captures"'),
|
||||
"triage block should not fire for triage units",
|
||||
);
|
||||
});
|
||||
|
|
@ -99,7 +99,7 @@ test("dispatch: triage check guards against quick-task triggering triage", () =>
|
|||
autoSrc.indexOf("In step mode, pause and show a wizard"),
|
||||
);
|
||||
assert.ok(
|
||||
triageBlock.includes('currentUnit.type !== "quick-task"'),
|
||||
triageBlock.includes('s.currentUnit.type !== "quick-task"'),
|
||||
"triage block should not fire for quick-task units",
|
||||
);
|
||||
});
|
||||
|
|
@ -231,7 +231,7 @@ test("dispatch: post-triage resolution executor fires after triage-captures unit
|
|||
autoSrc.indexOf("Path A fix: verify artifact"),
|
||||
);
|
||||
assert.ok(
|
||||
triageCompletionBlock.includes('currentUnit.type === "triage-captures"'),
|
||||
triageCompletionBlock.includes('s.currentUnit.type === "triage-captures"'),
|
||||
"should check for triage-captures unit completion",
|
||||
);
|
||||
assert.ok(
|
||||
|
|
@ -268,8 +268,8 @@ test("dispatch: post-triage executor queues quick-tasks", () => {
|
|||
autoSrc.indexOf("Path A fix: verify artifact"),
|
||||
);
|
||||
assert.ok(
|
||||
triageCompletionBlock.includes("pendingQuickTasks"),
|
||||
"should push quick-tasks to pendingQuickTasks queue",
|
||||
triageCompletionBlock.includes("s.pendingQuickTasks"),
|
||||
"should push quick-tasks to s.pendingQuickTasks queue",
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -336,9 +336,9 @@ test("dispatch: quick-task excluded from post-unit hook triggering", () => {
|
|||
// ─── pendingQuickTasks queue lifecycle ────────────────────────────────────────
|
||||
|
||||
test("dispatch: pendingQuickTasks queue is reset on auto-mode start/stop", () => {
|
||||
const resetMatches = autoSrc.match(/pendingQuickTasks = \[\]/g);
|
||||
const resetMatches = autoSrc.match(/s\.pendingQuickTasks = \[\]/g);
|
||||
assert.ok(
|
||||
resetMatches && resetMatches.length >= 3,
|
||||
"pendingQuickTasks should be reset in at least 3 places (start, stop, manual hook)",
|
||||
"s.pendingQuickTasks should be reset in at least 3 places (start, stop, manual hook)",
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue