fix: 终态 runner 优先进入回收候选

This commit is contained in:
root
2026-07-12 18:33:40 +02:00
parent ead06b35e1
commit 03491ea068
2 changed files with 32 additions and 23 deletions
+5 -5
View File
@@ -508,16 +508,12 @@ async function activityFor(store: AgentRunStore, input: {
return protectedActivity(input, "db-progress-facts-unavailable", lastActiveAtMs, null);
}
const dbFactsFingerprint = databaseFactsFingerprint(run, command, commands, events);
const freshClaim = Boolean(run.claimedBy && heartbeatFresh(run.leaseExpiresAt, input.activeHeartbeatMaxAgeMs));
if (freshClaim) return protectedActivity(input, "fresh-active-run", lastActiveAtMs, dbFactsFingerprint);
const activePodReason = activePodProtectionReason(input.podObservations);
if (activePodReason) return protectedActivity(input, activePodReason, lastActiveAtMs, dbFactsFingerprint);
if (isTerminalRunStatus(run.status)) {
const claimDrift = run.claimedBy !== null || run.leaseExpiresAt !== null;
return candidateActivity(input, "terminal", claimDrift ? "terminal-run-stale-claim" : "terminal-run", lastActiveAtMs, dbFactsFingerprint, claimDrift ? "release-terminal-run-claim" : "none");
}
if (isTerminalCommandState(command.state)) {
const claimDrift = input.terminal && (run.claimedBy !== null || run.leaseExpiresAt !== null);
const claimDrift = run.claimedBy !== null || run.leaseExpiresAt !== null;
return candidateActivity(
input,
input.terminal ? "terminal" : "idle",
@@ -527,6 +523,10 @@ async function activityFor(store: AgentRunStore, input: {
claimDrift ? "release-terminal-command-claim" : "none",
);
}
const freshClaim = Boolean(run.claimedBy && heartbeatFresh(run.leaseExpiresAt, input.activeHeartbeatMaxAgeMs));
if (freshClaim) return protectedActivity(input, "fresh-active-run", lastActiveAtMs, dbFactsFingerprint);
const activePodReason = activePodProtectionReason(input.podObservations);
if (activePodReason) return protectedActivity(input, activePodReason, lastActiveAtMs, dbFactsFingerprint);
if (input.terminal) return candidateActivity(input, "terminal", "terminal-runner-resource", lastActiveAtMs, dbFactsFingerprint);
const podBlockReason = stalePendingPodBlockReason(input.podObservations);
@@ -123,6 +123,7 @@ const selfTest: SelfTestCase = async (context) => {
await assertEventOnlyFailedMountCandidate({ fixturePath, statePath, kubectlCommand });
await assertTerminalClaimDriftCandidate({ fixturePath, statePath, kubectlCommand });
await assertTerminalCommandClaimDriftCandidate({ fixturePath, statePath, kubectlCommand });
await assertTerminalFactsOverrideHistoricalActivity({ fixturePath, statePath, kubectlCommand });
await assertTerminalClaimRepairCasConflictFailsClosed({ fixturePath, statePath, kubectlCommand });
await assertProtectedMatrix({ fixturePath, statePath, kubectlCommand });
await assertPendingToRunningRaceFailsClosed({ fixturePath, statePath, kubectlCommand });
@@ -167,9 +168,9 @@ const selfTest: SelfTestCase = async (context) => {
"container-started-pending-protected",
"pod-identity-mismatch-protected",
"event-facts-unavailable-protected",
"terminal-ledger-running-pod-protected",
"terminal-runner-stale-claim-typed-repair",
"terminal-command-runner-stale-claim-typed-repair",
"terminal-facts-override-historical-claim-heartbeat-and-running-pod",
"terminal-claim-repair-cas-conflict-fails-closed",
"terminal-runner-pod-completed-is-not-active",
"selected-runner-race-fails-closed",
@@ -268,23 +269,6 @@ async function assertProtectedMatrix(input: { fixturePath: string; statePath: st
await assertProtected(input, [base], [], "pod-facts-unavailable");
await assertProtected(input, [base], [runnerPod(base, 100, "pending")], "pod-waiting-event-facts-unavailable", base, { eventsError: true });
await assertProtected(input, [base], [runnerPod(base, 100, "pending"), runnerPod(base, 101, "running", 100)], "runner-pod-started-running-or-ready");
const terminalRunning = pendingFact(105, oldAt);
terminalRunning.run.status = "completed";
terminalRunning.run.terminalStatus = "completed";
terminalRunning.command.state = "completed";
await assertProtected(input, [terminalRunning], [runnerPod(terminalRunning, 105, "running")], "runner-pod-started-running-or-ready");
const terminalFreshClaim = pendingFact(107, oldAt, "claimed");
terminalFreshClaim.run.status = "completed";
terminalFreshClaim.run.terminalStatus = "completed";
terminalFreshClaim.command.state = "completed";
const terminalFreshClaimPod = runnerPod(terminalFreshClaim, 107, "running");
terminalFreshClaimPod.status = {
phase: "Succeeded",
startTime: terminalFreshClaim.run.createdAt,
conditions: [{ type: "Ready", status: "False", reason: "PodCompleted" }],
containerStatuses: [{ name: "runner", ready: false, started: false, restartCount: 0, state: { terminated: { exitCode: 0, reason: "Completed" } } }],
};
await assertProtected(input, [terminalFreshClaim], [terminalFreshClaimPod], "fresh-active-run");
const identityMismatchPod = runnerPod(base, 100, "pending");
((identityMismatchPod.metadata as JsonRecord).annotations as JsonRecord)["agentrun.pikastech.local/command-id"] = "cmd-mismatched";
await assertProtected(input, [base], [identityMismatchPod], "pod-identity-mismatch");
@@ -295,6 +279,31 @@ async function assertProtectedMatrix(input: { fixturePath: string; statePath: st
await assertProtected(input, [mismatchedUserMessage], [runnerPod(mismatchedUserMessage, 106, "failed-config")], "business-progress-observed-or-unverifiable");
}
async function assertTerminalFactsOverrideHistoricalActivity(input: { fixturePath: string; statePath: string; kubectlCommand: string }): Promise<void> {
const oldAt = new Date(Date.now() - 60 * 60_000).toISOString();
const terminalRun = pendingFact(105, oldAt, "claimed");
terminalRun.run.status = "completed";
terminalRun.run.terminalStatus = "completed";
terminalRun.command.state = "completed";
await writeFixture(input, { jobs: [runnerJob(terminalRun, 105)], pods: [runnerPod(terminalRun, 105, "running")], events: [] });
const runSummary = await enforceRunnerRetentionBeforeCreate({ store: new RetentionFactStore([terminalRun]), options: retentionOptions(input.kubectlCommand, 1), incomingRunnerCount: 0 });
const runCandidate = firstSummary(runSummary, "inactiveCandidates");
assert.equal(runSummary.protectedActiveRunnerCount, 0);
assert.equal(runCandidate.candidateKind, "terminal");
assert.equal(runCandidate.classificationReason, "terminal-run-stale-claim");
assert.equal(runCandidate.repairKind, "release-terminal-run-claim");
const terminalCommand = pendingFact(107, oldAt, "claimed");
terminalCommand.command.state = "completed";
await writeFixture(input, { jobs: [runnerJob(terminalCommand, 107)], pods: [runnerPod(terminalCommand, 107, "running")], events: [] });
const commandSummary = await enforceRunnerRetentionBeforeCreate({ store: new RetentionFactStore([terminalCommand]), options: retentionOptions(input.kubectlCommand, 1), incomingRunnerCount: 0 });
const commandCandidate = firstSummary(commandSummary, "inactiveCandidates");
assert.equal(commandSummary.protectedActiveRunnerCount, 0);
assert.equal(commandCandidate.candidateKind, "idle");
assert.equal(commandCandidate.classificationReason, "terminal-command-stale-claim");
assert.equal(commandCandidate.repairKind, "release-terminal-command-claim");
}
async function assertLegacyNoUserMessageCandidate(input: { fixturePath: string; statePath: string; kubectlCommand: string }): Promise<void> {
const fact = pendingFact(89, new Date(Date.now() - 60 * 60_000).toISOString());
fact.events = fact.events