feat: expose reusable readonly code agent sessions
This commit is contained in:
@@ -38,7 +38,7 @@ report、issue、PR 或截图。
|
||||
| 观测结果 | readiness |
|
||||
| --- | --- |
|
||||
| `status: "failed"`,`error.code: "provider_unavailable"`,且 `error.missingEnv` 包含 `OPENAI_API_KEY` | `BLOCKED/credential`;provider 凭证缺失,不能标真实回复通过。 |
|
||||
| `provider: "codex-readonly-runner"` 且 `sessionMode: "controlled-readonly-session-registry"`、`capabilityLevel: "read-only-session-tools"`、`session.status` 为 `idle/ready/busy`、`session.idleTimeoutMs` 和 `session.lastTraceId` 存在、`longLivedSessionGate.status: "blocked"`、`runnerLimitations` 包含 `not-codex-stdio` / `not-write-capable` / `not-durable-session` | 只能标为 #317 read-only session registry partial pass;不能关闭 long-lived Codex stdio/session blocker。 |
|
||||
| `provider: "codex-readonly-runner"` 且 `sessionMode: "controlled-readonly-session-registry"`、`capabilityLevel: "read-only-session-tools"`、`session.status` 为 `idle/ready/busy`、`session.idleTimeoutMs` 和 `session.lastTraceId` 存在、`session.longLivedSession: true`、`longLivedSessionGate.status: "blocked"`、`runnerLimitations` 包含 `not-codex-stdio` / `not-write-capable` / `process-local-session-registry` | 可标为 #317 read-only long-lived session registry pass;不能关闭 full Codex stdio/session capability blocker。 |
|
||||
| `codexStdioFeasibility.status: "blocked"`,或 blocker 包含 `codex_cli_binary_missing`、`runner_lifecycle_missing`、`stdio_protocol_not_wired`、`workspace_mount_missing`、`provider_token_boundary` | 真实 Codex stdio / 等价 long-lived runner 未具备;必须按 blocker 处理,不能表述为完整 Codex session。 |
|
||||
| `status: "completed"`,但来自 mock、fixture、本地 stub、source-only smoke、浏览器本地回显或人工拼接 | 不是 DEV-LIVE reply pass。 |
|
||||
| 真实 DEV `POST /v1/agent/chat` 返回 `status: "completed"`,且 `reply.content` 是非空 assistant 回复 | 可标 DEV-LIVE reply pass。 |
|
||||
@@ -54,10 +54,11 @@ report、issue、PR 或截图。
|
||||
- `controlled-readonly-session-registry`:由 cloud-api 进程内 registry 保存
|
||||
`conversationId/sessionId` 映射、`status`、workspace、sandbox、`createdAt`、`updatedAt`、
|
||||
`idleTimeoutMs`、`lastTraceId`、`turn` 计数与只读工具 trace。它可以覆盖 `pwd`、
|
||||
`skills.discover`、`ls`、`rg --files` 和 bounded `cat`,输出必须限长和脱敏。该模式的
|
||||
`longLivedSessionGate` 必须保持 `blocked`,直到 Codex stdio 或等价长期通道真实接通。
|
||||
- 该模式必须同时标记 `not-codex-stdio`、`not-write-capable`、`not-durable-session`。它不是
|
||||
long-lived Codex stdio session,不提供写文件、任意 shell、硬件写、Secret/kubeconfig/DB URL
|
||||
`skills.discover`、`ls`、`rg --files` 和 bounded `cat`,输出必须限长和脱敏。该模式是
|
||||
read-only long-lived session,但 `longLivedSessionGate` 必须保持 `blocked`,直到 Codex
|
||||
stdio 或等价 full Code Agent 协议通道真实接通。
|
||||
- 该模式必须同时标记 `not-codex-stdio`、`not-write-capable`、`process-local-session-registry`。它不是
|
||||
Codex stdio / workspace-write session,不提供写文件、任意 shell、硬件写、Secret/kubeconfig/DB URL
|
||||
读取,也不证明 M3/M4/M5 trusted green。
|
||||
- OpenAI Responses fallback 只能标记为 `openai-responses-fallback` /
|
||||
`text-chat-only`,不得满足 Codex runner capability gate。
|
||||
|
||||
@@ -76,7 +76,7 @@ const CODEX_STDIO_SOURCE_ISSUE = "pikasTech/HWLAB#275";
|
||||
const READONLY_LIMITATION_FLAGS = Object.freeze([
|
||||
"not-codex-stdio",
|
||||
"not-write-capable",
|
||||
"not-durable-session"
|
||||
"process-local-session-registry"
|
||||
]);
|
||||
const SKIPPED_READONLY_DIRS = new Set([
|
||||
".git",
|
||||
@@ -255,7 +255,7 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
|
||||
"text-chat-only",
|
||||
"not-codex-stdio",
|
||||
"not-workspace-tools",
|
||||
"not-durable-session"
|
||||
"not-session-runner"
|
||||
],
|
||||
codexStdioFeasibility: providerResult.codexStdioFeasibility ?? inspectCodexStdioFeasibility(options.env ?? process.env, options),
|
||||
longLivedSessionGate: providerLongLivedGate,
|
||||
@@ -613,7 +613,7 @@ function primaryCodeAgentBlocker({ blocked, providerContract, providerPlan, code
|
||||
function primaryCodeAgentReason({ blocked, codexStdio, runnerAvailability }) {
|
||||
if (codexStdio.blockers?.length > 0) return codexStdio.blockers[0].code;
|
||||
if (blocked && !runnerAvailability.ready) return "provider_unavailable";
|
||||
if (runnerAvailability.ready) return "controlled_readonly_not_long_lived_stdio";
|
||||
if (runnerAvailability.ready) return "codex_stdio_blocked_readonly_session_available";
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -714,7 +714,7 @@ function inspectReadOnlyRunnerAvailability(env, options = {}) {
|
||||
ready: workspaceReady,
|
||||
capabilityLevel: workspaceReady ? READONLY_SESSION_CAPABILITY_LEVEL : "blocked",
|
||||
implementationType: READONLY_IMPLEMENTATION_TYPE,
|
||||
longLivedSession: false,
|
||||
longLivedSession: true,
|
||||
durableSession: false,
|
||||
codexStdio: false,
|
||||
writeCapable: false,
|
||||
@@ -2449,7 +2449,7 @@ function sessionReplyLine(session) {
|
||||
}
|
||||
|
||||
function limitationReplyLine() {
|
||||
return "边界:controlled-readonly-session-registry;not-codex-stdio;not-write-capable;not-durable-session。";
|
||||
return "边界:controlled-readonly-session-registry;long-lived read-only session;not-codex-stdio;not-write-capable;process-local-session-registry。";
|
||||
}
|
||||
|
||||
function releaseReadOnlySession(registry, session, { now, traceId, conversationId } = {}) {
|
||||
@@ -2498,7 +2498,7 @@ function runnerDescriptor({ workspace, kind = READONLY_RUNNER_KIND, session = nu
|
||||
sessionReused: session?.reused ?? false,
|
||||
implementationType: kind === READONLY_RUNNER_KIND ? READONLY_IMPLEMENTATION_TYPE : "codex-cli-one-shot-ephemeral",
|
||||
codexStdio: false,
|
||||
longLivedSession: false,
|
||||
longLivedSession: kind === READONLY_RUNNER_KIND,
|
||||
durableSession: false,
|
||||
writeCapable: false,
|
||||
readOnly: true,
|
||||
@@ -2534,7 +2534,7 @@ function runnerTrace({ traceId, events, startedAt, outputTruncated, workspace =
|
||||
valuesPrinted: false,
|
||||
note: runnerKind === CODEX_CLI_ONE_SHOT_RUNNER_KIND
|
||||
? "codex exec --ephemeral is a one-shot/read-only bridge, not a long-lived stdio session."
|
||||
: "controlled-readonly-session-registry stores conversation/session mapping and turn counters only; it is not Codex stdio, write-capable, or durable."
|
||||
: "controlled-readonly-session-registry stores conversation/session mapping, idle timeout, status, trace id, and turn counters for a reusable read-only session; it is not Codex stdio, write-capable, or DB-durable."
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2623,7 +2623,7 @@ function openAiFallbackRunnerEvidence({ providerResult, traceId, conversationId,
|
||||
"text-chat-only",
|
||||
"not-codex-stdio",
|
||||
"not-workspace-tools",
|
||||
"not-durable-session"
|
||||
"not-session-runner"
|
||||
],
|
||||
traceId
|
||||
};
|
||||
@@ -3093,6 +3093,18 @@ function errorTaxonomy(code, error = {}) {
|
||||
retryable: true,
|
||||
userMessage: "当前 conversation 与 session 绑定不一致,可新建会话后重试。"
|
||||
},
|
||||
session_failed: {
|
||||
layer: "session",
|
||||
category: "session_blocked",
|
||||
retryable: true,
|
||||
userMessage: "Code Agent session 已失败,可重新发送建立新的 session。"
|
||||
},
|
||||
session_interrupted: {
|
||||
layer: "session",
|
||||
category: "session_blocked",
|
||||
retryable: true,
|
||||
userMessage: "Code Agent session 已中断,可重新发送建立新的 session。"
|
||||
},
|
||||
runner_unavailable: {
|
||||
layer: "runner",
|
||||
category: "runner_blocked",
|
||||
|
||||
@@ -67,6 +67,18 @@ export function createCodeAgentSessionRegistry(options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
if (session && ["failed", "interrupted"].includes(session.status)) {
|
||||
session.updatedAt = timestamp;
|
||||
session.lastTraceId = optionalId(params.traceId) ?? session.lastTraceId;
|
||||
return blockedAcquire({
|
||||
code: `session_${session.status}`,
|
||||
summary: `Code Agent runner session ${effectiveSessionId} is ${session.status}; create a new session before retrying.`,
|
||||
session,
|
||||
timestamp,
|
||||
traceId: params.traceId
|
||||
});
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
session = {
|
||||
sessionId: effectiveSessionId,
|
||||
@@ -86,8 +98,10 @@ export function createCodeAgentSessionRegistry(options = {}) {
|
||||
currentTraceId: null,
|
||||
turn: 0,
|
||||
durable: false,
|
||||
longLivedSession: false,
|
||||
longLivedSession: true,
|
||||
codexStdio: false,
|
||||
readOnly: true,
|
||||
writeCapable: false,
|
||||
secretMaterialStored: false
|
||||
};
|
||||
sessions.set(effectiveSessionId, session);
|
||||
@@ -181,6 +195,7 @@ export function createCodeAgentSessionRegistry(options = {}) {
|
||||
.slice(0, 10)
|
||||
.map((session) => publicSession(session)),
|
||||
longLivedCodexStdio: false,
|
||||
longLivedSession: true,
|
||||
durable: false,
|
||||
secretMaterialStored: false,
|
||||
valuesRedacted: true
|
||||
@@ -264,8 +279,11 @@ function publicSession(session, { conversationId = null, reused = null } = {}) {
|
||||
turn: session.turn,
|
||||
reused: reused === null ? undefined : Boolean(reused),
|
||||
durable: session.durable === true,
|
||||
durableSession: session.durable === true,
|
||||
longLivedSession: session.longLivedSession === true,
|
||||
codexStdio: session.codexStdio === true,
|
||||
readOnly: session.readOnly === true,
|
||||
writeCapable: session.writeCapable === true,
|
||||
secretMaterialStored: false,
|
||||
valuesRedacted: true,
|
||||
...(session.statusReason ? { statusReason: session.statusReason } : {})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -40,12 +41,19 @@ test("code agent session registry creates, reuses, and expires sessions without
|
||||
assert.equal(first.session.runnerKind, "hwlab-readonly-runner");
|
||||
assert.equal(first.session.sessionMode, "controlled-readonly-session-registry");
|
||||
assert.equal(first.session.capabilityLevel, "read-only-session-tools");
|
||||
assert.equal(first.session.longLivedSession, true);
|
||||
assert.equal(first.session.durableSession, false);
|
||||
assert.equal(first.session.codexStdio, false);
|
||||
assert.equal(first.session.writeCapable, false);
|
||||
assert.equal(first.session.readOnly, true);
|
||||
assert.equal(first.session.idleTimeoutMs, 1000);
|
||||
assert.equal(first.session.lastTraceId, "trc_registry_1");
|
||||
assert.equal(first.session.secretMaterialStored, false);
|
||||
assert.equal(first.session.valuesRedacted, true);
|
||||
const describedCreating = registry.describe();
|
||||
assert.equal(describedCreating.sessionMode, "controlled-readonly-session-registry");
|
||||
assert.equal(describedCreating.longLivedSession, true);
|
||||
assert.equal(describedCreating.longLivedCodexStdio, false);
|
||||
assert.equal(describedCreating.recentSessions[0].sessionId, "ses_test-registry");
|
||||
assert.equal(describedCreating.recentSessions[0].sessionMode, "controlled-readonly-session-registry");
|
||||
assert.equal(describedCreating.recentSessions[0].workspace, "/workspace/hwlab");
|
||||
@@ -122,6 +130,59 @@ test("code agent session registry reports busy sessions as structured blocker",
|
||||
assert.equal(busy.session.currentTraceId, "trc_busy_1");
|
||||
});
|
||||
|
||||
test("code agent session registry blocks failed and interrupted sessions without reusing them", () => {
|
||||
const failedRegistry = createCodeAgentSessionRegistry({
|
||||
idFactory: () => "ses_failed"
|
||||
});
|
||||
const failedSeed = failedRegistry.acquire({
|
||||
conversationId: "cnv_failed",
|
||||
workspace: "/workspace/hwlab",
|
||||
traceId: "trc_failed_1",
|
||||
now: "2026-05-23T00:00:00.000Z"
|
||||
});
|
||||
assert.equal(failedSeed.ok, true);
|
||||
failedRegistry.fail(failedSeed.session.sessionId, {
|
||||
conversationId: "cnv_failed",
|
||||
traceId: "trc_failed_1",
|
||||
now: "2026-05-23T00:00:00.010Z"
|
||||
});
|
||||
const failed = failedRegistry.acquire({
|
||||
conversationId: "cnv_failed",
|
||||
workspace: "/workspace/hwlab",
|
||||
traceId: "trc_failed_2",
|
||||
now: "2026-05-23T00:00:01.000Z"
|
||||
});
|
||||
assert.equal(failed.ok, false);
|
||||
assert.equal(failed.code, "session_failed");
|
||||
assert.equal(failed.session.status, "failed");
|
||||
assert.equal(failed.blocker.sourceIssue, "pikasTech/HWLAB#317");
|
||||
|
||||
const interruptedRegistry = createCodeAgentSessionRegistry({
|
||||
idFactory: () => "ses_interrupted"
|
||||
});
|
||||
const interruptedSeed = interruptedRegistry.acquire({
|
||||
conversationId: "cnv_interrupted",
|
||||
workspace: "/workspace/hwlab",
|
||||
traceId: "trc_interrupted_1",
|
||||
now: "2026-05-23T00:00:00.000Z"
|
||||
});
|
||||
assert.equal(interruptedSeed.ok, true);
|
||||
interruptedRegistry.interrupt(interruptedSeed.session.sessionId, {
|
||||
conversationId: "cnv_interrupted",
|
||||
traceId: "trc_interrupted_1",
|
||||
now: "2026-05-23T00:00:00.010Z"
|
||||
});
|
||||
const interrupted = interruptedRegistry.acquire({
|
||||
conversationId: "cnv_interrupted",
|
||||
workspace: "/workspace/hwlab",
|
||||
traceId: "trc_interrupted_2",
|
||||
now: "2026-05-23T00:00:01.000Z"
|
||||
});
|
||||
assert.equal(interrupted.ok, false);
|
||||
assert.equal(interrupted.code, "session_interrupted");
|
||||
assert.equal(interrupted.session.status, "interrupted");
|
||||
});
|
||||
|
||||
test("read-only runner returns session registry evidence and blocked long-lived gate", async () => {
|
||||
const registry = createCodeAgentSessionRegistry({
|
||||
idFactory: () => "ses_chat_registry"
|
||||
@@ -150,9 +211,11 @@ test("read-only runner returns session registry evidence and blocked long-lived
|
||||
assert.equal(first.session.workspace, process.cwd());
|
||||
assert.equal(first.session.lastTraceId, "trc_chat_registry_pwd");
|
||||
assert.equal(first.capabilityLevel, "read-only-session-tools");
|
||||
assert.equal(first.session.longLivedSession, true);
|
||||
assert.equal(first.runner.longLivedSession, true);
|
||||
assert.equal(first.longLivedSessionGate.status, "blocked");
|
||||
assert.equal(first.longLivedSessionGate.pass, false);
|
||||
assert.ok(first.longLivedSessionGate.blockers.some((blocker) => blocker.code === "controlled_readonly_not_long_lived_stdio"));
|
||||
assert.ok(first.longLivedSessionGate.blockers.some((blocker) => blocker.code === "codex_stdio_blocked_readonly_session_available"));
|
||||
|
||||
const second = await handleCodeAgentChat(
|
||||
{
|
||||
@@ -177,7 +240,7 @@ test("read-only runner returns session registry evidence and blocked long-lived
|
||||
assert.equal(classifyCodexRunnerCapability(second, { httpStatus: 200 }).capabilityPass, false);
|
||||
});
|
||||
|
||||
test("expired and busy session states are surfaced as structured blockers", async () => {
|
||||
test("expired, busy, and failed session states are surfaced as structured blockers", async () => {
|
||||
const expiredRegistry = createCodeAgentSessionRegistry({
|
||||
idleTimeoutMs: 10,
|
||||
idFactory: () => "ses_expired_chat"
|
||||
@@ -258,6 +321,47 @@ test("expired and busy session states are surfaced as structured blockers", asyn
|
||||
assert.equal(busy.session.status, "busy");
|
||||
assert.equal(busy.capabilityLevel, "blocked");
|
||||
assert.equal(busy.error.blockers[0].sourceIssue, "pikasTech/HWLAB#317");
|
||||
|
||||
const failedRegistry = createCodeAgentSessionRegistry({
|
||||
idFactory: () => "ses_failed_chat"
|
||||
});
|
||||
const failedSeed = failedRegistry.acquire({
|
||||
conversationId: "cnv_failed_chat",
|
||||
workspace: process.cwd(),
|
||||
sandbox: "read-only",
|
||||
runnerKind: "hwlab-readonly-runner",
|
||||
capabilityLevel: "read-only-session-tools",
|
||||
traceId: "trc_failed_seed",
|
||||
now: "2026-05-23T00:04:00.000Z"
|
||||
});
|
||||
failedRegistry.fail(failedSeed.session.sessionId, {
|
||||
conversationId: "cnv_failed_chat",
|
||||
traceId: "trc_failed_seed",
|
||||
now: "2026-05-23T00:04:00.001Z"
|
||||
});
|
||||
const failed = await handleCodeAgentChat(
|
||||
{
|
||||
conversationId: "cnv_failed_chat",
|
||||
traceId: "trc_failed_request",
|
||||
message: "pwd"
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-23T00:04:01.000Z",
|
||||
sessionRegistry: failedRegistry,
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd()
|
||||
}
|
||||
}
|
||||
);
|
||||
assert.equal(failed.status, "failed");
|
||||
assert.equal(failed.error.code, "session_failed");
|
||||
assert.equal(failed.error.layer, "session");
|
||||
assert.equal(failed.error.retryable, true);
|
||||
assert.match(failed.error.userMessage, /session 已失败/u);
|
||||
assert.equal(failed.error.blocker.code, "session_failed");
|
||||
assert.equal(failed.session.status, "failed");
|
||||
assert.equal(failed.capabilityLevel, "blocked");
|
||||
});
|
||||
|
||||
test("OpenAI fallback and codex one-shot do not pass the long-lived session gate", async () => {
|
||||
@@ -300,7 +404,8 @@ test("OpenAI fallback and codex one-shot do not pass the long-lived session gate
|
||||
kind: "codex-cli-one-shot-ephemeral",
|
||||
codexStdio: false,
|
||||
writeCapable: false,
|
||||
durableSession: false
|
||||
durableSession: false,
|
||||
longLivedSession: false
|
||||
},
|
||||
sessionMode: "ephemeral-one-shot",
|
||||
implementationType: "codex-cli-one-shot-ephemeral",
|
||||
@@ -937,7 +1042,8 @@ test("Codex stdio feasibility verifies writable workspace, CODEX_HOME, and versi
|
||||
const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-codex-home-"));
|
||||
try {
|
||||
const manager = createCodexStdioSessionManager();
|
||||
const codexCommand = path.join(process.cwd(), "node_modules", ".bin", "codex");
|
||||
const localCodexCommand = path.join(process.cwd(), "node_modules", ".bin", "codex");
|
||||
const codexCommand = existsSync(localCodexCommand) ? localCodexCommand : "codex";
|
||||
const availability = manager.describe({
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
|
||||
@@ -747,9 +747,9 @@ export function longLivedSessionGate({
|
||||
}
|
||||
if (normalizedImplementation === "controlled-readonly-session-registry") {
|
||||
blockers.push({
|
||||
code: "controlled_readonly_not_long_lived_stdio",
|
||||
code: "codex_stdio_blocked_readonly_session_available",
|
||||
sourceIssue: "pikasTech/HWLAB#317",
|
||||
summary: "This response is backed by the controlled read-only session registry, not Codex stdio or an equivalent long-lived protocol adapter."
|
||||
summary: "This response is backed by a reusable controlled read-only session registry, not Codex stdio or an equivalent full Code Agent protocol adapter."
|
||||
});
|
||||
}
|
||||
if (session && !["idle", "ready", "busy"].includes(session.status)) {
|
||||
|
||||
@@ -231,7 +231,7 @@ function buildSessionRunnerReadiness(codeAgent = {}) {
|
||||
const stdioReady = stdio.ready === true || stdio.canStartLongLivedCodexStdio === true;
|
||||
const runner = codeAgent?.runner ?? {};
|
||||
const runnerReady = stdioReady || runner.ready === true || codeAgent?.sessionRegistry?.status === "available";
|
||||
const status = stdioReady ? "long_lived_ready" : runnerReady ? "read_only_ready" : "blocked";
|
||||
const status = stdioReady ? "codex_stdio_ready" : runnerReady ? "read_only_long_lived_ready" : "blocked";
|
||||
const blocker = runnerReady ? null : firstCode(
|
||||
runner.blocker,
|
||||
codeAgent?.reason,
|
||||
@@ -247,7 +247,7 @@ function buildSessionRunnerReadiness(codeAgent = {}) {
|
||||
backend: stdioReady ? stdio.backend ?? "hwlab-cloud-api/codex-mcp-stdio" : runner.backend ?? codeAgent?.backend ?? "unknown",
|
||||
mode: stdioReady ? "codex-mcp-stdio-long-lived" : runner.sessionMode ?? runner.mode ?? codeAgent?.sessionMode ?? "unknown",
|
||||
capabilityLevel: stdioReady ? "long-lived-codex-stdio-session" : runner.capabilityLevel ?? codeAgent?.capabilityLevel ?? "unknown",
|
||||
longLivedSession: stdioReady || runner.longLivedSession === true,
|
||||
longLivedSession: stdioReady || runner.longLivedSession === true || runner.sessionMode === "controlled-readonly-session-registry",
|
||||
durableSession: stdioReady || runner.durableSession === true || runner.durable === true,
|
||||
codexStdio: stdioReady || runner.codexStdio === true,
|
||||
writeCapable: stdioReady || runner.writeCapable === true,
|
||||
|
||||
@@ -89,10 +89,11 @@ test("cloud api exposes /health, /health/live, and /live probes", async () => {
|
||||
assert.equal(healthPayload.readiness.provider.blocker, "provider_config_blocked");
|
||||
assert.equal(healthPayload.readiness.dbDurable.status, "blocked");
|
||||
assert.equal(healthPayload.readiness.dbDurable.ready, false);
|
||||
assert.equal(healthPayload.readiness.sessionRunner.status, "read_only_ready");
|
||||
assert.equal(healthPayload.readiness.sessionRunner.status, "read_only_long_lived_ready");
|
||||
assert.equal(healthPayload.readiness.sessionRunner.ready, true);
|
||||
assert.equal(healthPayload.readiness.sessionRunner.codexStdio, false);
|
||||
assert.equal(healthPayload.readiness.sessionRunner.durableSession, false);
|
||||
assert.equal(healthPayload.readiness.sessionRunner.longLivedSession, true);
|
||||
assert.equal(healthPayload.readiness.codexStdio.status, "blocked");
|
||||
assert.equal(healthPayload.readiness.codexStdio.ready, false);
|
||||
assertCodexStdioFeasibilityBlocker(healthPayload.readiness.codexStdio.blocker);
|
||||
@@ -344,7 +345,7 @@ test("cloud api health reports DB env presence and live connection classificatio
|
||||
assert.equal(payload.readiness.dbDurable.status, "blocked");
|
||||
assert.equal(payload.readiness.dbDurable.dbLiveEvidenceObserved, true);
|
||||
assert.equal(payload.readiness.dbDurable.blocker, "runtime_durable_adapter_missing");
|
||||
assert.equal(payload.readiness.sessionRunner.status, "read_only_ready");
|
||||
assert.equal(payload.readiness.sessionRunner.status, "read_only_long_lived_ready");
|
||||
assert.equal(payload.readiness.codexStdio.status, "blocked");
|
||||
assert.equal(payload.db.redaction.valuesRedacted, true);
|
||||
assert.equal(payload.db.redaction.endpointRedacted, true);
|
||||
@@ -1178,7 +1179,7 @@ test("cloud api health reports provider and durable DB ready while codex stdio r
|
||||
assert.equal(payload.readiness.provider.blocker, null);
|
||||
assert.equal(payload.readiness.dbDurable.status, "ready");
|
||||
assert.equal(payload.readiness.dbDurable.ready, true);
|
||||
assert.equal(payload.readiness.sessionRunner.status, "read_only_ready");
|
||||
assert.equal(payload.readiness.sessionRunner.status, "read_only_long_lived_ready");
|
||||
assert.equal(payload.readiness.sessionRunner.ready, true);
|
||||
assert.equal(payload.readiness.codexStdio.status, "blocked");
|
||||
assertCodexStdioFeasibilityBlocker(payload.readiness.codexStdio.blocker);
|
||||
@@ -1323,7 +1324,7 @@ test("cloud api /v1/agent/chat runs read-only runner pwd with workspace evidence
|
||||
assert.equal(payload.session.lastTraceId, "trc_server-test-runner-pwd");
|
||||
assert.equal(typeof payload.session.idleTimeoutMs, "number");
|
||||
assert.equal(payload.runner.kind, "hwlab-readonly-runner");
|
||||
assert.equal(payload.runner.longLivedSession, false);
|
||||
assert.equal(payload.runner.longLivedSession, true);
|
||||
assert.equal(payload.runner.codexStdio, false);
|
||||
assert.equal(payload.runner.writeCapable, false);
|
||||
assert.equal(payload.runner.durableSession, false);
|
||||
@@ -1334,12 +1335,13 @@ test("cloud api /v1/agent/chat runs read-only runner pwd with workspace evidence
|
||||
assert.equal(payload.sessionReuse.turn, 1);
|
||||
assert.equal(payload.sessionReuse.status, "idle");
|
||||
assert.ok(payload.runnerLimitations.includes("not-codex-stdio"));
|
||||
assert.ok(payload.runnerLimitations.includes("process-local-session-registry"));
|
||||
assert.ok(payload.codexStdioFeasibility.blockers.some((blocker) => blocker.code === "codex_stdio_supervisor_disabled"));
|
||||
assert.ok(payload.codexStdioFeasibility.blockers.some((blocker) => blocker.code === "stdio_protocol_not_wired"));
|
||||
assert.equal(payload.codexStdioFeasibility.runtimeContract.stdioProtocol.status, "blocked");
|
||||
assert.equal(payload.codexStdioFeasibility.runtimeContract.lifecycleSupervisor.status, "blocked");
|
||||
assert.equal(payload.longLivedSessionGate.status, "blocked");
|
||||
assert.ok(payload.longLivedSessionGate.blockers.some((blocker) => blocker.code === "controlled_readonly_not_long_lived_stdio"));
|
||||
assert.ok(payload.longLivedSessionGate.blockers.some((blocker) => blocker.code === "codex_stdio_blocked_readonly_session_available"));
|
||||
assert.equal(payload.toolCalls[0].name, "pwd");
|
||||
assert.equal(payload.toolCalls[0].status, "completed");
|
||||
assert.equal(payload.toolCalls[0].stdout, workspace);
|
||||
|
||||
@@ -198,6 +198,7 @@ async function runLocalContractSmoke() {
|
||||
assert.equal(runnerPwd.implementationType, "controlled-readonly-session-registry");
|
||||
assert.equal(runnerPwd.runner.session, "controlled-readonly-session-registry");
|
||||
assert.equal(runnerPwd.runner.codexStdio, false);
|
||||
assert.equal(runnerPwd.runner.longLivedSession, true);
|
||||
assert.equal(runnerPwd.runner.writeCapable, false);
|
||||
assert.equal(runnerPwd.runner.durableSession, false);
|
||||
assert.equal(runnerPwd.sessionReuse.reused, false);
|
||||
@@ -210,14 +211,14 @@ async function runLocalContractSmoke() {
|
||||
assert.equal(runnerPwd.codexStdioFeasibility.runtimeContract.lifecycleSupervisor.status, "blocked");
|
||||
assert.equal(runnerPwd.longLivedSessionGate.status, "blocked");
|
||||
assert.equal(runnerPwd.longLivedSessionGate.pass, false);
|
||||
assert.ok(runnerPwd.longLivedSessionGate.blockers.some((blocker) => blocker.code === "controlled_readonly_not_long_lived_stdio"));
|
||||
assert.ok(runnerPwd.longLivedSessionGate.blockers.some((blocker) => blocker.code === "codex_stdio_blocked_readonly_session_available"));
|
||||
assert.equal(runnerPwd.toolCalls[0].name, "pwd");
|
||||
assert.equal(runnerPwd.toolCalls[0].status, "completed");
|
||||
const runnerCapability = classifyCodexRunnerCapability(runnerPwd, { httpStatus: 200 });
|
||||
assert.equal(runnerCapability.status, "blocked");
|
||||
assert.equal(runnerCapability.capabilityPass, false);
|
||||
assert.equal(runnerCapability.blocker, "controlled-readonly-not-long-lived");
|
||||
logOk("read-only runner pwd remains partial capability");
|
||||
logOk("read-only runner pwd uses a reusable session but remains partial capability");
|
||||
|
||||
const runnerSecondTurn = await handleCodeAgentChat(
|
||||
{
|
||||
|
||||
@@ -219,7 +219,7 @@ async function smokeCloudApi() {
|
||||
assertCodexStdioFeasibilityBlocker(health.body.codeAgent.reason, "health codeAgent reason");
|
||||
assert.equal(health.body.readiness.provider.status, "blocked");
|
||||
assert.equal(health.body.readiness.provider.blocker, "provider_config_blocked");
|
||||
assert.equal(health.body.readiness.sessionRunner.status, "read_only_ready");
|
||||
assert.equal(health.body.readiness.sessionRunner.status, "read_only_long_lived_ready");
|
||||
assert.equal(health.body.readiness.sessionRunner.ready, true);
|
||||
assert.equal(health.body.readiness.codexStdio.status, "blocked");
|
||||
assertCodexStdioFeasibilityBlocker(health.body.readiness.codexStdio.blocker, "health readiness codexStdio blocker");
|
||||
@@ -252,7 +252,7 @@ async function smokeCloudApi() {
|
||||
assert.equal(adapter.body.codeAgent.backend, "hwlab-cloud-api/codex-cli");
|
||||
assert.equal(adapter.body.codeAgent.ready, false);
|
||||
assert.equal(adapter.body.readiness.provider.status, "blocked");
|
||||
assert.equal(adapter.body.readiness.sessionRunner.status, "read_only_ready");
|
||||
assert.equal(adapter.body.readiness.sessionRunner.status, "read_only_long_lived_ready");
|
||||
assert.equal(adapter.body.readiness.codexStdio.status, "blocked");
|
||||
assert.ok(adapter.body.codeAgent.missingEnv.includes("OPENAI_API_KEY"));
|
||||
assert.equal(adapter.body.codeAgent.secretRefs[0].secretName, "hwlab-code-agent-provider");
|
||||
|
||||
@@ -146,10 +146,10 @@ export function classifyCodexRunnerCapability(payload, { httpStatus = null } = {
|
||||
if (partialRunnerProviders.has(provider) && runnerKind === "hwlab-readonly-runner") {
|
||||
return {
|
||||
status: "blocked",
|
||||
level: "BLOCKED/controlled-readonly-session-registry",
|
||||
level: "BLOCKED/controlled-readonly-long-lived-session",
|
||||
blocker: "controlled-readonly-not-long-lived",
|
||||
capabilityPass: false,
|
||||
reason: "controlled-readonly-session-registry can expose bounded read-only tools, but it cannot pass the long-lived Codex stdio capability gate."
|
||||
reason: "controlled-readonly-session-registry exposes a reusable read-only session, but it is not Codex stdio, workspace-write, or a full Code Agent capability pass."
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -471,7 +471,7 @@ export function classifyCodeAgentRuntimeBlock(payload, { httpStatus = null } = {
|
||||
};
|
||||
}
|
||||
|
||||
if (["session_expired", "session_reuse_conflict"].includes(errorCode)) {
|
||||
if (["session_expired", "session_reuse_conflict", "session_failed", "session_interrupted"].includes(errorCode)) {
|
||||
return {
|
||||
blocked: true,
|
||||
status: "blocked",
|
||||
|
||||
@@ -310,10 +310,10 @@ const requiredCodeAgentEvidenceTerms = Object.freeze([
|
||||
"codex-mcp-stdio-long-lived",
|
||||
"repo-owned-codex-mcp-stdio-session",
|
||||
"controlled-readonly-session-registry",
|
||||
"read-only-session-tools:只读工具",
|
||||
"read-only-session-tools:只读长会话",
|
||||
"not-codex-stdio",
|
||||
"not-write-capable",
|
||||
"not-durable-session",
|
||||
"process-local-session-registry",
|
||||
"echo/mock/stub",
|
||||
"untrusted_completion",
|
||||
"SOURCE fixture",
|
||||
@@ -2475,7 +2475,7 @@ function hasCodeAgentReadinessVisibility({ html, app, liveStatus = "" }) {
|
||||
/isRealCompletedChatResult/u.test(app) &&
|
||||
/hasRealCodeAgentEvidence/u.test(app) &&
|
||||
/codeAgentBlockerDetail/u.test(app) &&
|
||||
/codex_stdio_blocked_readonly_available/u.test(liveStatus) &&
|
||||
/codex_stdio_blocked_readonly_session_available/u.test(liveStatus) &&
|
||||
/codex_stdio_supervisor_disabled/u.test(app) &&
|
||||
!/status === "failed" \? "dev-live"/u.test(app) &&
|
||||
!/tone:\s*state\.conversationId\s*\?\s*"dev-live"/u.test(app) &&
|
||||
@@ -5836,6 +5836,7 @@ async function handleLocalAgentFixtureApi({ request, response, url, options = {}
|
||||
runner: {
|
||||
kind: "openai-responses-fallback",
|
||||
codexStdio: false,
|
||||
longLivedSession: false,
|
||||
writeCapable: false,
|
||||
durableSession: false
|
||||
},
|
||||
|
||||
@@ -80,7 +80,7 @@ const CODEX_STDIO_BLOCKER_MARKERS = Object.freeze([
|
||||
"codex_stdio_supervisor_disabled",
|
||||
"not-codex-stdio",
|
||||
"not-write-capable",
|
||||
"not-durable-session"
|
||||
"process-local-session-registry"
|
||||
]);
|
||||
const UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN = /\b(?:echo|mock|fixture|stub|sample)\b/iu;
|
||||
const LAYOUT_STORAGE_KEY = "hwlab.workbench.layout.v1";
|
||||
@@ -2409,11 +2409,11 @@ function agentFailurePresentation(error, { result = null, traceId = null } = {})
|
||||
};
|
||||
}
|
||||
|
||||
if (["session_expired", "session_reuse_conflict"].includes(code)) {
|
||||
if (["session_expired", "session_reuse_conflict", "session_failed", "session_interrupted"].includes(code)) {
|
||||
return {
|
||||
category: "session_blocked",
|
||||
title: "Code Agent Session 受阻",
|
||||
text: `当前 session 已过期或与 conversation 绑定不一致;输入已保留,可重新发送建立新的受控只读 session。${traceSuffix}`
|
||||
text: `当前 session 已过期、失败、中断或与 conversation 绑定不一致;输入已保留,可重新发送建立新的受控只读 session。${traceSuffix}`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2504,7 +2504,7 @@ function messageAttributionPanel(message) {
|
||||
section.className = `message-attribution tone-border-${toneClass(attribution.tone)}`;
|
||||
section.setAttribute(
|
||||
"aria-label",
|
||||
"消息归因:字段缺失:证据不足;fallback 只是文本回答;blocker 当前不能执行;text-chat-only / 只是文本回答;read-only-session-tools:只读工具。"
|
||||
"消息归因:字段缺失:证据不足;fallback 只是文本回答;blocker 当前不能执行;text-chat-only / 只是文本回答;read-only-session-tools:只读长会话。"
|
||||
);
|
||||
section.dataset.codeAgentAttributionKind = attribution.kind;
|
||||
|
||||
@@ -3011,8 +3011,8 @@ function codeAgentStatusMessage(availability) {
|
||||
if (availability?.runner?.ready === true) {
|
||||
return {
|
||||
role: "system",
|
||||
title: "Code Agent 状态:只读 session registry 可用,Codex stdio 受阻",
|
||||
text: `pwd、skills、ls、rg --files 和 cat 会走 controlled-readonly-session-registry;它不是完整 Code Agent。${codeAgentBlockerDetail(availability)}`,
|
||||
title: "Code Agent 状态:只读长会话 registry 可用,Codex stdio 受阻",
|
||||
text: `pwd、skills、ls、rg --files 和 cat 会走可复用 controlled-readonly-session-registry;它不是完整 Codex stdio Code Agent。${codeAgentBlockerDetail(availability)}`,
|
||||
status: "source"
|
||||
};
|
||||
}
|
||||
@@ -3068,7 +3068,7 @@ function codeAgentControlSummary(availability) {
|
||||
return codeAgentBlockedSummary(availability);
|
||||
}
|
||||
if (availability?.partialReady === true || availability?.runner?.ready === true) {
|
||||
return `输入区会调用受控接口;当前只有只读 registry 或 text fallback,不能标成完整 Code Agent。${codeAgentBlockerDetail(availability)}`;
|
||||
return `输入区会调用受控接口;当前只有只读长会话 registry 或 text fallback,不能标成完整 Codex stdio Code Agent。${codeAgentBlockerDetail(availability)}`;
|
||||
}
|
||||
return "输入区会调用受控 Code Agent 接口;只有真实完成回复才显示为完成,不能因为只有会话编号就当成实况完成。";
|
||||
}
|
||||
@@ -3111,7 +3111,8 @@ function codeAgentBlockerChineseLabel(code) {
|
||||
runner_lifecycle_missing: "缺少 repo-owned lifecycle supervisor",
|
||||
stdio_protocol_not_wired: "Codex stdio 协议尚未接入",
|
||||
codex_stdio_supervisor_disabled: "Codex stdio supervisor 未启用",
|
||||
controlled_readonly_not_long_lived_stdio: "当前只是只读 session registry",
|
||||
codex_stdio_blocked_readonly_session_available: "只读长会话 registry 可用但 Codex stdio 受阻",
|
||||
controlled_readonly_not_long_lived_stdio: "只读 registry 不是 Codex stdio",
|
||||
openai_responses_fallback_not_session: "文本 fallback 不是长会话",
|
||||
one_shot_runner_not_long_lived: "一次性执行不是可复用 session",
|
||||
provider_token_boundary: "token 边界未配置",
|
||||
@@ -3133,7 +3134,7 @@ function codeAgentAvailabilitySummary() {
|
||||
return "同源 API 可响应;Codex stdio 长会话 gate 已通过,支持真实 session 复用和 trace capture。";
|
||||
}
|
||||
if (state.codeAgentAvailability?.runner?.ready === true) {
|
||||
return `同源 API 可响应;pwd/skills/ls/rg/cat 可走受控只读 session registry,但它不是 Codex stdio、不可写、非持久 session。${codeAgentBlockerDetail(state.codeAgentAvailability)}`;
|
||||
return `同源 API 可响应;pwd/skills/ls/rg/cat 可走受控只读长会话 registry,但它不是 Codex stdio、不可写、非 DB 持久 session。${codeAgentBlockerDetail(state.codeAgentAvailability)}`;
|
||||
}
|
||||
if (state.codeAgentAvailability?.status === "blocked") {
|
||||
return "同源 API 可响应;Code Agent 服务暂不可用,工作台保持只读和可重试。";
|
||||
@@ -3236,6 +3237,8 @@ function isReadOnlyRunnerPartialEvidence(value) {
|
||||
value?.implementationType === "controlled-readonly-session-registry" &&
|
||||
value?.runner?.codexStdio === false &&
|
||||
value?.runner?.writeCapable === false &&
|
||||
value?.session?.longLivedSession === true &&
|
||||
value?.runner?.longLivedSession === true &&
|
||||
value?.runner?.durableSession === false &&
|
||||
value?.longLivedSessionGate?.status === "blocked"
|
||||
);
|
||||
|
||||
@@ -129,9 +129,9 @@ export function classifyCodeAgentFacts(message) {
|
||||
if (provider === "codex-readonly-runner" || runnerKind === "hwlab-readonly-runner" || sessionMode === READONLY_SESSION_MODE) {
|
||||
return {
|
||||
kind: READONLY_SESSION_MODE,
|
||||
statusLabel: "只读工具:controlled-readonly-session-registry",
|
||||
statusLabel: "只读长会话:controlled-readonly-session-registry",
|
||||
tone: "source",
|
||||
summary: "只读 session registry 可用;只能使用 read-only-session-tools,不能写工作区,也不能冒充完整 Codex stdio 长会话。",
|
||||
summary: "只读 session registry 可复用 conversation/session;只能使用 read-only-session-tools,不能写工作区,也不能冒充完整 Codex stdio Code Agent。",
|
||||
fullCodeAgent: false,
|
||||
codexSessionGatePass: false
|
||||
};
|
||||
@@ -302,7 +302,7 @@ function hasCodeAgentFields(message) {
|
||||
function messageAttributionLabel(message, classification) {
|
||||
if (message?.status === "failed" || message?.sourceKind === "BLOCKED") return "BLOCKED:当前不能执行";
|
||||
if (classification.kind === "text-chat-only") return "OpenAI fallback:只是文本回答";
|
||||
if (classification.kind === READONLY_SESSION_MODE) return "read-only-session-tools:只读工具";
|
||||
if (classification.kind === READONLY_SESSION_MODE) return "read-only-session-tools:只读长会话";
|
||||
if (classification.kind === "hwlab-skill-cli") return "hwlab-skill-cli:Skill CLI 受控路径";
|
||||
if (classification.kind === "codex-stdio-long-lived") return "真实 runner:Codex stdio 长会话";
|
||||
if (classification.kind === "stateless-one-shot") return "一次性 runner:非长会话";
|
||||
|
||||
@@ -33,12 +33,13 @@ test("codex-readonly-runner is shown as partial read-only session registry", ()
|
||||
kind: "hwlab-readonly-runner",
|
||||
codexStdio: false,
|
||||
writeCapable: false,
|
||||
durableSession: false
|
||||
durableSession: false,
|
||||
longLivedSession: true
|
||||
},
|
||||
longLivedSessionGate: {
|
||||
status: "blocked",
|
||||
pass: false,
|
||||
blockers: [{ code: "controlled_readonly_not_long_lived_stdio" }]
|
||||
blockers: [{ code: "codex_stdio_blocked_readonly_session_available" }]
|
||||
},
|
||||
toolCalls: [{ name: "pwd", status: "completed" }],
|
||||
skills: { status: "not_requested", count: 0 },
|
||||
@@ -54,6 +55,8 @@ test("codex-readonly-runner is shown as partial read-only session registry", ()
|
||||
assert.equal(facts.kind, "controlled-readonly-session-registry");
|
||||
assert.equal(facts.fullCodeAgent, false);
|
||||
assert.equal(facts.codexSessionGatePass, false);
|
||||
assert.match(facts.statusLabel, /只读长会话/u);
|
||||
assert.match(facts.summary, /可复用/u);
|
||||
assert.match(facts.summary, /Codex stdio/u);
|
||||
assert.match(facts.rows.find((row) => row.label === "runner/provider/backend").value, /runner=hwlab-readonly-runner/u);
|
||||
assert.match(facts.rows.find((row) => row.label === "toolCalls").value, /1\/1 pwd\(completed\)/u);
|
||||
|
||||
@@ -134,7 +134,7 @@ function classifyPayload(payload, blockers) {
|
||||
}
|
||||
|
||||
if (isReadOnlySessionTools(payload)) {
|
||||
return status("read-only-session-tools", "只读会话工具", "warn", "◇", "只读 runner 工具可用,不能写工作区或冒充完整长会话。");
|
||||
return status("read-only-session-tools", "只读长会话工具", "warn", "◇", "只读 runner 会话可复用,不能写工作区或冒充完整 Codex stdio。");
|
||||
}
|
||||
|
||||
if (isStatelessOneShot(payload)) {
|
||||
|
||||
@@ -91,7 +91,7 @@ test("maps degraded readonly session tools to warning, not green", () => {
|
||||
});
|
||||
|
||||
assert.equal(summary.kind, "read-only-session-tools");
|
||||
assert.equal(summary.label, "只读会话工具");
|
||||
assert.equal(summary.label, "只读长会话工具");
|
||||
assert.equal(summary.tone, "warn");
|
||||
assert.equal(summary.sessionId, "ses_readonly");
|
||||
assert.equal(summary.sessionIdStatus, "ses_readonly / idle");
|
||||
@@ -272,12 +272,12 @@ test("uses structured readiness current blockers and ignores stale provider bloc
|
||||
queryResult: "durable_readiness_ready"
|
||||
},
|
||||
sessionRunner: {
|
||||
status: "read_only_ready",
|
||||
status: "read_only_long_lived_ready",
|
||||
ready: true,
|
||||
kind: "hwlab-readonly-runner",
|
||||
mode: "controlled-readonly-session-registry",
|
||||
capabilityLevel: "read-only-session-tools",
|
||||
longLivedSession: false,
|
||||
longLivedSession: true,
|
||||
durableSession: false,
|
||||
codexStdio: false,
|
||||
writeCapable: false,
|
||||
|
||||
@@ -39,10 +39,11 @@ const READ_ONLY_REASON_CODES = Object.freeze([
|
||||
"persistence",
|
||||
"not-codex-stdio",
|
||||
"not-write-capable",
|
||||
"not-durable-session",
|
||||
"process-local-session-registry",
|
||||
"text-chat-only",
|
||||
"fallback:text-chat-only",
|
||||
"codex_stdio_blocked_readonly_available",
|
||||
"codex_stdio_blocked_readonly_session_available",
|
||||
"controlled_readonly_not_long_lived_stdio",
|
||||
"codex_cli_binary_missing",
|
||||
"runner_lifecycle_missing",
|
||||
@@ -329,7 +330,7 @@ function classifyCodeAgentProbe(live = {}) {
|
||||
return readOnlyProbe({
|
||||
serviceId,
|
||||
apiPath,
|
||||
reasonCode: "codex_stdio_blocked_readonly_available",
|
||||
reasonCode: "codex_stdio_blocked_readonly_session_available",
|
||||
reason: `${availability.summary ?? "controlled-readonly-session-registry available; long-lived Codex stdio blocked"}; blockers=${blockerCodes(availability).join(",") || "not_observed"}`,
|
||||
traceId: traceIdFrom(availability),
|
||||
evidenceSummary: evidenceSummaryFrom(availability),
|
||||
@@ -409,7 +410,7 @@ function classifyStructuredCodeAgentProbe(readiness = {}) {
|
||||
return readOnlyProbe({
|
||||
serviceId,
|
||||
apiPath,
|
||||
reasonCode: readiness.codexStdio?.blocker ?? "codex_stdio_blocked_readonly_available",
|
||||
reasonCode: readiness.codexStdio?.blocker ?? "codex_stdio_blocked_readonly_session_available",
|
||||
reason: `providerReady=${Boolean(readiness.providerReady)}; durableDbReady=${Boolean(readiness.durableDbReady)}; sessionRunner=${readiness.sessionRunner?.status ?? "unknown"}; codexStdio=${readiness.codexStdio?.status ?? "unknown"}; current=${currentStructuredBlockers(readiness).join(",") || "not_observed"}`,
|
||||
meta: { provider, backend, capability }
|
||||
});
|
||||
|
||||
@@ -774,7 +774,7 @@ assert.match(app, /read-only-session-tools/);
|
||||
assert.match(app, /controlled-readonly-session-registry/);
|
||||
assert.match(app, /not-codex-stdio/);
|
||||
assert.match(app, /not-write-capable/);
|
||||
assert.match(app, /not-durable-session/);
|
||||
assert.match(app, /process-local-session-registry/);
|
||||
assert.match(app, /Code Agent 完成证据不足/);
|
||||
assert.match(app, /本次不会标记为真实完成/);
|
||||
assert.match(app, /untrusted_completion/);
|
||||
@@ -804,7 +804,7 @@ assert.match(codeAgentFacts, /字段缺失:证据不足/);
|
||||
assert.match(codeAgentFacts, /codeAgentAttributionFromMessage/);
|
||||
assert.match(codeAgentFacts, /stateless-one-shot/);
|
||||
assert.match(codeAgentFacts, /controlled-readonly-session-registry/);
|
||||
assert.match(codeAgentFacts, /read-only-session-tools:只读工具/);
|
||||
assert.match(codeAgentFacts, /read-only-session-tools:只读长会话/);
|
||||
assert.match(codeAgentFacts, /hwlab-m3-io-skill-cli/);
|
||||
assert.match(codeAgentFacts, /route", fact\.route/);
|
||||
assert.match(codeAgentFacts, /operationId", fact\.operationId/);
|
||||
|
||||
@@ -224,7 +224,7 @@ test("classifies controlled readonly Code Agent as read-only, not full pass", ()
|
||||
|
||||
assert.equal(status.kind, "readonly");
|
||||
assert.equal(status.label, "只读模式");
|
||||
assert.equal(status.reasonCode, "codex_stdio_blocked_readonly_available");
|
||||
assert.equal(status.reasonCode, "codex_stdio_blocked_readonly_session_available");
|
||||
assert.match(status.detail, /controlled-readonly-session-registry/u);
|
||||
});
|
||||
|
||||
@@ -264,7 +264,7 @@ test("structured readiness shows current codex stdio blocker instead of stale pr
|
||||
blocker: null
|
||||
},
|
||||
sessionRunner: {
|
||||
status: "read_only_ready",
|
||||
status: "read_only_long_lived_ready",
|
||||
ready: true,
|
||||
kind: "hwlab-readonly-runner",
|
||||
mode: "controlled-readonly-session-registry",
|
||||
@@ -729,7 +729,7 @@ test("keeps ready durable runtime green while attributing read-only mode to Code
|
||||
backend: "hwlab-cloud-api/controlled-readonly-session-registry",
|
||||
capabilityLevel: "read-only-session-tools",
|
||||
trackingIssue: "#317",
|
||||
runnerLimitations: ["not-codex-stdio", "not-write-capable", "not-durable-session"]
|
||||
runnerLimitations: ["not-codex-stdio", "not-write-capable", "process-local-session-registry"]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -748,7 +748,7 @@ test("keeps ready durable runtime green while attributing read-only mode to Code
|
||||
backend: "hwlab-cloud-api/controlled-readonly-session-registry",
|
||||
capabilityLevel: "read-only-session-tools",
|
||||
trackingIssue: "#317",
|
||||
runnerLimitations: ["not-codex-stdio", "not-write-capable", "not-durable-session"]
|
||||
runnerLimitations: ["not-codex-stdio", "not-write-capable", "process-local-session-registry"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user