fix: keep workbench projection checkpoint behind events

This commit is contained in:
lyon1998
2026-06-28 12:52:58 +08:00
parent 747c0eb797
commit 09327f1ded
3 changed files with 101 additions and 6 deletions
+1
View File
@@ -6,6 +6,7 @@ HWLAB 是硬件实验室运行面和控制面项目。本文件只作为低噪
- 先按 issue、PR、CLI 参数或受控 lane 配置解析目标 `node + lane`;没有明确目标时才读取受控配置,禁止把 G14、v0.2、D601 legacy 或任意历史路径写成全局默认。
- D601 v0.3 的固定 workspace 是 `/home/ubuntu/workspace/hwlab-v03`,固定跟踪 `origin/v0.3`;它是当前 D601/v03 source truth,不是 legacy 对照面。
- D518 v0.3 的固定主 workspace 是 `D518:/home/ubuntu/workspace/hwlab-v03`,固定跟踪 `github/v0.3`;D518 源码、配置、部署和 issue 修复必须从这里创建 `.worktree/<task>`,不要使用 master server 或 `/root/HWLAB` 作为 D518 source truth。
- k3s 操作必须通过 UniDesk route,例如 `D601:k3s``G14:k3s`;不要使用另一节点 kubeconfig、Docker Desktop Kubernetes、master server 本地构建或旧 JS CD 结果作为当前 lane 证据。
- `hyueapi.com` / `.hyueapi.com` 必须保留在 `NO_PROXY` / `no_proxy`Codex API 通道不得被 HTTP/SOCKS proxy 劫持。
@@ -357,6 +357,89 @@ test("workbench projection writer does not synthesize terminal trace events from
assert.equal(loaded.facts.messages.some((message) => message.role !== "user" && message.text === "BACKFILL_DONE"), true);
});
test("workbench projection terminal facts do not advance checkpoint past durable trace events", async () => {
const runtimeStore = createCloudRuntimeStore({ now: () => "2026-06-20T11:45:00.000Z" });
const traceId = "trc_writer_terminal_checkpoint_gap";
const sessionId = "ses_writer_terminal_checkpoint_gap";
const finalText = "CHECKPOINT_GAP_DONE";
const accessController = {
async recordAgentSessionOwner(input) {
return {
id: input.sessionId,
projectId: input.projectId,
ownerUserId: input.ownerUserId,
conversationId: input.conversationId,
threadId: input.threadId,
lastTraceId: input.traceId,
status: input.status,
session: input.session,
updatedAt: "2026-06-20T11:45:00.000Z"
};
}
};
for (let sourceSeq = 1; sourceSeq <= 33; sourceSeq += 1) {
await writeWorkbenchProjectionEvent({
runtimeStore,
event: {
traceId,
sessionId,
source: "agentrun",
sourceSeq,
type: sourceSeq === 33 ? "terminal" : "backend",
eventType: sourceSeq === 33 ? "terminal" : "backend",
status: sourceSeq === 33 ? "completed" : "running",
label: sourceSeq === 33 ? "agentrun:result:completed" : `agentrun:backend:${sourceSeq}`,
terminal: sourceSeq === 33,
runId: "run_writer_terminal_checkpoint_gap",
commandId: "cmd_writer_terminal_checkpoint_gap",
createdAt: `2026-06-20T11:44:${String(sourceSeq).padStart(2, "0")}.000Z`
}
});
}
await writeWorkbenchProjectionSession({
accessController,
runtimeStore,
traceId,
ownerUserId: "usr_writer",
ownerRole: "user",
sessionId,
projectId: "prj_writer",
conversationId: "cnv_writer_terminal_checkpoint_gap",
threadId: "thread-writer-terminal-checkpoint-gap",
status: "completed",
payload: {
traceId,
status: "completed",
finalResponse: { text: finalText, status: "completed", traceId },
runnerTrace: {
traceId,
status: "completed",
eventCount: 34,
events: Array.from({ length: 34 }, (_, index) => ({ seq: index + 1, type: "backend", status: index === 33 ? "completed" : "running", terminal: index === 33 }))
},
agentRun: { runId: "run_writer_terminal_checkpoint_gap", commandId: "cmd_writer_terminal_checkpoint_gap", status: "completed", terminalStatus: "completed", lastSeq: 30 },
updatedAt: "2026-06-20T11:45:00.000Z"
},
session: {
sessionStatus: "completed",
messages: [
{ messageId: "msg_writer_checkpoint_gap_user", role: "user", text: "question", status: "sent", turnId: traceId, traceId },
{ messageId: "msg_writer_checkpoint_gap_agent", role: "agent", text: finalText, status: "completed", turnId: traceId, traceId }
],
finalResponse: { text: finalText, status: "completed", traceId }
}
});
const loaded = runtimeStore.queryWorkbenchFacts({ traceId, families: ["traceEvents", "checkpoints", "turns"], limit: 80 });
assert.equal(loaded.facts.traceEvents.length, 33);
assert.equal(Math.max(...loaded.facts.traceEvents.map((event) => event.projectedSeq)), 33);
assert.equal(loaded.facts.checkpoints[0].projectedSeq, 33);
assert.equal(loaded.facts.checkpoints[0].sourceSeq, 30);
assert.equal(loaded.facts.turns[0].terminal, true);
});
test("workbench projection writer does not duplicate terminal trace events that already exist", async () => {
const runtimeStore = createCloudRuntimeStore({ now: () => "2026-06-20T11:25:00.000Z" });
const traceId = "trc_writer_terminal_backfill_existing";
+17 -6
View File
@@ -534,6 +534,7 @@ function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, own
const diagnostic = timingAuthorityIssue
? { ...baseDiagnostic, projectionHealth: baseDiagnostic.projectionHealth === "unavailable" ? "unavailable" : "degraded", blocker: baseDiagnostic.blocker ?? timingAuthorityIssue, timingAuthorityIssue, valuesRedacted: true }
: baseDiagnostic;
const factSeq = workbenchProjectionFactSequence({ projection, previousCheckpoint, agentRun });
const previousFinalText = finalResponseTextValue(previousCheckpoint?.finalResponse, previousCheckpoint?.assistantText, previousCheckpoint?.finalText);
const finalText = terminal ? finalResponseTextValue(projection.finalResponse, payload?.finalResponse, session?.finalResponse, payload?.assistantText, previousFinalText) : null;
const projectedFinalResponse = terminal && finalText ? { text: finalText, status: terminalStatus, traceId: safeId, valuesPrinted: false } : null;
@@ -558,8 +559,8 @@ function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, own
threadId: resolvedThreadId,
status: terminal ? terminalStatus : "running",
lastTraceId: safeId,
projectedSeq: nonNegativeInteger(projection.lastProjectedSeq),
sourceSeq: nonNegativeInteger(projection.lastProjectedSeq),
projectedSeq: factSeq.projectedSeq,
sourceSeq: factSeq.sourceSeq,
sourceEventId: safeId,
terminal,
sealed: terminal,
@@ -580,8 +581,8 @@ function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, own
traceId: safeId,
messageId: messages.find((message) => message.role !== "user")?.messageId ?? null,
status: projection.status ?? normalizedStatus,
projectedSeq: nonNegativeInteger(projection.lastProjectedSeq),
sourceSeq: nonNegativeInteger(projection.lastProjectedSeq),
projectedSeq: factSeq.projectedSeq,
sourceSeq: factSeq.sourceSeq,
sourceEventId: safeId,
terminal: projection.terminal,
sealed: projection.terminal,
@@ -602,8 +603,8 @@ function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, own
turnId: projection.turnId ?? safeId,
runId: textValue(agentRun?.runId ?? projection.sourceRunId) || null,
commandId: textValue(agentRun?.commandId ?? projection.sourceCommandId) || null,
projectedSeq: nonNegativeInteger(projection.lastProjectedSeq),
sourceSeq: nonNegativeInteger(agentRun?.lastSeq ?? projection.lastProjectedSeq),
projectedSeq: factSeq.projectedSeq,
sourceSeq: factSeq.sourceSeq,
sourceEventId: safeId,
projectionStatus: projection.terminal ? "caught_up" : "projecting",
projectionHealth: diagnostic.projectionHealth === "unavailable" ? "unavailable" : diagnostic.projectionHealth === "stalled" ? "stalled" : "healthy",
@@ -623,6 +624,16 @@ function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, own
};
}
function workbenchProjectionFactSequence({ projection = {}, previousCheckpoint = null, agentRun = null } = {}) {
const projectionSeq = nonNegativeInteger(projection?.lastProjectedSeq);
const previousSeq = nonNegativeInteger(previousCheckpoint?.projectedSeq ?? previousCheckpoint?.lastProjectedSeq);
const sourceSeq = nonNegativeInteger(agentRun?.lastSeq ?? projection?.sourceSeq ?? projectionSeq);
return {
projectedSeq: previousSeq > 0 ? Math.min(projectionSeq || previousSeq, previousSeq) : projectionSeq,
sourceSeq
};
}
function workbenchProjectionError(code, message) {
const error = new Error(message);
error.code = code;