From bd74492bb9bd51473758e6381f1e14b9cf9ef64c Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 18 May 2026 13:18:21 +0000 Subject: [PATCH] fix: preserve code queue prompt observation fields --- docs/reference/e2e.md | 4 +- scripts/code-queue-prompt-observation-test.ts | 108 +++++++++++ scripts/src/check.ts | 4 + scripts/src/code-queue.ts | 9 +- scripts/src/e2e.ts | 172 +++++++++++++++++- src/components/frontend/src/code-queue.tsx | 21 ++- .../code-queue-mgr/src-rs/main.rs | 167 +++++++++++++++-- .../microservices/code-queue-mgr/src/index.ts | 18 +- .../code-queue-mgr/src/prompt-observation.ts | 45 +++++ 9 files changed, 513 insertions(+), 35 deletions(-) create mode 100644 scripts/code-queue-prompt-observation-test.ts create mode 100644 src/components/microservices/code-queue-mgr/src/prompt-observation.ts diff --git a/docs/reference/e2e.md b/docs/reference/e2e.md index 2608496d..4ed0a913 100644 --- a/docs/reference/e2e.md +++ b/docs/reference/e2e.md @@ -1,6 +1,6 @@ # UniDesk E2E Reference -UniDesk delivery is not complete until the public production frontend, public dev frontend proxy when deployed, public provider ingress, internal core API, PostgreSQL database, local provider-gateway self-connection, and frontend Playwright flow pass the relevant end-to-end checks. The canonical automated command is `bun scripts/cli.ts e2e run`. +UniDesk delivery is not complete until the public production frontend, public dev frontend proxy when deployed, public provider ingress, internal core API, PostgreSQL database, local provider-gateway self-connection, and frontend Playwright flow pass the relevant end-to-end checks. The canonical automated command is `bun scripts/cli.ts e2e run`; set `UNIDESK_E2E_FRONTEND=dev` to target the public dev frontend proxy and `UNIDESK_E2E_AUTH_USERNAME` / `UNIDESK_E2E_AUTH_PASSWORD` when that environment uses credentials different from root `config.json`. ## Required Preconditions @@ -35,7 +35,7 @@ Typical targeted commands: - Core API: `docker exec unidesk-backend-core` calls internal `GET /api/overview`, which must report `dbReady: true`, `pgdata.volumeName=unidesk_pgdata_10gb`, a positive PostgreSQL database byte count, and at least one online node; internal `GET /api/performance` must report component request statistics, internal operation statistics, PGDATA usage and Code Queue PostgreSQL storage metadata. - Provider self-connection: internal `GET /api/nodes` must contain `main-server` with `status: online`, `labels.providerGatewayVersion` equal to `src/components/provider-gateway/package.json`, `labels.providerGatewayUpgradePolicy: "always-enabled"`, `labels.providerGatewayRestartPolicyOk: true`, `labels.providerGatewayPidModeOk: true`, and `labels.providerGatewayRuntimeGuardOk: true`; internal `GET /api/nodes/system-status` must contain CPU/memory/disk samples plus a non-empty process resource list sorted by `memoryBytes` by default, where `memoryBytes` should use PSS when `/proc/[pid]/smaps_rollup` is available, otherwise `rssBytes - statm.shared` before raw RSS, and must retain `rssBytes` for diagnostics; internal `GET /api/nodes/docker-status` must contain a Docker snapshot for `main-server`; every running `provider-gateway` container visible in Docker snapshots must report `restartPolicy: "always"` and `pidMode: "host"`; public provider ingress `/health` must return ok. - Provider remote control: internal `/api/dispatch` must successfully complete a real `provider.upgrade` task in `mode: "plan"` so the upgrade path is validated without recreating the running gateway during E2E. -- User services: internal `/api/microservices` must include `todo-note` and `oa-event-flow` on `main-server`, canonical `filebrowser` on `D518`, plus `k3sctl-adapter`, `code-queue`, `findjob`, `pipeline`, `met-nonlinear`, `claudeqq` and `filebrowser-d601` on `D601` with `public=false`; `/api/microservices/todo-note/health` must report `storage=postgres`, `/api/microservices/todo-note/proxy/api/instances` must expose the migrated Todo Note lists, and a temporary Todo Note list create/add/toggle/undo/delete cycle must succeed through the real provider-gateway proxy; `/api/microservices/oa-event-flow/health`, `/api/microservices/oa-event-flow/proxy/api/diagnostics`, `/api/microservices/oa-event-flow/proxy/api/events`, `/api/microservices/oa-event-flow/proxy/api/events?tags=service:pipeline` and `/api/microservices/oa-event-flow/proxy/api/stats/trace` must prove the independent OA event table、Pipeline bridge 和 stats center are reachable through UniDesk proxy; `/api/microservices/k3sctl-adapter/health` and `/api/microservices/k3sctl-adapter/proxy/api/control-plane` must expose the D601 `unidesk-k3s` control plane, `kubeApiProxy.mode=kubernetes-api-service-proxy`, D601 active Code Queue instance `servingHealthy=true`, `presentNodeIds` containing `D601`, `missingNodeIds=[]`, `status=healthy`, and `noFallback=true`; `/api/microservices/code-queue/health` must return the active Code Queue backend summary with default model `gpt-5.5`, `egressProxy.connected=true`, and `/api/microservices/code-queue/proxy/api/tasks/overview` must return queue state through backend-core -> k3sctl-adapter -> Kubernetes API service proxy -> k3s/k8s Service, not through a `serviceId=code-queue` provider-gateway direct task or `/api/code-queue-direct`; `/api/microservices/filebrowser/health`, `/api/microservices/filebrowser-d601/health` and `/api/microservices/filebrowser/proxy/` must prove File Browser health and WebUI access through UniDesk proxy; `/api/microservices/findjob/health` and `/api/microservices/findjob/proxy/api/summary` must succeed through the real provider-gateway proxy; `/api/microservices/findjob/proxy/api/jobs?__unideskArrayLimit=jobs:5` must return a bounded preview with `_unidesk.arrayLimits` metadata; `/api/microservices/pipeline/health`, `/api/microservices/pipeline/proxy/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3` and `/api/microservices/pipeline/proxy/api/oa-event-flow/diagnostics` must return Pipeline health, registry/run previews and OA event-flow evidence; `/api/microservices/met-nonlinear/health`, `/api/microservices/met-nonlinear/proxy/api/queue`, `/api/microservices/met-nonlinear/proxy/api/projects?root=projects&limit=500`, `/api/microservices/met-nonlinear/proxy/api/projects?root=ex_projects&limit=500`, `/api/microservices/met-nonlinear/proxy/api/projects/config?path=` and `/api/microservices/met-nonlinear/proxy/api/images` must return the D601 TS backend health, queue/GPU policy, full project tree inputs, structured project detail and ready `met-nonlinear-ml:tf26` image status. +- User services: internal `/api/microservices` must include `todo-note` and `oa-event-flow` on `main-server`, canonical `filebrowser` on `D518`, plus `k3sctl-adapter`, `code-queue`, `findjob`, `pipeline`, `met-nonlinear`, `claudeqq` and `filebrowser-d601` on `D601` with `public=false`; `/api/microservices/todo-note/health` must report `storage=postgres`, `/api/microservices/todo-note/proxy/api/instances` must expose the migrated Todo Note lists, and a temporary Todo Note list create/add/toggle/undo/delete cycle must succeed through the real provider-gateway proxy; `/api/microservices/oa-event-flow/health`, `/api/microservices/oa-event-flow/proxy/api/diagnostics`, `/api/microservices/oa-event-flow/proxy/api/events`, `/api/microservices/oa-event-flow/proxy/api/events?tags=service:pipeline` and `/api/microservices/oa-event-flow/proxy/api/stats/trace` must prove the independent OA event table、Pipeline bridge 和 stats center are reachable through UniDesk proxy; `/api/microservices/k3sctl-adapter/health` and `/api/microservices/k3sctl-adapter/proxy/api/control-plane` must expose the D601 `unidesk-k3s` control plane, `kubeApiProxy.mode=kubernetes-api-service-proxy`, D601 active Code Queue instance `servingHealthy=true`, `presentNodeIds` containing `D601`, `missingNodeIds=[]`, `status=healthy`, and `noFallback=true`; `/api/microservices/code-queue/health` must return the active Code Queue backend summary with default model `gpt-5.5`, `egressProxy.connected=true`, and `/api/microservices/code-queue/proxy/api/tasks/overview` must return queue state through backend-core -> k3sctl-adapter -> Kubernetes API service proxy -> k3s/k8s Service, not through a `serviceId=code-queue` provider-gateway direct task or `/api/code-queue-direct`; Code Queue raw prompt observation fields must preserve long prompt tails across create/list/detail/frontend paths, with any shortened text exposed only through explicit `*Preview` objects carrying `chars` and `truncated`; `/api/microservices/filebrowser/health`, `/api/microservices/filebrowser-d601/health` and `/api/microservices/filebrowser/proxy/` must prove File Browser health and WebUI access through UniDesk proxy; `/api/microservices/findjob/health` and `/api/microservices/findjob/proxy/api/summary` must succeed through the real provider-gateway proxy; `/api/microservices/findjob/proxy/api/jobs?__unideskArrayLimit=jobs:5` must return a bounded preview with `_unidesk.arrayLimits` metadata; `/api/microservices/pipeline/health`, `/api/microservices/pipeline/proxy/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3` and `/api/microservices/pipeline/proxy/api/oa-event-flow/diagnostics` must return Pipeline health, registry/run previews and OA event-flow evidence; `/api/microservices/met-nonlinear/health`, `/api/microservices/met-nonlinear/proxy/api/queue`, `/api/microservices/met-nonlinear/proxy/api/projects?root=projects&limit=500`, `/api/microservices/met-nonlinear/proxy/api/projects?root=ex_projects&limit=500`, `/api/microservices/met-nonlinear/proxy/api/projects/config?path=` and `/api/microservices/met-nonlinear/proxy/api/images` must return the D601 TS backend health, queue/GPU policy, full project tree inputs, structured project detail and ready `met-nonlinear-ml:tf26` image status. - ClaudeQQ availability: `/api/microservices/claudeqq/health` must only pass when `ready=true`, NapCat HTTP and WebSocket are connected, and `napcat.loginState=logged_in`; `/api/microservices/claudeqq/proxy/api/napcat/login` must show the same logged-in account state and `/api/microservices/claudeqq/proxy/api/events/recent` must prove the backend can read the persistent event cache. A QR-code-only or not-logged-in NapCat state must be treated as unhealthy. - Database: the command writes an `unidesk_e2e_markers` row through `docker exec unidesk-database psql`, confirms provider state is stored in PostgreSQL, and checks Todo Note rows exist in `todo_note_instances` using the same named volume. - Pipeline OA event flow: `microservice:pipeline-oa-event-flow` must prove both no-audit and monitor-audit runs are driven by OA events end to end. The event stream must show `node-finished` as a neutral fact with `pipeline:{pipelineId}` and `epoch:{runId}` tags, OA policy as the source of downstream/audit decisions, monitor decisions as OA control events, and runner control-result evidence. E2E must fail if delivery still depends on a legacy detail audit policy flag as policy authority, independent legacy audit-request points, a legacy batch completion gate, direct monitor-to-runner calls, or frontend/CLI writes to Pipeline `.state`. diff --git a/scripts/code-queue-prompt-observation-test.ts b/scripts/code-queue-prompt-observation-test.ts new file mode 100644 index 00000000..d850f5ef --- /dev/null +++ b/scripts/code-queue-prompt-observation-test.ts @@ -0,0 +1,108 @@ +import { codeQueuePromptResponseFields, type JsonRecord } from "../src/components/microservices/code-queue-mgr/src/prompt-observation"; + +function assertCondition(condition: unknown, message: string, detail: JsonRecord = {}): void { + if (!condition) { + throw new Error(`${message}: ${JSON.stringify(detail)}`); + } +} + +function previewText(value: unknown): string { + if (typeof value === "string") return value; + if (value && typeof value === "object" && !Array.isArray(value) && typeof (value as { text?: unknown }).text === "string") { + return (value as { text: string }).text; + } + return ""; +} + +function taskDisplayPrompt(task: JsonRecord, preview = false): string { + if (preview) { + const previewValue = previewText(task.displayPromptPreview || task.basePromptPreview || task.promptPreview); + if (previewValue.length > 0) return previewValue; + } + const displayPrompt = String(task.displayPrompt || ""); + if (displayPrompt.length > 0) return displayPrompt; + return String(task.prompt || ""); +} + +function taskBasePromptText(task: JsonRecord): string { + const basePrompt = String(task.basePrompt || ""); + return basePrompt.length > 0 ? basePrompt : taskDisplayPrompt(task); +} + +function buildLongPrompt(marker: string): string { + const filler = Array.from( + { length: 76 }, + (_, index) => `long-prompt-line-${String(index + 1).padStart(2, "0")} ${"abcdefghijklmnopqrstuvwxyz0123456789".repeat(2)}`, + ).join("\n"); + return [ + "Code Queue long prompt observation contract", + "", + filler, + "", + "验收标准:", + `所有原始 prompt 观察层必须保留这个唯一 tail marker: ${marker}`, + ].join("\n"); +} + +export function runCodeQueuePromptObservationContract(): JsonRecord { + const marker = `CODE_QUEUE_PROMPT_OBSERVATION_TAIL_${Date.now()}`; + const prompt = buildLongPrompt(marker); + assertCondition(prompt.length > 2500, "long prompt fixture must exceed 2500 chars", { promptChars: prompt.length }); + + const task = { prompt, basePrompt: prompt }; + const createTask = codeQueuePromptResponseFields(task, { lite: false, userPromptForDisplay: (value) => value }); + const overviewTask = codeQueuePromptResponseFields(task, { lite: true, userPromptForDisplay: (value) => value }); + const metaTask: JsonRecord = { + ...codeQueuePromptResponseFields(task, { lite: false, userPromptForDisplay: (value) => value }), + initialPrompt: prompt, + }; + const frontendCardPrompt = taskDisplayPrompt(overviewTask, true); + const frontendFullPrompt = taskBasePromptText(metaTask); + const rawFields = [ + createTask.prompt, + createTask.basePrompt, + createTask.displayPrompt, + overviewTask.prompt, + overviewTask.basePrompt, + overviewTask.displayPrompt, + metaTask.prompt, + metaTask.basePrompt, + metaTask.displayPrompt, + metaTask.initialPrompt, + frontendFullPrompt, + ].map((value) => String(value || "")); + const previews = [ + createTask.promptPreview, + createTask.basePromptPreview, + createTask.displayPromptPreview, + overviewTask.promptPreview, + overviewTask.basePromptPreview, + overviewTask.displayPromptPreview, + ] as JsonRecord[]; + + assertCondition(rawFields.every((value) => value.includes(marker)), "raw prompt fields must include tail marker", { + marker, + lengths: rawFields.map((value) => value.length), + }); + assertCondition(rawFields.every((value) => !value.includes("...")), "raw prompt fields must not contain truncation marker"); + assertCondition(previews.every((value) => typeof value?.text === "string" && typeof value?.chars === "number" && typeof value?.truncated === "boolean"), "preview fields must be explicit metadata objects"); + assertCondition(previews.some((value) => value.truncated === true && String(value.text || "").includes("...")), "preview fields may truncate explicitly"); + assertCondition(!frontendCardPrompt.includes(marker) && frontendCardPrompt.includes("..."), "frontend card summary should use explicit preview text", { frontendCardChars: frontendCardPrompt.length }); + assertCondition(frontendFullPrompt.includes(marker), "frontend full prompt expansion source must include tail marker", { frontendFullChars: frontendFullPrompt.length }); + + return { + ok: true, + marker, + promptChars: prompt.length, + createPromptChars: String(createTask.prompt || "").length, + overviewPromptChars: String(overviewTask.prompt || "").length, + metaDisplayPromptChars: String(metaTask.displayPrompt || "").length, + frontendCardChars: frontendCardPrompt.length, + frontendFullChars: frontendFullPrompt.length, + previewTruncated: previews.some((value) => value.truncated === true), + }; +} + +if (import.meta.main) { + process.stdout.write(`${JSON.stringify(runCodeQueuePromptObservationContract(), null, 2)}\n`); +} diff --git a/scripts/src/check.ts b/scripts/src/check.ts index 301064b3..a60cdb87 100644 --- a/scripts/src/check.ts +++ b/scripts/src/check.ts @@ -234,8 +234,10 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default fileItem("src/components/microservices/mdtodo/src/index.ts"), fileItem("src/components/microservices/decision-center/src/index.ts"), fileItem("src/components/microservices/code-queue-mgr/src/index.ts"), + fileItem("src/components/microservices/code-queue-mgr/src/prompt-observation.ts"), fileItem("scripts/src/deploy.ts"), fileItem("scripts/src/e2e.ts"), + fileItem("scripts/code-queue-prompt-observation-test.ts"), fileItem("scripts/src/artifact-registry.ts"), ); } else { @@ -243,8 +245,10 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default } if (options.scriptsTypecheck) { items.push(commandItem("typescript:scripts", ["bunx", "tsc", "-p", "scripts/tsconfig.json", "--noEmit", "--pretty", "false"], 120_000)); + items.push(commandItem("code-queue:prompt-observation-contract", ["bun", "scripts/code-queue-prompt-observation-test.ts"], 30_000)); } else { items.push(skippedItem("typescript:scripts", "scripts TypeScript typecheck is opt-in", "--scripts-typecheck or --full")); + items.push(skippedItem("code-queue:prompt-observation-contract", "prompt observation contract is opt-in with script checks", "--scripts-typecheck or --full")); } if (options.logs) { items.push(unifiedLogRotationItem()); diff --git a/scripts/src/code-queue.ts b/scripts/src/code-queue.ts index 5086be2d..0b49510e 100644 --- a/scripts/src/code-queue.ts +++ b/scripts/src/code-queue.ts @@ -228,6 +228,8 @@ function compactSummary(summary: unknown, options: CodexTaskOptions, taskId: str const transcriptCount = asNumber(record.transcriptCount, 0); const transcriptMaxSeq = transcriptCount > 0 ? record.transcriptMaxSeq ?? null : null; const initialPrompt = asString(record.initialPrompt ?? record.prompt); + const initialPromptView = textView(initialPrompt, options.full, 3000); + const basePromptView = textView(asString(record.basePrompt), options.full, 2000); return { id: record.id ?? taskId, queueId: record.queueId ?? null, @@ -256,8 +258,9 @@ function compactSummary(summary: unknown, options: CodexTaskOptions, taskId: str startedAt: record.startedAt ?? null, updatedAt: record.updatedAt ?? null, finishedAt: record.finishedAt ?? null, - initialPrompt: textView(initialPrompt, options.full, 3000), - basePrompt: textView(asString(record.basePrompt), options.full, 2000), + ...(options.full + ? { initialPrompt: initialPromptView, basePrompt: basePromptView } + : { initialPromptPreview: initialPromptView, basePromptPreview: basePromptView }), referenceTaskIds: record.referenceTaskIds ?? [], referenceInjection: record.referenceInjection ?? null, lastAssistantMessage: compactLastAssistant(record.lastAssistantMessage, options.full), @@ -825,7 +828,7 @@ function compactTaskMutationResponse(task: unknown, options: CompactTaskMutation startedAt: record.startedAt ?? null, updatedAt: record.updatedAt ?? null, finishedAt: record.finishedAt ?? null, - prompt: textView(prompt, options.fullPrompt === true, 1200), + ...(options.fullPrompt === true ? { prompt: textView(prompt, true, 1200) } : { promptPreview: textView(prompt, false, 1200) }), commands: taskId.length === 0 ? null : { show: `bun scripts/cli.ts codex task ${taskId}`, trace: `bun scripts/cli.ts codex task ${taskId} --trace --tail --limit ${defaultTraceLimit}`, diff --git a/scripts/src/e2e.ts b/scripts/src/e2e.ts index 0b6b3f22..7c97b604 100644 --- a/scripts/src/e2e.ts +++ b/scripts/src/e2e.ts @@ -139,6 +139,7 @@ const FRONTEND_CHECK_NAMES = [ "frontend:code-queue-integrated-visible", "frontend:code-queue-enqueue-await-smoke", "frontend:code-queue-summary-mobile-wrap", + "frontend:code-queue-long-prompt-observation", "frontend:code-queue-initial-prompt-full-expand", "frontend:code-queue-trace-full-load", "frontend:code-queue-judge-wrap", @@ -486,8 +487,10 @@ function wantsPrefix(options: E2ERunOptions, prefix: string): boolean { } function publicUrls(config: UniDeskConfig): PublicUrls { + const frontendTarget = String(process.env.UNIDESK_E2E_FRONTEND || "prod").trim().toLowerCase(); + const frontendPort = frontendTarget === "dev" ? config.network.devFrontend.port : config.network.frontend.port; return { - frontendUrl: `http://${config.network.publicHost}:${config.network.frontend.port}`, + frontendUrl: `http://${config.network.publicHost}:${frontendPort}`, providerIngressHealthUrl: `http://${config.network.publicHost}:${config.network.providerIngress.port}/health`, providerIngressWsUrl: `ws://${config.network.publicHost}:${config.network.providerIngress.port}/ws/provider`, blockedCoreUrl: `http://${config.network.publicHost}:${config.network.core.port}`, @@ -750,6 +753,148 @@ async function runCodeQueueEnqueueAwaitSmoke(page: Page): Promise { } } +async function runCodeQueueLongPromptObservation(page: Page): Promise { + const marker = `E2E_LONG_PROMPT_TAIL_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`; + const submitQueueId = "e2e-long-prompt"; + const filler = Array.from({ length: 76 }, (_, index) => `long-prompt-line-${String(index + 1).padStart(2, "0")} ${"abcdefghijklmnopqrstuvwxyz0123456789".repeat(2)}`).join("\n"); + const prompt = [ + "Code Queue long prompt observation regression", + "", + filler, + "", + "验收标准:", + `必须在所有原始 prompt 观察层保留这个唯一 tail marker: ${marker}`, + ].join("\n"); + const promptChars = prompt.length; + if (promptChars <= 2500) throw new Error(`long prompt fixture too short: ${promptChars}`); + const apiFetch = async (path: string, init: RequestInit = {}): Promise => { + const response = await fetch(path, { + credentials: "same-origin", + headers: { "content-type": "application/json", ...(init.headers || {}) }, + ...init, + }); + const text = await response.text(); + let body: any = null; + try { body = text.length > 0 ? JSON.parse(text) : null; } catch { body = { text }; } + return { ok: response.ok, status: response.status, body }; + }; + + await page.getByTestId("code-queue-filter-select").selectOption("__all__").catch(() => undefined); + await page.getByTestId("code-queue-id-select").selectOption(submitQueueId).catch(async () => { + page.once("dialog", (dialog) => { void dialog.accept(submitQueueId); }); + await page.getByTestId("codex-create-queue-button").click(); + await page.waitForFunction((queueId) => { + const select = document.querySelector('[data-testid="code-queue-id-select"]') as HTMLSelectElement | null; + return Array.from(select?.options || []).some((option) => option.value === queueId); + }, submitQueueId, { timeout: 10000 }); + await page.getByTestId("code-queue-id-select").selectOption(submitQueueId); + }); + await page.getByTestId("codex-max-attempts-input").fill("1"); + await page.getByTestId("codex-repeat-count-input").fill("1"); + await page.locator('[data-testid="code-queue-task-form"] textarea').fill(prompt); + await page.waitForFunction(() => { + const button = document.querySelector('[data-testid="codex-enqueue-button"]') as HTMLButtonElement | null; + return button !== null && !button.disabled; + }, undefined, { timeout: 5000 }); + const responsePromise = page.waitForResponse((response) => isCodeQueueTaskEnqueueRequest(response.url(), response.request().method()), { timeout: 30000 }); + await page.getByTestId("codex-enqueue-button").click(); + const response = await responsePromise; + const createBody = await response.json().catch((error: unknown) => ({ parseError: error instanceof Error ? error.message : String(error) })); + const createdTask = Array.isArray(createBody?.tasks) ? createBody.tasks[0] : null; + const taskId = String(createdTask?.id || ""); + if (!taskId) return { checked: true, marker, promptChars, createStatus: response.status(), taskId: "", error: "missing task id", createBody }; + await page.waitForSelector(`[data-testid="codex-task-${taskId}"]`, { timeout: 30000 }).catch(() => undefined); + + const overview = await page.evaluate(async (id) => { + const response = await fetch(`/api/microservices/code-queue/proxy/api/tasks/overview?limit=120&transcriptLimit=0&compact=1&selected=1&includeActive=1&stats=0&afterSeq=0&preferId=${encodeURIComponent(String(id))}`, { credentials: "same-origin" }); + const body = await response.json().catch(() => null); + return { ok: response.ok, status: response.status, body }; + }, taskId); + const meta = await page.evaluate(async (id) => { + const response = await fetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(String(id))}?meta=1`, { credentials: "same-origin" }); + const body = await response.json().catch(() => null); + return { ok: response.ok, status: response.status, body }; + }, taskId); + const initialPrompt = await page.evaluate(async (id) => { + const response = await fetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(String(id))}/prompt?part=initial`, { credentials: "same-origin" }); + const body = await response.json().catch(() => null); + return { ok: response.ok, status: response.status, body }; + }, taskId); + + await page.getByTestId(`codex-task-${taskId}`).click().catch(() => undefined); + await page.waitForSelector('[data-testid="codex-initial-prompt-base"]', { timeout: 15000 }).catch(() => undefined); + const ui = await page.evaluate(() => { + const base = document.querySelector('[data-testid="codex-initial-prompt-base"]') as HTMLElement | null; + return { + baseText: base?.innerText || "", + baseChars: (base?.innerText || "").length, + }; + }); + + const overviewTasks = Array.isArray(overview.body?.tasks) ? overview.body.tasks : []; + const overviewTask = overviewTasks.find((task: any) => String(task?.id || "") === taskId) || null; + const selectedTask = overview.body?.selected?.task || null; + const metaTask = meta.body?.task || null; + const createPrompt = String(createdTask?.prompt || ""); + const createDisplayPrompt = String(createdTask?.displayPrompt || ""); + const createPreview = createdTask?.displayPromptPreview || createdTask?.promptPreview || null; + const overviewPrompt = String(overviewTask?.prompt || ""); + const overviewDisplayPrompt = String(overviewTask?.displayPrompt || ""); + const overviewPreview = overviewTask?.displayPromptPreview || overviewTask?.promptPreview || null; + const selectedDisplayPrompt = String(selectedTask?.displayPrompt || ""); + const selectedPreview = selectedTask?.displayPromptPreview || selectedTask?.promptPreview || null; + const metaPrompt = String(metaTask?.prompt || ""); + const metaBasePrompt = String(metaTask?.basePrompt || ""); + const metaDisplayPrompt = String(metaTask?.displayPrompt || ""); + const promptText = String(initialPrompt.body?.text || ""); + const previewHasTruncationMarker = [createPreview?.text, overviewPreview?.text, selectedPreview?.text].some((value) => String(value || "").includes("...")); + const originalValues = [createPrompt, createDisplayPrompt, overviewPrompt, overviewDisplayPrompt, selectedDisplayPrompt, metaPrompt, metaBasePrompt, metaDisplayPrompt, promptText]; + await apiFetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/interrupt`, { method: "POST", body: "{}" }).catch(() => null); + return { + checked: true, + marker, + taskId, + promptChars, + createStatus: response.status(), + createOk: createBody?.ok === true, + createPromptChars: createPrompt.length, + createDisplayPromptChars: createDisplayPrompt.length, + overviewStatus: overview.status, + overviewTaskFound: overviewTask !== null, + overviewPromptChars: overviewPrompt.length, + overviewDisplayPromptChars: overviewDisplayPrompt.length, + selectedTaskFound: selectedTask !== null && String(selectedTask?.id || "") === taskId, + selectedDisplayPromptChars: selectedDisplayPrompt.length, + metaStatus: meta.status, + metaPromptChars: metaPrompt.length, + metaBasePromptChars: metaBasePrompt.length, + metaDisplayPromptChars: metaDisplayPrompt.length, + initialPromptStatus: initialPrompt.status, + initialPromptChars: Number(initialPrompt.body?.chars || promptText.length), + uiBaseChars: ui.baseChars, + preview: { + create: createPreview, + overview: overviewPreview, + selected: selectedPreview, + previewHasTruncationMarker, + createPromptPreviewFieldPresent: createdTask?.promptPreview !== undefined, + overviewPromptPreviewFieldPresent: overviewTask?.promptPreview !== undefined, + selectedPromptPreviewFieldPresent: selectedTask?.promptPreview !== undefined, + }, + checks: { + createOriginalHasTail: createPrompt.includes(marker) && createDisplayPrompt.includes(marker), + overviewOriginalHasTail: overviewPrompt.includes(marker) && overviewDisplayPrompt.includes(marker), + selectedOriginalHasTail: selectedDisplayPrompt.includes(marker), + metaOriginalHasTail: metaPrompt.includes(marker) && metaBasePrompt.includes(marker) && metaDisplayPrompt.includes(marker), + initialPromptHasTail: promptText.includes(marker), + uiFullPromptHasTail: String(ui.baseText || "").includes(marker), + originalsHaveNoTruncationMarker: originalValues.every((value) => !value.includes("...")), + previewFieldsExplicit: createdTask?.promptPreview !== undefined && overviewTask?.promptPreview !== undefined && selectedTask?.promptPreview !== undefined, + previewCanTruncate: Boolean(createPreview?.truncated || overviewPreview?.truncated || selectedPreview?.truncated), + }, + }; +} + function runPsql(config: UniDeskConfig, sql: string): { ok: boolean; stdout: string; stderr: string; exitCode: number | null } { const result = runCommand([ "docker", @@ -1486,8 +1631,10 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 page.on("dialog", (dialog) => dialog.accept()); await page.goto(urls.frontendUrl, { waitUntil: "domcontentloaded", timeout: 15000 }); await page.waitForSelector('[data-testid="login-screen"]', { timeout: 10000 }); - await page.fill('input[name="username"]', config.auth.username); - await page.fill('input[name="password"]', config.auth.password); + const frontendAuthUsername = process.env.UNIDESK_E2E_AUTH_USERNAME || config.auth.username; + const frontendAuthPassword = process.env.UNIDESK_E2E_AUTH_PASSWORD || config.auth.password; + await page.fill('input[name="username"]', frontendAuthUsername); + await page.fill('input[name="password"]', frontendAuthPassword); await page.click('button[type="submit"]'); await page.waitForSelector('[data-testid="app-shell"]', { timeout: 10000 }); await page.waitForFunction(() => document.querySelector('[data-testid="conn-text"]')?.textContent?.includes("核心在线"), undefined, { timeout: 15000 }); @@ -1547,6 +1694,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 let codeQueueSidebarUpdateMetrics: any = { cardCount: 0, labels: [], hasRecentUpdateLabel: false }; let codeQueueHtmlGuard: any = { rootAttrMissing: false, sourceAttrMissing: false, sourceNoBasePrompt: false }; let codeQueueSummaryMobileMetrics: any = { checked: false, summaryCount: 0, ok: false }; + let codeQueueLongPromptMetrics: any = { checked: false }; let codeQueuePromptDefaultEmpty = false; let codeQueueSubmitGuard: any = { batchRowVisible: false, disabledBeforeConfirm: false, enabledAfterConfirm: false, waitElementMissingBeforeSubmit: false }; let codeQueueEnqueueAwaitSmoke: any = { checked: false }; @@ -1995,6 +2143,9 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 if (wants("frontend:code-queue-enqueue-await-smoke")) { codeQueueEnqueueAwaitSmoke = await runCodeQueueEnqueueAwaitSmoke(page); } + if (wants("frontend:code-queue-long-prompt-observation")) { + codeQueueLongPromptMetrics = await runCodeQueueLongPromptObservation(page); + } codeQueueOptions = await page.locator('[data-testid="code-queue-filter-select"] option').evaluateAll((options) => options.map((option) => (option as HTMLOptionElement).textContent || "")); codeQueueSwitchMetrics = await page.locator('[data-testid="code-queue-filter-select"] option').evaluateAll((options) => ({ optionCount: options.length, @@ -3049,6 +3200,21 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 && (codeQueueEnqueueAwaitSmoke.interrupt?.ok === true || codeQueueEnqueueAwaitSmoke.interrupt?.status === 409), { codeQueueEnqueueAwaitSmoke }); addSelectedCheck(checks, options, "frontend:code-queue-summary-mobile-wrap", codeQueueSummaryMobileMetrics.checked === true && (codeQueueSummaryMobileMetrics.summaryCount === 0 || codeQueueSummaryMobileMetrics.ok === true), { codeQueueSummaryMobileMetrics }); + addSelectedCheck(checks, options, "frontend:code-queue-long-prompt-observation", + codeQueueLongPromptMetrics.checked === true + && codeQueueLongPromptMetrics.createOk === true + && /^codex_\d+_[A-Za-z0-9_-]+$/u.test(String(codeQueueLongPromptMetrics.taskId || "")) + && Number(codeQueueLongPromptMetrics.promptChars || 0) > 2500 + && codeQueueLongPromptMetrics.checks?.createOriginalHasTail === true + && codeQueueLongPromptMetrics.checks?.overviewOriginalHasTail === true + && codeQueueLongPromptMetrics.checks?.selectedOriginalHasTail === true + && codeQueueLongPromptMetrics.checks?.metaOriginalHasTail === true + && codeQueueLongPromptMetrics.checks?.initialPromptHasTail === true + && codeQueueLongPromptMetrics.checks?.uiFullPromptHasTail === true + && codeQueueLongPromptMetrics.checks?.originalsHaveNoTruncationMarker === true + && codeQueueLongPromptMetrics.checks?.previewFieldsExplicit === true + && codeQueueLongPromptMetrics.checks?.previewCanTruncate === true, + { codeQueueLongPromptMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-error-red-markers", codeQueueErrorHighlightMetrics.checked === true && codeQueueErrorHighlightMetrics.candidateFound === true && codeQueueErrorHighlightMetrics.ok === true, { codeQueueErrorHighlightMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-initial-prompt-full-expand", codexInitialPromptFullMetrics.candidateFound === false diff --git a/src/components/frontend/src/code-queue.tsx b/src/components/frontend/src/code-queue.tsx index 6476653b..8b734830 100644 --- a/src/components/frontend/src/code-queue.tsx +++ b/src/components/frontend/src/code-queue.tsx @@ -535,7 +535,17 @@ function promptLineCount(text: string): number { return text.length > 0 ? text.split(/\r\n|\r|\n/u).length : 0; } -function taskDisplayPrompt(task: any): string { +function previewTextField(value: any): string { + if (typeof value === "string") return value; + if (value && typeof value === "object" && !Array.isArray(value) && typeof value.text === "string") return value.text; + return ""; +} + +function taskDisplayPrompt(task: any, options: AnyRecord = {}): string { + if (options.preview === true) { + const preview = previewTextField(task?.displayPromptPreview || task?.basePromptPreview || task?.promptPreview); + if (preview.length > 0) return preview; + } const explicit = String(task?.displayPrompt || ""); if (explicit.length > 0) return explicit; const prompt = String(task?.prompt || ""); @@ -640,7 +650,9 @@ function traceSummaryIsCurrent(task: any): boolean { function taskBasePromptText(task: any): string { const summaryPrompt = taskPromptSummary(task); const basePrompt = String(summaryPrompt.basePrompt || ""); - return basePrompt.length > 0 ? basePrompt : taskDisplayPrompt(task); + if (basePrompt.length > 0) return basePrompt; + const direct = String(task?.basePrompt || ""); + return direct.length > 0 ? direct : taskDisplayPrompt(task); } function taskFinalResponseText(task: any): string { @@ -1473,7 +1485,7 @@ function TaskCard({ task, selected, onSelect, onCopy, onReference, onMarkRead, c }, markingRead ? "标记中" : "标为已读") : null, ), ), - h("strong", null, shortText(taskDisplayPrompt(task), 120) || "空任务"), + h("strong", null, shortText(taskDisplayPrompt(task, { preview: true }), 120) || "空任务"), h("div", { className: "codex-task-meta" }, h("span", null, `queue=${taskQueueLabel(task)}`), h("span", null, `provider=${task?.providerId || "D601"}`), @@ -2232,6 +2244,9 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi task?.displayPrompt, task?.basePrompt, task?.prompt, + previewTextField(task?.displayPromptPreview), + previewTextField(task?.basePromptPreview), + previewTextField(task?.promptPreview), task?.finalResponse, task?.lastError?.message, ].map((value) => String(value || "").toLowerCase()).join("\n"); diff --git a/src/components/microservices/code-queue-mgr/src-rs/main.rs b/src/components/microservices/code-queue-mgr/src-rs/main.rs index e4ed5375..e4f4c029 100644 --- a/src/components/microservices/code-queue-mgr/src-rs/main.rs +++ b/src/components/microservices/code-queue-mgr/src-rs/main.rs @@ -497,14 +497,15 @@ fn preview(text: &str, max_chars: usize) -> String { result } -fn prefix_preview(text: &str, max_chars: usize) -> String { - if text.chars().count() <= max_chars { - return text.to_string(); - } - let take = max_chars.saturating_sub(1); - let mut result = text.chars().take(take).collect::(); - result.push('…'); - result +fn text_preview(value: &str, max_chars: usize) -> Value { + let chars = value.chars().count(); + let truncated = chars > max_chars; + json!({ + "text": if truncated { preview(value, max_chars) } else { value.to_string() }, + "chars": chars, + "truncated": truncated, + "omittedChars": if truncated { chars.saturating_sub(max_chars.saturating_sub(20)) } else { 0 } + }) } fn strip_after_marker(text: &str, marker: &str) -> Option { @@ -664,13 +665,17 @@ fn task_list_response(_state: &AppState, task: &TaskMeta, lite: bool) -> Value { }; let final_text = final_response(task); let agent_port = code_agent_port(&task.model); + let preview_limit = if lite { 360 } else { 2000 }; json!({ "id": task.id, "queueId": task.queue_id, "queueEnteredAt": task.task_json.as_ref().and_then(|value| value.get("queueEnteredAt")).and_then(Value::as_str).unwrap_or(&task.created_at), - "prompt": if lite { prefix_preview(&display_prompt, 360) } else { preview(&task.prompt, 2000) }, - "basePrompt": if lite { prefix_preview(&task.base_prompt, 360) } else { preview(&task.base_prompt, 2000) }, - "displayPrompt": if lite { prefix_preview(&display_prompt, 360) } else { preview(&display_prompt, 2000) }, + "prompt": task.prompt, + "basePrompt": task.base_prompt, + "displayPrompt": display_prompt, + "promptPreview": text_preview(&task.prompt, preview_limit), + "basePromptPreview": text_preview(&task.base_prompt, preview_limit), + "displayPromptPreview": text_preview(&display_prompt, preview_limit), "promptChars": task.prompt.chars().count(), "basePromptChars": task.base_prompt.chars().count(), "displayPromptChars": display_prompt.chars().count(), @@ -729,8 +734,15 @@ fn task_meta_response(state: &AppState, task: &TaskMeta) -> Value { let map = base.as_object_mut().expect("task response object"); let output_count = json_array_len(task.task_json.as_ref(), "output", task.output_count); let prompt_history_count = json_array_len(task.task_json.as_ref(), "promptHistory", 0); + let display_prompt = if task.base_prompt.is_empty() { + user_prompt_for_display(&task.prompt) + } else { + task.base_prompt.clone() + }; map.insert("prompt".to_string(), json!(task.prompt)); map.insert("basePrompt".to_string(), json!(task.base_prompt)); + map.insert("displayPrompt".to_string(), json!(display_prompt)); + map.insert("initialPrompt".to_string(), json!(task.prompt)); map.insert("finalResponse".to_string(), json!(final_response(task))); map.insert( "promptHistory".to_string(), @@ -2200,3 +2212,136 @@ fn run_healthcheck(port: u16) -> bool { let mut body = String::new(); stream.read_to_string(&mut body).is_ok() && body.contains("\"ok\": true") } + +#[cfg(test)] +mod tests { + use super::*; + + fn test_state() -> AppState { + AppState { + config: Config { + host: "127.0.0.1".to_string(), + port: 4278, + database_url: "postgres://unused".to_string(), + log_file: "/tmp/unidesk-code-queue-mgr-test.jsonl".to_string(), + deploy_service_id: "code-queue-mgr".to_string(), + deploy_repo: "".to_string(), + deploy_commit: "".to_string(), + deploy_requested_commit: "".to_string(), + mgr_pool_max: 1, + trace_pool_max: 1, + default_provider_id: "D601".to_string(), + default_workdir: "/workspace".to_string(), + remote_default_workdir: "/home/ubuntu".to_string(), + default_model: "gpt-5.5".to_string(), + code_models: vec!["gpt-5.5".to_string()], + default_max_attempts: 1, + default_reasoning_effort: None, + model_reasoning_efforts: json!({}), + }, + started_at: now_iso(), + logs: Arc::new(Mutex::new(VecDeque::new())), + schema_ready: Arc::new(Mutex::new(false)), + schema_last_error: Arc::new(Mutex::new(None)), + last_database_error: Arc::new(Mutex::new(None)), + } + } + + fn long_prompt(marker: &str) -> String { + let filler = (0..76) + .map(|index| format!("long-prompt-line-{index:02} {}", "abcdefghijklmnopqrstuvwxyz0123456789".repeat(2))) + .collect::>() + .join("\n"); + [ + "Code Queue long prompt observation contract".to_string(), + "".to_string(), + filler, + "".to_string(), + "验收标准:".to_string(), + format!("所有原始 prompt 观察层必须保留这个唯一 tail marker: {marker}"), + ] + .join("\n") + } + + fn test_task(prompt: String) -> TaskMeta { + let at = now_iso(); + TaskMeta { + id: "codex_1779000000000_longprompt".to_string(), + queue_id: "e2e-long-prompt".to_string(), + status: "queued".to_string(), + provider_id: "D601".to_string(), + execution_mode: "default".to_string(), + model: "gpt-5.5".to_string(), + cwd: "/home/ubuntu".to_string(), + prompt: prompt.clone(), + base_prompt: prompt, + reference_task_ids: json!([]), + reference_injection: None, + reasoning_effort: None, + max_attempts: 1, + current_attempt: 0, + current_mode: None, + codex_thread_id: None, + active_turn_id: None, + created_at: at.clone(), + updated_at: at, + started_at: None, + finished_at: None, + read_at: None, + last_error: None, + last_judge: None, + output_count: 1, + event_count: 0, + attempt_count: 0, + last_output_seq: 1, + task_json: Some(json!({ + "queueEnteredAt": now_iso(), + "finalResponse": "", + "stepCount": 0, + "llmStepCount": 0, + "judgeFailCount": 0, + "cancelRequested": false, + "attempts": [], + "output": [], + "events": [], + "promptHistory": [] + })), + } + } + + fn field_text<'a>(value: &'a Value, key: &str) -> &'a str { + value.get(key).and_then(Value::as_str).unwrap_or("") + } + + #[test] + fn prompt_observation_preserves_raw_fields_and_explicit_previews() { + let state = test_state(); + let marker = "RUST_PROMPT_OBSERVATION_TAIL_MARKER"; + let prompt = long_prompt(marker); + assert!(prompt.chars().count() > 2500); + let task = test_task(prompt); + let create = task_list_response(&state, &task, false); + let overview = task_list_response(&state, &task, true); + let meta = task_meta_response(&state, &task); + let raw_fields = [ + field_text(&create, "prompt"), + field_text(&create, "basePrompt"), + field_text(&create, "displayPrompt"), + field_text(&overview, "prompt"), + field_text(&overview, "basePrompt"), + field_text(&overview, "displayPrompt"), + field_text(&meta, "prompt"), + field_text(&meta, "basePrompt"), + field_text(&meta, "displayPrompt"), + field_text(&meta, "initialPrompt"), + ]; + assert!(raw_fields.iter().all(|value| value.contains(marker)), "{raw_fields:?}"); + assert!(raw_fields.iter().all(|value| !value.contains("...")), "{raw_fields:?}"); + + let preview = overview.get("promptPreview").and_then(Value::as_object).expect("promptPreview metadata"); + assert_eq!(preview.get("truncated").and_then(Value::as_bool), Some(true)); + assert_eq!(preview.get("chars").and_then(Value::as_u64), Some(field_text(&overview, "prompt").chars().count() as u64)); + assert!(preview.get("text").and_then(Value::as_str).unwrap_or("").contains("...")); + assert!(!preview.get("text").and_then(Value::as_str).unwrap_or("").contains(marker)); + } +} diff --git a/src/components/microservices/code-queue-mgr/src/index.ts b/src/components/microservices/code-queue-mgr/src/index.ts index daf56e86..984c95bd 100644 --- a/src/components/microservices/code-queue-mgr/src/index.ts +++ b/src/components/microservices/code-queue-mgr/src/index.ts @@ -1,5 +1,6 @@ import postgres from "postgres"; import { createHourlyJsonlWriter, logRetentionBytesForService } from "../../../shared/src/rotating-jsonl"; +import { codeQueuePromptResponseFields, safePreview } from "./prompt-observation"; type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }; type JsonRecord = Record; @@ -434,12 +435,6 @@ function timestampMs(value: string | Date | null | undefined): number | null { return Number.isFinite(ms) ? ms : null; } -function safePreview(value: unknown, maxChars: number): string { - const text = String(value ?? ""); - if (text.length <= maxChars) return text; - return `${text.slice(0, Math.max(0, maxChars - 20))}\n...`; -} - function prefixPreview(value: unknown, maxChars: number): string { const text = String(value ?? ""); return text.length <= maxChars ? text : `${text.slice(0, Math.max(0, maxChars - 1))}…`; @@ -1635,17 +1630,12 @@ function taskStatisticsSummary(tasks: QueueTask[], days = 14): JsonRecord { } function taskListResponse(task: QueueTask, lite = true): JsonRecord { - const displayPrompt = task.basePrompt || userPromptForDisplay(task.prompt); + const promptFields = codeQueuePromptResponseFields(task, { lite, userPromptForDisplay }); return { id: task.id, queueId: queueIdOf(task), queueEnteredAt: task.queueEnteredAt, - prompt: lite ? prefixPreview(displayPrompt, 360) : safePreview(displayPrompt, 2000), - basePrompt: lite ? prefixPreview(task.basePrompt, 360) : safePreview(task.basePrompt, 2000), - displayPrompt: lite ? prefixPreview(displayPrompt, 360) : safePreview(displayPrompt, 2000), - promptChars: task.prompt.length, - basePromptChars: task.basePrompt.length, - displayPromptChars: displayPrompt.length, + ...promptFields, promptEditable: queuedTaskPromptEditable(task), finalResponseChars: task.finalResponse.length, stepCount: numberField(task.stepCount ?? task.llmStepCount, 0), @@ -1695,6 +1685,8 @@ function taskMetaResponse(task: QueueTask): JsonRecord { ...taskListResponse(task, false), prompt: task.prompt, basePrompt: task.basePrompt, + displayPrompt: task.basePrompt || userPromptForDisplay(task.prompt), + initialPrompt: task.prompt, finalResponse: task.finalResponse, promptHistory: toJsonValue(task.promptHistory), attempts: toJsonValue(task.attempts), diff --git a/src/components/microservices/code-queue-mgr/src/prompt-observation.ts b/src/components/microservices/code-queue-mgr/src/prompt-observation.ts new file mode 100644 index 00000000..ecacaf4e --- /dev/null +++ b/src/components/microservices/code-queue-mgr/src/prompt-observation.ts @@ -0,0 +1,45 @@ +export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }; +export type JsonRecord = Record; + +export interface PromptObservationTask { + prompt: string; + basePrompt: string; +} + +export interface PromptObservationOptions { + lite?: boolean; + userPromptForDisplay: (prompt: string) => string; +} + +export function safePreview(value: unknown, maxChars: number): string { + const text = String(value ?? ""); + if (text.length <= maxChars) return text; + return `${text.slice(0, Math.max(0, maxChars - 20))}\n...`; +} + +export function textPreview(value: unknown, maxChars: number): JsonRecord { + const text = String(value ?? ""); + const truncated = text.length > maxChars; + return { + text: truncated ? safePreview(text, maxChars) : text, + chars: text.length, + truncated, + omittedChars: truncated ? text.length - Math.max(0, maxChars - 20) : 0, + }; +} + +export function codeQueuePromptResponseFields(task: PromptObservationTask, options: PromptObservationOptions): JsonRecord { + const displayPrompt = task.basePrompt || options.userPromptForDisplay(task.prompt); + const previewLimit = options.lite === true ? 360 : 2000; + return { + prompt: task.prompt, + basePrompt: task.basePrompt, + displayPrompt, + promptPreview: textPreview(task.prompt, previewLimit), + basePromptPreview: textPreview(task.basePrompt, previewLimit), + displayPromptPreview: textPreview(displayPrompt, previewLimit), + promptChars: task.prompt.length, + basePromptChars: task.basePrompt.length, + displayPromptChars: displayPrompt.length, + }; +}