Merge pull request #458 from pikasTech/fix/code-agent-session-retry-438
fix: preserve Code Agent session continuity
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
runDevCloudWorkbenchExternalNetworkFixtureSmoke,
|
||||
runDevCloudWorkbenchLayoutSmoke,
|
||||
runDevCloudWorkbenchQuickPromptsFixtureSmoke,
|
||||
runDevCloudWorkbenchSessionContinuityFixtureSmoke,
|
||||
runDevCloudWorkbenchSlowBlockerFixtureSmoke,
|
||||
runDevCloudWorkbenchStaticSmoke,
|
||||
runDevCloudWorkbenchTimeoutFixtureSmoke,
|
||||
@@ -746,6 +747,29 @@ test("local external network fixture renders GitHub blocker with trace and witho
|
||||
assert.equal(blockerCheck.observations.prompt.classification.blocker, "runner-blocked");
|
||||
});
|
||||
|
||||
test("local session continuity fixture reuses Code Agent context and retries with degraded-copy guard", async () => {
|
||||
const report = await runDevCloudWorkbenchSessionContinuityFixtureSmoke();
|
||||
if (report.status === "skip") {
|
||||
assert.match(report.summary, /Playwright is unavailable/u);
|
||||
return;
|
||||
}
|
||||
|
||||
assert.equal(report.status, "pass", JSON.stringify(report.blockers, null, 2));
|
||||
assert.equal(report.evidenceLevel, "SOURCE");
|
||||
assert.equal(report.devLive, false);
|
||||
assert.equal(report.checks.find((check) => check.id === "local-agent-session-continuity-two-turns")?.status, "pass");
|
||||
assert.equal(report.checks.find((check) => check.id === "local-agent-session-continuity-retry")?.status, "pass");
|
||||
assert.equal(report.checks.find((check) => check.id === "local-agent-session-continuity-degraded-copy")?.status, "pass");
|
||||
const continuity = report.checks.find((check) => check.id === "local-agent-session-continuity-two-turns")?.observations;
|
||||
assert.equal(continuity.secondRequest.conversationId, continuity.firstRequest.conversationId);
|
||||
assert.equal(continuity.secondRequest.sessionId, "ses_source_fixture_continuity");
|
||||
assert.equal(continuity.secondRequest.threadId, "thread_source_fixture_continuity");
|
||||
const retry = report.checks.find((check) => check.id === "local-agent-session-continuity-retry")?.observations;
|
||||
assert.equal(retry.retryRequest.message, retry.failedRequest.message);
|
||||
assert.equal(retry.retryRequest.sessionId, retry.failedRequest.sessionId);
|
||||
assert.equal(retry.retryRequest.threadId, retry.failedRequest.threadId);
|
||||
});
|
||||
|
||||
test("Code Agent quick prompt fixture fills input, does not autosend writes, and fits mobile", async () => {
|
||||
const report = await runDevCloudWorkbenchQuickPromptsFixtureSmoke();
|
||||
if (report.status === "skip") {
|
||||
|
||||
@@ -321,6 +321,7 @@ const requiredCodeAgentEvidenceTerms = Object.freeze([
|
||||
"providerTrace.command",
|
||||
"providerTrace.terminalStatus",
|
||||
"message-runtime-path",
|
||||
"threadId",
|
||||
"not-codex-stdio",
|
||||
"not-write-capable",
|
||||
"process-local-session-registry",
|
||||
@@ -393,6 +394,7 @@ export async function runDevCloudWorkbenchSmoke(argv = []) {
|
||||
if (args.mode === "m3-status-fixture") return runM3StatusFixtureSmoke();
|
||||
if (args.mode === "layout") return runDevCloudWorkbenchLayoutSmoke(args);
|
||||
if (args.mode === "local-agent-fixture") return runDevCloudWorkbenchLocalAgentFixtureSmoke();
|
||||
if (args.mode === "session-continuity-fixture") return runDevCloudWorkbenchSessionContinuityFixtureSmoke();
|
||||
if (args.mode === "quick-prompts-fixture") return runDevCloudWorkbenchQuickPromptsFixtureSmoke();
|
||||
if (args.mode === "mobile") return runDevCloudWorkbenchMobileSmoke();
|
||||
return runStaticSmoke();
|
||||
@@ -456,6 +458,8 @@ export function parseSmokeArgs(argv) {
|
||||
args.mobile = true;
|
||||
} else if (arg === "--local-agent-fixture") {
|
||||
args.mode = "local-agent-fixture";
|
||||
} else if (arg === "--session-continuity-fixture") {
|
||||
args.mode = "session-continuity-fixture";
|
||||
} else if (arg === "--quick-prompts-fixture") {
|
||||
args.mode = "quick-prompts-fixture";
|
||||
} else if (arg === "--auth-fixture") {
|
||||
@@ -1607,6 +1611,7 @@ function baseReport({
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --layout",
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --mobile",
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --local-agent-fixture",
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --session-continuity-fixture",
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --quick-prompts-fixture",
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --local-agent-timeout-fixture",
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --layout --url http://74.48.78.17:16666/",
|
||||
@@ -1624,10 +1629,11 @@ function baseReport({
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --layout",
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --mobile",
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --local-agent-fixture",
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --session-continuity-fixture",
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --quick-prompts-fixture",
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --local-agent-timeout-fixture"
|
||||
],
|
||||
evidence: ["Source mode verifies the source contract and same-origin Code Agent wiring only.", "Layout browser mode verifies desktop/mobile sidebar collapse, hit targets, non-overlap, and outer-scroll lock at SOURCE level.", "Mobile browser mode verifies the 390x844 outer-scroll lock and help route at SOURCE level.", "Local fixture browser mode verifies both Code Agent prompts past the old 4500ms window at SOURCE level.", "Quick prompt fixture verifies fill-only behavior, explicit-send write semantics, copy boundary, and mobile containment.", "Local timeout fixture verifies bounded timeout UI classification without live acceptance."],
|
||||
evidence: ["Source mode verifies the source contract and same-origin Code Agent wiring only.", "Layout browser mode verifies desktop/mobile sidebar collapse, hit targets, non-overlap, and outer-scroll lock at SOURCE level.", "Mobile browser mode verifies the 390x844 outer-scroll lock and help route at SOURCE level.", "Local fixture browser mode verifies both Code Agent prompts past the old 4500ms window at SOURCE level.", "Session continuity fixture verifies same-browser conversation/session/thread reuse and retry request context at SOURCE level.", "Quick prompt fixture verifies fill-only behavior, explicit-send write semantics, copy boundary, and mobile containment.", "Local timeout fixture verifies bounded timeout UI classification without live acceptance."],
|
||||
summary: "Source, layout, mobile, and local fixture workbench smokes are SOURCE-level evidence and do not prove the deployed browser journey."
|
||||
},
|
||||
dryRun: {
|
||||
@@ -2410,7 +2416,7 @@ function hasStableRouteControls({ html, styles }) {
|
||||
/\.(?:status-dot|state-tag|badge)[^{]*\{[^}]*max-width:\s*100%;[^}]*line-height:\s*1\.25;[^}]*white-space:\s*normal;[^}]*overflow-wrap:\s*anywhere;/su.test(styles) &&
|
||||
/\.probe-card\s*\{[^}]*min-width:\s*0;/su.test(styles) &&
|
||||
/\.probe-card strong\s*\{[^}]*line-height:\s*1\.2;[^}]*overflow-wrap:\s*anywhere;/su.test(styles) &&
|
||||
/@media\s*\(max-width:\s*860px\)[\s\S]*?\.message-trace,\s*\n\s*\.message-m3-evidence(?:,\s*\n\s*\.message-actions)?\s*\{[\s\S]*?grid-column:\s*1;/u.test(styles)
|
||||
/@media\s*\(max-width:\s*860px\)[\s\S]*?\.message-runtime-path,\s*\n\s*\.message-session-context,\s*\n\s*\.message-trace,\s*\n\s*\.message-m3-evidence,\s*\n\s*\.message-actions\s*\{[\s\S]*?grid-column:\s*1;/u.test(styles)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2596,6 +2602,8 @@ function hasCodeAgentCompletedEvidenceVisibility({ app, codeAgentM3Evidence }) {
|
||||
/CODEX_APP_SERVER_IMPLEMENTATION_TYPE/u.test(runnerEvidenceBody) &&
|
||||
/CODEX_APP_SERVER_PROTOCOL/u.test(runnerEvidenceBody) &&
|
||||
/long-lived-codex-stdio-session/u.test(runnerEvidenceBody) &&
|
||||
/threadIdFrom\(value\)/u.test(runnerEvidenceBody) &&
|
||||
/hasProviderTrace\(value\)/u.test(runnerEvidenceBody) &&
|
||||
/isTextFallbackChatResult/u.test(app) &&
|
||||
/value\?\.session\?\.status === "idle"/u.test(runnerEvidenceBody) &&
|
||||
/value\?\.longLivedSessionGate\?\.status === "pass"/u.test(runnerEvidenceBody) &&
|
||||
@@ -2606,8 +2614,10 @@ function hasCodeAgentCompletedEvidenceVisibility({ app, codeAgentM3Evidence }) {
|
||||
/TRUSTED_CODE_AGENT_PROVIDERS/u.test(app) &&
|
||||
/UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN/u.test(realEvidenceBody) &&
|
||||
/value\?\.conversationId \|\| value\?\.sessionId/u.test(realEvidenceBody) &&
|
||||
/codeAgentContinuityBlocker/u.test(app) &&
|
||||
/code_agent_session_evidence_missing/u.test(app) &&
|
||||
/isRealCompletedChatResult\(result\)/u.test(app) &&
|
||||
/classifyCodeAgentCompletion\(result\)/u.test(app) &&
|
||||
/classifyCodeAgentCompletion\(result,\s*\{\s*blockedError\s*\}\)/u.test(app) &&
|
||||
/status:\s*"completed"/u.test(functionBody(app, "classifyCodeAgentCompletion")) &&
|
||||
/m3_io_result/u.test(functionBody(app, "classifyCodeAgentCompletion")) &&
|
||||
/sourceKind:\s*result\?\.m3Io\?\.type === "m3_io_result"[\s\S]*?"CONTROLLED"[\s\S]*?"DEV-LIVE"/u.test(functionBody(app, "classifyCodeAgentCompletion")) &&
|
||||
@@ -2725,6 +2735,7 @@ function hasCodeAgentConversationUxStates({ app, styles }) {
|
||||
/message-user/u.test(styles) &&
|
||||
/message-agent/u.test(styles) &&
|
||||
/message-pending-context\s*\{/u.test(styles) &&
|
||||
/message-session-context\s*\{/u.test(styles) &&
|
||||
/message-m3-evidence\s*\{/u.test(styles) &&
|
||||
/message-m3-rows\s*\{/u.test(styles) &&
|
||||
/copy-chip\s*\{/u.test(styles) &&
|
||||
@@ -4067,7 +4078,11 @@ async function runCodeAgentPromptJourney(page, prompt, options = {}) {
|
||||
method: request.method(),
|
||||
urlPath: new URL(request.url()).pathname,
|
||||
traceId: stringOrFallback(body?.traceId, request.headers()["x-trace-id"] ?? "unknown"),
|
||||
messageLength: typeof body?.message === "string" ? body.message.length : 0
|
||||
message: typeof body?.message === "string" ? body.message : "",
|
||||
messageLength: typeof body?.message === "string" ? body.message.length : 0,
|
||||
conversationId: typeof body?.conversationId === "string" ? body.conversationId : null,
|
||||
sessionId: typeof body?.sessionId === "string" ? body.sessionId : null,
|
||||
threadId: typeof body?.threadId === "string" ? body.threadId : null
|
||||
});
|
||||
};
|
||||
const responseListener = async (response) => {
|
||||
@@ -4566,6 +4581,180 @@ export async function runDevCloudWorkbenchExternalNetworkFixtureSmoke(options =
|
||||
});
|
||||
}
|
||||
|
||||
export async function runDevCloudWorkbenchSessionContinuityFixtureSmoke(options = {}) {
|
||||
let chromium;
|
||||
try {
|
||||
({ chromium } = await importPlaywright());
|
||||
} catch (error) {
|
||||
const summary = `Code Agent session continuity fixture skipped because Playwright is unavailable: ${error.message}`;
|
||||
return {
|
||||
status: "skip",
|
||||
task: "DC-DCSN-P0-2026-003",
|
||||
mode: "local-agent-session-continuity-fixture-browser",
|
||||
evidenceLevel: "SOURCE",
|
||||
devLive: false,
|
||||
summary,
|
||||
checks: [{
|
||||
id: "local-agent-session-continuity-dependency",
|
||||
status: "skip",
|
||||
summary: playwrightDependencySummary,
|
||||
evidence: [playwrightDependencyCommand, "package.json dependencies.playwright"]
|
||||
}],
|
||||
blockers: [{
|
||||
type: "environment_blocker",
|
||||
scope: "local-agent-session-continuity-dependency",
|
||||
status: "open",
|
||||
summary
|
||||
}],
|
||||
safety: staticSafety()
|
||||
};
|
||||
}
|
||||
|
||||
const server = await startStaticWebServer({
|
||||
agentFixture: true,
|
||||
agentFixtureMode: "session-continuity",
|
||||
agentDelayMs: options.responseDelayMs ?? 80
|
||||
});
|
||||
let browser;
|
||||
const requests = [];
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
|
||||
page.on("request", (request) => {
|
||||
const requestUrl = new URL(request.url());
|
||||
if (request.method() !== "POST" || requestUrl.pathname !== "/v1/agent/chat") return;
|
||||
const body = parseJsonOrNull(request.postData() ?? "");
|
||||
requests.push({
|
||||
message: body?.message ?? "",
|
||||
conversationId: body?.conversationId ?? null,
|
||||
sessionId: body?.sessionId ?? null,
|
||||
threadId: body?.threadId ?? null,
|
||||
traceId: body?.traceId ?? null
|
||||
});
|
||||
});
|
||||
await page.goto(server.url, { waitUntil: "networkidle", timeout: 15000 });
|
||||
await loginWithDefaultCredentials(page);
|
||||
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
|
||||
await page.waitForFunction(
|
||||
() => document.querySelector("#live-status")?.textContent?.trim() === "只读模式",
|
||||
null,
|
||||
{ timeout: 8000 }
|
||||
);
|
||||
|
||||
const first = await runCodeAgentPromptJourney(page, {
|
||||
id: "continuity-first",
|
||||
label: "第一轮",
|
||||
text: "第一轮:请记住当前会话"
|
||||
}, { sourceFixture: true });
|
||||
const second = await runCodeAgentPromptJourney(page, {
|
||||
id: "continuity-second",
|
||||
label: "第二轮",
|
||||
text: "第二轮:继续同一会话"
|
||||
}, { sourceFixture: true });
|
||||
await page.locator("#command-input").fill("触发 providerTrace 缺失");
|
||||
await page.locator("#command-send").click();
|
||||
await page.waitForFunction(
|
||||
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "Session 受阻",
|
||||
null,
|
||||
{ timeout: 8000 }
|
||||
);
|
||||
await page.locator(".message-card.status-failed .message-action-retry").last().click();
|
||||
await page.waitForFunction(
|
||||
() => document.querySelectorAll(".message-card.status-failed").length >= 2,
|
||||
null,
|
||||
{ timeout: 8000 }
|
||||
);
|
||||
const ui = await inspectJourneyUi(page);
|
||||
const text = await page.evaluate(() => document.body.textContent?.replace(/\s+/gu, " ").trim() ?? "");
|
||||
const [firstRequest, secondRequest, failedRequest, retryRequest] = requests;
|
||||
const continuityOk = Boolean(
|
||||
firstRequest?.conversationId &&
|
||||
secondRequest?.conversationId === firstRequest.conversationId &&
|
||||
secondRequest?.sessionId === "ses_source_fixture_continuity" &&
|
||||
secondRequest?.threadId === "thread_source_fixture_continuity"
|
||||
);
|
||||
const retryOk = Boolean(
|
||||
retryRequest?.message === failedRequest?.message &&
|
||||
retryRequest?.conversationId === failedRequest?.conversationId &&
|
||||
retryRequest?.sessionId === failedRequest?.sessionId &&
|
||||
retryRequest?.threadId === failedRequest?.threadId
|
||||
);
|
||||
const degradedOk = /Code Agent completed 但会话证据缺失|会话证据缺失|新会话\/会话降级|按会话降级处理/u.test(text);
|
||||
const checks = [
|
||||
{
|
||||
id: "local-agent-session-continuity-two-turns",
|
||||
status: continuityOk && first.status === "pass" && second.status === "pass" ? "pass" : "blocked",
|
||||
summary: "第二轮 /v1/agent/chat 请求复用第一轮建立的 conversationId、sessionId 和 threadId。",
|
||||
observations: { firstRequest, secondRequest, first, second }
|
||||
},
|
||||
{
|
||||
id: "local-agent-session-continuity-retry",
|
||||
status: retryOk ? "pass" : "blocked",
|
||||
summary: "重试上一条会重发上一条用户输入,并携带同一 conversation/session/thread 上下文。",
|
||||
observations: { failedRequest, retryRequest }
|
||||
},
|
||||
{
|
||||
id: "local-agent-session-continuity-degraded-copy",
|
||||
status: degradedOk && ui.failedRetryActionVisible && ui.failedTraceActionVisible ? "pass" : "blocked",
|
||||
summary: "缺失 providerTrace/thread 的 completed 响应显示结构化会话降级/受阻文案,不静默冒充同一会话成功。",
|
||||
observations: { degradedOk, ui, visibleTextMatched: degradedOk }
|
||||
}
|
||||
];
|
||||
const blockers = checks
|
||||
.filter((check) => check.status !== "pass")
|
||||
.map((check) => ({
|
||||
type: "observability_blocker",
|
||||
scope: check.id,
|
||||
status: "open",
|
||||
summary: check.summary
|
||||
}));
|
||||
return {
|
||||
status: blockers.length === 0 ? "pass" : "blocked",
|
||||
task: "DC-DCSN-P0-2026-003",
|
||||
issue: "pikasTech/HWLAB#438",
|
||||
mode: "local-agent-session-continuity-fixture-browser",
|
||||
url: server.url,
|
||||
generatedAt: new Date().toISOString(),
|
||||
evidenceLevel: "SOURCE",
|
||||
devLive: false,
|
||||
summary: "Local SOURCE fixture verifies browser Code Agent session continuity, retry context, and degraded missing providerTrace/thread copy without claiming DEV-LIVE.",
|
||||
checks,
|
||||
blockers,
|
||||
safety: {
|
||||
...staticSafety(),
|
||||
localFixtureOnly: true,
|
||||
fixtureTrustBoundary: "SOURCE fixture verifies browser request semantics only and never updates DEV live.",
|
||||
observedPostCount: requests.length
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "blocked",
|
||||
task: "DC-DCSN-P0-2026-003",
|
||||
mode: "local-agent-session-continuity-fixture-browser",
|
||||
evidenceLevel: "SOURCE",
|
||||
devLive: false,
|
||||
summary: `Code Agent session continuity fixture failed: ${error.message}`,
|
||||
checks: [{
|
||||
id: "local-agent-session-continuity-fixture-runtime",
|
||||
status: "blocked",
|
||||
summary: error.message,
|
||||
observations: { requests }
|
||||
}],
|
||||
blockers: [{
|
||||
type: "observability_blocker",
|
||||
scope: "local-agent-session-continuity-fixture-runtime",
|
||||
status: "open",
|
||||
summary: error.message
|
||||
}],
|
||||
safety: staticSafety()
|
||||
};
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
await server.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function runDevCloudWorkbenchQuickPromptsFixtureSmoke() {
|
||||
let chromium;
|
||||
try {
|
||||
@@ -5670,6 +5859,198 @@ function longSourceFixtureTraceEvents(traceId, count = 18) {
|
||||
});
|
||||
}
|
||||
|
||||
function sessionContinuityFixturePayload({ body, traceId, conversationId, messageId, timestamp, options }) {
|
||||
const fixtureState = options.sessionContinuityState ?? {
|
||||
sessionId: "ses_source_fixture_continuity",
|
||||
threadId: "thread_source_fixture_continuity",
|
||||
turn: 0,
|
||||
traceIds: []
|
||||
};
|
||||
options.sessionContinuityState = fixtureState;
|
||||
fixtureState.turn += 1;
|
||||
fixtureState.traceIds.push(traceId);
|
||||
const message = String(body?.message ?? "");
|
||||
const missingProviderTrace = /providerTrace\s*缺失|缺失\s*providerTrace/u.test(message);
|
||||
const sessionId = fixtureState.sessionId;
|
||||
const threadId = missingProviderTrace ? null : fixtureState.threadId;
|
||||
const turn = fixtureState.turn;
|
||||
const toolName = turn > 1 ? "codex-app-server.thread/resume+turn/start" : "codex-app-server.thread/start+turn/start";
|
||||
const events = longSourceFixtureTraceEvents(traceId, missingProviderTrace ? 6 : 18).map((event, index) => ({
|
||||
...event,
|
||||
label: index === 0 ? (turn > 1 ? "session:reused" : "session:created") : event.label,
|
||||
sessionId,
|
||||
...(threadId ? { threadId } : {})
|
||||
}));
|
||||
const session = {
|
||||
sessionId,
|
||||
conversationId,
|
||||
conversationIds: [conversationId],
|
||||
status: "idle",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "workspace-write",
|
||||
runnerKind: "codex-app-server-stdio-runner",
|
||||
sessionMode: "codex-app-server-stdio-long-lived",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
implementationType: "repo-owned-codex-app-server-stdio-session",
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
idleTimeoutMs: 1800000,
|
||||
expiresAt: "2026-05-22T00:30:00.000Z",
|
||||
lastTraceId: traceId,
|
||||
currentTraceId: null,
|
||||
turn,
|
||||
threadId,
|
||||
reused: turn > 1,
|
||||
durable: true,
|
||||
longLivedSession: true,
|
||||
codexStdio: true,
|
||||
writeCapable: true,
|
||||
secretMaterialStored: false,
|
||||
valuesRedacted: true
|
||||
};
|
||||
const payload = {
|
||||
conversationId,
|
||||
sessionId,
|
||||
messageId,
|
||||
status: "completed",
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
traceId,
|
||||
provider: "codex-stdio",
|
||||
model: "gpt-5.5",
|
||||
backend: "local-source-fixture/codex-app-server-stdio",
|
||||
projectId: stringOrFallback(body?.projectId, gateSummary.topology.projectId),
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "workspace-write",
|
||||
session,
|
||||
sessionMode: "codex-app-server-stdio-long-lived",
|
||||
sessionReuse: {
|
||||
conversationId,
|
||||
sessionId,
|
||||
threadId,
|
||||
mapped: true,
|
||||
reused: turn > 1,
|
||||
turn,
|
||||
previousTurns: Math.max(0, turn - 1),
|
||||
workspace: "/workspace/hwlab",
|
||||
status: "idle"
|
||||
},
|
||||
implementationType: "repo-owned-codex-app-server-stdio-session",
|
||||
runnerLimitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted"],
|
||||
codexStdioFeasibility: {
|
||||
ready: true,
|
||||
canStartLongLivedCodexStdio: true,
|
||||
commandProbe: { ready: true },
|
||||
blockers: [],
|
||||
blockerCodes: []
|
||||
},
|
||||
longLivedSessionGate: {
|
||||
status: "pass",
|
||||
pass: true,
|
||||
blockers: []
|
||||
},
|
||||
toolCalls: [{
|
||||
id: `tool_${turn}`,
|
||||
type: "codex-app-server-stdio",
|
||||
name: toolName,
|
||||
status: "completed",
|
||||
cwd: "/workspace/hwlab",
|
||||
command: "codex app-server --listen stdio://",
|
||||
exitCode: 0,
|
||||
stdout: [threadId ? `threadId=${threadId}` : "threadId=not_observed", `turnId=turn_source_fixture_${turn}`, "terminalStatus=completed"].join(" "),
|
||||
stderrSummary: "",
|
||||
outputTruncated: false,
|
||||
traceId
|
||||
}],
|
||||
skills: { status: "not_requested", count: 0, totalCount: 0, items: [], blockers: [] },
|
||||
runner: {
|
||||
kind: "codex-app-server-stdio-runner",
|
||||
provider: "codex-stdio",
|
||||
backend: "hwlab-cloud-api/codex-app-server-stdio",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "workspace-write",
|
||||
session: "codex-app-server-stdio-long-lived",
|
||||
sessionMode: "codex-app-server-stdio-long-lived",
|
||||
sessionId,
|
||||
turn,
|
||||
sessionReused: turn > 1,
|
||||
implementationType: "repo-owned-codex-app-server-stdio-session",
|
||||
codexStdio: true,
|
||||
longLivedSession: true,
|
||||
durableSession: true,
|
||||
writeCapable: true,
|
||||
readOnly: false,
|
||||
capabilityLevel: "long-lived-codex-stdio-session"
|
||||
},
|
||||
runnerTrace: {
|
||||
traceId,
|
||||
runnerKind: "codex-app-server-stdio-runner",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "workspace-write",
|
||||
sessionMode: "codex-app-server-stdio-long-lived",
|
||||
implementationType: "repo-owned-codex-app-server-stdio-session",
|
||||
sessionId,
|
||||
threadId,
|
||||
sessionStatus: "idle",
|
||||
turn,
|
||||
sessionReused: turn > 1,
|
||||
events,
|
||||
eventLabels: events.map((event) => event.label),
|
||||
lastEvent: events.at(-1),
|
||||
updatedAt: timestamp,
|
||||
startedAt: timestamp,
|
||||
elapsedMs: 120,
|
||||
outputTruncated: false
|
||||
},
|
||||
conversationFacts: {
|
||||
conversationId,
|
||||
sessionId,
|
||||
sessionStatus: "idle",
|
||||
sessionMode: "codex-app-server-stdio-long-lived",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
runnerKind: "codex-app-server-stdio-runner",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "workspace-write",
|
||||
turnCount: turn,
|
||||
latestTraceId: traceId,
|
||||
traceIds: [...fixtureState.traceIds],
|
||||
latestSkills: null,
|
||||
recentToolCalls: [{ name: toolName, status: "completed" }],
|
||||
facts: [],
|
||||
valuesRedacted: true,
|
||||
secretMaterialStored: false
|
||||
},
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
reply: {
|
||||
messageId,
|
||||
role: "assistant",
|
||||
content: missingProviderTrace
|
||||
? "本响应故意缺少 providerTrace/thread,用于验证前端会话降级显示。"
|
||||
: `SOURCE fixture:第 ${turn} 轮已在同一 Code Agent conversation/session/thread 中处理。`,
|
||||
createdAt: timestamp
|
||||
},
|
||||
usage: null
|
||||
};
|
||||
if (!missingProviderTrace) {
|
||||
payload.sourceKind = "SOURCE";
|
||||
payload.evidenceLevel = "SOURCE";
|
||||
payload.providerTrace = {
|
||||
source: "SOURCE-local-browser-fixture",
|
||||
sourceKind: "SOURCE",
|
||||
transport: "stdio",
|
||||
protocol: "codex-app-server-jsonrpc-stdio",
|
||||
command: "codex app-server --listen stdio://",
|
||||
toolName,
|
||||
threadId,
|
||||
turnId: `turn_source_fixture_${turn}`,
|
||||
terminalStatus: "completed",
|
||||
valuesPrinted: false,
|
||||
redacted: true
|
||||
};
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function startStaticWebServer(options = {}) {
|
||||
const rootDir = path.resolve(options.rootDir ?? webRoot);
|
||||
const authFixtureSessions = new Set();
|
||||
@@ -6446,6 +6827,17 @@ async function handleLocalAgentFixtureApi({ request, response, url, options = {}
|
||||
const messageId = isExternalNetworkPrompt
|
||||
? "msg_source_fixture_browser_external_network"
|
||||
: isSkillListPrompt ? "msg_source_fixture_browser_skills" : "msg_source_fixture_browser_simple";
|
||||
if (options.agentFixtureMode === "session-continuity") {
|
||||
jsonResponse(response, 200, sessionContinuityFixturePayload({
|
||||
body,
|
||||
traceId,
|
||||
conversationId,
|
||||
messageId,
|
||||
timestamp,
|
||||
options
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
if (options.agentFixtureMode === "external-network-blocker") {
|
||||
const blocker = {
|
||||
code: "external_network_blocked",
|
||||
@@ -9086,12 +9478,13 @@ function escapeRegExp(value) {
|
||||
export function printSmokeHelp() {
|
||||
return {
|
||||
status: "usage",
|
||||
command: `node scripts/dev-cloud-workbench-smoke.mjs --source | --layout [--url http://74.48.78.17:16666/] | --mobile | --local-agent-fixture | --quick-prompts-fixture | --local-agent-timeout-fixture | --dom-only --url http://74.48.78.17:16666/ [--report ${domOnlyReportPath}] | --live --confirm-dev-live --url http://74.48.78.17:16666/ [--report ${liveReportPath}]`,
|
||||
command: `node scripts/dev-cloud-workbench-smoke.mjs --source | --layout [--url http://74.48.78.17:16666/] | --mobile | --local-agent-fixture | --session-continuity-fixture | --quick-prompts-fixture | --local-agent-timeout-fixture | --dom-only --url http://74.48.78.17:16666/ [--report ${domOnlyReportPath}] | --live --confirm-dev-live --url http://74.48.78.17:16666/ [--report ${liveReportPath}]`,
|
||||
notes: [
|
||||
"Default/source mode reads repository files and emits SOURCE-level evidence only; it never calls the live provider and must not claim DEV-LIVE.",
|
||||
"--layout runs desktop and mobile browser geometry/hit-target checks for sidebar collapse/expand; with --url it remains layout-only live evidence and does not claim M3 acceptance.",
|
||||
"--mobile runs a local static 390x844 browser hit-test and still emits SOURCE-level evidence only.",
|
||||
"--local-agent-fixture runs two local browser send/reply paths with a sanitized SOURCE fixture delayed beyond 4500ms and does not claim DEV-LIVE.",
|
||||
"--session-continuity-fixture verifies same-browser conversation/session/thread reuse, retry context, and degraded missing providerTrace/thread copy without claiming DEV-LIVE.",
|
||||
"--quick-prompts-fixture clicks Code Agent quick prompts locally and verifies write prompts fill only, without automatic send or hardware write.",
|
||||
"--local-agent-timeout-fixture runs a local slow SOURCE fixture to verify bounded timeout UI classification and retry preservation without live acceptance.",
|
||||
"--dom-only preserves runtime/web-asset identity preflight, inspects read-only deployed DOM/help wiring, and never posts /v1/agent/chat.",
|
||||
|
||||
+317
-15
@@ -167,6 +167,7 @@ await ensureWorkbenchAuth({
|
||||
const state = {
|
||||
conversationId: null,
|
||||
sessionId: null,
|
||||
threadId: null,
|
||||
codeAgentAvailability: null,
|
||||
chatMessages: [],
|
||||
traceStreams: new Map(),
|
||||
@@ -666,6 +667,7 @@ function initCommandBar() {
|
||||
state.chatMessages = [];
|
||||
state.conversationId = null;
|
||||
state.sessionId = null;
|
||||
state.threadId = null;
|
||||
state.sessionStatus = null;
|
||||
state.currentRequest = null;
|
||||
state.canceledTraces.clear();
|
||||
@@ -684,6 +686,13 @@ async function submitAgentMessage(value, options = {}) {
|
||||
const traceId = options.traceId ?? nextProtocolId("trc");
|
||||
const activeConversationId = options.conversationId ?? state.conversationId ?? nextProtocolId("cnv");
|
||||
const requestedSessionId = options.sessionId === undefined ? sessionIdForNextRequest() : options.sessionId;
|
||||
const requestedThreadId = options.threadId === undefined ? threadIdForNextRequest() : options.threadId;
|
||||
const pendingContinuity = pendingSessionContinuity({
|
||||
conversationId: activeConversationId,
|
||||
sessionId: requestedSessionId,
|
||||
threadId: requestedThreadId,
|
||||
retryOf: options.retryOf
|
||||
});
|
||||
state.conversationId = activeConversationId;
|
||||
const userMessage = {
|
||||
id: nextProtocolId("msg"),
|
||||
@@ -694,6 +703,7 @@ async function submitAgentMessage(value, options = {}) {
|
||||
traceId,
|
||||
conversationId: activeConversationId,
|
||||
sessionId: requestedSessionId,
|
||||
threadId: requestedThreadId,
|
||||
retryOf: options.retryOf ?? null,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
@@ -706,6 +716,8 @@ async function submitAgentMessage(value, options = {}) {
|
||||
traceId,
|
||||
conversationId: activeConversationId,
|
||||
sessionId: requestedSessionId,
|
||||
threadId: requestedThreadId,
|
||||
sessionContinuity: pendingContinuity,
|
||||
retryInput: value,
|
||||
retryOf: options.retryOf ?? null,
|
||||
sourceKind: "PENDING",
|
||||
@@ -717,6 +729,7 @@ async function submitAgentMessage(value, options = {}) {
|
||||
traceId,
|
||||
conversationId: activeConversationId,
|
||||
sessionId: requestedSessionId,
|
||||
threadId: requestedThreadId,
|
||||
messageId: pendingMessage.id,
|
||||
input: value,
|
||||
lastActivityAt: Date.now(),
|
||||
@@ -735,24 +748,29 @@ async function submitAgentMessage(value, options = {}) {
|
||||
const stopTraceStream = subscribeRunnerTrace(traceId, pendingMessage.id);
|
||||
|
||||
try {
|
||||
const result = await sendAgentMessage(value, activeConversationId, traceId, requestedSessionId);
|
||||
const result = await sendAgentMessage(value, activeConversationId, traceId, requestedSessionId, requestedThreadId);
|
||||
stopTraceStream();
|
||||
if (state.canceledTraces.has(traceId)) return;
|
||||
state.conversationId = result.conversationId || result.sessionId || state.conversationId;
|
||||
state.sessionId = result.sessionId || result.session?.sessionId || state.sessionId || state.conversationId;
|
||||
const resultThreadId = threadIdFrom(result);
|
||||
if (resultThreadId) state.threadId = resultThreadId;
|
||||
state.sessionStatus = result.session?.status ?? result.runnerTrace?.sessionStatus ?? result.status ?? state.sessionStatus;
|
||||
const index = state.chatMessages.findIndex((message) => message.id === pendingMessage.id);
|
||||
const completion = classifyCodeAgentCompletion(result);
|
||||
const sessionContinuity = completedSessionContinuity(result, pendingContinuity);
|
||||
const resultBlockedError = structuredBlockedErrorFromResult(result);
|
||||
const continuityBlocker = codeAgentContinuityBlocker(result, sessionContinuity);
|
||||
const blockedError = resultBlockedError ?? continuityBlocker;
|
||||
const completion = classifyCodeAgentCompletion(result, { blockedError });
|
||||
const status = completion.status;
|
||||
const structuredBlockedError = structuredBlockedErrorFromResult(result);
|
||||
const m3Presentation = m3IoPresentation(result);
|
||||
state.chatMessages[index] = {
|
||||
...pendingMessage,
|
||||
title: m3Presentation?.title ?? completion.title,
|
||||
text: m3Presentation?.text ?? (completion.replied
|
||||
? result.reply?.content || "Code Agent 没有返回文本。"
|
||||
: structuredBlockedError
|
||||
? failureMessage({ ...result, error: structuredBlockedError })
|
||||
: blockedError
|
||||
? failureMessage({ ...result, error: blockedError })
|
||||
: result.status === "completed"
|
||||
? untrustedCompletionMessage(result)
|
||||
: failureMessage(result)),
|
||||
@@ -760,6 +778,8 @@ async function submitAgentMessage(value, options = {}) {
|
||||
traceId: result.traceId,
|
||||
conversationId: result.conversationId || result.sessionId || state.conversationId,
|
||||
sessionId: result.sessionId || result.session?.sessionId || result.sessionReuse?.sessionId || state.sessionId,
|
||||
threadId: resultThreadId || requestedThreadId || state.threadId,
|
||||
sessionContinuity,
|
||||
messageId: result.messageId,
|
||||
provider: result.provider,
|
||||
model: result.model,
|
||||
@@ -778,6 +798,8 @@ async function submitAgentMessage(value, options = {}) {
|
||||
runner: result.runner,
|
||||
runnerTrace: result.runnerTrace,
|
||||
conversationFacts: result.conversationFacts,
|
||||
responseType: result.responseType,
|
||||
m3Io: result.m3Io,
|
||||
m3Evidence: extractCodeAgentM3Evidence(result),
|
||||
capabilityLevel: result.capabilityLevel,
|
||||
sourceKind: completion.sourceKind,
|
||||
@@ -787,7 +809,7 @@ async function submitAgentMessage(value, options = {}) {
|
||||
updatedAt: result.updatedAt,
|
||||
retryInput: value,
|
||||
retryOf: options.retryOf ?? null,
|
||||
error: structuredBlockedError ?? result.error ?? (result.status === "completed" && !completion.replied
|
||||
error: blockedError ?? result.error ?? (result.status === "completed" && !completion.replied
|
||||
? {
|
||||
code: "untrusted_completion",
|
||||
message: "completed 回复缺少真实 provider/model/trace/conversation 证据,或 provider 属于 echo/mock/stub。"
|
||||
@@ -801,7 +823,7 @@ async function submitAgentMessage(value, options = {}) {
|
||||
if (!completion.replied) {
|
||||
el.commandInput.value = value;
|
||||
}
|
||||
renderAgentChatStatus(status, result);
|
||||
renderAgentChatStatus(status, state.chatMessages[index]);
|
||||
renderCodeAgentSummary();
|
||||
} catch (error) {
|
||||
stopTraceStream();
|
||||
@@ -815,6 +837,8 @@ async function submitAgentMessage(value, options = {}) {
|
||||
text: presentation.text,
|
||||
status: failedStatus,
|
||||
traceId: error.traceId || traceId,
|
||||
threadId: requestedThreadId,
|
||||
sessionContinuity: failedSessionContinuity(pendingContinuity, error),
|
||||
updatedAt: new Date().toISOString(),
|
||||
retryInput: value,
|
||||
error: {
|
||||
@@ -860,6 +884,10 @@ function sessionIdForNextRequest() {
|
||||
return isTerminalSessionStatus(state.sessionStatus) ? undefined : state.sessionId;
|
||||
}
|
||||
|
||||
function threadIdForNextRequest() {
|
||||
return isTerminalSessionStatus(state.sessionStatus) ? undefined : state.threadId;
|
||||
}
|
||||
|
||||
function isTerminalSessionStatus(status) {
|
||||
return ["failed", "interrupted", "timeout", "error", "canceled", "expired"].includes(String(status ?? "").toLowerCase());
|
||||
}
|
||||
@@ -870,6 +898,189 @@ function errorStatusFromPresentation(presentation, error) {
|
||||
return "failed";
|
||||
}
|
||||
|
||||
function pendingSessionContinuity({ conversationId, sessionId, threadId, retryOf = null } = {}) {
|
||||
const hasReusableSession = Boolean(nonEmptyString(sessionId));
|
||||
const hasReusableThread = Boolean(nonEmptyString(threadId));
|
||||
const status = hasReusableSession || hasReusableThread
|
||||
? "continued"
|
||||
: retryOf ? "degraded" : "new";
|
||||
return {
|
||||
status,
|
||||
tone: status === "continued" ? "ok" : status === "degraded" ? "warn" : "source",
|
||||
label: status === "continued" ? "继续当前会话" : status === "degraded" ? "重试降级" : "新会话待建立",
|
||||
summary: status === "continued"
|
||||
? "本次请求会带上已保存的 conversation/session/thread 上下文。"
|
||||
: status === "degraded"
|
||||
? "上一轮未留下可复用 session/thread;重试会保留输入和 conversation,并等待后端建立新会话。"
|
||||
: "这是当前浏览器会话中的首轮请求,等待后端分配 session/thread。",
|
||||
requested: {
|
||||
conversationId: nonEmptyString(conversationId),
|
||||
sessionId: nonEmptyString(sessionId),
|
||||
threadId: nonEmptyString(threadId),
|
||||
retryOf: nonEmptyString(retryOf)
|
||||
},
|
||||
returned: {
|
||||
conversationId: null,
|
||||
sessionId: null,
|
||||
threadId: null
|
||||
},
|
||||
missing: [],
|
||||
changed: []
|
||||
};
|
||||
}
|
||||
|
||||
function completedSessionContinuity(result, pending = null) {
|
||||
const requested = pending?.requested ?? {};
|
||||
const returned = {
|
||||
conversationId: nonEmptyString(result?.conversationId),
|
||||
sessionId: sessionIdFrom(result),
|
||||
threadId: threadIdFrom(result)
|
||||
};
|
||||
const missing = [];
|
||||
const changed = [];
|
||||
if (!returned.conversationId) missing.push("conversationId");
|
||||
if (requiresCodexSessionEvidence(result) && !returned.sessionId) missing.push("sessionId");
|
||||
if (requiresCodexThreadEvidence(result) && !returned.threadId) missing.push("threadId");
|
||||
if (requiresCodexProviderTrace(result) && !hasProviderTrace(result)) missing.push("providerTrace");
|
||||
if (requested.conversationId && returned.conversationId && requested.conversationId !== returned.conversationId) changed.push("conversationId");
|
||||
if (requested.sessionId && returned.sessionId && requested.sessionId !== returned.sessionId) changed.push("sessionId");
|
||||
if (requested.threadId && returned.threadId && requested.threadId !== returned.threadId) changed.push("threadId");
|
||||
const reused = result?.sessionReuse?.reused ?? result?.session?.reused ?? result?.runnerTrace?.sessionReused ?? null;
|
||||
const requestedReusable = Boolean(requested.sessionId || requested.threadId);
|
||||
const status = missing.length > 0 || changed.length > 0 || (requestedReusable && reused === false)
|
||||
? "degraded"
|
||||
: requestedReusable ? "continued" : "new";
|
||||
const label = status === "continued" ? "继续当前会话" : status === "new" ? "新会话已建立" : "新会话/会话降级";
|
||||
const summary = status === "continued"
|
||||
? "后端确认复用当前 conversation/session/thread。"
|
||||
: status === "new"
|
||||
? "后端已为当前 conversation 建立新的 session/thread。"
|
||||
: `后端未完整确认原 session/thread 复用;${continuityIssueText({ missing, changed, reused })}`;
|
||||
return {
|
||||
status,
|
||||
tone: status === "continued" ? "ok" : status === "new" ? "source" : "warn",
|
||||
label,
|
||||
summary,
|
||||
requested,
|
||||
returned,
|
||||
missing,
|
||||
changed,
|
||||
reused
|
||||
};
|
||||
}
|
||||
|
||||
function failedSessionContinuity(pending = null, error = null) {
|
||||
const base = pending ?? pendingSessionContinuity();
|
||||
const missing = [...(base.missing ?? [])];
|
||||
if (!base.requested?.sessionId) missing.push("sessionId");
|
||||
if (!base.requested?.threadId) missing.push("threadId");
|
||||
return {
|
||||
...base,
|
||||
status: "blocked",
|
||||
tone: "blocked",
|
||||
label: "会话受阻",
|
||||
summary: `本次请求没有完成可复用会话确认;输入已保留。${error?.code ? ` code=${error.code}` : ""}`,
|
||||
missing: [...new Set(missing)]
|
||||
};
|
||||
}
|
||||
|
||||
function continuityIssueText({ missing = [], changed = [], reused = null } = {}) {
|
||||
const parts = [
|
||||
missing.length ? `缺失 ${missing.join("、")}` : null,
|
||||
changed.length ? `${changed.join("、")} 已变化` : null,
|
||||
reused === false ? "sessionReuse=false" : null
|
||||
].filter(Boolean);
|
||||
return parts.length ? parts.join(";") : "会话复用证据不足";
|
||||
}
|
||||
|
||||
function codeAgentContinuityBlocker(result, continuity) {
|
||||
if (
|
||||
!result ||
|
||||
result.status !== "completed" ||
|
||||
isSourceFixtureCompletion(result) ||
|
||||
isTextFallbackChatResult(result) ||
|
||||
isCodeAgentM3SkillCompletion(result) ||
|
||||
!isTrustedCodeAgentProvider(result.provider)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const missing = [
|
||||
...(!hasProviderTrace(result) ? ["providerTrace"] : []),
|
||||
...(!sessionIdFrom(result) ? ["sessionId"] : []),
|
||||
...(requiresCodexThreadEvidence(result) && !threadIdFrom(result) ? ["threadId"] : [])
|
||||
];
|
||||
if (missing.length === 0) return null;
|
||||
return {
|
||||
code: "code_agent_session_evidence_missing",
|
||||
category: "session_blocked",
|
||||
layer: "cloud-web",
|
||||
retryable: true,
|
||||
missingFields: missing,
|
||||
traceId: result.traceId,
|
||||
message: `completed response missing Code Agent session evidence: ${missing.join(", ")}`,
|
||||
userMessage: `Code Agent completed 但会话证据缺失:${missing.join("、")};本次按会话降级处理,输入已保留,可重试建立新会话。`,
|
||||
blocker: {
|
||||
code: "code_agent_session_evidence_missing",
|
||||
category: "session_blocked",
|
||||
layer: "cloud-web",
|
||||
retryable: true,
|
||||
missingFields: missing,
|
||||
traceId: result.traceId,
|
||||
userMessage: `Code Agent completed 但会话证据缺失:${missing.join("、")};本次按会话降级处理,输入已保留,可重试建立新会话。`,
|
||||
sessionContinuity: continuity
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function hasProviderTrace(value) {
|
||||
return Boolean(value?.providerTrace && typeof value.providerTrace === "object" && Object.keys(value.providerTrace).length > 0);
|
||||
}
|
||||
|
||||
function requiresCodexProviderTrace(value) {
|
||||
return isTrustedCodeAgentProvider(value?.provider) && !isSourceFixtureCompletion(value) && !isCodeAgentM3SkillCompletion(value);
|
||||
}
|
||||
|
||||
function requiresCodexSessionEvidence(value) {
|
||||
return isTrustedCodeAgentProvider(value?.provider) && !isCodeAgentM3SkillCompletion(value);
|
||||
}
|
||||
|
||||
function requiresCodexThreadEvidence(value) {
|
||||
return isCodexStdioLongLivedShape(value) && !isCodeAgentM3SkillCompletion(value);
|
||||
}
|
||||
|
||||
function isCodexStdioLongLivedShape(value) {
|
||||
return (
|
||||
isTrustedCodeAgentProvider(value?.provider) ||
|
||||
String(value?.runner?.kind ?? value?.runnerTrace?.runnerKind ?? "").trim() === CODEX_APP_SERVER_RUNNER_KIND ||
|
||||
value?.runner?.codexStdio === true ||
|
||||
value?.session?.codexStdio === true
|
||||
) && (
|
||||
String(value?.sessionMode ?? value?.session?.sessionMode ?? value?.runnerTrace?.sessionMode ?? "").trim() === CODEX_APP_SERVER_SESSION_MODE ||
|
||||
String(value?.implementationType ?? value?.session?.implementationType ?? "").trim() === CODEX_APP_SERVER_IMPLEMENTATION_TYPE
|
||||
);
|
||||
}
|
||||
|
||||
function sessionIdFrom(value) {
|
||||
return firstNonEmptyString(value?.sessionId, value?.session?.sessionId, value?.sessionReuse?.sessionId, value?.runner?.sessionId, value?.runnerTrace?.sessionId);
|
||||
}
|
||||
|
||||
function threadIdFrom(value) {
|
||||
return firstNonEmptyString(value?.threadId, value?.session?.threadId, value?.sessionReuse?.threadId, value?.providerTrace?.threadId, value?.runnerTrace?.threadId);
|
||||
}
|
||||
|
||||
function firstNonEmptyString(...values) {
|
||||
for (const value of values) {
|
||||
const text = nonEmptyString(value);
|
||||
if (text) return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function nonEmptyString(value) {
|
||||
const text = String(value ?? "").trim();
|
||||
return text ? text : null;
|
||||
}
|
||||
|
||||
function subscribeRunnerTrace(traceId, messageId) {
|
||||
if (typeof EventSource !== "function") return pollRunnerTrace(traceId, messageId);
|
||||
const existing = state.traceStreams.get(traceId);
|
||||
@@ -963,19 +1174,25 @@ function updateMessageTrace(messageId, snapshot, options = {}) {
|
||||
const current = state.chatMessages[index];
|
||||
const runnerTrace = runnerTraceFromSnapshot(snapshot, current.runnerTrace);
|
||||
const sessionId = snapshot.sessionId ?? runnerTrace.sessionId ?? current.sessionId;
|
||||
const threadId = snapshot.threadId ?? runnerTrace.threadId ?? current.threadId;
|
||||
noteCurrentRequestTraceActivity(snapshot);
|
||||
state.chatMessages[index] = {
|
||||
...current,
|
||||
runnerTrace,
|
||||
traceId: snapshot.traceId ?? current.traceId,
|
||||
sessionId,
|
||||
threadId,
|
||||
updatedAt: snapshot.updatedAt ?? current.updatedAt
|
||||
};
|
||||
if (sessionId) state.sessionId = sessionId;
|
||||
if (threadId) state.threadId = threadId;
|
||||
if (runnerTrace.sessionStatus) state.sessionStatus = runnerTrace.sessionStatus;
|
||||
if (state.currentRequest?.traceId === (snapshot.traceId ?? current.traceId) && sessionId) {
|
||||
state.currentRequest.sessionId = sessionId;
|
||||
}
|
||||
if (state.currentRequest?.traceId === (snapshot.traceId ?? current.traceId) && threadId) {
|
||||
state.currentRequest.threadId = threadId;
|
||||
}
|
||||
if (options.quiet !== true) {
|
||||
renderCodeAgentSummary();
|
||||
renderConversation();
|
||||
@@ -1015,6 +1232,8 @@ function runnerTraceFromSnapshot(snapshot, previous = null) {
|
||||
sandbox: snapshot.sandbox ?? previous?.sandbox,
|
||||
sessionMode: snapshot.sessionMode ?? previous?.sessionMode,
|
||||
sessionId: snapshot.sessionId ?? previous?.sessionId,
|
||||
threadId: snapshot.threadId ?? previous?.threadId,
|
||||
turnId: snapshot.turnId ?? previous?.turnId,
|
||||
sessionStatus: snapshot.sessionStatus ?? previous?.sessionStatus,
|
||||
implementationType: snapshot.implementationType ?? previous?.implementationType,
|
||||
startedAt: snapshot.startedAt ?? previous?.startedAt,
|
||||
@@ -1070,8 +1289,9 @@ function renderProbePending() {
|
||||
renderCodeAgentSummary();
|
||||
}
|
||||
|
||||
async function sendAgentMessage(message, conversationId, traceId = nextProtocolId("trc"), requestedSessionId = sessionIdForNextRequest()) {
|
||||
async function sendAgentMessage(message, conversationId, traceId = nextProtocolId("trc"), requestedSessionId = sessionIdForNextRequest(), requestedThreadId = threadIdForNextRequest()) {
|
||||
const sessionId = requestedSessionId || undefined;
|
||||
const threadId = requestedThreadId || undefined;
|
||||
const response = await fetchJson("/v1/agent/chat", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -1085,6 +1305,7 @@ async function sendAgentMessage(message, conversationId, traceId = nextProtocolI
|
||||
message,
|
||||
conversationId,
|
||||
sessionId,
|
||||
threadId,
|
||||
traceId,
|
||||
projectId: gateSummary.topology.projectId
|
||||
})
|
||||
@@ -2374,6 +2595,7 @@ function codeAgentRecordCards() {
|
||||
recordField("conversation", message.conversationId ?? state.conversationId),
|
||||
recordField("trace", message.traceId),
|
||||
recordField("session", message.sessionId ?? sessionSummary(message.session)),
|
||||
recordField("thread", message.threadId ?? threadIdFrom(message)),
|
||||
recordField("message", message.messageId),
|
||||
message.error?.message ? `失败原因=${safeFailureReason(message.error.message)}` : null
|
||||
].filter(Boolean).join(" / "),
|
||||
@@ -2839,7 +3061,7 @@ function textFallbackTitle(result) {
|
||||
return `Code Agent 文本 fallback 回复 ${shortTime(result.updatedAt ?? new Date().toISOString())}`;
|
||||
}
|
||||
|
||||
function classifyCodeAgentCompletion(result) {
|
||||
function classifyCodeAgentCompletion(result, { blockedError = null } = {}) {
|
||||
if (result?.status === "canceled") {
|
||||
return {
|
||||
status: "canceled",
|
||||
@@ -2880,6 +3102,14 @@ function classifyCodeAgentCompletion(result) {
|
||||
title: agentFailurePresentation(structuredBlockedErrorFromResult(result), { result }).title
|
||||
};
|
||||
}
|
||||
if (blockedError) {
|
||||
return {
|
||||
status: "failed",
|
||||
replied: false,
|
||||
sourceKind: "BLOCKED",
|
||||
title: agentFailurePresentation(blockedError, { result }).title
|
||||
};
|
||||
}
|
||||
if (isRealCompletedChatResult(result)) {
|
||||
return {
|
||||
status: "completed",
|
||||
@@ -3178,6 +3408,8 @@ function messageCard(message) {
|
||||
article.append(textSpan(message.text, "message-copy"));
|
||||
const pendingContext = messagePendingContextPanel(message);
|
||||
if (pendingContext) article.append(pendingContext);
|
||||
const sessionContext = messageSessionContinuityPanel(message);
|
||||
if (sessionContext) article.append(sessionContext);
|
||||
const m3Evidence = messageM3EvidencePanel(message);
|
||||
if (m3Evidence) article.append(m3Evidence);
|
||||
const runtimePath = messageRuntimePathPanel(message);
|
||||
@@ -3245,6 +3477,9 @@ async function restartRunningAgentMessage(messageId) {
|
||||
sessionId: isTerminalSessionStatus(canceled.session?.status ?? canceled.runnerTrace?.sessionStatus ?? canceled.status)
|
||||
? undefined
|
||||
: canceled.sessionId,
|
||||
threadId: isTerminalSessionStatus(canceled.session?.status ?? canceled.runnerTrace?.sessionStatus ?? canceled.status)
|
||||
? undefined
|
||||
: canceled.threadId ?? threadIdFrom(canceled),
|
||||
retryOf: canceled.traceId
|
||||
});
|
||||
}
|
||||
@@ -3261,6 +3496,9 @@ async function retryAgentMessage(messageId) {
|
||||
sessionId: isTerminalSessionStatus(message.session?.status ?? message.runnerTrace?.sessionStatus ?? message.status)
|
||||
? undefined
|
||||
: message.sessionId,
|
||||
threadId: isTerminalSessionStatus(message.session?.status ?? message.runnerTrace?.sessionStatus ?? message.status)
|
||||
? undefined
|
||||
: message.threadId ?? threadIdFrom(message),
|
||||
retryOf: message.traceId
|
||||
});
|
||||
}
|
||||
@@ -3282,10 +3520,12 @@ async function cancelAgentMessage(messageId) {
|
||||
const message = state.chatMessages[index];
|
||||
const traceId = message.traceId;
|
||||
const sessionId = message.sessionId || message.runnerTrace?.sessionId || state.currentRequest?.sessionId || state.sessionId;
|
||||
const threadId = message.threadId || message.runnerTrace?.threadId || state.currentRequest?.threadId || state.threadId;
|
||||
const response = await cancelAgentRequest({
|
||||
traceId,
|
||||
conversationId: message.conversationId || state.conversationId,
|
||||
sessionId
|
||||
sessionId,
|
||||
threadId
|
||||
});
|
||||
const payload = response.data ?? {};
|
||||
const runnerTrace = payload.runnerTrace ?? latestTraceSnapshot(traceId);
|
||||
@@ -3301,6 +3541,8 @@ async function cancelAgentMessage(messageId) {
|
||||
text: payload.userMessage || "当前 Codex stdio 请求已取消;输入、sessionId、traceId 和最后 trace event 已保留,可重试上一条消息。",
|
||||
status: "canceled",
|
||||
sessionId: payload.sessionId || sessionId || message.sessionId,
|
||||
threadId: payload.threadId || threadId || message.threadId,
|
||||
sessionContinuity: failedSessionContinuity(message.sessionContinuity, { code: "codex_stdio_canceled" }),
|
||||
session: payload.session ?? message.session,
|
||||
runnerTrace,
|
||||
traceReplayStatus: lastTraceEventLabel(runnerTrace),
|
||||
@@ -3319,6 +3561,8 @@ async function cancelAgentMessage(messageId) {
|
||||
title: "Code Agent 取消受阻",
|
||||
text: payload.error?.userMessage || response.error || "当前请求没有可取消的 in-flight Codex stdio session;输入和 trace 已保留。",
|
||||
status: "failed",
|
||||
threadId,
|
||||
sessionContinuity: failedSessionContinuity(message.sessionContinuity, { code: "cancel_blocked" }),
|
||||
runnerTrace,
|
||||
traceReplayStatus: lastTraceEventLabel(runnerTrace),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -3338,7 +3582,7 @@ async function cancelAgentMessage(messageId) {
|
||||
renderRecords(state.liveSurface);
|
||||
}
|
||||
|
||||
async function cancelAgentRequest({ traceId, conversationId, sessionId }) {
|
||||
async function cancelAgentRequest({ traceId, conversationId, sessionId, threadId }) {
|
||||
return fetchJson("/v1/agent/chat/cancel", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -3350,7 +3594,8 @@ async function cancelAgentRequest({ traceId, conversationId, sessionId }) {
|
||||
body: JSON.stringify({
|
||||
traceId,
|
||||
conversationId,
|
||||
sessionId
|
||||
sessionId,
|
||||
threadId
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -3369,6 +3614,7 @@ async function replayAgentTrace(messageId) {
|
||||
state.chatMessages[index] = {
|
||||
...message,
|
||||
runnerTrace,
|
||||
threadId: runnerTrace.threadId ?? message.threadId,
|
||||
traceReplayStatus: `已回放 ${runnerTrace.events?.length ?? 0} 个真实 trace event;${lastTraceEventLabel(runnerTrace)}`,
|
||||
updatedAt: response.data?.updatedAt ?? new Date().toISOString()
|
||||
};
|
||||
@@ -3390,13 +3636,61 @@ function lastTraceEventLabel(runnerTrace) {
|
||||
return `lastEvent=${event.label ?? `${event.type ?? "event"}:${event.status ?? "observed"}`}`;
|
||||
}
|
||||
|
||||
function messageSessionContinuityPanel(message) {
|
||||
if (message.role !== "agent" || message.status === "running") return null;
|
||||
const continuity = message.sessionContinuity ?? completedSessionContinuity(message, pendingSessionContinuity({
|
||||
conversationId: message.conversationId,
|
||||
sessionId: message.sessionId,
|
||||
threadId: message.threadId,
|
||||
retryOf: message.retryOf
|
||||
}));
|
||||
if (!continuity) return null;
|
||||
const fields = [
|
||||
["状态", continuity.label],
|
||||
["conversation", continuity.returned?.conversationId ?? message.conversationId],
|
||||
["session", continuity.returned?.sessionId ?? message.sessionId ?? "未确认"],
|
||||
["thread", continuity.returned?.threadId ?? message.threadId ?? "未确认"],
|
||||
["requestedSession", continuity.requested?.sessionId],
|
||||
["requestedThread", continuity.requested?.threadId],
|
||||
["retryOf", continuity.requested?.retryOf ?? message.retryOf],
|
||||
["missing", continuity.missing?.join("、")],
|
||||
["changed", continuity.changed?.join("、")]
|
||||
];
|
||||
const section = document.createElement("section");
|
||||
section.className = `message-session-context tone-border-${toneClass(continuity.tone)}`;
|
||||
const header = document.createElement("div");
|
||||
header.className = "message-session-head";
|
||||
header.append(badge(continuity.label, continuity.tone), textSpan(continuity.summary, "message-session-summary"));
|
||||
const list = document.createElement("div");
|
||||
list.className = "message-session-grid";
|
||||
for (const [label, value] of fields) {
|
||||
if (value === undefined || value === null || value === "") continue;
|
||||
const item = document.createElement("div");
|
||||
item.className = "message-session-row";
|
||||
item.append(textSpan(label, "message-session-key"), textSpan(value, "message-session-value mono"));
|
||||
if (label === "conversation" || label === "session" || label === "thread") {
|
||||
item.append(copyButton(value, label));
|
||||
}
|
||||
list.append(item);
|
||||
}
|
||||
section.append(header, list);
|
||||
return section;
|
||||
}
|
||||
|
||||
function messagePendingContextPanel(message) {
|
||||
if (message.status !== "running") return null;
|
||||
const continuity = message.sessionContinuity ?? pendingSessionContinuity({
|
||||
conversationId: message.conversationId,
|
||||
sessionId: message.sessionId,
|
||||
threadId: message.threadId,
|
||||
retryOf: message.retryOf
|
||||
});
|
||||
const fields = [
|
||||
["状态", "后端处理中"],
|
||||
["状态", continuity.label],
|
||||
["traceId", message.traceId],
|
||||
["conversation", message.conversationId],
|
||||
["session", message.sessionId ?? message.runnerTrace?.sessionId ?? "等待后端分配"],
|
||||
["thread", message.threadId ?? message.runnerTrace?.threadId ?? "等待后端分配"],
|
||||
["sessionStatus", message.runnerTrace?.sessionStatus],
|
||||
["activityTimeout", `${CODE_AGENT_TIMEOUT_MS}ms 无新事件`]
|
||||
];
|
||||
@@ -3404,7 +3698,7 @@ function messagePendingContextPanel(message) {
|
||||
section.className = "message-pending-context tone-border-pending";
|
||||
const header = document.createElement("div");
|
||||
header.className = "message-pending-head";
|
||||
header.append(badge("处理中", "pending"), textSpan("请求仍在同一会话上下文中等待后端结果。", "message-pending-summary"));
|
||||
header.append(badge("处理中", "pending"), textSpan(continuity.summary, "message-pending-summary"));
|
||||
const list = document.createElement("div");
|
||||
list.className = "message-pending-grid";
|
||||
for (const [label, value] of fields) {
|
||||
@@ -3636,7 +3930,8 @@ function runnerTraceHeadline(message, trace) {
|
||||
const last = trace?.lastEvent?.label ?? trace?.eventLabels?.at?.(-1) ?? "等待事件";
|
||||
const elapsed = typeof trace?.elapsedMs === "number" ? ` / ${trace.elapsedMs}ms` : "";
|
||||
const waiting = trace?.waitingFor ? ` / 等待 ${trace.waitingFor}` : "";
|
||||
return `运行 trace ${trace?.traceId ?? message.traceId ?? "pending"} / events=${count} / last=${last}${elapsed}${waiting}`;
|
||||
const thread = trace?.threadId ?? message.threadId ? ` / thread=${trace?.threadId ?? message.threadId}` : "";
|
||||
return `运行 trace ${trace?.traceId ?? message.traceId ?? "pending"} / events=${count} / last=${last}${elapsed}${waiting}${thread}`;
|
||||
}
|
||||
|
||||
function traceEventMeta(event) {
|
||||
@@ -3713,6 +4008,7 @@ function sessionSummary(session) {
|
||||
session.sessionId,
|
||||
session.status,
|
||||
typeof session.turn === "number" ? `turn${session.turn}` : null,
|
||||
session.threadId ? `thread=${session.threadId}` : null,
|
||||
session.lastTraceId ? `lastTrace=${session.lastTraceId}` : null
|
||||
].filter(Boolean);
|
||||
return parts.join(":");
|
||||
@@ -4110,6 +4406,9 @@ function hasRealCodeAgentEvidence(value) {
|
||||
Boolean(value?.backend) &&
|
||||
Boolean(value?.traceId) &&
|
||||
Boolean(value?.conversationId || value?.sessionId) &&
|
||||
hasProviderTrace(value) &&
|
||||
Boolean(sessionIdFrom(value)) &&
|
||||
(!requiresCodexThreadEvidence(value) || Boolean(threadIdFrom(value))) &&
|
||||
!UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN.test(`${value?.provider ?? ""} ${value?.backend ?? ""}`)
|
||||
);
|
||||
}
|
||||
@@ -4125,6 +4424,9 @@ function isCodexRunnerCapableEvidence(value) {
|
||||
value?.sessionMode === CODEX_APP_SERVER_SESSION_MODE &&
|
||||
value?.implementationType === CODEX_APP_SERVER_IMPLEMENTATION_TYPE &&
|
||||
Boolean(value?.sessionReuse) &&
|
||||
hasProviderTrace(value) &&
|
||||
Boolean(sessionIdFrom(value)) &&
|
||||
Boolean(threadIdFrom(value)) &&
|
||||
value?.session?.longLivedSession === true &&
|
||||
value?.session?.codexStdio === true &&
|
||||
value?.runner?.codexStdio === true &&
|
||||
|
||||
@@ -9,6 +9,9 @@ const GREEN_TONES = new Set(["ok", "available", "ready", "dev-live", "live", "pa
|
||||
const SECRET_MARKER = /\b(?:OPENAI_API_KEY|DATABASE_URL|secret|secretref|token|password|passwd|credential|private key|kubeconfig|密钥|密码|凭证)\b/iu;
|
||||
const NON_LIVE_VALUES = new Set(["", "n/a", "none", "null", "undefined", "unknown", "not_observed", "not-observed"]);
|
||||
const MAX_FIELD_LENGTH = 96;
|
||||
const CODEX_APP_SERVER_RUNNER_KIND = "codex-app-server-stdio-runner";
|
||||
const CODEX_APP_SERVER_SESSION_MODE = "codex-app-server-stdio-long-lived";
|
||||
const CODEX_APP_SERVER_IMPLEMENTATION_TYPE = "repo-owned-codex-app-server-stdio-session";
|
||||
|
||||
export function classifyCodeAgentStatusSummary({ availability = null, latestMessage = null, live = null } = {}) {
|
||||
const observedAvailability = availability ?? codeAgentAvailabilityFromLive(live);
|
||||
@@ -23,6 +26,7 @@ export function classifyCodeAgentStatusSummary({ availability = null, latestMess
|
||||
const capabilityLevel = safeField(firstString(payload.capabilityLevel, payload.runner?.capabilityLevel, observedAvailability?.capabilityLevel));
|
||||
const sessionMode = safeField(firstString(payload.sessionMode, payload.session?.sessionMode, payload.runnerTrace?.sessionMode, payload.runner?.sessionMode));
|
||||
const sessionId = safeField(firstString(payload.session?.sessionId, payload.sessionId, payload.runner?.sessionId, payload.sessionReuse?.sessionId));
|
||||
const threadId = safeField(firstString(payload.threadId, payload.session?.threadId, payload.sessionReuse?.threadId, payload.providerTrace?.threadId, payload.runnerTrace?.threadId));
|
||||
const sessionStatus = safeField(firstString(payload.session?.status, payload.sessionStatus, payload.runnerTrace?.sessionStatus, payload.sessionReuse?.status));
|
||||
const workspace = safeField(firstString(payload.workspace, payload.session?.workspace, payload.runner?.workspace, payload.sessionReuse?.workspace));
|
||||
const sandbox = safeField(firstString(payload.sandbox, payload.session?.sandbox, payload.runner?.sandbox));
|
||||
@@ -60,9 +64,10 @@ export function classifyCodeAgentStatusSummary({ availability = null, latestMess
|
||||
capabilityLevel,
|
||||
sessionMode,
|
||||
sessionId,
|
||||
threadId,
|
||||
sessionStatus,
|
||||
sessionSummary: compactJoin([sessionMode, sessionStatus], " / "),
|
||||
sessionIdStatus: compactJoin([sessionId, sessionStatus], " / "),
|
||||
sessionIdStatus: compactJoin([sessionId, threadId, sessionStatus], " / "),
|
||||
workspace,
|
||||
sandbox,
|
||||
runnerKind,
|
||||
@@ -88,6 +93,7 @@ export function classifyCodeAgentStatusSummary({ availability = null, latestMess
|
||||
capabilityLevel,
|
||||
sessionMode,
|
||||
sessionId,
|
||||
threadId,
|
||||
sessionStatus,
|
||||
workspace,
|
||||
sandbox,
|
||||
@@ -331,12 +337,16 @@ function isLongLivedReady(payload) {
|
||||
) &&
|
||||
(
|
||||
payload.provider === "codex-stdio" ||
|
||||
payload.runner?.kind === "codex-app-server-stdio-runner" ||
|
||||
payload.runner?.kind === CODEX_APP_SERVER_RUNNER_KIND ||
|
||||
payload.runner?.codexStdio === true ||
|
||||
payload.session?.codexStdio === true
|
||||
) &&
|
||||
payload.capabilityLevel === "long-lived-codex-stdio-session" &&
|
||||
payload.sessionMode === "codex-app-server-stdio-long-lived"
|
||||
payload.sessionMode === CODEX_APP_SERVER_SESSION_MODE &&
|
||||
(
|
||||
!payload.implementationType ||
|
||||
payload.implementationType === CODEX_APP_SERVER_IMPLEMENTATION_TYPE
|
||||
)
|
||||
) && payload.status !== "failed" && payload.status !== "blocked" && payload.ready !== false;
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,52 @@ test("does not mark legacy MCP-shaped Codex stdio as app-server long-lived sessi
|
||||
assert.equal(summary.runtimePathLabel, "DEGRADED:MCP/其他 runner");
|
||||
});
|
||||
|
||||
test("summarizes Codex app-server stdio thread as reusable long-lived session", () => {
|
||||
const summary = classifyCodeAgentStatusSummary({
|
||||
latestMessage: {
|
||||
status: "completed",
|
||||
provider: "codex-stdio",
|
||||
mode: "codex-stdio",
|
||||
backend: "hwlab-cloud-api/codex-app-server-stdio",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
sessionMode: "codex-app-server-stdio-long-lived",
|
||||
implementationType: "repo-owned-codex-app-server-stdio-session",
|
||||
session: {
|
||||
sessionId: "ses_app_server",
|
||||
threadId: "thread_app_server",
|
||||
status: "idle",
|
||||
lastTraceId: "trc_app_server",
|
||||
codexStdio: true
|
||||
},
|
||||
sessionReuse: {
|
||||
sessionId: "ses_app_server",
|
||||
threadId: "thread_app_server",
|
||||
reused: true,
|
||||
turn: 2
|
||||
},
|
||||
runner: {
|
||||
kind: "codex-app-server-stdio-runner",
|
||||
codexStdio: true,
|
||||
writeCapable: true,
|
||||
durableSession: true
|
||||
},
|
||||
longLivedSessionGate: {
|
||||
status: "pass"
|
||||
},
|
||||
providerTrace: {
|
||||
protocol: "codex-app-server-jsonrpc-stdio",
|
||||
threadId: "thread_app_server",
|
||||
terminalStatus: "completed"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(summary.kind, "long-lived-session");
|
||||
assert.equal(summary.tone, "ok");
|
||||
assert.equal(summary.sessionIdStatus, "ses_app_server / thread_app_server / idle");
|
||||
assert.equal(summary.fields.threadId, "thread_app_server");
|
||||
});
|
||||
|
||||
test("does not mark Codex stdio-shaped fields green without runner and gate evidence", () => {
|
||||
const summary = classifyCodeAgentStatusSummary({
|
||||
availability: {
|
||||
|
||||
@@ -759,6 +759,8 @@ assert.match(app, /Boolean\(value\?\.session\?\.lastTraceId\)/);
|
||||
assert.match(app, /Boolean\(value\?\.sessionReuse\)/);
|
||||
assert.match(app, /value\?\.sessionMode === CODEX_APP_SERVER_SESSION_MODE/);
|
||||
assert.match(app, /value\?\.implementationType === CODEX_APP_SERVER_IMPLEMENTATION_TYPE/);
|
||||
assert.match(app, /threadIdFrom\(value\)/);
|
||||
assert.match(app, /hasProviderTrace\(value\)/);
|
||||
assert.match(app, /value\?\.capabilityLevel === "long-lived-codex-stdio-session"/);
|
||||
assert.match(app, /value\?\.session\?\.codexStdio === true/);
|
||||
assert.match(app, /value\?\.runner\?\.codexStdio === true/);
|
||||
@@ -890,6 +892,7 @@ assert.match(styles, /\.message-card\.status-source\s*{/);
|
||||
assert.match(styles, /\.message-user\s*{/);
|
||||
assert.match(styles, /\.message-agent,\s*\n\.message-system\s*{/);
|
||||
assert.match(styles, /\.message-pending-context\s*{/);
|
||||
assert.match(styles, /\.message-session-context\s*{/);
|
||||
assert.match(styles, /\.message-m3-evidence\s*{/);
|
||||
assert.match(styles, /\.message-m3-rows\s*{/);
|
||||
assert.match(styles, /\.message-m3-row\s*{/);
|
||||
@@ -912,7 +915,7 @@ assert.match(styles, /\.(?:status-dot|state-tag|badge)[^{]*{[^}]*max-width:\s*10
|
||||
assert.match(styles, /\.probe-card\s*{[^}]*min-width:\s*0;/s);
|
||||
assert.match(styles, /\.probe-card strong\s*{[^}]*line-height:\s*1\.2;[^}]*overflow-wrap:\s*anywhere;/s);
|
||||
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.message-m3-rows\s*{[\s\S]*?grid-template-columns:\s*1fr;/);
|
||||
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.message-trace,\s*\n\s*\.message-m3-evidence(?:,\s*\n\s*\.message-actions)?\s*{[\s\S]*?grid-column:\s*1;/);
|
||||
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.message-runtime-path,\s*\n\s*\.message-session-context,\s*\n\s*\.message-trace,\s*\n\s*\.message-m3-evidence,\s*\n\s*\.message-actions\s*{[\s\S]*?grid-column:\s*1;/);
|
||||
for (const userFacingFunctionName of [
|
||||
"codeAgentStatusMessage",
|
||||
"codeAgentPromptText",
|
||||
@@ -926,6 +929,15 @@ for (const userFacingFunctionName of [
|
||||
}
|
||||
assert.match(app, /state\.conversationId/);
|
||||
assert.match(app, /conversationId/);
|
||||
assert.match(app, /state\.threadId/);
|
||||
assert.match(app, /threadIdForNextRequest/);
|
||||
assert.match(app, /threadId:\s*requestedThreadId/);
|
||||
assert.match(app, /function\s+completedSessionContinuity/);
|
||||
assert.match(app, /function\s+codeAgentContinuityBlocker/);
|
||||
assert.match(app, /code_agent_session_evidence_missing/);
|
||||
assert.match(app, /Code Agent completed 但会话证据缺失/);
|
||||
assert.match(app, /继续当前会话/);
|
||||
assert.match(app, /新会话\/会话降级/);
|
||||
assert.match(app, /messageId/);
|
||||
assert.match(app, /createdAt/);
|
||||
assert.match(app, /updatedAt/);
|
||||
|
||||
@@ -967,6 +967,7 @@ h3 {
|
||||
|
||||
.message-pending-context,
|
||||
.message-runtime-path,
|
||||
.message-session-context,
|
||||
.message-trace,
|
||||
.message-m3-evidence,
|
||||
.message-actions {
|
||||
@@ -987,6 +988,11 @@ h3 {
|
||||
border-left-width: 3px;
|
||||
}
|
||||
|
||||
.message-session-context {
|
||||
background: rgba(18, 25, 24, 0.64);
|
||||
border-left-width: 3px;
|
||||
}
|
||||
|
||||
.message-trace {
|
||||
background: rgba(20, 24, 23, 0.76);
|
||||
border-left-width: 3px;
|
||||
@@ -1037,6 +1043,7 @@ h3 {
|
||||
|
||||
.message-pending-head,
|
||||
.message-runtime-head,
|
||||
.message-session-head,
|
||||
.message-trace-head {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
@@ -1051,8 +1058,15 @@ h3 {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.message-session-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.message-pending-summary,
|
||||
.message-runtime-summary,
|
||||
.message-session-summary,
|
||||
.message-trace-summary {
|
||||
min-width: 0;
|
||||
color: var(--muted);
|
||||
@@ -1060,21 +1074,18 @@ h3 {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.message-pending-grid {
|
||||
.message-pending-grid,
|
||||
.message-runtime-grid,
|
||||
.message-session-grid {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.message-runtime-grid {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.message-pending-row {
|
||||
.message-pending-row,
|
||||
.message-runtime-row,
|
||||
.message-session-row {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(70px, auto) minmax(0, 1fr) auto;
|
||||
@@ -1086,14 +1097,7 @@ h3 {
|
||||
}
|
||||
|
||||
.message-runtime-row {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(132px, 0.8fr) minmax(0, 1.2fr) auto;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
padding: 5px 6px;
|
||||
background: rgba(24, 27, 24, 0.68);
|
||||
border: 1px solid var(--line-soft);
|
||||
}
|
||||
|
||||
.message-runtime-row-wide {
|
||||
@@ -1106,34 +1110,29 @@ h3 {
|
||||
|
||||
.message-pending-key,
|
||||
.message-runtime-key,
|
||||
.message-pending-value {
|
||||
.message-session-key,
|
||||
.message-pending-value,
|
||||
.message-runtime-value,
|
||||
.message-session-value {
|
||||
min-width: 0;
|
||||
font-size: 10px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.message-runtime-value {
|
||||
min-width: 0;
|
||||
font-size: 10px;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.message-pending-key {
|
||||
.message-pending-key,
|
||||
.message-runtime-key,
|
||||
.message-session-key {
|
||||
color: var(--dim);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
.message-runtime-key {
|
||||
color: var(--dim);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
.message-pending-value {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.message-runtime-value {
|
||||
.message-pending-value,
|
||||
.message-runtime-value,
|
||||
.message-session-value {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
@@ -2463,6 +2462,7 @@ tbody tr:last-child td {
|
||||
|
||||
.message-pending-context,
|
||||
.message-runtime-path,
|
||||
.message-session-context,
|
||||
.message-trace,
|
||||
.message-m3-evidence,
|
||||
.message-actions {
|
||||
@@ -2471,7 +2471,9 @@ tbody tr:last-child td {
|
||||
|
||||
.message-pending-grid,
|
||||
.message-runtime-grid,
|
||||
.message-pending-row {
|
||||
.message-pending-row,
|
||||
.message-session-grid,
|
||||
.message-session-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user