Merge remote-tracking branch 'origin/v0.3' into feat/2668-tasktree-github-backup
This commit is contained in:
@@ -101,6 +101,7 @@ import { handleTaskTreeProxyHttp } from "./tasktree-proxy.ts";
|
||||
import { handleDashboardSummaryHttp } from "./server-dashboard-http.ts";
|
||||
import { HWPOD_NODE_OPS, HWPOD_NODE_OPS_CONTRACT_VERSION } from "../../tools/src/hwpod-node-ops-contract.ts";
|
||||
import { proxyWorkbenchCommandHttp, workbenchActivityDispatchRequested, workbenchTemporalProxyEnabled } from "./workbench-command-proxy.ts";
|
||||
import { normalizeWorkbenchCloudAuthorization } from "../workbench/cloud-authorization.ts";
|
||||
|
||||
const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
|
||||
const HWLAB_WEB_SESSION_COOKIE = "hwlab_session";
|
||||
@@ -1078,7 +1079,7 @@ function navIdForRestPath(pathname, method = "GET") {
|
||||
|
||||
async function codeAgentOptions(request, response, options, authOptions = {}) {
|
||||
if (workbenchActivityDispatchRequested(request)) {
|
||||
const expected = String(options.env?.WORKBENCH_CLOUD_API_AUTHORIZATION ?? "").trim();
|
||||
const expected = normalizeWorkbenchCloudAuthorization(String(options.env?.WORKBENCH_CLOUD_API_AUTHORIZATION ?? ""));
|
||||
const actual = String(request.headers?.authorization ?? "").trim();
|
||||
const actorId = String(request.headers?.["x-hwlab-actor-id"] ?? "").trim();
|
||||
if (!expected || actual !== expected || !actorId) {
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
import { workbenchActivityDispatchRequested } from "./workbench-command-proxy.ts";
|
||||
import { normalizeWorkbenchCloudAuthorization } from "../workbench/cloud-authorization.ts";
|
||||
|
||||
test("accepts activity dispatch header from Node and Fetch request shapes", () => {
|
||||
expect(workbenchActivityDispatchRequested({ headers: { "x-workbench-activity-dispatch": "1" } })).toBe(true);
|
||||
expect(workbenchActivityDispatchRequested({ headers: new Headers({ "x-workbench-activity-dispatch": "1" }) })).toBe(true);
|
||||
expect(workbenchActivityDispatchRequested({ headers: new Headers({ "x-workbench-activity-dispatch": "0" }) })).toBe(false);
|
||||
});
|
||||
|
||||
test("normalizes the unified Workbench Cloud authorization on both sides", () => {
|
||||
expect(normalizeWorkbenchCloudAuthorization(" unified-internal-key ")).toBe("Bearer unified-internal-key");
|
||||
expect(normalizeWorkbenchCloudAuthorization("Bearer unified-internal-key")).toBe("Bearer unified-internal-key");
|
||||
expect(normalizeWorkbenchCloudAuthorization(" ")).toBe("");
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ const POST_PROXY_ROUTES = new Set([
|
||||
"/v1/agent/chat/cancel",
|
||||
"/v1/agent/chat/steer",
|
||||
"/v1/agent/sessions",
|
||||
"/v1/workbench/commands",
|
||||
"/v1/workbench/launches",
|
||||
"/v1/hwpod-node-ops",
|
||||
"/v1/web-performance",
|
||||
|
||||
@@ -236,3 +236,12 @@ test("cloud web proxies authenticated Workbench launch writes to cloud-api", ()
|
||||
routeKey: "POST /v1/workbench/launches"
|
||||
});
|
||||
});
|
||||
|
||||
test("cloud web proxies authenticated Workbench commands to cloud-api", () => {
|
||||
assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/workbench/commands"), {
|
||||
proxy: true,
|
||||
authRequired: true,
|
||||
publicRoute: false,
|
||||
routeKey: "POST /v1/workbench/commands"
|
||||
});
|
||||
});
|
||||
|
||||
@@ -105,7 +105,7 @@ test("cloud web auth requests are proxied once", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud web proxies Workbench launches with trace context", async () => {
|
||||
test("cloud web proxies Workbench writes with trace context", async () => {
|
||||
const upstreamRequests = [];
|
||||
const upstream = createServer(async (request, response) => {
|
||||
let body = "";
|
||||
@@ -145,27 +145,29 @@ test("cloud web proxies Workbench launches with trace context", async () => {
|
||||
|
||||
try {
|
||||
const body = JSON.stringify({ projectId: "project_d601", taskRef: "task_2136" });
|
||||
const response = await fetch(`${serverUrl(cloudWeb)}/v1/workbench/launches`, {
|
||||
for (const route of ["/v1/workbench/launches", "/v1/workbench/commands"]) {
|
||||
const response = await fetch(`${serverUrl(cloudWeb)}${route}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
cookie: "hwlab_session=session-launch",
|
||||
traceparent: "00-11111111111111111111111111111111-2222222222222222-01",
|
||||
"x-hwlab-otel-trace-id": "11111111111111111111111111111111"
|
||||
},
|
||||
body
|
||||
});
|
||||
assert.equal(response.status, 404);
|
||||
assert.equal(response.headers.get("x-hwlab-otel-trace-id"), "11111111111111111111111111111111");
|
||||
assert.deepEqual(await response.json(), { error: "not_found" });
|
||||
}
|
||||
assert.deepEqual(upstreamRequests, ["/v1/workbench/launches", "/v1/workbench/commands"].map((url) => ({
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
cookie: "hwlab_session=session-launch",
|
||||
traceparent: "00-11111111111111111111111111111111-2222222222222222-01",
|
||||
"x-hwlab-otel-trace-id": "11111111111111111111111111111111"
|
||||
},
|
||||
body
|
||||
});
|
||||
assert.equal(response.status, 404);
|
||||
assert.equal(response.headers.get("x-hwlab-otel-trace-id"), "11111111111111111111111111111111");
|
||||
assert.deepEqual(await response.json(), { error: "not_found" });
|
||||
assert.deepEqual(upstreamRequests, [{
|
||||
method: "POST",
|
||||
url: "/v1/workbench/launches",
|
||||
url,
|
||||
cookie: "hwlab_session=session-launch",
|
||||
traceparent: "00-11111111111111111111111111111111-2222222222222222-01",
|
||||
otelTraceId: "11111111111111111111111111111111",
|
||||
body
|
||||
}]);
|
||||
})));
|
||||
} finally {
|
||||
await close(cloudWeb);
|
||||
await close(upstream);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { WorkbenchApplication } from "./contracts.ts";
|
||||
import { normalizeWorkbenchCloudAuthorization } from "./cloud-authorization.ts";
|
||||
|
||||
export function createCloudWorkbenchApplication(options: {
|
||||
baseUrl: string;
|
||||
@@ -6,13 +7,14 @@ export function createCloudWorkbenchApplication(options: {
|
||||
fetchImpl?: typeof fetch;
|
||||
}): WorkbenchApplication {
|
||||
if (!options.baseUrl) throw codedError("workbench_cloud_api_url_required", "WORKBENCH_CLOUD_API_URL is required");
|
||||
if (!options.authorization) throw codedError("workbench_cloud_api_auth_required", "WORKBENCH_CLOUD_API_AUTHORIZATION is required");
|
||||
const authorization = normalizeWorkbenchCloudAuthorization(options.authorization);
|
||||
if (!authorization) throw codedError("workbench_cloud_api_auth_required", "WORKBENCH_CLOUD_API_AUTHORIZATION is required");
|
||||
const fetchImpl = options.fetchImpl ?? fetch;
|
||||
const request = async (method: string, route: string, body: Record<string, unknown> | undefined, actor: { id: string; role?: string | null }) => {
|
||||
const response = await fetchImpl(`${options.baseUrl.replace(/\/$/u, "")}${route}`, {
|
||||
method,
|
||||
headers: {
|
||||
authorization: options.authorization,
|
||||
authorization,
|
||||
"content-type": "application/json",
|
||||
"x-workbench-activity-dispatch": "1",
|
||||
"x-hwlab-actor-id": actor.id,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export function normalizeWorkbenchCloudAuthorization(value: string) {
|
||||
const authorization = value.trim();
|
||||
if (!authorization) return "";
|
||||
return /^Bearer\s+/iu.test(authorization) ? authorization : `Bearer ${authorization}`;
|
||||
}
|
||||
@@ -54,10 +54,24 @@ describe("Workbench Cloud application adapter", () => {
|
||||
await application.admitTurn(input);
|
||||
await application.dispatchTurn(input);
|
||||
expect(calls).toEqual([
|
||||
{ path: "/v1/internal/workbench/admit", authorization: "unified-internal-key" },
|
||||
{ path: "/v1/agent/chat", authorization: "unified-internal-key" }
|
||||
{ path: "/v1/internal/workbench/admit", authorization: "Bearer unified-internal-key" },
|
||||
{ path: "/v1/agent/chat", authorization: "Bearer unified-internal-key" }
|
||||
]);
|
||||
});
|
||||
|
||||
test("preserves an existing Bearer authorization scheme", async () => {
|
||||
let authorization: string | null = null;
|
||||
const application = createCloudWorkbenchApplication({
|
||||
baseUrl: "http://cloud.internal",
|
||||
authorization: "Bearer unified-internal-key",
|
||||
fetchImpl: (async (_url: string | URL | Request, init?: RequestInit) => {
|
||||
authorization = new Headers(init?.headers).get("authorization");
|
||||
return Response.json({ ok: true, session: { sessionId: "ses_test" } });
|
||||
}) as typeof fetch
|
||||
});
|
||||
await application.createSession({ actor: { id: "usr_test" }, params: {} });
|
||||
expect(authorization).toBe("Bearer unified-internal-key");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Workbench native HTTP adapter", () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { providerProfileOptionsFromPayload } from "../src/stores/workbench-session.ts";
|
||||
import { preferredProviderProfile, providerProfileOptionsFromPayload } from "../src/stores/workbench-session.ts";
|
||||
|
||||
test("Workbench provider catalog marks missing selected profile as unavailable", () => {
|
||||
const options = providerProfileOptionsFromPayload({ profiles: [{ profile: "dsflash-go", label: "DeepSeek V4 Flash", configured: true }] }, "codex");
|
||||
@@ -11,4 +11,11 @@ test("Workbench provider catalog marks missing selected profile as unavailable",
|
||||
|
||||
assert.equal(available?.configured, true);
|
||||
assert.equal(missingSelected?.configured, false);
|
||||
assert.equal(preferredProviderProfile(options, "codex"), "dsflash-go");
|
||||
});
|
||||
|
||||
test("Workbench provider selection stays non-blocking without a configured catalog option", () => {
|
||||
const options = providerProfileOptionsFromPayload({ profiles: [{ profile: "codex", configured: false }] }, "codex");
|
||||
assert.equal(preferredProviderProfile(options, "codex"), "codex");
|
||||
assert.equal(preferredProviderProfile(providerProfileOptionsFromPayload({}, "codex"), "codex"), "codex");
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ import { messageDiagnosticView } from "../src/utils/workbench-error-runtime.ts";
|
||||
import { projectRejectedWorkbenchAdmission } from "../src/stores/workbench-admission-failure.ts";
|
||||
import { WORKBENCH_TIMELINE_OPENCODE_PARITY, buildWorkbenchTimelineRows, normalizeWorkbenchTimelineMessages, workbenchTimelineSignature } from "../src/stores/workbench-timeline-model.ts";
|
||||
import { reduceWorkbenchRealtimeEvent } from "../src/stores/workbench-event-reducer.ts";
|
||||
import { planWorkbenchRealtimeApply, planWorkbenchRealtimeRecovery } from "../src/stores/workbench-realtime-plan.ts";
|
||||
import { assessWorkbenchRealtimeConnected, planWorkbenchRealtimeApply, planWorkbenchRealtimeRecovery } from "../src/stores/workbench-realtime-plan.ts";
|
||||
import { WORKBENCH_REALTIME_AUTHORITY_VERSION, workbenchRealtimePrimaryAuthorityDecision } from "../src/stores/workbench-realtime-authority.ts";
|
||||
import { cleanupWorkbenchServerStateDroppedSessions, cleanupWorkbenchServerStateSessions, createWorkbenchServerState, reduceWorkbenchServerState } from "../src/stores/workbench-server-state.ts";
|
||||
import { cleanupDroppedWorkbenchSessionCaches, trimWorkbenchSessionCache } from "../src/stores/workbench-session-cache.ts";
|
||||
@@ -93,6 +93,40 @@ test("the active projection trace scope follows the current request", () => {
|
||||
assert.equal(workbenchRealtimeTraceIdForCapabilities({ liveKafkaSse: true, kafkaRefreshReplay: true, projectionRealtime: false }, "trc_current_request"), null);
|
||||
});
|
||||
|
||||
test("connected realtime metadata drift warns without blocking the active session", () => {
|
||||
const assessment = assessWorkbenchRealtimeConnected({
|
||||
deliverySemantics: "live-only",
|
||||
liveOnly: true,
|
||||
replay: false,
|
||||
filters: { sessionId: "ses_active" }
|
||||
}, "ses_active", false);
|
||||
|
||||
assert.equal(assessment.ready, true);
|
||||
assert.equal(assessment.connectedSessionId, "ses_active");
|
||||
assert.equal(assessment.blocker, null);
|
||||
assert.deepEqual(assessment.warning?.fields, [
|
||||
"capabilities.liveKafkaSse",
|
||||
"capabilities.kafkaRefreshReplay"
|
||||
]);
|
||||
assert.equal(assessment.warning?.severity, "warning");
|
||||
});
|
||||
|
||||
test("connected realtime session scope mismatch remains blocking", () => {
|
||||
const assessment = assessWorkbenchRealtimeConnected({
|
||||
deliverySemantics: "live-only",
|
||||
liveOnly: true,
|
||||
replay: false,
|
||||
capabilities: { liveKafkaSse: true, kafkaRefreshReplay: false, projectionRealtime: false },
|
||||
filters: { sessionId: "ses_other" }
|
||||
}, "ses_active", false);
|
||||
|
||||
assert.equal(assessment.ready, false);
|
||||
assert.equal(assessment.warning, null);
|
||||
assert.equal(assessment.blocker?.code, "workbench_realtime_session_scope_mismatch");
|
||||
assert.equal(assessment.blocker?.expectedSessionId, "ses_active");
|
||||
assert.equal(assessment.blocker?.connectedSessionId, "ses_other");
|
||||
});
|
||||
|
||||
test("Error runtime owns Workbench message diagnostic view model", () => {
|
||||
const degraded = messageDiagnosticView(agentMessage({
|
||||
status: "running",
|
||||
|
||||
@@ -51,6 +51,23 @@ export interface WorkbenchRealtimeRecoveryPlan {
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorkbenchRealtimeConnectedAssessment {
|
||||
connectedSessionId: string | null;
|
||||
ready: boolean;
|
||||
warning: {
|
||||
code: "workbench_realtime_metadata_drift_warning";
|
||||
fields: string[];
|
||||
severity: "warning";
|
||||
valuesRedacted: true;
|
||||
} | null;
|
||||
blocker: {
|
||||
code: "workbench_realtime_session_scope_mismatch";
|
||||
expectedSessionId: string | null;
|
||||
connectedSessionId: string | null;
|
||||
valuesRedacted: true;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export function planWorkbenchRealtimeApply(action: WorkbenchRealtimeAction): WorkbenchRealtimeApplyPlan {
|
||||
const steps = applySteps(action);
|
||||
return {
|
||||
@@ -65,6 +82,40 @@ export function planWorkbenchRealtimeApply(action: WorkbenchRealtimeAction): Wor
|
||||
};
|
||||
}
|
||||
|
||||
export function assessWorkbenchRealtimeConnected(
|
||||
event: WorkbenchRealtimeEvent,
|
||||
expectedSessionId: string | null | undefined,
|
||||
kafkaRefreshReplay: boolean
|
||||
): WorkbenchRealtimeConnectedAssessment {
|
||||
const filters = event.filters && typeof event.filters === "object" ? event.filters as Record<string, unknown> : null;
|
||||
const connectedSessionId = normalizeWorkbenchSessionId(filters?.sessionId);
|
||||
const normalizedExpectedSessionId = normalizeWorkbenchSessionId(expectedSessionId);
|
||||
const scopeMatches = Boolean(connectedSessionId && normalizedExpectedSessionId && connectedSessionId === normalizedExpectedSessionId);
|
||||
const fields: string[] = [];
|
||||
const expectedDelivery = kafkaRefreshReplay ? "kafka-retention-then-live" : "live-only";
|
||||
if (event.deliverySemantics !== expectedDelivery) fields.push("deliverySemantics");
|
||||
if (event.liveOnly !== !kafkaRefreshReplay) fields.push("liveOnly");
|
||||
if (event.replay !== kafkaRefreshReplay) fields.push("replay");
|
||||
if (kafkaRefreshReplay && event.lossPossible !== false) fields.push("lossPossible");
|
||||
if (event.capabilities?.liveKafkaSse !== true) fields.push("capabilities.liveKafkaSse");
|
||||
if (event.capabilities?.kafkaRefreshReplay !== kafkaRefreshReplay) fields.push("capabilities.kafkaRefreshReplay");
|
||||
return {
|
||||
connectedSessionId,
|
||||
ready: scopeMatches,
|
||||
warning: fields.length > 0
|
||||
? { code: "workbench_realtime_metadata_drift_warning", fields, severity: "warning", valuesRedacted: true }
|
||||
: null,
|
||||
blocker: scopeMatches
|
||||
? null
|
||||
: {
|
||||
code: "workbench_realtime_session_scope_mismatch",
|
||||
expectedSessionId: normalizedExpectedSessionId,
|
||||
connectedSessionId,
|
||||
valuesRedacted: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function planWorkbenchRealtimeRecovery(recovery: WorkbenchStreamTransportRecovery, context: WorkbenchRealtimeRecoveryContext): WorkbenchRealtimeRecoveryPlan {
|
||||
const sessionId = normalizeWorkbenchSessionId(recovery.sessionId ?? context.selectedSessionId);
|
||||
const traceId = firstNonEmptyString(recovery.traceId, context.fallbackTraceId);
|
||||
|
||||
@@ -259,6 +259,12 @@ export function defaultProviderProfileOptions(selectedProfile: ProviderProfile):
|
||||
return ensureSelectedProviderProfile(BUILTIN_PROVIDER_PROFILE_ORDER.map((profile) => ({ value: profile, label: providerProfileLabel(profile) })), selectedProfile);
|
||||
}
|
||||
|
||||
export function preferredProviderProfile(options: ProviderProfileOption[], selectedProfile: ProviderProfile): ProviderProfile {
|
||||
const selected = options.find((option) => option.value === selectedProfile);
|
||||
if (selected?.configured !== false) return selectedProfile;
|
||||
return options.find((option) => option.configured === true)?.value ?? selectedProfile;
|
||||
}
|
||||
|
||||
function activeSession(sessions: WorkbenchSessionRecord[], sessionId: string | null): WorkbenchSessionRecord | null {
|
||||
return sessionId ? sessions.find((item) => item.sessionId === sessionId) ?? null : null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import type { AgentChatResponse, AgentChatResultResponse, AgentRunProvenance, Ap
|
||||
import { firstNonEmptyString, nextProtocolId, normalizeWorkbenchSessionId, normalizeWorkbenchSessionRouteId } from "@/utils";
|
||||
import { composeWorkbenchScopedKey, workbenchRealtimeScopeKey } from "@/utils/workbench-key";
|
||||
import { failWorkbenchSessionSwitch, failWorkbenchSubmitJourney, finishWorkbenchSessionSwitchFullLoad, markWorkbenchSubmitApiAccepted, markWorkbenchTraceEventsReceived, markWorkbenchTraceProjected, recordWorkbenchLoadingState, recordWorkbenchRuntimeDiagnostic, startWorkbenchSessionSwitch, startWorkbenchSubmitJourney } from "@/utils/workbench-performance";
|
||||
import { RECENT_DRAFTS_STORAGE_KEY, appendSessionPage, defaultProviderProfileOptions, isArchivedSession, mergeSessionIntoList, normalizeChatMessageStatus, normalizeRecentDrafts, normalizeWorkbenchMessageTitle, providerProfileOptionsFromPayload, recordRecentDraft, resolveCancelableAgentMessage, resolveComposerState, shouldReadTerminalTraceDetail, shouldShowSessionListLoading, sortSessionTabs, stableSessionList, type DraftEntry, type ProviderProfileOption, type TurnStatusAuthority } from "./workbench-session";
|
||||
import { RECENT_DRAFTS_STORAGE_KEY, appendSessionPage, defaultProviderProfileOptions, isArchivedSession, mergeSessionIntoList, normalizeChatMessageStatus, normalizeRecentDrafts, normalizeWorkbenchMessageTitle, preferredProviderProfile, providerProfileOptionsFromPayload, recordRecentDraft, resolveCancelableAgentMessage, resolveComposerState, shouldReadTerminalTraceDetail, shouldShowSessionListLoading, sortSessionTabs, stableSessionList, type DraftEntry, type ProviderProfileOption, type TurnStatusAuthority } from "./workbench-session";
|
||||
import { initialWorkbenchSessionIdFromLocation } from "./workbench-projection";
|
||||
import { cleanupWorkbenchServerStateSessions, selectActiveMessages, selectActiveSession, selectSessionList, selectSessionStatusAuthority, selectTraceAuthorityById, selectTurnStatusAuthority, type WorkbenchServerAction } from "./workbench-server-state";
|
||||
import { cleanupDroppedWorkbenchSessionCaches, trimWorkbenchSessionCache } from "./workbench-session-cache";
|
||||
@@ -65,7 +65,7 @@ import {
|
||||
traceHasEvents,
|
||||
traceResultHasTerminalEvidence,
|
||||
} from "./workbench-message-projection-runtime";
|
||||
import { planWorkbenchRealtimeApply, planWorkbenchRealtimeRecovery, type WorkbenchRealtimeApplyStep, type WorkbenchRealtimeRecoveryStep } from "./workbench-realtime-plan";
|
||||
import { assessWorkbenchRealtimeConnected, planWorkbenchRealtimeApply, planWorkbenchRealtimeRecovery, type WorkbenchRealtimeApplyStep, type WorkbenchRealtimeRecoveryStep } from "./workbench-realtime-plan";
|
||||
import { useWorkbenchColadaMutations } from "./workbench-colada-mutations";
|
||||
import { useWorkbenchColadaQueries } from "./workbench-colada-queries";
|
||||
import { useWorkbenchColadaReducer } from "./workbench-colada-reducer";
|
||||
@@ -921,7 +921,13 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
|
||||
async function refreshProviderOptions(): Promise<boolean> {
|
||||
const response = await workbenchColadaQueries.fetchProviderProfiles({ minIntervalMs: runtimePolicy.workbenchReadFailureCooldownMs });
|
||||
providerOptions.value = response.ok ? providerProfileOptionsFromPayload(response.data, providerProfile.value) : defaultProviderProfileOptions(providerProfile.value);
|
||||
if (response.ok) {
|
||||
providerOptions.value = providerProfileOptionsFromPayload(response.data, providerProfile.value);
|
||||
const preferred = preferredProviderProfile(providerOptions.value, providerProfile.value);
|
||||
if (preferred !== providerProfile.value) setProviderProfile(preferred);
|
||||
} else {
|
||||
providerOptions.value = defaultProviderProfileOptions(providerProfile.value);
|
||||
}
|
||||
return response.ok === true;
|
||||
}
|
||||
|
||||
@@ -983,18 +989,30 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
onRecovery: (recovery) => handleRealtimeRecovery(recovery),
|
||||
onEvent: (event, eventName) => {
|
||||
if (realtimeCapabilities.liveKafkaSse && eventName === "workbench.connected") {
|
||||
const filters = recordValue(event.filters);
|
||||
const connectedSessionId = normalizeWorkbenchSessionId(filters?.sessionId);
|
||||
const refreshReplay = realtimeCapabilities.kafkaRefreshReplay;
|
||||
const deliveryValid = refreshReplay
|
||||
? event.deliverySemantics === "kafka-retention-then-live" && event.liveOnly === false && event.replay === true && event.lossPossible === false
|
||||
: event.deliverySemantics === "live-only" && event.liveOnly === true && event.replay === false;
|
||||
const valid = deliveryValid
|
||||
&& event.capabilities?.liveKafkaSse === true
|
||||
&& event.capabilities?.kafkaRefreshReplay === refreshReplay
|
||||
&& connectedSessionId === sessionId;
|
||||
liveRealtimeReadySessionId.value = valid ? connectedSessionId : null;
|
||||
if (!valid) error.value = "workbench_live_realtime_contract_invalid";
|
||||
const assessment = assessWorkbenchRealtimeConnected(event, sessionId, realtimeCapabilities.kafkaRefreshReplay);
|
||||
liveRealtimeReadySessionId.value = assessment.ready ? assessment.connectedSessionId : null;
|
||||
if (assessment.warning) {
|
||||
recordWorkbenchRuntimeDiagnostic({
|
||||
module: "workbench-stream-contract",
|
||||
sessionId,
|
||||
traceId,
|
||||
outcome: "partial",
|
||||
level: "warning",
|
||||
diagnostic: assessment.warning
|
||||
});
|
||||
}
|
||||
if (assessment.blocker) {
|
||||
error.value = assessment.blocker.code;
|
||||
recordWorkbenchRuntimeDiagnostic({
|
||||
module: "workbench-stream-contract",
|
||||
sessionId,
|
||||
traceId,
|
||||
outcome: "error",
|
||||
diagnostic: assessment.blocker
|
||||
});
|
||||
} else if (error.value === "workbench_live_realtime_contract_invalid" || error.value === "workbench_realtime_session_scope_mismatch") {
|
||||
error.value = null;
|
||||
}
|
||||
}
|
||||
applyRealtimeEvent(event, eventName);
|
||||
}
|
||||
|
||||
@@ -365,25 +365,26 @@ export function recordWorkbenchSseLifecycle(input: { state: "connect" | "open" |
|
||||
});
|
||||
}
|
||||
|
||||
export function recordWorkbenchRuntimeDiagnostic(input: { module?: string | null; diagnostic?: Record<string, unknown> | null; sessionId?: string | null; traceId?: string | null; outcome?: WorkbenchOutcome | null }): void {
|
||||
export function recordWorkbenchRuntimeDiagnostic(input: { module?: string | null; diagnostic?: Record<string, unknown> | null; sessionId?: string | null; traceId?: string | null; outcome?: WorkbenchOutcome | null; level?: "error" | "warning" | "sample" }): void {
|
||||
ensureInstalled();
|
||||
const diagnostic = recordValue(input.diagnostic);
|
||||
const endedAt = wallNow();
|
||||
const code = diagnosticText(diagnostic, "code") ?? "runtime_diagnostic";
|
||||
const flushDurationMs = diagnosticNumber(diagnostic, "flushDurationMs");
|
||||
const isSseFlush = code === "workbench_sse_flush";
|
||||
const warning = input.level === "warning" || diagnosticText(diagnostic, "severity", "level") === "warning";
|
||||
enqueueWorkbenchUiEvent({
|
||||
eventType: "runtime_diagnostic",
|
||||
loadingScope: "sse",
|
||||
state: isSseFlush ? "sample" : "error",
|
||||
reason: isSseFlush ? "sse_flush" : "sse_error",
|
||||
state: isSseFlush || warning ? "sample" : "error",
|
||||
reason: isSseFlush ? "sse_flush" : warning ? "unknown" : "sse_error",
|
||||
route: pageRoute(),
|
||||
valueMs: flushDurationMs ?? 0,
|
||||
startedAtEpochMs: Math.max(0, endedAt - (flushDurationMs ?? 0)),
|
||||
endedAtEpochMs: endedAt,
|
||||
sessionHash: hashIdentifier(input.sessionId ?? diagnosticText(diagnostic, "sessionId"), "ses"),
|
||||
traceHash: hashIdentifier(input.traceId ?? diagnosticText(diagnostic, "traceId"), "trc"),
|
||||
outcome: input.outcome ?? normalizeUiOutcome(diagnosticText(diagnostic, "outcome")),
|
||||
outcome: input.outcome ?? (warning ? "partial" : normalizeUiOutcome(diagnosticText(diagnostic, "outcome"))),
|
||||
errorName: code.slice(0, 80),
|
||||
module: diagnosticLabel(input.module ?? diagnosticText(diagnostic, "module") ?? diagnosticText(diagnostic, "layer")),
|
||||
diagnosticCode: diagnosticLabel(code),
|
||||
|
||||
Reference in New Issue
Block a user