Compare commits

...
Author SHA1 Message Date
Kaoru0822andClaude Opus 5.5 34b6c73b83 fix(kuna): resolve retained process failures once their owner releases
KunaTargetCommands.close() dropped a retained process failure from its
retry list after a confirmed supervisor stop but never resolved it. The
producer and every caller projection bound to it (#4693) kept reporting
cleanupIncomplete after a successful close. Resolve those failures once
the workspace root they also name has closed; a failed root close keeps
them unresolved for the next retry.

Refs #4693

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 06:21:04 +00:00
2 changed files with 24 additions and 1 deletions

No files matched your search

+6
View File
@@ -61,6 +61,8 @@ export class KunaTargetCommands {
readonly owner: ProviderProcessSupervisor | undefined;
readonly releaseRun: KunaRunRelease | undefined;
}[] = [];
/** Stopped process failures that still name the unreleased workspace root. */
readonly #stopped: ProviderCleanupError[] = [];
readonly #roots: PrivateRuntimeRoot[] = [];
#workspace: Promise<Result<Workspace, AnalysisError>> | undefined;
#closing = false;
@@ -176,6 +178,7 @@ export class KunaTargetCommands {
if (stopped !== undefined && stopped.status !== "incomplete") {
retained.releaseRun?.();
this.#retained.splice(this.#retained.indexOf(retained), 1);
this.#stopped.push(retained.error);
}
}
failures.push(...this.#retained.map(({ error }) => error));
@@ -201,6 +204,9 @@ export class KunaTargetCommands {
),
);
}
// Each retained failure owned its process tree and this workspace root.
if (this.#roots.length === 0)
for (const error of this.#stopped.splice(0)) error.resolveCleanup();
}
return failures.length === 0
? ok(null)
@@ -3,6 +3,7 @@ import { readKunaConfiguration } from "../../../../src/kuna/KunaConfiguration.js
import { KUNA_DIAGNOSTIC_BYTES } from "../../../../src/kuna/KunaLimits.js";
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
import { ProviderCleanupError } from "../../../../src/domain/providerCleanupError.js";
import type { AnalysisError } from "../../../../src/domain/analysisErrorBase.js";
import { fakeKunaCalls, setFakeKunaBehavior } from "./fakeKuna.js";
import { run, session, unprovenCleanupLauncher } from "./kunaSessionFixture.js";
@@ -236,6 +237,7 @@ it("retains metadata admission until a string process stop is proven", async ()
);
const client = open();
const abort = new AbortController();
let callerError: AnalysisError | undefined;
try {
const pending = client.execute(
"list_strings",
@@ -244,10 +246,20 @@ it("retains metadata admission until a string process stop is proven", async ()
);
await vi.waitFor(async () => expect(await scans(kuna)).toHaveLength(1));
abort.abort();
expect(await pending).toMatchObject({
const stopped = await pending;
expect(stopped).toMatchObject({
ok: false,
error: expect.any(ProviderCleanupError),
});
if (stopped.ok) throw new Error("stopped scan unexpectedly passed");
callerError = stopped.error;
// The caller copy keeps the shared scan as its cleanup owner.
expect(callerError).toMatchObject({
operation: "list_strings",
cleanupIncomplete: true,
diagnostics: { producer_operation: "kuna:strings" },
cause: { operation: "kuna:strings", cleanupIncomplete: true },
});
const queuedAbort = new AbortController();
const queued = client.execute(
"procedure_callees",
@@ -272,6 +284,11 @@ it("retains metadata admission until a string process stop is proven", async ()
cleanup.release();
expect(await client.close()).toEqual({ ok: true, value: null });
}
// Confirmed producer cleanup must clear every caller-specific diagnostic.
expect(callerError).toMatchObject({
cleanupIncomplete: false,
cause: { cleanupIncomplete: false },
});
});
it("uses the whole-program deadline independently of the single-function budget", async () => {