From a2a8bd634b07ebc433108c1ee26ac8b66c2a19b7 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 28 Jun 2026 19:26:10 +0800 Subject: [PATCH] fix: preserve workbench launch context in session facts --- .../server-workbench-launch-http.test.ts | 13 ++- .../cloud/workbench-projection-writer.test.ts | 100 ++++++++++++++++++ internal/cloud/workbench-projection-writer.ts | 48 ++++++++- 3 files changed, 155 insertions(+), 6 deletions(-) diff --git a/internal/cloud/server-workbench-launch-http.test.ts b/internal/cloud/server-workbench-launch-http.test.ts index 4d59fc05..9d4532b0 100644 --- a/internal/cloud/server-workbench-launch-http.test.ts +++ b/internal/cloud/server-workbench-launch-http.test.ts @@ -14,6 +14,7 @@ test("workbench launch records OTel stage for Project Management link write 404" }); const response = jsonResponse(); const fetchCalls = []; + const factWrites = []; globalThis.fetch = async (url, init) => { fetchCalls.push({ url: String(url), method: init?.method, body: init?.body }); if (String(url).includes("/v1/project-management/mdtodo/launch-context")) { @@ -51,8 +52,9 @@ test("workbench launch records OTel stage for Project Management link write 404" } }, runtimeStore: { - async writeWorkbenchFacts() { - return { ok: true }; + async writeWorkbenchFacts(params, requestMeta) { + factWrites.push({ params, requestMeta }); + return { ok: true, facts: params.facts }; } } }); @@ -83,6 +85,13 @@ test("workbench launch records OTel stage for Project Management link write 404" assert.equal(context.otelAttributes["workbench.launch.context_fingerprint"], "ctx_launch_otel"); assert.match(context.otelAttributes["workbench.launch.task_ref_hash"], /^[0-9a-f]{24}$/u); assert.equal(context.otelAttributes["workbench.launch.values_redacted"], true); + assert.equal(factWrites.length, 1); + const sessionJson = factWrites[0].params.facts.sessions[0].sessionJson; + assert.equal(sessionJson.launchContext.sourceId, "hwlab-v03-mdtodo"); + assert.equal(sessionJson.launchContext.hwpodId, "constart-71freq-c"); + assert.equal(sessionJson.launchContext.contextFingerprint, "ctx_launch_otel"); + assert.equal(sessionJson.launchContext.executionContext.hwpodWorkspaceArgs, "--hwpod-id constart-71freq-c --workspace-path 'F:\\Work\\ConStart'"); + assert.equal(sessionJson.launchContext.valuesRedacted, true); }); function jsonRequest(body) { diff --git a/internal/cloud/workbench-projection-writer.test.ts b/internal/cloud/workbench-projection-writer.test.ts index d93a1b77..6e8e7d58 100644 --- a/internal/cloud/workbench-projection-writer.test.ts +++ b/internal/cloud/workbench-projection-writer.test.ts @@ -7,6 +7,106 @@ import { test } from "bun:test"; import { createCloudRuntimeStore } from "../db/runtime-store.ts"; import { writeWorkbenchProjectionEvent, writeWorkbenchProjectionSession } from "./workbench-projection-writer.ts"; +test("workbench projection writer preserves Workbench launch context on Code Agent session facts", async () => { + const factWrites = []; + const accessWrites = []; + const launchContext = { + source: "project-management", + projectId: "project_constart_71freq", + taskRef: "mdtodo:constart-71freq-mdtodo:file_c5abae4e69c60370:R1", + sourceId: "constart-71freq-mdtodo", + sourceKind: "hwpod-workspace", + fileRef: "file_c5abae4e69c60370", + taskId: "R1", + hwpodId: "constart-71freq-c", + nodeId: "node-d601-f103-v2", + mdtodoRootRef: "docs/MDTODO", + workspaceRootHash: "workspace-hash", + hwpodWorkspaceArgs: "--hwpod-id constart-71freq-c --workspace-path 'F:\\Work\\ConStart'", + contextFingerprint: "ctx_projection_launch", + executionContext: { + sourceId: "constart-71freq-mdtodo", + sourceKind: "hwpod-workspace", + projectId: "project_constart_71freq", + taskRef: "mdtodo:constart-71freq-mdtodo:file_c5abae4e69c60370:R1", + fileRef: "file_c5abae4e69c60370", + taskId: "R1", + hwpodId: "constart-71freq-c", + nodeId: "node-d601-f103-v2", + mdtodoRootRef: "docs/MDTODO", + workspaceRootHash: "workspace-hash", + hwpodWorkspaceArgs: "--hwpod-id constart-71freq-c --workspace-path 'F:\\Work\\ConStart'", + contextFingerprint: "ctx_projection_launch", + valuesRedacted: true + }, + valuesRedacted: true + }; + const runtimeStore = { + async writeWorkbenchFacts(params, requestMeta) { + factWrites.push({ params, requestMeta }); + return { written: true, facts: params.facts }; + } + }; + const accessController = { + async recordAgentSessionOwner(input) { + accessWrites.push(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-28T11:15:00.000Z" + }; + } + }; + + await writeWorkbenchProjectionSession({ + accessController, + runtimeStore, + traceId: "trc_projection_launch_context", + ownerUserId: "usr_projection_launch", + ownerRole: "admin", + sessionId: "ses_projection_launch_context", + projectId: "project_constart_71freq", + conversationId: "cnv_projection_launch_context", + threadId: "thread-projection-launch-context", + status: "running", + payload: { + traceId: "trc_projection_launch_context", + status: "running", + updatedAt: "2026-06-28T11:15:00.000Z" + }, + params: { + projectId: "project_constart_71freq", + taskRef: "mdtodo:constart-71freq-mdtodo:file_c5abae4e69c60370:R1", + message: "run this MDTODO task", + launchContext + }, + session: { + sessionStatus: "running", + messages: [ + { messageId: "msg_projection_launch_user", role: "user", text: "run this MDTODO task", status: "sent", turnId: "trc_projection_launch_context", traceId: "trc_projection_launch_context" } + ], + valuesRedacted: true, + secretMaterialStored: false + } + }); + + assert.equal(accessWrites.length, 1); + assert.equal(factWrites.length, 1); + const sessionJson = factWrites[0].params.facts.sessions[0].sessionJson; + assert.equal(sessionJson.launchContext.sourceId, "constart-71freq-mdtodo"); + assert.equal(sessionJson.launchContext.hwpodId, "constart-71freq-c"); + assert.equal(sessionJson.launchContext.contextFingerprint, "ctx_projection_launch"); + assert.equal(sessionJson.launchContext.executionContext.hwpodWorkspaceArgs, "--hwpod-id constart-71freq-c --workspace-path 'F:\\Work\\ConStart'"); + assert.equal(sessionJson.launchContext.valuesRedacted, true); + assert.equal(sessionJson.secretMaterialStored, false); +}); + test("workbench projection writer commits terminal owner evidence as sealed durable facts", async () => { const factWrites = []; const accessWrites = []; diff --git a/internal/cloud/workbench-projection-writer.ts b/internal/cloud/workbench-projection-writer.ts index 0fcb4f22..1e965314 100644 --- a/internal/cloud/workbench-projection-writer.ts +++ b/internal/cloud/workbench-projection-writer.ts @@ -68,6 +68,7 @@ export async function writeWorkbenchSessionAdmissionFact({ runtimeStore = null, const createdAt = timestampValue(session.startedAt ?? session.createdAt ?? session.session?.createdAt ?? now); const updatedAt = timestampValue(session.updatedAt ?? createdAt); const normalizedStatus = normalizeWorkbenchStatus(session.status ?? session.session?.sessionStatus ?? status) || "idle"; + const sessionJson = workbenchSessionJsonWithLaunchContext(plainObjectValue(session.session) ?? {}, { payload: session }); const fact = { sessionId, ownerUserId: textValue(session.ownerUserId ?? ownerUserId) || null, @@ -84,10 +85,10 @@ export async function writeWorkbenchSessionAdmissionFact({ runtimeStore = null, terminal: false, sealed: false, sessionJson: { - ...(session.session && typeof session.session === "object" ? session.session : {}), + ...sessionJson, sessionStatus: normalizedStatus, - source: textValue(session.session?.source) || "manual-session-create", - providerProfile: textValue(session.session?.providerProfile) || null, + source: textValue(sessionJson.source ?? session.session?.source) || "manual-session-create", + providerProfile: textValue(sessionJson.providerProfile ?? session.session?.providerProfile) || null, valuesRedacted: true, secretMaterialStored: false }, @@ -549,6 +550,7 @@ function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, own timestamp, timing }); + const sessionJson = workbenchSessionJsonWithLaunchContext(session, { payload, params }); return { sessions: resolvedSessionId ? [{ sessionId: resolvedSessionId, @@ -564,7 +566,7 @@ function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, own sourceEventId: safeId, terminal, sealed: terminal, - sessionJson: session, + sessionJson, timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, @@ -624,6 +626,44 @@ function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, own }; } +function workbenchSessionJsonWithLaunchContext(session = {}, { payload = null, params = null } = {}) { + const base = plainObjectValue(session) ?? {}; + const launchContext = firstPlainObjectValue( + base.launchContext, + base.sessionJson?.launchContext, + base.session?.launchContext, + payload?.launchContext, + payload?.sessionJson?.launchContext, + payload?.session?.launchContext, + params?.launchContext, + params?.sessionJson?.launchContext, + params?.session?.launchContext + ); + if (!launchContext) return base; + const next = { + ...base, + launchContext: { + ...launchContext, + valuesRedacted: true + }, + valuesRedacted: true + }; + if (!Object.prototype.hasOwnProperty.call(next, "secretMaterialStored")) next.secretMaterialStored = false; + return next; +} + +function firstPlainObjectValue(...values) { + for (const value of values) { + const object = plainObjectValue(value); + if (object) return object; + } + return null; +} + +function plainObjectValue(value) { + return value && typeof value === "object" && !Array.isArray(value) ? value : null; +} + function workbenchProjectionFactSequence({ projection = {}, previousCheckpoint = null, agentRun = null } = {}) { const projectionSeq = nonNegativeInteger(projection?.lastProjectedSeq); const previousSeq = nonNegativeInteger(previousCheckpoint?.projectedSeq ?? previousCheckpoint?.lastProjectedSeq);