refactor(workbench): split session detail response helpers
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
} from "./server-http-utils.ts";
|
||||
import { createWorkbenchReadModel } from "./workbench-read-model.ts";
|
||||
import { createWorkbenchRuntimeClient } from "./workbench-runtime-client.ts";
|
||||
import { buildWorkbenchSessionDetail, compactLaunchContext, includeMessagesForSessionDetail } from "./workbench-session-detail-response.ts";
|
||||
import { durableTraceStatus, RUNNING_STATUSES, terminalFinalResponse, TERMINAL_STATUSES } from "./workbench-turn-projection.ts";
|
||||
import { emitCodeAgentOtelSpan, emitHttpServerRequestSpan } from "./otel-trace.ts";
|
||||
|
||||
@@ -1018,87 +1019,13 @@ function factSessionSummary(session, facts = {}) {
|
||||
|
||||
function factSessionDetail(session, facts = {}, options = {}) {
|
||||
const sessionId = factSessionId(session);
|
||||
const detail = {
|
||||
...factSessionSummary(session, facts),
|
||||
metadata: {
|
||||
startedAt: session?.startedAt ?? session?.createdAt ?? null,
|
||||
endedAt: session?.endedAt ?? null,
|
||||
ownerUserId: session?.ownerUserId ?? null,
|
||||
launchContext: compactLaunchContext(session?.sessionJson?.launchContext)
|
||||
},
|
||||
messagePageUrl: sessionId ? `/v1/workbench/sessions/${encodeURIComponent(sessionId)}/messages` : null,
|
||||
valuesRedacted: true,
|
||||
secretMaterialStored: false
|
||||
};
|
||||
return options.includeMessages === true ? { ...detail, messages: factMessagesForSession(session, facts) } : detail;
|
||||
}
|
||||
|
||||
function compactLaunchContext(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||||
const projectId = textValue(value.projectId) || null;
|
||||
const taskRef = textValue(value.taskRef) || null;
|
||||
if (!projectId && !taskRef) return null;
|
||||
const execution = compactLaunchExecutionContext(value.executionContext ?? value.execution ?? value);
|
||||
return {
|
||||
source: textValue(value.source) || "project-management",
|
||||
projectId,
|
||||
taskRef,
|
||||
sourceId: textValue(value.sourceId) || null,
|
||||
sourceKind: textValue(value.sourceKind) || execution?.sourceKind || null,
|
||||
fileRef: textValue(value.fileRef) || null,
|
||||
relativePath: textValue(value.relativePath) || execution?.relativePath || null,
|
||||
taskId: textValue(value.taskId) || null,
|
||||
title: textValue(value.title) || null,
|
||||
status: textValue(value.status) || null,
|
||||
hwpodId: (execution?.hwpodId ?? textValue(value.hwpodId)) || null,
|
||||
nodeId: (execution?.nodeId ?? textValue(value.nodeId)) || null,
|
||||
mdtodoRootRef: (execution?.mdtodoRootRef ?? textValue(value.mdtodoRootRef)) || null,
|
||||
workspaceRootHash: (execution?.workspaceRootHash ?? textValue(value.workspaceRootHash)) || null,
|
||||
workspaceRootLabel: (execution?.workspaceRootLabel ?? textValue(value.workspaceRootLabel)) || null,
|
||||
hwpodWorkspaceArgs: (execution?.hwpodWorkspaceArgs ?? textValue(value.hwpodWorkspaceArgs)) || null,
|
||||
contextFingerprint: (execution?.contextFingerprint ?? textValue(value.contextFingerprint)) || null,
|
||||
capabilities: compactPlainObject(value.capabilities),
|
||||
executionContext: execution,
|
||||
route: textValue(value.route) || "/projects/mdtodo",
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function compactLaunchExecutionContext(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||||
const sourceId = textValue(value.sourceId) || null;
|
||||
const taskRef = textValue(value.taskRef) || null;
|
||||
const hwpodId = textValue(value.hwpodId) || null;
|
||||
if (!sourceId && !taskRef && !hwpodId) return null;
|
||||
return {
|
||||
sourceId,
|
||||
sourceKind: textValue(value.sourceKind) || null,
|
||||
projectId: textValue(value.projectId) || null,
|
||||
taskRef,
|
||||
fileRef: textValue(value.fileRef) || null,
|
||||
relativePath: textValue(value.relativePath) || null,
|
||||
taskId: textValue(value.taskId) || null,
|
||||
hwpodId,
|
||||
nodeId: textValue(value.nodeId) || null,
|
||||
mdtodoRootRef: textValue(value.mdtodoRootRef) || null,
|
||||
workspaceRootRef: textValue(value.workspaceRootRef) || null,
|
||||
workspaceRootHash: textValue(value.workspaceRootHash) || null,
|
||||
workspaceRootLabel: textValue(value.workspaceRootLabel) || null,
|
||||
hwpodWorkspaceArgs: textValue(value.hwpodWorkspaceArgs) || null,
|
||||
contextFingerprint: textValue(value.contextFingerprint) || null,
|
||||
capabilities: compactPlainObject(value.capabilities),
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function compactPlainObject(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
||||
const out = {};
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
if (!/^[A-Za-z0-9_.:-]{1,80}$/u.test(key)) continue;
|
||||
if (["string", "number", "boolean"].includes(typeof item) || item === null) out[key] = item;
|
||||
}
|
||||
return out;
|
||||
return buildWorkbenchSessionDetail({
|
||||
summary: factSessionSummary(session, facts),
|
||||
session,
|
||||
sessionId,
|
||||
includeMessages: options.includeMessages === true,
|
||||
messages: factMessagesForSession(session, facts)
|
||||
});
|
||||
}
|
||||
|
||||
function factMessagesForSession(session, facts = {}) {
|
||||
@@ -2931,17 +2858,6 @@ function boundedSessionListLimit(value) {
|
||||
return Math.min(MAX_PAGE_LIMIT, parsePositiveInteger(value, DEFAULT_SESSION_LIST_LIMIT));
|
||||
}
|
||||
|
||||
function includeMessagesForSessionDetail(params) {
|
||||
if (params.get("includeMessages") !== null) return truthyQueryFlag(params.get("includeMessages"));
|
||||
if (params.get("messages") !== null) return truthyQueryFlag(params.get("messages"));
|
||||
return false;
|
||||
}
|
||||
|
||||
function truthyQueryFlag(value) {
|
||||
const normalized = textValue(value).toLowerCase();
|
||||
return normalized === "1" || normalized === "true" || normalized === "yes";
|
||||
}
|
||||
|
||||
function cursorOffset(value) {
|
||||
const text = textValue(value);
|
||||
if (!text) return 0;
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* SPEC: pikasTech/HWLAB#2359 Workbench session detail metadata-only contract.
|
||||
* Responsibility: Build Workbench session detail payloads and parse detail payload mode flags.
|
||||
*/
|
||||
|
||||
export function includeMessagesForSessionDetail(params: URLSearchParams): boolean {
|
||||
if (params.get("includeMessages") !== null) return truthyQueryFlag(params.get("includeMessages"));
|
||||
if (params.get("messages") !== null) return truthyQueryFlag(params.get("messages"));
|
||||
return false;
|
||||
}
|
||||
|
||||
export function buildWorkbenchSessionDetail(input: {
|
||||
summary: Record<string, unknown>;
|
||||
session: Record<string, unknown> | null | undefined;
|
||||
sessionId: string | null | undefined;
|
||||
includeMessages: boolean;
|
||||
messages: unknown[];
|
||||
}): Record<string, unknown> {
|
||||
const detail = {
|
||||
...input.summary,
|
||||
metadata: {
|
||||
startedAt: input.session?.startedAt ?? input.session?.createdAt ?? null,
|
||||
endedAt: input.session?.endedAt ?? null,
|
||||
ownerUserId: input.session?.ownerUserId ?? null,
|
||||
launchContext: compactLaunchContext(sessionJson(input.session)?.launchContext)
|
||||
},
|
||||
messagePageUrl: input.sessionId ? `/v1/workbench/sessions/${encodeURIComponent(input.sessionId)}/messages` : null,
|
||||
valuesRedacted: true,
|
||||
secretMaterialStored: false
|
||||
};
|
||||
return input.includeMessages ? { ...detail, messages: input.messages } : detail;
|
||||
}
|
||||
|
||||
export function compactLaunchContext(value: unknown): Record<string, unknown> | null {
|
||||
const record = objectRecord(value);
|
||||
if (!record) return null;
|
||||
const projectId = textValue(record.projectId) || null;
|
||||
const taskRef = textValue(record.taskRef) || null;
|
||||
if (!projectId && !taskRef) return null;
|
||||
const execution = compactLaunchExecutionContext(record.executionContext ?? record.execution ?? record);
|
||||
return {
|
||||
source: textValue(record.source) || "project-management",
|
||||
projectId,
|
||||
taskRef,
|
||||
sourceId: textValue(record.sourceId) || null,
|
||||
sourceKind: textValue(record.sourceKind) || execution?.sourceKind || null,
|
||||
fileRef: textValue(record.fileRef) || null,
|
||||
relativePath: textValue(record.relativePath) || execution?.relativePath || null,
|
||||
taskId: textValue(record.taskId) || null,
|
||||
title: textValue(record.title) || null,
|
||||
status: textValue(record.status) || null,
|
||||
hwpodId: (execution?.hwpodId ?? textValue(record.hwpodId)) || null,
|
||||
nodeId: (execution?.nodeId ?? textValue(record.nodeId)) || null,
|
||||
mdtodoRootRef: (execution?.mdtodoRootRef ?? textValue(record.mdtodoRootRef)) || null,
|
||||
workspaceRootHash: (execution?.workspaceRootHash ?? textValue(record.workspaceRootHash)) || null,
|
||||
workspaceRootLabel: (execution?.workspaceRootLabel ?? textValue(record.workspaceRootLabel)) || null,
|
||||
hwpodWorkspaceArgs: (execution?.hwpodWorkspaceArgs ?? textValue(record.hwpodWorkspaceArgs)) || null,
|
||||
contextFingerprint: (execution?.contextFingerprint ?? textValue(record.contextFingerprint)) || null,
|
||||
capabilities: compactPlainObject(record.capabilities),
|
||||
executionContext: execution,
|
||||
route: textValue(record.route) || "/projects/mdtodo",
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function compactLaunchExecutionContext(value: unknown): Record<string, unknown> | null {
|
||||
const record = objectRecord(value);
|
||||
if (!record) return null;
|
||||
const sourceId = textValue(record.sourceId) || null;
|
||||
const taskRef = textValue(record.taskRef) || null;
|
||||
const hwpodId = textValue(record.hwpodId) || null;
|
||||
if (!sourceId && !taskRef && !hwpodId) return null;
|
||||
return {
|
||||
sourceId,
|
||||
sourceKind: textValue(record.sourceKind) || null,
|
||||
projectId: textValue(record.projectId) || null,
|
||||
taskRef,
|
||||
fileRef: textValue(record.fileRef) || null,
|
||||
relativePath: textValue(record.relativePath) || null,
|
||||
taskId: textValue(record.taskId) || null,
|
||||
hwpodId,
|
||||
nodeId: textValue(record.nodeId) || null,
|
||||
mdtodoRootRef: textValue(record.mdtodoRootRef) || null,
|
||||
workspaceRootRef: textValue(record.workspaceRootRef) || null,
|
||||
workspaceRootHash: textValue(record.workspaceRootHash) || null,
|
||||
workspaceRootLabel: textValue(record.workspaceRootLabel) || null,
|
||||
hwpodWorkspaceArgs: textValue(record.hwpodWorkspaceArgs) || null,
|
||||
contextFingerprint: textValue(record.contextFingerprint) || null,
|
||||
capabilities: compactPlainObject(record.capabilities),
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function compactPlainObject(value: unknown): Record<string, unknown> {
|
||||
const record = objectRecord(value);
|
||||
if (!record) return {};
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [key, item] of Object.entries(record)) {
|
||||
if (!/^[A-Za-z0-9_.:-]{1,80}$/u.test(key)) continue;
|
||||
if (["string", "number", "boolean"].includes(typeof item) || item === null) out[key] = item;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function sessionJson(session: Record<string, unknown> | null | undefined): Record<string, unknown> | null {
|
||||
return objectRecord(session?.sessionJson);
|
||||
}
|
||||
|
||||
function objectRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
function textValue(value: unknown): string {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : "";
|
||||
}
|
||||
|
||||
function truthyQueryFlag(value: string | null): boolean {
|
||||
const normalized = textValue(value).toLowerCase();
|
||||
return normalized === "1" || normalized === "true" || normalized === "yes";
|
||||
}
|
||||
Reference in New Issue
Block a user