Merge pull request #1839 from pikasTech/fix/1837-runner-admission-visibility
AgentRun session send 展示 durable runner admission
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { renderSessionSendError, renderSessionSendResult } from "./agentrun/session-send-render";
|
||||
import type { AgentRunOutputMode, AgentRunResourceOptions } from "./agentrun/utils";
|
||||
|
||||
const command = "agentrun send session/sess_admission_fixture --prompt-stdin";
|
||||
const sessionId = "sess_admission_fixture";
|
||||
const runId = "run_admission_fixture";
|
||||
const commandId = "cmd_admission_fixture";
|
||||
const dispatchIntentId = "rdi_admission_fixture";
|
||||
const plannedRunnerJobId = "rjob_planned_fixture";
|
||||
|
||||
const durableAdmissionFixture: Record<string, unknown> = {
|
||||
ok: true,
|
||||
command,
|
||||
data: {
|
||||
action: "session-send",
|
||||
dryRun: false,
|
||||
mutation: true,
|
||||
partialWrite: false,
|
||||
sessionId,
|
||||
decision: "turn",
|
||||
internalCommandType: "turn",
|
||||
run: { id: runId, status: "queued", sessionRef: { sessionId } },
|
||||
command: {
|
||||
id: commandId,
|
||||
state: "pending",
|
||||
type: "turn",
|
||||
dispatchIntent: {
|
||||
id: dispatchIntentId,
|
||||
state: "pending",
|
||||
runnerJobId: plannedRunnerJobId,
|
||||
actualRunnerJobId: null,
|
||||
attemptCount: 0,
|
||||
durable: true,
|
||||
},
|
||||
},
|
||||
runnerJob: {
|
||||
action: "runner-dispatch-admitted",
|
||||
state: "pending",
|
||||
runId,
|
||||
commandId,
|
||||
runnerJobId: plannedRunnerJobId,
|
||||
dispatchIntentId,
|
||||
durable: true,
|
||||
},
|
||||
runnerAdmission: {
|
||||
mode: "durable-dispatch-intent",
|
||||
state: "pending",
|
||||
reason: "session-runner-admission-recovered",
|
||||
disposition: "recovered",
|
||||
failureKind: null,
|
||||
sessionId,
|
||||
runId,
|
||||
commandId,
|
||||
dispatchIntentId,
|
||||
plannedRunnerJobId,
|
||||
actualRunnerJobId: null,
|
||||
attemptCount: 0,
|
||||
durable: true,
|
||||
mutation: true,
|
||||
partialWrite: false,
|
||||
recoveredPriorPartialWrite: true,
|
||||
retryable: true,
|
||||
retryAuthority: "dispatcher",
|
||||
recoveryActions: [
|
||||
{ operation: "describe", resourceKind: "session", resourceName: sessionId },
|
||||
{ operation: "events", resourceKind: "run", resourceName: runId, runId, afterSeq: 0, limit: 100 },
|
||||
],
|
||||
valuesPrinted: false,
|
||||
},
|
||||
pollActions: [
|
||||
{ operation: "logs", resourceKind: "session", resourceName: sessionId, sessionId, limit: 100 },
|
||||
{ operation: "cancel", resourceKind: "session", resourceName: sessionId },
|
||||
],
|
||||
valuesPrinted: false,
|
||||
},
|
||||
};
|
||||
|
||||
const legacySuccessFixture: Record<string, unknown> = {
|
||||
ok: true,
|
||||
command,
|
||||
data: {
|
||||
action: "session-send",
|
||||
dryRun: false,
|
||||
mutation: true,
|
||||
partialWrite: false,
|
||||
sessionId,
|
||||
decision: "turn",
|
||||
internalCommandType: "turn",
|
||||
run: { id: runId, status: "queued", sessionRef: { sessionId } },
|
||||
command: { id: commandId, state: "pending", type: "turn" },
|
||||
runnerJob: { id: "rjob_legacy_fixture", state: "queued", runId, commandId },
|
||||
pollActions: [
|
||||
{ operation: "describe", resourceKind: "session", resourceName: sessionId },
|
||||
{ operation: "events", resourceKind: "run", resourceName: runId, runId, afterSeq: 0, limit: 100 },
|
||||
],
|
||||
valuesPrinted: false,
|
||||
},
|
||||
};
|
||||
|
||||
const typedFailureFixture: Record<string, unknown> = {
|
||||
ok: false,
|
||||
command,
|
||||
failureKind: "validation-failed",
|
||||
message: "durable runner dispatch is not enabled for session turn admission",
|
||||
httpStatus: 503,
|
||||
agentrun: {
|
||||
failureKind: "infra-failed",
|
||||
error: {
|
||||
name: "AgentRunError",
|
||||
failureKind: "infra-failed",
|
||||
details: {
|
||||
code: "runner-admission-failed",
|
||||
reason: "session-runner-dispatcher-disabled",
|
||||
phase: "runner-admission",
|
||||
sessionId,
|
||||
mutation: false,
|
||||
partialWrite: false,
|
||||
retryable: false,
|
||||
retryAuthority: "none",
|
||||
recoveryAction: "restore-runner-dispatcher",
|
||||
recoveryActions: [
|
||||
{ operation: "describe", resourceKind: "session", resourceName: sessionId },
|
||||
],
|
||||
valuesPrinted: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("AgentRun session send runner admission rendering", () => {
|
||||
test("default output exposes durable admission without claiming a Kubernetes Job exists", () => {
|
||||
const result = renderSessionSendResult(command, durableAdmissionFixture, options("human"));
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.renderedText).toContain("RunnerAdmission:");
|
||||
expect(result.renderedText).toContain("mode=durable-dispatch-intent state=pending disposition=recovered");
|
||||
expect(result.renderedText).toContain(`runId=${runId} commandId=${commandId} dispatchIntentId=${dispatchIntentId}`);
|
||||
expect(result.renderedText).toContain(`plannedRunnerJobId=${plannedRunnerJobId} actualRunnerJobId=-`);
|
||||
expect(result.renderedText).toContain("mutation=true partialWrite=false recoveredPriorPartialWrite=true");
|
||||
expect(result.renderedText).toContain("retryable=true retryAuthority=dispatcher");
|
||||
expect(result.renderedText).toContain(`planned-runnerjob/${plannedRunnerJobId}`);
|
||||
expect(result.renderedText).toContain("它本身不是 Kubernetes Job 已创建的证据");
|
||||
expect(result.renderedText).toContain("durable dispatcher 负责自动重试;这不是重复执行 session send 的授权");
|
||||
expect(result.renderedText).not.toContain(`describe runnerjob/${plannedRunnerJobId}`);
|
||||
expect(result.renderedText).not.toContain("agentrun send session/");
|
||||
expect(Buffer.byteLength(result.renderedText, "utf8")).toBeLessThan(7000);
|
||||
});
|
||||
|
||||
test("JSON and YAML expose the same bounded runner admission contract and read-only Next", () => {
|
||||
const jsonText = renderSessionSendResult(command, durableAdmissionFixture, options("json")).renderedText;
|
||||
const yamlText = renderSessionSendResult(command, durableAdmissionFixture, options("yaml")).renderedText;
|
||||
const json = JSON.parse(jsonText) as Record<string, any>;
|
||||
const yaml = Bun.YAML.parse(yamlText) as Record<string, any>;
|
||||
expect(yaml).toEqual(json);
|
||||
expect(json.runnerAdmission).toMatchObject({
|
||||
mode: "durable-dispatch-intent",
|
||||
state: "pending",
|
||||
reason: "session-runner-admission-recovered",
|
||||
disposition: "recovered",
|
||||
runId,
|
||||
commandId,
|
||||
dispatchIntentId,
|
||||
plannedRunnerJobId,
|
||||
actualRunnerJobId: null,
|
||||
mutation: true,
|
||||
partialWrite: false,
|
||||
recoveredPriorPartialWrite: true,
|
||||
retryable: true,
|
||||
retryAuthority: "dispatcher",
|
||||
});
|
||||
expect(json.resources.runnerJob).toBeNull();
|
||||
expect(json.resources.plannedRunnerJob).toEqual({
|
||||
kind: "planned-runnerjob",
|
||||
id: plannedRunnerJobId,
|
||||
identityRole: "durable-planned",
|
||||
kubernetesJobCreated: null,
|
||||
kubernetesJobCreationEvidence: "not-asserted",
|
||||
state: "pending",
|
||||
durable: true,
|
||||
});
|
||||
expect(json.next.actions.length).toBeGreaterThan(0);
|
||||
for (const action of json.next.actions) {
|
||||
expect(action.mutation).toBe(false);
|
||||
expect(action.command).toMatch(/^bun scripts\/cli\.ts agentrun (?:describe|events|logs|result)\b/u);
|
||||
expect(action.command).not.toContain("agentrun send");
|
||||
expect(action.command).not.toContain(`runnerjob/${plannedRunnerJobId}`);
|
||||
}
|
||||
for (const output of [jsonText, yamlText]) expect(Buffer.byteLength(output, "utf8")).toBeLessThan(7000);
|
||||
});
|
||||
|
||||
test("legacy manager success output keeps runnerJob behavior when runnerAdmission is absent", () => {
|
||||
const human = renderSessionSendResult(command, legacySuccessFixture, options("human")).renderedText;
|
||||
const json = JSON.parse(renderSessionSendResult(command, legacySuccessFixture, options("json")).renderedText) as Record<string, any>;
|
||||
expect(human).not.toContain("RunnerAdmission:");
|
||||
expect(human).toContain("runnerjob/rjob_legacy_fixture state=queued");
|
||||
expect(human).toContain("只使用 manager 返回的资源 identity 做只读下钻");
|
||||
expect(json.runnerAdmission).toBeNull();
|
||||
expect(json.resources.runnerJob).toEqual({ kind: "runnerjob", id: "rjob_legacy_fixture", state: "queued" });
|
||||
expect(json.resources).not.toHaveProperty("plannedRunnerJob");
|
||||
expect(json.next.actions.some((action: Record<string, unknown>) => String(action.command).includes("describe runnerjob/rjob_legacy_fixture"))).toBe(true);
|
||||
});
|
||||
|
||||
test("typed failure remains fail-closed, bounded, and does not suggest a repeated send", () => {
|
||||
const human = renderSessionSendError(command, typedFailureFixture, options("human"));
|
||||
const jsonText = renderSessionSendError(command, typedFailureFixture, options("json")).renderedText;
|
||||
const yamlText = renderSessionSendError(command, typedFailureFixture, options("yaml")).renderedText;
|
||||
const json = JSON.parse(jsonText) as Record<string, any>;
|
||||
const yaml = Bun.YAML.parse(yamlText) as Record<string, any>;
|
||||
expect(human.ok).toBe(false);
|
||||
expect(human.renderedText).toContain("TypedFailure: infra-failed");
|
||||
expect(human.renderedText).toContain("Reason: session-runner-dispatcher-disabled");
|
||||
expect(human.renderedText).toContain("Mutation: false");
|
||||
expect(human.renderedText).toContain("PartialWrite: false");
|
||||
expect(human.renderedText).toContain("Retryable: false");
|
||||
expect(human.renderedText).toContain("RetryAuthority: none");
|
||||
expect(human.renderedText).toContain("RecoveryAction: restore-runner-dispatcher");
|
||||
expect(human.renderedText).not.toContain("agentrun send session/");
|
||||
expect(yaml).toEqual(json);
|
||||
expect(json).toMatchObject({
|
||||
kind: "SessionSendError",
|
||||
mutation: false,
|
||||
partialWrite: false,
|
||||
error: {
|
||||
failureKind: "validation-failed",
|
||||
typedFailureKind: "infra-failed",
|
||||
code: "runner-admission-failed",
|
||||
reason: "session-runner-dispatcher-disabled",
|
||||
retryable: false,
|
||||
retryAuthority: "none",
|
||||
recoveryAction: "restore-runner-dispatcher",
|
||||
},
|
||||
});
|
||||
for (const output of [human.renderedText, jsonText, yamlText]) expect(Buffer.byteLength(output, "utf8")).toBeLessThan(7000);
|
||||
});
|
||||
});
|
||||
|
||||
function options(output: AgentRunOutputMode): AgentRunResourceOptions {
|
||||
return {
|
||||
output,
|
||||
full: false,
|
||||
raw: false,
|
||||
debug: false,
|
||||
taskInput: false,
|
||||
limit: 20,
|
||||
cursor: null,
|
||||
queue: null,
|
||||
state: null,
|
||||
unread: false,
|
||||
readerId: "cli",
|
||||
taskId: null,
|
||||
runId: null,
|
||||
commandId: null,
|
||||
sessionId: null,
|
||||
afterSeq: null,
|
||||
eventDetailSeq: null,
|
||||
tail: null,
|
||||
fullText: false,
|
||||
reason: null,
|
||||
dryRun: false,
|
||||
file: null,
|
||||
aipod: null,
|
||||
idempotencyKey: null,
|
||||
promptStdin: false,
|
||||
node: null,
|
||||
lane: null,
|
||||
passthroughArgs: [],
|
||||
};
|
||||
}
|
||||
@@ -48,28 +48,37 @@ export function sessionSendSuccessProjection(command: string, raw: Record<string
|
||||
const commandResource = record(data.command);
|
||||
const runnerJob = record(data.runnerJob);
|
||||
const dispatchIntent = record(commandResource.dispatchIntent);
|
||||
const runnerAdmission = compactRunnerAdmission(record(data.runnerAdmission));
|
||||
const rawRunnerAdmission = record(data.runnerAdmission);
|
||||
const runnerAdmission = compactRunnerAdmission(rawRunnerAdmission, dispatchIntent);
|
||||
const request = compactRequest(record(data.request));
|
||||
const decision = stringOrNull(data.decision);
|
||||
const dryRun = data.dryRun === true;
|
||||
const reuseRun = decision === null
|
||||
? null
|
||||
: decision === "steer" || Object.keys(reusedIdleRun).length > 0;
|
||||
const createRunner = resolveCreateRunner({ decision, reuseRun, request: record(data.request), runnerJob, runnerAdmission: record(data.runnerAdmission) });
|
||||
const createRunner = resolveCreateRunner({ decision, reuseRun, request: record(data.request), runnerJob, runnerAdmission: rawRunnerAdmission });
|
||||
const sessionId = firstString(data.sessionId, record(run.sessionRef).sessionId, activeBefore.sessionId, reusedIdleRun.sessionId);
|
||||
const runId = firstString(run.id, data.runId, activeBefore.runId, reusedIdleRun.runId, record(data.runnerAdmission).runId);
|
||||
const commandId = firstString(commandResource.id, data.commandId, activeBefore.commandId, reusedIdleRun.commandId, record(data.runnerAdmission).commandId);
|
||||
const runnerJobId = firstString(runnerJob.id, runnerJob.runnerJobId, data.runnerJobId, record(data.runnerAdmission).plannedRunnerJobId, dispatchIntent.runnerJobId);
|
||||
const dispatchIntentId = firstString(dispatchIntent.id, record(data.runnerAdmission).dispatchIntentId);
|
||||
const mutation = booleanOrNull(data.mutation) ?? booleanOrNull(raw.mutation);
|
||||
const partialWrite = booleanOrNull(record(data.runnerAdmission).partialWrite) ?? false;
|
||||
const runId = firstString(run.id, data.runId, activeBefore.runId, reusedIdleRun.runId, rawRunnerAdmission.runId);
|
||||
const commandId = firstString(commandResource.id, data.commandId, activeBefore.commandId, reusedIdleRun.commandId, rawRunnerAdmission.commandId);
|
||||
const dispatchIntentId = firstString(dispatchIntent.id, rawRunnerAdmission.dispatchIntentId);
|
||||
const plannedRunnerJobId = firstString(rawRunnerAdmission.plannedRunnerJobId, dispatchIntent.runnerJobId, isPlannedRunnerJob(runnerJob) ? runnerJob.runnerJobId : null);
|
||||
const actualRunnerJobId = firstString(
|
||||
rawRunnerAdmission.actualRunnerJobId,
|
||||
dispatchIntent.actualRunnerJobId,
|
||||
isPlannedRunnerJob(runnerJob) ? null : runnerJob.id,
|
||||
isPlannedRunnerJob(runnerJob) ? null : runnerJob.runnerJobId,
|
||||
Object.keys(rawRunnerAdmission).length === 0 ? data.runnerJobId : null,
|
||||
);
|
||||
const mutation = booleanOrNull(data.mutation) ?? booleanOrNull(raw.mutation) ?? booleanOrNull(rawRunnerAdmission.mutation);
|
||||
const partialWrite = booleanOrNull(rawRunnerAdmission.partialWrite) ?? false;
|
||||
const recoveryActions = mergeActionSources(rawRunnerAdmission.recoveryActions, data.pollActions);
|
||||
const nextActions = dryRun
|
||||
? [{
|
||||
command: sessionSendConfirmCommand(command, sessionId),
|
||||
mutation: true,
|
||||
reason: "复用同一 prompt stdin,仅在确认计划后提交一次。",
|
||||
}]
|
||||
: readOnlyNextActions({ sessionId, runId, commandId, runnerJobId }, data.pollActions);
|
||||
: readOnlyNextActions({ sessionId, runId, commandId, runnerJobId: actualRunnerJobId }, recoveryActions);
|
||||
return {
|
||||
kind: dryRun ? "SessionSendPlan" : "SessionSendResult",
|
||||
ok: raw.ok !== false,
|
||||
@@ -85,7 +94,8 @@ export function sessionSendSuccessProjection(command: string, raw: Record<string
|
||||
session: resourceIdentity("session", sessionId, record(data.session)),
|
||||
run: resourceIdentity("run", runId, run),
|
||||
command: resourceIdentity("command", commandId, commandResource),
|
||||
runnerJob: resourceIdentity("runnerjob", runnerJobId, runnerJob),
|
||||
runnerJob: resourceIdentity("runnerjob", actualRunnerJobId, isPlannedRunnerJob(runnerJob) ? {} : runnerJob),
|
||||
plannedRunnerJob: plannedRunnerIdentity(plannedRunnerJobId, rawRunnerAdmission),
|
||||
dispatchIntent: resourceIdentity("dispatch-intent", dispatchIntentId, dispatchIntent),
|
||||
}),
|
||||
runnerAdmission,
|
||||
@@ -95,7 +105,9 @@ export function sessionSendSuccessProjection(command: string, raw: Record<string
|
||||
actions: nextActions,
|
||||
note: dryRun
|
||||
? "dry-run 未写入 durable facts;移除 --dry-run 会由 manager 再次按 session durable state 决定 steer 或 turn。"
|
||||
: "只使用 manager 返回的资源 identity 做只读下钻;不要在客户端补做 runner 调度或状态修复。",
|
||||
: runnerAdmission === null
|
||||
? "只使用 manager 返回的资源 identity 做只读下钻;不要在客户端补做 runner 调度或状态修复。"
|
||||
: "plannedRunnerJobId 只是 durable planned identity,不证明 Kubernetes Job 已创建;只按 manager 返回的只读入口观察 durable state。",
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
@@ -116,12 +128,20 @@ export function sessionSendErrorProjection(raw: Record<string, unknown>): Sessio
|
||||
const commandResource = firstRecord(record(resources.command), record(resourceIdentities.command), record(details.command));
|
||||
const runnerJob = firstRecord(record(resources.runnerJob), record(resourceIdentities.runnerJob), record(details.runnerJob));
|
||||
const dispatchIntent = firstRecord(record(resources.dispatchIntent), record(resourceIdentities.dispatchIntent), record(commandResource.dispatchIntent), record(details.dispatchIntent));
|
||||
const runnerAdmission = compactRunnerAdmission(firstRecord(record(details.runnerAdmission), record(agentrun.runnerAdmission)));
|
||||
const rawRunnerAdmission = firstRecord(record(details.runnerAdmission), record(agentrun.runnerAdmission));
|
||||
const runnerAdmission = compactRunnerAdmission(rawRunnerAdmission, dispatchIntent);
|
||||
const sessionId = firstString(details.sessionId, session.id, session.sessionId, record(runnerAdmission).sessionId);
|
||||
const runId = firstString(details.runId, run.id, run.runId, record(runnerAdmission).runId);
|
||||
const commandId = firstString(details.commandId, commandResource.id, commandResource.commandId, record(runnerAdmission).commandId);
|
||||
const runnerJobId = firstString(details.runnerJobId, runnerJob.id, runnerJob.runnerJobId, record(runnerAdmission).plannedRunnerJobId, dispatchIntent.runnerJobId);
|
||||
const dispatchIntentId = firstString(details.dispatchIntentId, dispatchIntent.id, record(runnerAdmission).dispatchIntentId);
|
||||
const plannedRunnerJobId = firstString(rawRunnerAdmission.plannedRunnerJobId, dispatchIntent.runnerJobId, isPlannedRunnerJob(runnerJob) ? runnerJob.runnerJobId : null);
|
||||
const actualRunnerJobId = firstString(
|
||||
rawRunnerAdmission.actualRunnerJobId,
|
||||
dispatchIntent.actualRunnerJobId,
|
||||
isPlannedRunnerJob(runnerJob) ? null : details.runnerJobId,
|
||||
isPlannedRunnerJob(runnerJob) ? null : runnerJob.id,
|
||||
isPlannedRunnerJob(runnerJob) ? null : runnerJob.runnerJobId,
|
||||
);
|
||||
const dispatchIntentId = firstString(details.dispatchIntentId, dispatchIntent.id, rawRunnerAdmission.dispatchIntentId);
|
||||
const typedFailureKind = firstString(nestedError.failureKind, details.failureKind, agentrun.failureKind);
|
||||
const code = firstString(details.code, nestedError.code, agentrun.code);
|
||||
const reason = firstString(details.reason, details.degradedReason, details.changeReason, record(runnerAdmission).reason);
|
||||
@@ -138,8 +158,26 @@ export function sessionSendErrorProjection(raw: Record<string, unknown>): Sessio
|
||||
: Array.isArray(agentrun.recoveryActions)
|
||||
? agentrun.recoveryActions
|
||||
: [];
|
||||
const nextActions = readOnlyNextActions({ sessionId, runId, commandId, runnerJobId }, recoveryActions);
|
||||
const nextActions = readOnlyNextActions({ sessionId, runId, commandId, runnerJobId: actualRunnerJobId }, mergeActionSources(rawRunnerAdmission.recoveryActions, recoveryActions));
|
||||
const message = truncateOneLine(firstString(raw.message, nestedError.message, agentrun.message) ?? "AgentRun session send failed", 400);
|
||||
const errorRetryable = firstBoolean(details.retryable, nestedError.retryable, agentrun.retryable, record(runnerAdmission).retryable);
|
||||
const errorRetryAuthority = firstString(details.retryAuthority, nestedError.retryAuthority, agentrun.retryAuthority, record(runnerAdmission).retryAuthority);
|
||||
const errorRecoveryAction = firstString(details.recoveryAction, nestedError.recoveryAction, agentrun.recoveryAction, record(runnerAdmission).recoveryAction);
|
||||
const errorProjection: Record<string, unknown> = {
|
||||
failureKind: stringOrNull(raw.failureKind),
|
||||
typedFailureKind,
|
||||
code,
|
||||
reason,
|
||||
message,
|
||||
httpStatus: numberOrNull(raw.httpStatus),
|
||||
traceId: firstString(details.traceId, agentrun.traceId),
|
||||
phase: firstString(details.phase, details.stage),
|
||||
durable: firstBoolean(details.durable, record(runnerAdmission).durable),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
if (errorRetryable !== null) errorProjection.retryable = errorRetryable;
|
||||
if (errorRetryAuthority !== null) errorProjection.retryAuthority = errorRetryAuthority;
|
||||
if (errorRecoveryAction !== null) errorProjection.recoveryAction = errorRecoveryAction;
|
||||
return {
|
||||
kind: "SessionSendError",
|
||||
ok: false,
|
||||
@@ -155,28 +193,20 @@ export function sessionSendErrorProjection(raw: Record<string, unknown>): Sessio
|
||||
session: resourceIdentity("session", sessionId, session),
|
||||
run: resourceIdentity("run", runId, run),
|
||||
command: resourceIdentity("command", commandId, commandResource),
|
||||
runnerJob: resourceIdentity("runnerjob", runnerJobId, runnerJob),
|
||||
runnerJob: resourceIdentity("runnerjob", actualRunnerJobId, isPlannedRunnerJob(runnerJob) ? {} : runnerJob),
|
||||
plannedRunnerJob: plannedRunnerIdentity(plannedRunnerJobId, rawRunnerAdmission),
|
||||
dispatchIntent: resourceIdentity("dispatch-intent", dispatchIntentId, dispatchIntent),
|
||||
}),
|
||||
runnerAdmission,
|
||||
request: null,
|
||||
error: {
|
||||
failureKind: stringOrNull(raw.failureKind),
|
||||
typedFailureKind,
|
||||
code,
|
||||
reason,
|
||||
message,
|
||||
httpStatus: numberOrNull(raw.httpStatus),
|
||||
traceId: firstString(details.traceId, agentrun.traceId),
|
||||
phase: firstString(details.phase, details.stage),
|
||||
durable: firstBoolean(details.durable, record(runnerAdmission).durable),
|
||||
valuesPrinted: false,
|
||||
},
|
||||
error: errorProjection,
|
||||
next: {
|
||||
actions: nextActions,
|
||||
note: partialWrite === true
|
||||
? "manager 已报告 partial write;先按资源 identity 只读核对 durable state,禁止盲目重发、客户端清理或另建 run。"
|
||||
: "先按资源 identity 核对 manager durable state;只有 manager 明确 fail-closed 或给出可重试合同后才能重发。",
|
||||
: runnerAdmission === null
|
||||
? "先按资源 identity 核对 manager durable state;只有 manager 明确 fail-closed 或给出可重试合同后才能重发。"
|
||||
: "plannedRunnerJobId 不是 Kubernetes Job 创建证据;先按只读入口核对 durable state,仅在 manager 明确给出可重试合同后重发。",
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
@@ -193,6 +223,9 @@ function renderSessionSendHuman(command: string, projection: SessionSendProjecti
|
||||
if (error.reason !== null) lines.push(`Reason: ${display(error.reason)}`);
|
||||
lines.push(`Message: ${display(error.message)}`);
|
||||
if (error.httpStatus !== null) lines.push(`HTTP: ${display(error.httpStatus)}`);
|
||||
if (error.retryable !== null && error.retryable !== undefined) lines.push(`Retryable: ${display(error.retryable)}`);
|
||||
if (error.retryAuthority !== null && error.retryAuthority !== undefined) lines.push(`RetryAuthority: ${display(error.retryAuthority)}`);
|
||||
if (error.recoveryAction !== null && error.recoveryAction !== undefined) lines.push(`RecoveryAction: ${display(error.recoveryAction)}`);
|
||||
} else {
|
||||
lines.push(projection.kind === "SessionSendPlan" ? "Session send plan" : "Session send submitted");
|
||||
lines.push(`OK: ${String(projection.ok)}`);
|
||||
@@ -217,7 +250,17 @@ function renderSessionSendHuman(command: string, projection: SessionSendProjecti
|
||||
if (projection.runnerAdmission !== null) {
|
||||
const admission = projection.runnerAdmission;
|
||||
lines.push("", "RunnerAdmission:");
|
||||
lines.push(` mode=${display(admission.mode)} state=${display(admission.state)} reason=${display(admission.reason)} durable=${display(admission.durable)}`);
|
||||
lines.push(` mode=${display(admission.mode)} state=${display(admission.state)} disposition=${display(admission.disposition)} reason=${display(admission.reason)} durable=${display(admission.durable)}`);
|
||||
lines.push(` runId=${display(admission.runId)} commandId=${display(admission.commandId)} dispatchIntentId=${display(admission.dispatchIntentId)}`);
|
||||
lines.push(` plannedRunnerJobId=${display(admission.plannedRunnerJobId)} actualRunnerJobId=${display(admission.actualRunnerJobId)}`);
|
||||
lines.push(` mutation=${display(admission.mutation)} partialWrite=${display(admission.partialWrite)} recoveredPriorPartialWrite=${display(admission.recoveredPriorPartialWrite)}`);
|
||||
lines.push(` retryable=${display(admission.retryable)} retryAuthority=${display(admission.retryAuthority)} willRetry=${display(admission.willRetry)} recoveryAction=${display(admission.recoveryAction)}`);
|
||||
if (admission.plannedRunnerJobId !== null && admission.plannedRunnerJobId !== undefined) {
|
||||
lines.push(" Semantics: plannedRunnerJobId 是 durable planned identity;它本身不是 Kubernetes Job 已创建的证据。");
|
||||
}
|
||||
if (admission.retryable === true && admission.retryAuthority === "dispatcher") {
|
||||
lines.push(" Retry: durable dispatcher 负责自动重试;这不是重复执行 session send 的授权。");
|
||||
}
|
||||
}
|
||||
if (projection.next.actions.length > 0 || projection.next.note !== null) {
|
||||
lines.push("", "Next:");
|
||||
@@ -228,13 +271,15 @@ function renderSessionSendHuman(command: string, projection: SessionSendProjecti
|
||||
}
|
||||
|
||||
function compactResources(resources: Record<string, Record<string, unknown> | null>): Record<string, unknown> {
|
||||
return {
|
||||
const result: Record<string, unknown> = {
|
||||
session: resources.session,
|
||||
run: resources.run,
|
||||
command: resources.command,
|
||||
runnerJob: resources.runnerJob,
|
||||
dispatchIntent: resources.dispatchIntent,
|
||||
};
|
||||
if (resources.plannedRunnerJob !== null && resources.plannedRunnerJob !== undefined) result.plannedRunnerJob = resources.plannedRunnerJob;
|
||||
return result;
|
||||
}
|
||||
|
||||
function resourceIdentity(kind: string, id: string | null, value: Record<string, unknown>): Record<string, unknown> | null {
|
||||
@@ -253,17 +298,43 @@ function resourceIdentity(kind: string, id: string | null, value: Record<string,
|
||||
return identity;
|
||||
}
|
||||
|
||||
function compactRunnerAdmission(value: Record<string, unknown>): Record<string, unknown> | null {
|
||||
function compactRunnerAdmission(value: Record<string, unknown>, dispatchIntent: Record<string, unknown> = {}): Record<string, unknown> | null {
|
||||
if (Object.keys(value).length === 0) return null;
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const key of ["mode", "state", "reason", "failureKind", "sessionId", "runId", "commandId", "dispatchIntentId", "plannedRunnerJobId", "mutation", "partialWrite", "durable"] as const) {
|
||||
for (const key of ["mode", "state", "reason", "disposition", "failureKind", "sessionId", "runId", "commandId", "dispatchIntentId", "plannedRunnerJobId", "actualRunnerJobId", "attemptCount", "mutation", "partialWrite", "recoveredPriorPartialWrite", "durable", "retryable", "retryAuthority", "willRetry", "retryAfterMs", "recoveryAction"] as const) {
|
||||
const field = value[key];
|
||||
if (typeof field === "string" || typeof field === "number" || typeof field === "boolean" || field === null) result[key] = typeof field === "string" ? truncateOneLine(field, 240) : field;
|
||||
}
|
||||
const actualRunnerJobId = firstString(value.actualRunnerJobId, dispatchIntent.actualRunnerJobId);
|
||||
if (actualRunnerJobId !== null) result.actualRunnerJobId = actualRunnerJobId;
|
||||
result.valuesPrinted = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
function plannedRunnerIdentity(id: string | null, admission: Record<string, unknown>): Record<string, unknown> | null {
|
||||
if (id === null) return null;
|
||||
const result: Record<string, unknown> = {
|
||||
kind: "planned-runnerjob",
|
||||
id,
|
||||
identityRole: "durable-planned",
|
||||
kubernetesJobCreated: null,
|
||||
kubernetesJobCreationEvidence: "not-asserted",
|
||||
};
|
||||
const state = admission.state;
|
||||
if (typeof state === "string" || typeof state === "number" || typeof state === "boolean") result.state = state;
|
||||
const durable = admission.durable;
|
||||
if (typeof durable === "boolean") result.durable = durable;
|
||||
return result;
|
||||
}
|
||||
|
||||
function isPlannedRunnerJob(value: Record<string, unknown>): boolean {
|
||||
return value.action === "runner-dispatch-admitted";
|
||||
}
|
||||
|
||||
function mergeActionSources(...values: unknown[]): unknown[] {
|
||||
return values.flatMap((value) => Array.isArray(value) ? value.slice(0, 12) : []).slice(0, 12);
|
||||
}
|
||||
|
||||
function compactRequest(value: Record<string, unknown>): Record<string, unknown> | null {
|
||||
if (Object.keys(value).length === 0) return null;
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
Reference in New Issue
Block a user