diff --git a/src/mgr/server.ts b/src/mgr/server.ts index 1ec7c5d..97a9b70 100644 --- a/src/mgr/server.ts +++ b/src/mgr/server.ts @@ -1295,9 +1295,13 @@ function runnerAdmissionSummary(sessionId: string, run: RunRecord, command: Comm }); } const mutation = disposition !== "replayed"; + const retryable = intent.state === "pending" || intent.state === "dispatching" || intent.state === "retry"; return { mode: "durable-dispatch-intent", state: intent.state, + disposition, + retryable, + retryAuthority: retryable ? "dispatcher" : "none", reason: disposition === "recovered" ? "session-runner-admission-recovered" : disposition === "replayed" ? "session-runner-admission-replayed" : "session-runner-admission-created", failureKind: null, sessionId, @@ -1319,7 +1323,7 @@ function runnerAdmissionSummary(sessionId: string, run: RunRecord, command: Comm } function runnerAdmissionNotRequested(sessionId: string, run: RunRecord, command: CommandRecord, disposition: "created" | "replayed" | "recovered"): JsonRecord { - return { mode: "none", state: "not-requested", reason: "session-runner-dispatch-not-requested", failureKind: null, sessionId, runId: run.id, commandId: command.id, dispatchIntentId: null, plannedRunnerJobId: null, durable: false, mutation: disposition !== "replayed", partialWrite: false, recoveredPriorPartialWrite: false, recoveryActions: [], valuesPrinted: false }; + return { mode: "none", state: "not-requested", disposition, retryable: false, retryAuthority: "none", reason: "session-runner-dispatch-not-requested", failureKind: null, sessionId, runId: run.id, commandId: command.id, dispatchIntentId: null, plannedRunnerJobId: null, durable: false, mutation: disposition !== "replayed", partialWrite: false, recoveredPriorPartialWrite: false, recoveryActions: [], valuesPrinted: false }; } function plannedRunnerJob(admission: JsonRecord): JsonRecord { diff --git a/src/selftest/cases/66-session-turn-admission.ts b/src/selftest/cases/66-session-turn-admission.ts index b55131a..41a0882 100644 --- a/src/selftest/cases/66-session-turn-admission.ts +++ b/src/selftest/cases/66-session-turn-admission.ts @@ -68,12 +68,37 @@ async function assertHttpAdmissionDoesNotCallKubectl(kubectlCommand: string): Pr assert.equal(response.partialWrite, false); assert.equal(admission.mode, "durable-dispatch-intent"); assert.equal(admission.state, "pending"); + assert.equal(admission.disposition, "created"); + assert.equal(admission.retryable, true); + assert.equal(admission.retryAuthority, "dispatcher"); assert.equal(admission.durable, true); + assert.equal(admission.recoveredPriorPartialWrite, false); assert.equal(admission.runId, run.id); assert.equal(admission.commandId, command.id); assert.equal(command.dispatchIntent?.state, "pending"); assert.equal(store.getRunnerDispatchIntent(command.id)?.state, "pending"); assert.deepEqual(store.listEvents(run.id, 0, 100).map((event) => event.type === "backend_status" ? event.payload.phase : event.type), ["user_message", "run-created", "command-created"]); + + const replayResponse = await client.post(`/api/v1/sessions/${sessionId}/send`, sessionSendBody(sessionId, "atomic HTTP admission", "session-admission-http")) as JsonRecord; + const replayAdmission = replayResponse.runnerAdmission as JsonRecord; + assert.equal(replayResponse.mutation, false); + assert.equal((replayResponse.run as RunRecord).id, run.id); + assert.equal((replayResponse.command as CommandRecord).id, command.id); + assert.equal(replayAdmission.state, "pending"); + assert.equal(replayAdmission.disposition, "replayed"); + assert.equal(replayAdmission.retryable, true); + assert.equal(replayAdmission.retryAuthority, "dispatcher"); + assert.equal(replayAdmission.recoveredPriorPartialWrite, false); + assert.equal(store.listEvents(run.id, 0, 100).length, 3); + + const noRunnerSessionId = "ses_session_admission_no_runner"; + const noRunnerBody = { ...sessionSendBody(noRunnerSessionId, "admit without runner", "session-admission-no-runner"), createRunnerJob: false }; + const noRunnerResponse = await client.post(`/api/v1/sessions/${noRunnerSessionId}/send`, noRunnerBody) as JsonRecord; + const noRunnerAdmission = noRunnerResponse.runnerAdmission as JsonRecord; + assert.equal(noRunnerAdmission.state, "not-requested"); + assert.equal(noRunnerAdmission.disposition, "created"); + assert.equal(noRunnerAdmission.retryable, false); + assert.equal(noRunnerAdmission.retryAuthority, "none"); } finally { await closeServer(server.server); } @@ -177,6 +202,10 @@ async function assertRestartVersionForwardRecovery(kubectlCommand: string): Prom assert.equal((recoveredRun.resourceBundleRef as JsonRecord | null)?.commitId, "1111111111111111111111111111111111111111"); assert.equal((store.getRun(legacyRun.id).resourceBundleRef as JsonRecord | null)?.commitId, "1111111111111111111111111111111111111111"); assert.equal(admission.reason, "session-runner-admission-recovered"); + assert.equal(admission.state, "pending"); + assert.equal(admission.disposition, "recovered"); + assert.equal(admission.retryable, true); + assert.equal(admission.retryAuthority, "dispatcher"); assert.equal(admission.recoveredPriorPartialWrite, true); const events = store.listEvents(legacyRun.id, 0, 100); assert.equal(events.filter((event) => event.type === "user_message").length, 1);