diff --git a/internal/cloud/code-agent-agentrun-adapter.ts b/internal/cloud/code-agent-agentrun-adapter.ts index 734ef3a5..d21600de 100644 --- a/internal/cloud/code-agent-agentrun-adapter.ts +++ b/internal/cloud/code-agent-agentrun-adapter.ts @@ -233,8 +233,7 @@ export function initialAgentRunChatResult({ params = {}, options = {}, traceId } sessionId: safeSessionId(params.sessionId) || agentRunSessionId(traceId), threadId: safeOpaqueId(params.threadId) || null, messageId: `msg_${safeTraceId(traceId)?.slice(4) || randomUUID()}`, - createdAt: timestamp, - updatedAt: timestamp, + ...agentRunAdmissionTimingFields(timestamp), provider: providerForBackendProfile(backendProfile), model: modelForBackendProfile(backendProfile, env), backend: backendForBackendProfile(backendProfile), @@ -258,6 +257,28 @@ export function initialAgentRunChatResult({ params = {}, options = {}, traceId } }; } +function agentRunAdmissionTimingFields(value) { + const admittedAt = timestampIsoOrNow(value); + const timing = { + startedAt: admittedAt, + lastEventAt: admittedAt, + finishedAt: null, + durationMs: null, + observedAt: admittedAt, + lastEventAgeMs: 0, + valuesRedacted: true + }; + return { + createdAt: admittedAt, + updatedAt: admittedAt, + timing, + startedAt: admittedAt, + lastEventAt: admittedAt, + finishedAt: null, + durationMs: null + }; +} + export async function submitAgentRunChatTurn({ params = {}, options = {}, traceId, traceStore = defaultCodeAgentTraceStore, results }) { const env = options.env ?? process.env; const managerUrl = resolveAgentRunManagerUrl(env); @@ -3222,6 +3243,11 @@ function nowIso(now) { return typeof now === "function" ? now() : new Date().toISOString(); } +function timestampIsoOrNow(value = null) { + const parsed = Date.parse(String(value ?? "")); + return Number.isFinite(parsed) ? new Date(parsed).toISOString() : new Date().toISOString(); +} + function redactUrl(value) { try { const url = new URL(value); diff --git a/internal/cloud/server-code-agent-http.ts b/internal/cloud/server-code-agent-http.ts index f47c6427..65f49809 100644 --- a/internal/cloud/server-code-agent-http.ts +++ b/internal/cloud/server-code-agent-http.ts @@ -113,8 +113,9 @@ export async function handleCodeAgentChatHttp(request, response, options) { if (codeAgentChatShortConnectionRequested(request, params, options)) { if (perf) perf.recordPhase({ phase: "short_connection_accept", durationMs: 0, outcome: "ok" }); + let submitted = null; try { - await submitCodeAgentChatTurn({ + submitted = await submitCodeAgentChatTurn({ params: nativeSessionChatParams, options, traceId @@ -129,6 +130,7 @@ export async function handleCodeAgentChatHttp(request, response, options) { const traceUrl = `/v1/agent/traces/${encodeURIComponent(traceId)}`; const turnUrl = `/v1/agent/turns/${encodeURIComponent(traceId)}`; const statusCode = 202; + const admittedTiming = codeAgentAdmissionTimingFields(submitted?.createdAt ?? submitted?.updatedAt); void emitCodeAgentOtelSpan("POST /v1/agent/chat", traceId, options.env ?? process.env, { kind: 2, attributes: { "http.method": "POST", "http.route": "/v1/agent/chat", "http.status_code": statusCode, sessionId: safeSessionId(nativeSessionChatParams.sessionId) || null, turnId: lifecycle.turnId } @@ -142,6 +144,13 @@ export async function handleCodeAgentChatHttp(request, response, options) { ...codeAgentOtelTraceFields(traceId, options.env ?? process.env), ...lifecycle, sessionId: safeSessionId(nativeSessionChatParams.sessionId) || null, + createdAt: submitted?.createdAt ?? admittedTiming.createdAt, + updatedAt: submitted?.updatedAt ?? admittedTiming.updatedAt, + timing: submitted?.timing ?? admittedTiming.timing, + startedAt: submitted?.startedAt ?? admittedTiming.startedAt, + lastEventAt: submitted?.lastEventAt ?? admittedTiming.lastEventAt, + finishedAt: submitted?.finishedAt ?? admittedTiming.finishedAt, + durationMs: submitted?.durationMs ?? admittedTiming.durationMs, traceUrl, resultUrl: `/v1/agent/chat/result/${encodeURIComponent(traceId)}`, turnUrl, @@ -779,6 +788,7 @@ function firstNonEmptyValue(...values) { async function submitCodeAgentChatTurn({ params, options, traceId }) { const traceStore = options.traceStore ?? defaultCodeAgentTraceStore; const results = options.codeAgentChatResults ?? createCodeAgentChatResultStore(); + const timestamp = new Date().toISOString(); const acceptedPayload = { accepted: true, status: "running", @@ -789,7 +799,7 @@ async function submitCodeAgentChatTurn({ params, options, traceId }) { sessionId: safeSessionId(params.sessionId) || null, threadId: safeOpaqueId(params.threadId) || null, projectId: params.projectId ?? null, - updatedAt: new Date().toISOString() + ...codeAgentAdmissionTimingFields(timestamp) }; await recordCodeAgentTurnAdmission({ payload: acceptedPayload, params, options, traceId }); results.set(traceId, annotateOwner(acceptedPayload, params)); @@ -858,7 +868,7 @@ async function submitCodeAgentChatTurn({ params, options, traceId }) { } }; setImmediate(() => { run(); }); - return; + return annotateOwner(acceptedPayload, params); } traceStore.ensure(traceId, { runnerKind: "codex-app-server-stdio-runner", @@ -945,6 +955,7 @@ async function submitCodeAgentChatTurn({ params, options, traceId }) { setImmediate(() => { run(); }); + return annotateOwner(acceptedPayload, params); } async function recordCodeAgentTurnAdmission({ payload = {}, params = {}, options = {}, traceId } = {}) { @@ -1070,8 +1081,7 @@ function codeAgentAdmittedFailureBasePayload({ params = {}, options = {}, traceI conversationId: safeConversationId(params.conversationId) || null, sessionId: safeSessionId(params.sessionId) || null, threadId: safeOpaqueId(params.threadId) || null, - createdAt: now, - updatedAt: now, + ...codeAgentAdmissionTimingFields(now), provider: codeAgentAgentRunAdapterEnabled(options.env ?? process.env) ? "agentrun" : "codex-stdio", model: options.env?.HWLAB_CODE_AGENT_MODEL ?? "unknown", backend: codeAgentAgentRunAdapterEnabled(options.env ?? process.env) ? "agentrun-v01" : "hwlab-cloud-api/code-agent-chat", @@ -1079,6 +1089,79 @@ function codeAgentAdmittedFailureBasePayload({ params = {}, options = {}, traceI }; } +function codeAgentAdmissionTimingFields(value = null) { + const admittedAt = timestampIsoOrNow(value); + const timing = { + startedAt: admittedAt, + lastEventAt: admittedAt, + finishedAt: null, + durationMs: null, + observedAt: admittedAt, + lastEventAgeMs: 0, + valuesRedacted: true + }; + return { + createdAt: admittedAt, + updatedAt: admittedAt, + timing, + startedAt: admittedAt, + lastEventAt: admittedAt, + finishedAt: null, + durationMs: null + }; +} + +function codeAgentResultTimingFields(...sources) { + const records = sources.filter((source) => source && typeof source === "object"); + const timingSource = records.map((source) => source.timing).find((timing) => timing && typeof timing === "object") ?? {}; + const startedAt = firstTimestampIso(timingSource.startedAt, ...records.map((source) => source.startedAt), ...records.map((source) => source.createdAt)); + const lastEventAt = firstLatestTimestampIso(timingSource.lastEventAt, ...records.map((source) => source.lastEventAt), ...records.map((source) => source.updatedAt), ...records.map((source) => source.createdAt)); + const finishedAt = firstLatestTimestampIso(timingSource.finishedAt, ...records.map((source) => source.finishedAt), ...records.map((source) => source.completedAt)); + const durationMs = numberOrNull(timingSource.durationMs ?? records.map((source) => source.durationMs).find((value) => numberOrNull(value) !== null)); + const observedAt = timestampIsoOrNull(timingSource.observedAt); + const lastEventAgeMs = numberOrNull(timingSource.lastEventAgeMs); + if (!startedAt && !lastEventAt && !finishedAt && durationMs === null && lastEventAgeMs === null) return {}; + const timing = { + ...(timingSource && typeof timingSource === "object" ? timingSource : {}), + startedAt: startedAt ?? null, + lastEventAt: lastEventAt ?? null, + finishedAt: finishedAt ?? null, + durationMs, + observedAt: observedAt ?? null, + lastEventAgeMs, + valuesRedacted: timingSource.valuesRedacted !== false + }; + return { + timing, + startedAt: timing.startedAt, + lastEventAt: timing.lastEventAt, + finishedAt: timing.finishedAt, + durationMs: timing.durationMs + }; +} + +function firstTimestampIso(...values) { + for (const value of values) { + const timestamp = timestampIsoOrNull(value); + if (timestamp) return timestamp; + } + return null; +} + +function firstLatestTimestampIso(...values) { + let latest = null; + let latestMs = Number.NEGATIVE_INFINITY; + for (const value of values) { + const timestamp = timestampIsoOrNull(value); + if (!timestamp) continue; + const ms = Date.parse(timestamp); + if (!Number.isFinite(ms) || ms < latestMs) continue; + latest = timestamp; + latestMs = ms; + } + return latest; +} + async function settleAdmittedCodeAgentFailure({ base = {}, params = {}, options = {}, traceId, results, failure = {}, traceLabel = "code-agent:failed" } = {}) { const traceStore = options.traceStore ?? defaultCodeAgentTraceStore; const error = failure.payload?.error ?? failure.error ?? {}; @@ -2225,6 +2308,7 @@ function codeAgentTurnStatusPayload({ traceId, result, snapshot, resultPollError const running = isTurnRunningStatus(status); const terminal = isTurnTerminalStatus(status); const runnerTrace = snapshotObject && snapshotObject.status !== "missing" ? snapshotObject : resultObject?.runnerTrace ?? null; + const timingFields = codeAgentResultTimingFields(resultObject, snapshotObject, runnerTrace); return { ok: found, action: "code-agent.turn.status", @@ -2237,7 +2321,8 @@ function codeAgentTurnStatusPayload({ traceId, result, snapshot, resultPollError conversationId: safeConversationId(resultObject?.conversationId ?? snapshotObject?.conversationId) || null, sessionId: safeSessionId(resultObject?.sessionId ?? resultObject?.session?.sessionId ?? snapshotObject?.sessionId) || null, threadId: safeOpaqueId(resultObject?.threadId ?? resultObject?.session?.threadId ?? snapshotObject?.threadId) || null, - updatedAt: textValue(resultObject?.updatedAt ?? resultObject?.agentRun?.updatedAt ?? snapshotObject?.updatedAt) || null, + updatedAt: textValue(resultObject?.updatedAt ?? resultObject?.agentRun?.updatedAt ?? snapshotObject?.updatedAt ?? timingFields.lastEventAt ?? timingFields.startedAt) || null, + ...timingFields, lastEventLabel: textValue(snapshotObject?.lastEventLabel ?? runnerTrace?.lastEventLabel ?? lastEvent?.label ?? lastEvent?.type) || null, waitingFor: textValue(snapshotObject?.waitingFor ?? runnerTrace?.waitingFor) || null, resultUrl: `/v1/agent/chat/result/${encodeURIComponent(traceId)}`, @@ -2970,6 +3055,16 @@ function textValue(value) { return String(value ?? "").trim(); } +function timestampIsoOrNow(value = null) { + const parsed = Date.parse(String(value ?? "")); + return Number.isFinite(parsed) ? new Date(parsed).toISOString() : new Date().toISOString(); +} + +function timestampIsoOrNull(value = null) { + const parsed = Date.parse(String(value ?? "")); + return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null; +} + function codeAgentSessionOwnerEvidence(payload = {}, params = {}) { const traceId = payload.traceId ?? params.traceId ?? null; const finalResponse = codeAgentFinalResponseEvidence(payload, traceId); diff --git a/web/hwlab-cloud-web/src/stores/workbench.ts b/web/hwlab-cloud-web/src/stores/workbench.ts index 0d6482ba..9ba3d642 100644 --- a/web/hwlab-cloud-web/src/stores/workbench.ts +++ b/web/hwlab-cloud-web/src/stores/workbench.ts @@ -767,8 +767,9 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project const sessionId = composer.value.sessionId; const threadId = composer.value.threadId; const providerThreadId = providerThreadIdForRequest(threadId); - const user = makeMessage("user", value, "sent", { traceId: steerTraceId ?? traceId, sessionId, threadId, title: steerMode ? "用户引导" : "用户" }); - const pending = makeMessage("agent", "", "running", { traceId, sessionId, threadId, title: "Code Agent", retryInput: value, traceAutoLifecycle: "running" }); + const submittedAt = new Date().toISOString(); + const user = makeMessage("user", value, "sent", { traceId: steerTraceId ?? traceId, sessionId, threadId, title: steerMode ? "用户引导" : "用户", createdAt: submittedAt, updatedAt: submittedAt }); + const pending = makeMessage("agent", "", "running", { traceId, sessionId, threadId, title: "Code Agent", retryInput: value, traceAutoLifecycle: "running", createdAt: submittedAt, updatedAt: submittedAt, ...optimisticRunningTimingPatch(submittedAt) }); appendActiveMessages(user, pending); startWorkbenchSubmitJourney({ traceId, sessionId, entry: submitEntry, backend: providerProfile.value, transport: "sse" }); chatPending.value = true; @@ -1456,6 +1457,17 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project return { ...patch, timing, startedAt: timing.startedAt ?? null, lastEventAt: timing.lastEventAt ?? null, finishedAt: null, durationMs: null }; } + function optimisticRunningTimingPatch(startedAt: string): Partial { + const timing = { + startedAt, + lastEventAt: startedAt, + finishedAt: null, + durationMs: null, + valuesRedacted: true, + } as WorkbenchTurnTimingProjection; + return { timing, startedAt, lastEventAt: startedAt, finishedAt: null, durationMs: null }; + } + async function clearActiveTrace(traceId: string, reason: string): Promise { if (currentRequest.value?.traceId === traceId) currentRequest.value = null; }