Merge pull request #2435 from pikasTech/issue-1585-1587-realtime-authority-p3p5
Pipelines as Code CI / hwlab-jd01-v03-ci-poll- Failed
Pipelines as Code CI / hwlab-jd01-v03-ci-poll- Failed
fix: route workbench recovery through sync authority
This commit is contained in:
@@ -386,6 +386,17 @@ test("Workbench UI OTel spans include timeout activity diagnostics without raw t
|
|||||||
activityWaitingFor: "code-agent",
|
activityWaitingFor: "code-agent",
|
||||||
activityLastEventLabel: "realtime:heartbeat",
|
activityLastEventLabel: "realtime:heartbeat",
|
||||||
activityIdleMs: 13042,
|
activityIdleMs: 13042,
|
||||||
|
eventCount: 1,
|
||||||
|
contractVersion: "workbench-sync-v1",
|
||||||
|
realtimeAuthority: "workbench-realtime-authority-v2",
|
||||||
|
sinceOutboxSeq: 7,
|
||||||
|
syncCursorOutboxSeq: 12,
|
||||||
|
entityFamilyCount: 1,
|
||||||
|
entityFamilies: "turn",
|
||||||
|
maxEntityVersion: 3,
|
||||||
|
projectionRevision: "rev_sync",
|
||||||
|
terminalSeal: true,
|
||||||
|
detailProjection: false,
|
||||||
traceId: "trc_secret"
|
traceId: "trc_secret"
|
||||||
}]
|
}]
|
||||||
} as Record<string, unknown>, { OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "http://otel.example/v1/traces" });
|
} as Record<string, unknown>, { OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "http://otel.example/v1/traces" });
|
||||||
@@ -393,12 +404,23 @@ test("Workbench UI OTel spans include timeout activity diagnostics without raw t
|
|||||||
assert.equal(result.submitted, 1);
|
assert.equal(result.submitted, 1);
|
||||||
assert.equal(calls.length, 1);
|
assert.equal(calls.length, 1);
|
||||||
const span = calls[0].resourceSpans[0].scopeSpans[0].spans[0];
|
const span = calls[0].resourceSpans[0].scopeSpans[0].spans[0];
|
||||||
const attrs = Object.fromEntries(span.attributes.map((item) => [item.key, item.value.stringValue ?? Number(item.value.intValue) ?? item.value.boolValue]));
|
const attrs = Object.fromEntries(span.attributes.map((item) => [item.key, item.value.stringValue ?? (item.value.intValue !== undefined ? Number(item.value.intValue) : item.value.boolValue)]));
|
||||||
assert.equal(attrs["http.route"], "/v1/workbench/turns/:id");
|
assert.equal(attrs["http.route"], "/v1/workbench/turns/:id");
|
||||||
assert.equal(attrs["workbench.ui.timeout_name"], "workbench turn");
|
assert.equal(attrs["workbench.ui.timeout_name"], "workbench turn");
|
||||||
assert.equal(attrs["workbench.ui.activity_waiting_for"], "code-agent");
|
assert.equal(attrs["workbench.ui.activity_waiting_for"], "code-agent");
|
||||||
assert.equal(attrs["workbench.ui.activity_last_event_label"], "realtime:heartbeat");
|
assert.equal(attrs["workbench.ui.activity_last_event_label"], "realtime:heartbeat");
|
||||||
assert.equal(attrs["workbench.ui.activity_idle_ms"], 13042);
|
assert.equal(attrs["workbench.ui.activity_idle_ms"], 13042);
|
||||||
|
assert.equal(attrs["workbench.sync.event_count"], 1);
|
||||||
|
assert.equal(attrs["workbench.sync.contract_version"], "workbench-sync-v1");
|
||||||
|
assert.equal(attrs["workbench.realtime.authority"], "workbench-realtime-authority-v2");
|
||||||
|
assert.equal(attrs["workbench.sync.since_outbox_seq"], 7);
|
||||||
|
assert.equal(attrs["workbench.sync.cursor_outbox_seq"], 12);
|
||||||
|
assert.equal(attrs["workbench.sync.entity_family_count"], 1);
|
||||||
|
assert.equal(attrs["workbench.sync.entity_families"], "turn");
|
||||||
|
assert.equal(attrs["workbench.sync.max_entity_version"], 3);
|
||||||
|
assert.equal(attrs["workbench.projection.revision"], "rev_sync");
|
||||||
|
assert.equal(attrs["workbench.terminal.seal"], true);
|
||||||
|
assert.equal(attrs["workbench.detail.projection"], false);
|
||||||
assert.doesNotMatch(JSON.stringify(calls[0]), /trc_secret|ses_secret|prompt|api key/iu);
|
assert.doesNotMatch(JSON.stringify(calls[0]), /trc_secret|ses_secret|prompt|api key/iu);
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
|
|||||||
@@ -126,6 +126,17 @@ interface WebPerformanceEvent {
|
|||||||
resourceDecodedBodySize?: unknown;
|
resourceDecodedBodySize?: unknown;
|
||||||
resourceNextHopProtocol?: unknown;
|
resourceNextHopProtocol?: unknown;
|
||||||
resourceServerTiming?: unknown;
|
resourceServerTiming?: unknown;
|
||||||
|
eventCount?: unknown;
|
||||||
|
contractVersion?: unknown;
|
||||||
|
realtimeAuthority?: unknown;
|
||||||
|
sinceOutboxSeq?: unknown;
|
||||||
|
syncCursorOutboxSeq?: unknown;
|
||||||
|
entityFamilyCount?: unknown;
|
||||||
|
entityFamilies?: unknown;
|
||||||
|
maxEntityVersion?: unknown;
|
||||||
|
projectionRevision?: unknown;
|
||||||
|
terminalSeal?: unknown;
|
||||||
|
detailProjection?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NormalizedPerformanceEvent {
|
interface NormalizedPerformanceEvent {
|
||||||
@@ -839,6 +850,15 @@ function normalizeWorkbenchUiOtelSpan(rawEvent: unknown, payload: WebPerformance
|
|||||||
const resourceDecodedBodySize = Math.min(Math.max(finiteNumber(input.resourceDecodedBodySize, NaN), 0), 100 * 1024 * 1024);
|
const resourceDecodedBodySize = Math.min(Math.max(finiteNumber(input.resourceDecodedBodySize, NaN), 0), 100 * 1024 * 1024);
|
||||||
const resourceNextHopProtocol = sanitizeOtelAttribute(input.resourceNextHopProtocol, 40);
|
const resourceNextHopProtocol = sanitizeOtelAttribute(input.resourceNextHopProtocol, 40);
|
||||||
const resourceServerTiming = sanitizeOtelAttribute(input.resourceServerTiming, 240);
|
const resourceServerTiming = sanitizeOtelAttribute(input.resourceServerTiming, 240);
|
||||||
|
const eventCount = Math.min(Math.max(finiteNumber(input.eventCount, NaN), 0), 100_000);
|
||||||
|
const contractVersion = sanitizeOtelAttribute(input.contractVersion, 80);
|
||||||
|
const realtimeAuthority = sanitizeOtelAttribute(input.realtimeAuthority, 120);
|
||||||
|
const entityFamilies = sanitizeOtelAttribute(input.entityFamilies, 180);
|
||||||
|
const projectionRevision = sanitizeOtelAttribute(input.projectionRevision, 120);
|
||||||
|
const sinceOutboxSeq = Math.min(Math.max(finiteNumber(input.sinceOutboxSeq, NaN), 0), Number.MAX_SAFE_INTEGER);
|
||||||
|
const syncCursorOutboxSeq = Math.min(Math.max(finiteNumber(input.syncCursorOutboxSeq, NaN), 0), Number.MAX_SAFE_INTEGER);
|
||||||
|
const entityFamilyCount = Math.min(Math.max(finiteNumber(input.entityFamilyCount, NaN), 0), 1000);
|
||||||
|
const maxEntityVersion = Math.min(Math.max(finiteNumber(input.maxEntityVersion, NaN), 0), Number.MAX_SAFE_INTEGER);
|
||||||
return {
|
return {
|
||||||
name: ["workbench", eventType, scope, state].filter((part) => part && part !== "unknown").join("."),
|
name: ["workbench", eventType, scope, state].filter((part) => part && part !== "unknown").join("."),
|
||||||
uiTraceId,
|
uiTraceId,
|
||||||
@@ -874,6 +894,17 @@ function normalizeWorkbenchUiOtelSpan(rawEvent: unknown, payload: WebPerformance
|
|||||||
...(Number.isFinite(resourceDecodedBodySize) ? { "workbench.ui.resource_decoded_body_size": Math.trunc(resourceDecodedBodySize) } : {}),
|
...(Number.isFinite(resourceDecodedBodySize) ? { "workbench.ui.resource_decoded_body_size": Math.trunc(resourceDecodedBodySize) } : {}),
|
||||||
...(resourceNextHopProtocol ? { "workbench.ui.resource_next_hop_protocol": resourceNextHopProtocol } : {}),
|
...(resourceNextHopProtocol ? { "workbench.ui.resource_next_hop_protocol": resourceNextHopProtocol } : {}),
|
||||||
...(resourceServerTiming ? { "workbench.ui.resource_server_timing": resourceServerTiming } : {}),
|
...(resourceServerTiming ? { "workbench.ui.resource_server_timing": resourceServerTiming } : {}),
|
||||||
|
...(Number.isFinite(eventCount) ? { "workbench.sync.event_count": Math.trunc(eventCount) } : {}),
|
||||||
|
...(contractVersion ? { "workbench.sync.contract_version": contractVersion } : {}),
|
||||||
|
...(realtimeAuthority ? { "workbench.realtime.authority": realtimeAuthority } : {}),
|
||||||
|
...(Number.isFinite(sinceOutboxSeq) ? { "workbench.sync.since_outbox_seq": Math.trunc(sinceOutboxSeq) } : {}),
|
||||||
|
...(Number.isFinite(syncCursorOutboxSeq) ? { "workbench.sync.cursor_outbox_seq": Math.trunc(syncCursorOutboxSeq) } : {}),
|
||||||
|
...(Number.isFinite(entityFamilyCount) ? { "workbench.sync.entity_family_count": Math.trunc(entityFamilyCount) } : {}),
|
||||||
|
...(entityFamilies ? { "workbench.sync.entity_families": entityFamilies } : {}),
|
||||||
|
...(Number.isFinite(maxEntityVersion) ? { "workbench.sync.max_entity_version": Math.trunc(maxEntityVersion) } : {}),
|
||||||
|
...(projectionRevision ? { "workbench.projection.revision": projectionRevision } : {}),
|
||||||
|
...(typeof input.terminalSeal === "boolean" ? { "workbench.terminal.seal": input.terminalSeal } : {}),
|
||||||
|
...(typeof input.detailProjection === "boolean" ? { "workbench.detail.projection": input.detailProjection } : {}),
|
||||||
"workbench.ui.values_printed": false,
|
"workbench.ui.values_printed": false,
|
||||||
"http.route": route,
|
"http.route": route,
|
||||||
"http.request.method": normalizeMethod(input.method),
|
"http.request.method": normalizeMethod(input.method),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPEC: pikasTech/HWLAB#2345 Workbench Pinia Colada migration.
|
// SPEC: pikasTech/HWLAB#2345 Workbench Pinia Colada migration; PJ2026-010401080313 Workbench实时权威 draft-2026-07-08-p0-workbench-realtime-authority-v2.
|
||||||
// Responsibility: Stable Workbench query and mutation keys for Pinia Colada cache authority.
|
// Responsibility: Stable Workbench semantic query and mutation keys for Pinia Colada cache authority.
|
||||||
|
|
||||||
import type { EntryKey } from "@pinia/colada";
|
import type { EntryKey } from "@pinia/colada";
|
||||||
|
|
||||||
@@ -19,19 +19,40 @@ export interface WorkbenchTraceEventsKeyInput {
|
|||||||
limit?: number | null;
|
limit?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorkbenchSyncReplayKeyInput {
|
||||||
|
sessionId?: string | null;
|
||||||
|
traceId?: string | null;
|
||||||
|
since?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
export const workbenchColadaKeys = {
|
export const workbenchColadaKeys = {
|
||||||
root: (): EntryKey => ["workbench"],
|
root: (): EntryKey => ["workbench"],
|
||||||
serverState: (): EntryKey => ["workbench", "server-state"],
|
serverState: (): EntryKey => ["workbench", "projection", "server-state"],
|
||||||
sessionsRoot: (): EntryKey => ["workbench", "sessions"],
|
snapshotRoot: (): EntryKey => ["workbench", "snapshot"],
|
||||||
sessions: (input: WorkbenchSessionsKeyInput = {}): EntryKey => ["workbench", "sessions", normalizeKeyObject({ includeSessionId: input.includeSessionId ?? null, cursor: input.cursor ?? null, limit: finiteNumber(input.limit) })],
|
snapshotSessionsRoot: (): EntryKey => ["workbench", "snapshot", "sessions"],
|
||||||
sessionRoot: (): EntryKey => ["workbench", "session"],
|
snapshotSessions: (input: WorkbenchSessionsKeyInput = {}): EntryKey => ["workbench", "snapshot", "sessions", normalizeKeyObject({ includeSessionId: input.includeSessionId ?? null, cursor: input.cursor ?? null, limit: finiteNumber(input.limit) })],
|
||||||
session: (sessionId: string): EntryKey => ["workbench", "session", sessionId],
|
snapshotSessionRoot: (): EntryKey => ["workbench", "snapshot", "session"],
|
||||||
sessionMessagesRoot: (): EntryKey => ["workbench", "session-messages"],
|
snapshotSession: (sessionId: string): EntryKey => ["workbench", "snapshot", "session", sessionId],
|
||||||
sessionMessages: (sessionId: string, input: WorkbenchSessionMessagesKeyInput = {}): EntryKey => ["workbench", "session-messages", sessionId, normalizeKeyObject({ cursor: input.cursor ?? null, limit: finiteNumber(input.limit) })],
|
syncRoot: (): EntryKey => ["workbench", "sync"],
|
||||||
turnRoot: (): EntryKey => ["workbench", "turn"],
|
syncReplay: (input: WorkbenchSyncReplayKeyInput = {}): EntryKey => ["workbench", "sync", "replay", normalizeKeyObject({ sessionId: input.sessionId ?? null, traceId: input.traceId ?? null, since: finiteNumber(input.since) })],
|
||||||
turn: (traceId: string): EntryKey => ["workbench", "turn", traceId],
|
historyRoot: (): EntryKey => ["workbench", "history"],
|
||||||
traceEventsRoot: (): EntryKey => ["workbench", "trace-events"],
|
historySessionMessagesRoot: (): EntryKey => ["workbench", "history", "session-messages"],
|
||||||
traceEvents: (traceId: string, input: WorkbenchTraceEventsKeyInput = {}): EntryKey => ["workbench", "trace-events", traceId, normalizeKeyObject({ afterProjectedSeq: finiteNumber(input.afterProjectedSeq), limit: finiteNumber(input.limit) })],
|
historySessionMessages: (sessionId: string, input: WorkbenchSessionMessagesKeyInput = {}): EntryKey => ["workbench", "history", "session-messages", sessionId, normalizeKeyObject({ cursor: input.cursor ?? null, limit: finiteNumber(input.limit) })],
|
||||||
|
detailRoot: (): EntryKey => ["workbench", "detail"],
|
||||||
|
detailTurnRoot: (): EntryKey => ["workbench", "detail", "turn"],
|
||||||
|
detailTurn: (traceId: string): EntryKey => ["workbench", "detail", "turn", traceId],
|
||||||
|
detailTraceEventsRoot: (): EntryKey => ["workbench", "detail", "trace-events"],
|
||||||
|
detailTraceEvents: (traceId: string, input: WorkbenchTraceEventsKeyInput = {}): EntryKey => ["workbench", "detail", "trace-events", traceId, normalizeKeyObject({ afterProjectedSeq: finiteNumber(input.afterProjectedSeq), limit: finiteNumber(input.limit) })],
|
||||||
|
sessionsRoot: (): EntryKey => workbenchColadaKeys.snapshotSessionsRoot(),
|
||||||
|
sessions: (input: WorkbenchSessionsKeyInput = {}): EntryKey => workbenchColadaKeys.snapshotSessions(input),
|
||||||
|
sessionRoot: (): EntryKey => workbenchColadaKeys.snapshotSessionRoot(),
|
||||||
|
session: (sessionId: string): EntryKey => workbenchColadaKeys.snapshotSession(sessionId),
|
||||||
|
sessionMessagesRoot: (): EntryKey => workbenchColadaKeys.historySessionMessagesRoot(),
|
||||||
|
sessionMessages: (sessionId: string, input: WorkbenchSessionMessagesKeyInput = {}): EntryKey => workbenchColadaKeys.historySessionMessages(sessionId, input),
|
||||||
|
turnRoot: (): EntryKey => workbenchColadaKeys.detailTurnRoot(),
|
||||||
|
turn: (traceId: string): EntryKey => workbenchColadaKeys.detailTurn(traceId),
|
||||||
|
traceEventsRoot: (): EntryKey => workbenchColadaKeys.detailTraceEventsRoot(),
|
||||||
|
traceEvents: (traceId: string, input: WorkbenchTraceEventsKeyInput = {}): EntryKey => workbenchColadaKeys.detailTraceEvents(traceId, input),
|
||||||
providerProfiles: (): EntryKey => ["workbench", "provider-profiles"],
|
providerProfiles: (): EntryKey => ["workbench", "provider-profiles"],
|
||||||
mutationSubmit: (): EntryKey => ["workbench", "submit"],
|
mutationSubmit: (): EntryKey => ["workbench", "submit"],
|
||||||
mutationSteer: (): EntryKey => ["workbench", "steer"],
|
mutationSteer: (): EntryKey => ["workbench", "steer"],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPEC: pikasTech/HWLAB#2345 Workbench Pinia Colada migration.
|
// SPEC: pikasTech/HWLAB#2345 Workbench Pinia Colada migration; PJ2026-010401080313 Workbench实时权威 draft-2026-07-08-p0-workbench-realtime-authority-v2.
|
||||||
// Responsibility: Colada-governed Workbench mutation execution and query invalidation.
|
// Responsibility: Colada-governed Workbench mutation execution and semantic query invalidation.
|
||||||
|
|
||||||
import { useMutation, useQueryCache } from "@pinia/colada";
|
import { useMutation, useQueryCache } from "@pinia/colada";
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
@@ -35,7 +35,7 @@ export function useWorkbenchColadaMutations(): WorkbenchColadaMutations {
|
|||||||
const createSessionMutation = useMutation<ApiResult<{ session?: Record<string, unknown> }>, Record<string, unknown> | undefined>({
|
const createSessionMutation = useMutation<ApiResult<{ session?: Record<string, unknown> }>, Record<string, unknown> | undefined>({
|
||||||
key: workbenchColadaKeys.mutationCreateSession(),
|
key: workbenchColadaKeys.mutationCreateSession(),
|
||||||
mutation: (payload) => api.agent.createAgentSession(payload ?? {}),
|
mutation: (payload) => api.agent.createAgentSession(payload ?? {}),
|
||||||
onSettled: () => { void queryCache.invalidateQueries({ key: workbenchColadaKeys.sessionsRoot() }); }
|
onSettled: () => { void queryCache.invalidateQueries({ key: workbenchColadaKeys.snapshotRoot() }); }
|
||||||
});
|
});
|
||||||
const submitMutation = useMutation<ApiResult<AgentChatResponse>, WorkbenchAgentMutationInput>({
|
const submitMutation = useMutation<ApiResult<AgentChatResponse>, WorkbenchAgentMutationInput>({
|
||||||
key: workbenchColadaKeys.mutationSubmit(),
|
key: workbenchColadaKeys.mutationSubmit(),
|
||||||
@@ -69,10 +69,9 @@ export function useWorkbenchColadaMutations(): WorkbenchColadaMutations {
|
|||||||
|
|
||||||
async function invalidateMutationScope(queryCache: ReturnType<typeof useQueryCache>, sessionId: string | null | undefined, traceId: string | null | undefined): Promise<void> {
|
async function invalidateMutationScope(queryCache: ReturnType<typeof useQueryCache>, sessionId: string | null | undefined, traceId: string | null | undefined): Promise<void> {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
queryCache.invalidateQueries({ key: workbenchColadaKeys.sessionsRoot() }),
|
queryCache.invalidateQueries({ key: workbenchColadaKeys.syncRoot() }),
|
||||||
sessionId ? queryCache.invalidateQueries({ key: workbenchColadaKeys.session(sessionId) }) : Promise.resolve(),
|
queryCache.invalidateQueries({ key: workbenchColadaKeys.snapshotRoot() }),
|
||||||
sessionId ? queryCache.invalidateQueries({ key: workbenchColadaKeys.sessionMessagesRoot().concat(sessionId) }) : Promise.resolve(),
|
sessionId ? queryCache.invalidateQueries({ key: workbenchColadaKeys.historySessionMessagesRoot().concat(sessionId) }) : Promise.resolve(),
|
||||||
traceId ? queryCache.invalidateQueries({ key: workbenchColadaKeys.turn(traceId) }) : Promise.resolve(),
|
traceId ? queryCache.invalidateQueries({ key: workbenchColadaKeys.detailRoot() }) : Promise.resolve()
|
||||||
traceId ? queryCache.invalidateQueries({ key: workbenchColadaKeys.traceEventsRoot().concat(traceId) }) : Promise.resolve()
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
// SPEC: pikasTech/HWLAB#2345 Workbench Pinia Colada migration.
|
// SPEC: pikasTech/HWLAB#2345 Workbench Pinia Colada migration; PJ2026-010401080313 Workbench实时权威 draft-2026-07-08-p0-workbench-realtime-authority-v2.
|
||||||
// Responsibility: Colada-governed Workbench read-model query execution, cache keys, invalidation and refetch.
|
// Responsibility: Colada-governed Workbench semantic query execution, cache keys, invalidation and refetch.
|
||||||
|
|
||||||
import { useQueryCache, type EntryKey, type QueryCache } from "@pinia/colada";
|
import { useQueryCache, type EntryKey, type QueryCache } from "@pinia/colada";
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
import type { ApiRequestOptions } from "@/api/client";
|
import type { ApiRequestOptions } from "@/api/client";
|
||||||
|
import { fetchWorkbenchSyncReplay, type WorkbenchSyncReplayRequest, type WorkbenchSyncReplayResponse } from "@/api/workbench-events";
|
||||||
import type { AgentChatResultResponse, ApiResult } from "@/types";
|
import type { AgentChatResultResponse, ApiResult } from "@/types";
|
||||||
import type { SessionListOptions, SessionMessageOptions, WorkbenchMessagePageResponse, WorkbenchSessionDetailResponse, WorkbenchSessionListResponse, WorkbenchTraceRequestOptions } from "@/api/workbench";
|
import type { SessionListOptions, SessionMessageOptions, WorkbenchMessagePageResponse, WorkbenchSessionDetailResponse, WorkbenchSessionListResponse, WorkbenchTraceRequestOptions } from "@/api/workbench";
|
||||||
import { workbenchColadaKeys } from "./workbench-colada-keys";
|
import { workbenchColadaKeys } from "./workbench-colada-keys";
|
||||||
@@ -32,6 +33,7 @@ export interface WorkbenchColadaQueries {
|
|||||||
queryCache: QueryCache;
|
queryCache: QueryCache;
|
||||||
fetchSessions: (options?: SessionListOptions & QueryRunOptions) => Promise<ApiResult<WorkbenchSessionListResponse>>;
|
fetchSessions: (options?: SessionListOptions & QueryRunOptions) => Promise<ApiResult<WorkbenchSessionListResponse>>;
|
||||||
fetchSession: (sessionId: string, options?: WorkbenchSessionDetailQueryOptions) => Promise<ApiResult<WorkbenchSessionDetailResponse>>;
|
fetchSession: (sessionId: string, options?: WorkbenchSessionDetailQueryOptions) => Promise<ApiResult<WorkbenchSessionDetailResponse>>;
|
||||||
|
fetchSyncReplay: (input: WorkbenchSyncReplayRequest, options?: ApiRequestOptions & QueryRunOptions) => Promise<ApiResult<WorkbenchSyncReplayResponse>>;
|
||||||
fetchSessionMessages: (sessionId: string, options?: SessionMessageOptions & QueryRunOptions) => Promise<ApiResult<WorkbenchMessagePageResponse>>;
|
fetchSessionMessages: (sessionId: string, options?: SessionMessageOptions & QueryRunOptions) => Promise<ApiResult<WorkbenchMessagePageResponse>>;
|
||||||
fetchTurn: (traceId: string, options?: WorkbenchTurnQueryOptions) => Promise<ApiResult<AgentChatResultResponse>>;
|
fetchTurn: (traceId: string, options?: WorkbenchTurnQueryOptions) => Promise<ApiResult<AgentChatResultResponse>>;
|
||||||
fetchTraceEvents: (traceId: string, options?: WorkbenchTraceEventsQueryOptions) => Promise<ApiResult<AgentChatResultResponse>>;
|
fetchTraceEvents: (traceId: string, options?: WorkbenchTraceEventsQueryOptions) => Promise<ApiResult<AgentChatResultResponse>>;
|
||||||
@@ -51,6 +53,7 @@ export function useWorkbenchColadaQueries(): WorkbenchColadaQueries {
|
|||||||
queryCache,
|
queryCache,
|
||||||
fetchSessions: (options = {}) => runWorkbenchQuery(queryCache, workbenchColadaKeys.sessions(options), () => api.workbench.sessions(options), options),
|
fetchSessions: (options = {}) => runWorkbenchQuery(queryCache, workbenchColadaKeys.sessions(options), () => api.workbench.sessions(options), options),
|
||||||
fetchSession: (sessionId, options = {}) => runWorkbenchQuery(queryCache, workbenchColadaKeys.session(sessionId), () => api.workbench.session(sessionId, { timeoutMs: options.timeoutMs ?? null, includeMessages: false }), options),
|
fetchSession: (sessionId, options = {}) => runWorkbenchQuery(queryCache, workbenchColadaKeys.session(sessionId), () => api.workbench.session(sessionId, { timeoutMs: options.timeoutMs ?? null, includeMessages: false }), options),
|
||||||
|
fetchSyncReplay: (input, options = {}) => runWorkbenchQuery(queryCache, workbenchColadaKeys.syncReplay(input), () => fetchWorkbenchSyncReplay(input, options), options),
|
||||||
fetchSessionMessages: (sessionId, options = {}) => runWorkbenchQuery(queryCache, workbenchColadaKeys.sessionMessages(sessionId, options), () => api.workbench.sessionMessages(sessionId, options), options),
|
fetchSessionMessages: (sessionId, options = {}) => runWorkbenchQuery(queryCache, workbenchColadaKeys.sessionMessages(sessionId, options), () => api.workbench.sessionMessages(sessionId, options), options),
|
||||||
fetchTurn: (traceId, options = {}) => runWorkbenchQuery(queryCache, workbenchColadaKeys.turn(traceId), () => api.workbench.turn(traceId, options.timeoutMs ?? 8000, options.activityRef), options),
|
fetchTurn: (traceId, options = {}) => runWorkbenchQuery(queryCache, workbenchColadaKeys.turn(traceId), () => api.workbench.turn(traceId, options.timeoutMs ?? 8000, options.activityRef), options),
|
||||||
fetchTraceEvents: (traceId, options = {}) => runWorkbenchQuery(queryCache, workbenchColadaKeys.traceEvents(traceId, options), () => api.workbench.traceEvents(traceId, options.timeoutMs ?? 8000, options.activityRef, { afterProjectedSeq: options.afterProjectedSeq, limit: options.limit }), options),
|
fetchTraceEvents: (traceId, options = {}) => runWorkbenchQuery(queryCache, workbenchColadaKeys.traceEvents(traceId, options), () => api.workbench.traceEvents(traceId, options.timeoutMs ?? 8000, options.activityRef, { afterProjectedSeq: options.afterProjectedSeq, limit: options.limit }), options),
|
||||||
@@ -61,11 +64,10 @@ export function useWorkbenchColadaQueries(): WorkbenchColadaQueries {
|
|||||||
invalidateTurn: (traceId) => traceId ? queryCache.invalidateQueries({ key: workbenchColadaKeys.turn(traceId) }) : Promise.resolve(),
|
invalidateTurn: (traceId) => traceId ? queryCache.invalidateQueries({ key: workbenchColadaKeys.turn(traceId) }) : Promise.resolve(),
|
||||||
invalidateTraceEvents: (traceId) => traceId ? queryCache.invalidateQueries({ key: workbenchColadaKeys.traceEventsRoot().concat(traceId) }) : Promise.resolve(),
|
invalidateTraceEvents: (traceId) => traceId ? queryCache.invalidateQueries({ key: workbenchColadaKeys.traceEventsRoot().concat(traceId) }) : Promise.resolve(),
|
||||||
invalidateTraceScope: async (input) => Promise.all([
|
invalidateTraceScope: async (input) => Promise.all([
|
||||||
queryCache.invalidateQueries({ key: workbenchColadaKeys.sessionsRoot() }),
|
queryCache.invalidateQueries({ key: workbenchColadaKeys.syncRoot() }),
|
||||||
input.sessionId ? queryCache.invalidateQueries({ key: workbenchColadaKeys.session(input.sessionId) }) : Promise.resolve(),
|
queryCache.invalidateQueries({ key: workbenchColadaKeys.snapshotRoot() }),
|
||||||
input.sessionId ? queryCache.invalidateQueries({ key: workbenchColadaKeys.sessionMessagesRoot().concat(input.sessionId) }) : Promise.resolve(),
|
input.sessionId ? queryCache.invalidateQueries({ key: workbenchColadaKeys.historySessionMessagesRoot().concat(input.sessionId) }) : Promise.resolve(),
|
||||||
input.traceId ? queryCache.invalidateQueries({ key: workbenchColadaKeys.turn(input.traceId) }) : Promise.resolve(),
|
input.traceId ? queryCache.invalidateQueries({ key: workbenchColadaKeys.detailRoot() }) : Promise.resolve()
|
||||||
input.traceId ? queryCache.invalidateQueries({ key: workbenchColadaKeys.traceEventsRoot().concat(input.traceId) }) : Promise.resolve()
|
|
||||||
])
|
])
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { PiniaColada, useQueryCache } from "@pinia/colada";
|
|||||||
import { createWorkbenchServerState, type WorkbenchServerState } from "./workbench-server-state";
|
import { createWorkbenchServerState, type WorkbenchServerState } from "./workbench-server-state";
|
||||||
import { workbenchColadaKeys } from "./workbench-colada-keys";
|
import { workbenchColadaKeys } from "./workbench-colada-keys";
|
||||||
import { useWorkbenchColadaReducer } from "./workbench-colada-reducer";
|
import { useWorkbenchColadaReducer } from "./workbench-colada-reducer";
|
||||||
|
import { workbenchSyncReplayDiagnostic, workbenchSyncReplayEvents } from "./workbench-realtime-authority";
|
||||||
|
|
||||||
const storeDir = path.dirname(fileURLToPath(import.meta.url));
|
const storeDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
@@ -23,9 +24,10 @@ function installColadaContext(): ReturnType<typeof createApp> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test("workbench colada keys keep serializable hierarchical cache scopes", () => {
|
test("workbench colada keys keep serializable hierarchical cache scopes", () => {
|
||||||
assert.deepEqual(workbenchColadaKeys.sessions({ limit: 50, includeSessionId: "ses_colada", cursor: "cur_2" }), ["workbench", "sessions", { cursor: "cur_2", includeSessionId: "ses_colada", limit: 50 }]);
|
assert.deepEqual(workbenchColadaKeys.sessions({ limit: 50, includeSessionId: "ses_colada", cursor: "cur_2" }), ["workbench", "snapshot", "sessions", { cursor: "cur_2", includeSessionId: "ses_colada", limit: 50 }]);
|
||||||
assert.deepEqual(workbenchColadaKeys.sessionMessages("ses_colada", { limit: 20 }), ["workbench", "session-messages", "ses_colada", { cursor: null, limit: 20 }]);
|
assert.deepEqual(workbenchColadaKeys.syncReplay({ sessionId: "ses_colada", traceId: "trc_colada", since: 42 }), ["workbench", "sync", "replay", { sessionId: "ses_colada", since: 42, traceId: "trc_colada" }]);
|
||||||
assert.deepEqual(workbenchColadaKeys.traceEvents("trc_colada", { afterProjectedSeq: 4, limit: 25 }), ["workbench", "trace-events", "trc_colada", { afterProjectedSeq: 4, limit: 25 }]);
|
assert.deepEqual(workbenchColadaKeys.sessionMessages("ses_colada", { limit: 20 }), ["workbench", "history", "session-messages", "ses_colada", { cursor: null, limit: 20 }]);
|
||||||
|
assert.deepEqual(workbenchColadaKeys.traceEvents("trc_colada", { afterProjectedSeq: 4, limit: 25 }), ["workbench", "detail", "trace-events", "trc_colada", { afterProjectedSeq: 4, limit: 25 }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("workbench colada reducer stores projection state in the query cache", () => {
|
test("workbench colada reducer stores projection state in the query cache", () => {
|
||||||
@@ -50,3 +52,53 @@ test("workbench colada read path preserves freshness governance", () => {
|
|||||||
assert.match(source, /const state = await queryCache\.refresh\(entry\);/u);
|
assert.match(source, /const state = await queryCache\.refresh\(entry\);/u);
|
||||||
assert.doesNotMatch(source, /queryCache\.fetch\(entry/u);
|
assert.doesNotMatch(source, /queryCache\.fetch\(entry/u);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("workbench colada sync replay is the only live repair query scope", () => {
|
||||||
|
const querySource = fs.readFileSync(path.join(storeDir, "workbench-colada-queries.ts"), "utf8");
|
||||||
|
const mutationSource = fs.readFileSync(path.join(storeDir, "workbench-colada-mutations.ts"), "utf8");
|
||||||
|
const storeSource = fs.readFileSync(path.join(storeDir, "workbench.ts"), "utf8");
|
||||||
|
|
||||||
|
assert.match(querySource, /fetchSyncReplay: \(input, options = \{\}\) => runWorkbenchQuery\(queryCache, workbenchColadaKeys\.syncReplay\(input\),/u);
|
||||||
|
assert.match(storeSource, /workbenchColadaQueries\.fetchSyncReplay\(\{ sessionId, traceId, since: sinceOutboxSeq \}/u);
|
||||||
|
assert.doesNotMatch(storeSource, /import \{ fetchWorkbenchSyncReplay \} from "@\/api\/workbench-events"/u);
|
||||||
|
|
||||||
|
assert.match(mutationSource, /workbenchColadaKeys\.syncRoot\(\)/u);
|
||||||
|
assert.match(mutationSource, /workbenchColadaKeys\.snapshotRoot\(\)/u);
|
||||||
|
assert.match(mutationSource, /workbenchColadaKeys\.historySessionMessagesRoot\(\)/u);
|
||||||
|
assert.match(mutationSource, /workbenchColadaKeys\.detailRoot\(\)/u);
|
||||||
|
assert.doesNotMatch(mutationSource, /workbenchColadaKeys\.traceEventsRoot\(\)\.concat/u);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("workbench sync replay diagnostic exposes authority and terminal seal metadata", () => {
|
||||||
|
const payload = {
|
||||||
|
contractVersion: "workbench-sync-v1",
|
||||||
|
realtimeAuthority: "workbench-realtime-authority-v2",
|
||||||
|
cursor: { outboxSeq: 12 },
|
||||||
|
events: [{
|
||||||
|
type: "workbench.turn.snapshot",
|
||||||
|
realtimeAuthority: "workbench-realtime-authority-v2",
|
||||||
|
entity: { family: "turn", id: "trc_sync", version: 3, outboxSeq: 12, projectionRevision: "rev_sync" },
|
||||||
|
turn: { traceId: "trc_sync", status: "completed", terminal: true }
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
const events = workbenchSyncReplayEvents(payload);
|
||||||
|
const diagnostic = workbenchSyncReplayDiagnostic(payload, events, { reason: "sse-gap", sinceOutboxSeq: 7 });
|
||||||
|
|
||||||
|
assert.deepEqual(diagnostic, {
|
||||||
|
code: "workbench_sync_replay_applied",
|
||||||
|
reason: "sse-gap",
|
||||||
|
eventCount: 1,
|
||||||
|
sinceOutboxSeq: 7,
|
||||||
|
syncCursorOutboxSeq: 12,
|
||||||
|
contractVersion: "workbench-sync-v1",
|
||||||
|
realtimeAuthority: "workbench-realtime-authority-v2",
|
||||||
|
entityFamilyCount: 1,
|
||||||
|
entityFamilies: "turn",
|
||||||
|
maxEntityVersion: 3,
|
||||||
|
projectionRevision: "rev_sync",
|
||||||
|
terminalSeal: true,
|
||||||
|
detailProjection: false,
|
||||||
|
valuesRedacted: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -96,6 +96,34 @@ export function workbenchSyncReplayEvents(payload: WorkbenchSyncReplayResponse |
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function workbenchSyncReplayDiagnostic(payload: WorkbenchSyncReplayResponse | null | undefined, events: WorkbenchRealtimeEvent[], input: { reason: string; sinceOutboxSeq?: number | null }): Record<string, unknown> {
|
||||||
|
const eventRows = Array.isArray(events) ? events : [];
|
||||||
|
const cursor = recordValue(payload?.cursor);
|
||||||
|
const entities = eventRows.map(workbenchRealtimeEntityAuthority).filter((item): item is WorkbenchRealtimeEntityAuthority => item !== null);
|
||||||
|
const families = Array.from(new Set(entities.map((item) => item.family).filter(Boolean))).sort();
|
||||||
|
const versions = entities.map((item) => item.version).filter((value) => Number.isFinite(value));
|
||||||
|
const projectionRevision = firstString(
|
||||||
|
...entities.map((item) => item.projectionRevision),
|
||||||
|
...eventRows.map((event) => stringValue(event.projectionRevision ?? event.entity?.projectionRevision))
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
code: "workbench_sync_replay_applied",
|
||||||
|
reason: input.reason,
|
||||||
|
eventCount: eventRows.length,
|
||||||
|
sinceOutboxSeq: finiteNumber(input.sinceOutboxSeq),
|
||||||
|
syncCursorOutboxSeq: finiteNumber(cursor?.outboxSeq ?? payload?.cursor?.outboxSeq),
|
||||||
|
contractVersion: stringValue(payload?.contractVersion),
|
||||||
|
realtimeAuthority: stringValue(payload?.realtimeAuthority),
|
||||||
|
entityFamilyCount: families.length,
|
||||||
|
entityFamilies: families.slice(0, 8).join(","),
|
||||||
|
maxEntityVersion: versions.length > 0 ? Math.max(...versions) : null,
|
||||||
|
projectionRevision,
|
||||||
|
terminalSeal: eventRows.some(workbenchRealtimeEventHasTerminalSeal),
|
||||||
|
detailProjection: eventRows.some((event) => workbenchRealtimeDetailOnly(event, workbenchRealtimeEntityAuthority(event))),
|
||||||
|
valuesRedacted: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function authorityFailureCode(event: WorkbenchRealtimeEvent, entity: WorkbenchRealtimeEntityAuthority | null, detailProjection: boolean): string | null {
|
function authorityFailureCode(event: WorkbenchRealtimeEvent, entity: WorkbenchRealtimeEntityAuthority | null, detailProjection: boolean): string | null {
|
||||||
if (detailProjection) return "workbench_realtime_detail_only_rejected";
|
if (detailProjection) return "workbench_realtime_detail_only_rejected";
|
||||||
if (stringValue(event.realtimeAuthority) !== WORKBENCH_REALTIME_AUTHORITY_VERSION) return "workbench_realtime_authority_missing";
|
if (stringValue(event.realtimeAuthority) !== WORKBENCH_REALTIME_AUTHORITY_VERSION) return "workbench_realtime_authority_missing";
|
||||||
@@ -114,6 +142,35 @@ function syncEventKey(event: WorkbenchRealtimeEvent): string {
|
|||||||
return [event.type ?? "~", event.traceId ?? "~", event.sessionId ?? "~", event.cursor?.outboxSeq ?? event.outboxSeq ?? "~", event.cursor?.traceSeq ?? event.traceSeq ?? "~"].join("|");
|
return [event.type ?? "~", event.traceId ?? "~", event.sessionId ?? "~", event.cursor?.outboxSeq ?? event.outboxSeq ?? "~", event.cursor?.traceSeq ?? event.traceSeq ?? "~"].join("|");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function workbenchRealtimeEventHasTerminalSeal(event: WorkbenchRealtimeEvent): boolean {
|
||||||
|
const turn = recordValue(event.turn);
|
||||||
|
if (turn?.terminal === true) return true;
|
||||||
|
const message = recordValue(event.message);
|
||||||
|
const snapshot = recordValue(event.snapshot);
|
||||||
|
const traceEvent = recordValue(event.event);
|
||||||
|
return [
|
||||||
|
event.status,
|
||||||
|
turn?.status,
|
||||||
|
message?.status,
|
||||||
|
snapshot?.status,
|
||||||
|
traceEvent?.status,
|
||||||
|
traceEvent?.type,
|
||||||
|
].some(isTerminalStatusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTerminalStatusText(value: unknown): boolean {
|
||||||
|
const text = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-");
|
||||||
|
return /^(completed|complete|succeeded|success|failed|failure|error|canceled|cancelled|done|terminal|sealed|thread-resume-failed)$/u.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstString(...values: unknown[]): string | null {
|
||||||
|
for (const value of values) {
|
||||||
|
const text = stringValue(value);
|
||||||
|
if (text) return text;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function arrayOfRecords(value: unknown): Record<string, unknown>[] {
|
function arrayOfRecords(value: unknown): Record<string, unknown>[] {
|
||||||
return Array.isArray(value) ? value.filter((item): item is Record<string, unknown> => Boolean(recordValue(item))) : [];
|
return Array.isArray(value) ? value.filter((item): item is Record<string, unknown> => Boolean(recordValue(item))) : [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
import { computed, nextTick, ref } from "vue";
|
import { computed, nextTick, ref } from "vue";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
import { fetchWorkbenchSyncReplay } from "@/api/workbench-events";
|
|
||||||
import { workbenchRuntimePolicy } from "@/config/workbench-runtime-policy";
|
import { workbenchRuntimePolicy } from "@/config/workbench-runtime-policy";
|
||||||
import { createKeyedSingleflight } from "@/utils/scheduler/keyed-singleflight";
|
import { createKeyedSingleflight } from "@/utils/scheduler/keyed-singleflight";
|
||||||
import { createWorkbenchHealthProbeCache } from "@/utils/workbench-health";
|
import { createWorkbenchHealthProbeCache } from "@/utils/workbench-health";
|
||||||
@@ -68,7 +67,7 @@ import {
|
|||||||
traceSnapshotError
|
traceSnapshotError
|
||||||
} from "./workbench-message-projection-runtime";
|
} from "./workbench-message-projection-runtime";
|
||||||
import { planWorkbenchRealtimeApply, planWorkbenchRealtimeRecovery, type WorkbenchRealtimeApplyStep, type WorkbenchRealtimeRecoveryStep } from "./workbench-realtime-plan";
|
import { planWorkbenchRealtimeApply, planWorkbenchRealtimeRecovery, type WorkbenchRealtimeApplyStep, type WorkbenchRealtimeRecoveryStep } from "./workbench-realtime-plan";
|
||||||
import { workbenchSyncReplayEvents } from "./workbench-realtime-authority";
|
import { workbenchSyncReplayDiagnostic, workbenchSyncReplayEvents } from "./workbench-realtime-authority";
|
||||||
import { useWorkbenchColadaMutations } from "./workbench-colada-mutations";
|
import { useWorkbenchColadaMutations } from "./workbench-colada-mutations";
|
||||||
import { useWorkbenchColadaQueries } from "./workbench-colada-queries";
|
import { useWorkbenchColadaQueries } from "./workbench-colada-queries";
|
||||||
import { useWorkbenchColadaReducer } from "./workbench-colada-reducer";
|
import { useWorkbenchColadaReducer } from "./workbench-colada-reducer";
|
||||||
@@ -1151,14 +1150,14 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshWorkbenchSyncReplay(sessionId: string | null, traceId: string | null, sinceOutboxSeq: number | null, reason: string): Promise<void> {
|
async function refreshWorkbenchSyncReplay(sessionId: string | null, traceId: string | null, sinceOutboxSeq: number | null, reason: string): Promise<void> {
|
||||||
const result = await fetchWorkbenchSyncReplay({ sessionId, traceId, since: sinceOutboxSeq }, { timeoutMs: 8000, activityRef: () => activityRef.value });
|
const result = await workbenchColadaQueries.fetchSyncReplay({ sessionId, traceId, since: sinceOutboxSeq }, { timeoutMs: 8000, activityRef: () => activityRef.value });
|
||||||
if (!result.ok || !result.data) {
|
if (!result.ok || !result.data) {
|
||||||
recordWorkbenchRuntimeDiagnostic({ module: "workbench-sync-replay", sessionId, traceId, outcome: "network", diagnostic: { code: "workbench_sync_replay_failed", reason, status: result.status, apiError: result.apiError, valuesRedacted: true } });
|
recordWorkbenchRuntimeDiagnostic({ module: "workbench-sync-replay", sessionId, traceId, outcome: "network", diagnostic: { code: "workbench_sync_replay_failed", reason, status: result.status, apiError: result.apiError, valuesRedacted: true } });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const events = workbenchSyncReplayEvents(result.data);
|
const events = workbenchSyncReplayEvents(result.data);
|
||||||
for (const event of events) applyRealtimeEvent(event, realtimeEventName(event));
|
for (const event of events) applyRealtimeEvent(event, realtimeEventName(event));
|
||||||
recordWorkbenchRuntimeDiagnostic({ module: "workbench-sync-replay", sessionId, traceId, outcome: "ok", diagnostic: { code: "workbench_sync_replay_applied", reason, eventCount: events.length, sinceOutboxSeq, valuesRedacted: true } });
|
recordWorkbenchRuntimeDiagnostic({ module: "workbench-sync-replay", sessionId, traceId, outcome: "ok", diagnostic: workbenchSyncReplayDiagnostic(result.data, events, { reason, sinceOutboxSeq }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleActiveTraceRestGapFill(traceId: string | null | undefined, reason: string, delayMs = runtimePolicy.workbenchActiveTraceRestGapFillInitialMs): void {
|
function scheduleActiveTraceRestGapFill(traceId: string | null | undefined, reason: string, delayMs = runtimePolicy.workbenchActiveTraceRestGapFillInitialMs): void {
|
||||||
|
|||||||
@@ -58,6 +58,16 @@ interface WorkbenchPerformanceEvent {
|
|||||||
diagnosticCode?: string;
|
diagnosticCode?: string;
|
||||||
rootCause?: string;
|
rootCause?: string;
|
||||||
recoveryAction?: string;
|
recoveryAction?: string;
|
||||||
|
contractVersion?: string;
|
||||||
|
realtimeAuthority?: string;
|
||||||
|
sinceOutboxSeq?: number;
|
||||||
|
syncCursorOutboxSeq?: number;
|
||||||
|
entityFamilyCount?: number;
|
||||||
|
entityFamilies?: string;
|
||||||
|
maxEntityVersion?: number;
|
||||||
|
projectionRevision?: string;
|
||||||
|
terminalSeal?: boolean;
|
||||||
|
detailProjection?: boolean;
|
||||||
transportState?: string;
|
transportState?: string;
|
||||||
scopedKey?: string;
|
scopedKey?: string;
|
||||||
eventCount?: number;
|
eventCount?: number;
|
||||||
@@ -379,6 +389,16 @@ export function recordWorkbenchRuntimeDiagnostic(input: { module?: string | null
|
|||||||
diagnosticCode: diagnosticLabel(code),
|
diagnosticCode: diagnosticLabel(code),
|
||||||
rootCause: diagnosticText(diagnostic, "rootCause", "reason")?.slice(0, 160),
|
rootCause: diagnosticText(diagnostic, "rootCause", "reason")?.slice(0, 160),
|
||||||
recoveryAction: diagnosticText(diagnostic, "recoveryAction")?.slice(0, 160),
|
recoveryAction: diagnosticText(diagnostic, "recoveryAction")?.slice(0, 160),
|
||||||
|
contractVersion: diagnosticLabel(diagnosticText(diagnostic, "contractVersion")),
|
||||||
|
realtimeAuthority: diagnosticLabel(diagnosticText(diagnostic, "realtimeAuthority", "authority")),
|
||||||
|
sinceOutboxSeq: diagnosticNumber(diagnostic, "sinceOutboxSeq"),
|
||||||
|
syncCursorOutboxSeq: diagnosticNumber(diagnostic, "syncCursorOutboxSeq", "cursorOutboxSeq"),
|
||||||
|
entityFamilyCount: diagnosticNumber(diagnostic, "entityFamilyCount"),
|
||||||
|
entityFamilies: diagnosticText(diagnostic, "entityFamilies")?.slice(0, 160),
|
||||||
|
maxEntityVersion: diagnosticNumber(diagnostic, "maxEntityVersion", "entityVersion"),
|
||||||
|
projectionRevision: diagnosticText(diagnostic, "projectionRevision")?.slice(0, 120),
|
||||||
|
terminalSeal: diagnosticBoolean(diagnostic, "terminalSeal"),
|
||||||
|
detailProjection: diagnosticBoolean(diagnostic, "detailProjection"),
|
||||||
transportState: diagnosticLabel(diagnosticText(diagnostic, "transportState")),
|
transportState: diagnosticLabel(diagnosticText(diagnostic, "transportState")),
|
||||||
scopedKey: hashIdentifier(diagnosticText(diagnostic, "scopedKey"), "scope"),
|
scopedKey: hashIdentifier(diagnosticText(diagnostic, "scopedKey"), "scope"),
|
||||||
eventCount: diagnosticNumber(diagnostic, "eventCount"),
|
eventCount: diagnosticNumber(diagnostic, "eventCount"),
|
||||||
@@ -872,6 +892,20 @@ function diagnosticNumber(record: Record<string, unknown> | null, ...keys: strin
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function diagnosticBoolean(record: Record<string, unknown> | null, ...keys: string[]): boolean | undefined {
|
||||||
|
if (!record) return undefined;
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = record[key];
|
||||||
|
if (typeof value === "boolean") return value;
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const text = value.trim().toLowerCase();
|
||||||
|
if (text === "true") return true;
|
||||||
|
if (text === "false") return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function diagnosticLabel(value: unknown): string | undefined {
|
function diagnosticLabel(value: unknown): string | undefined {
|
||||||
const text = safeText(value).replace(/[^A-Za-z0-9_.:-]/gu, "_").slice(0, 80);
|
const text = safeText(value).replace(/[^A-Za-z0-9_.:-]/gu, "_").slice(0, 80);
|
||||||
return text || undefined;
|
return text || undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user