Merge pull request #2249 from pikasTech/fix/2245-session-context-followup

fix: preserve Workbench launch context in session facts
This commit is contained in:
Lyon
2026-06-28 19:28:36 +08:00
committed by GitHub
3 changed files with 155 additions and 6 deletions
@@ -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) {
@@ -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 = [];
+44 -4
View File
@@ -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);