Skip to content

Commit e529adc

Browse files
committed
improvement(provenance): attribute stored-envelope display reads to their execution
A display materialization of an execution log imports the row's stored provenance envelopes into throwaway registries, and each import of an incomplete envelope re-emitted the registry's own summary — per envelope, per view, carrying counts and a workspace but never the execution id. A reader repeatedly materializing the same stored rows produced hundreds of identical lines that could not say which executions to go look at, and the volume scaled with views of a state that was fully recorded when the run wrote it. Verified against production before changing anything: essentially no new incomplete envelopes are being stored since the writer fix shipped, and no data drains exist — the stream is bounded re-reads of old rows through the display paths, not a live producer. The display registries are now staged — the existing concept for a registry that filters one value for a caller that reports against the real boundary — and each display function reports once per materialization with the execution id, workflow, workspace, and the parts that could not be vouched for. Severity is preserved: an incomplete stored envelope stays at warn, a malformed one stays at error. Projection behavior is unchanged everywhere — incomplete and malformed envelopes still fail their values closed exactly as before; only the reporting moves to the boundary that knows the execution.
1 parent 04380b7 commit e529adc

2 files changed

Lines changed: 220 additions & 9 deletions

File tree

apps/sim/lib/logs/execution/trace-store.test.ts

Lines changed: 145 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@
33
*/
44
import { beforeEach, describe, expect, it, vi } from 'vitest'
55

6-
const { decryptSecretMock, materializeLargeValueRefMock, storeLargeValueMock } = vi.hoisted(() => ({
7-
decryptSecretMock: vi.fn(),
8-
materializeLargeValueRefMock: vi.fn(),
9-
storeLargeValueMock: vi.fn(),
6+
const { decryptSecretMock, materializeLargeValueRefMock, storeLargeValueMock, mockLogger } =
7+
vi.hoisted(() => ({
8+
decryptSecretMock: vi.fn(),
9+
materializeLargeValueRefMock: vi.fn(),
10+
storeLargeValueMock: vi.fn(),
11+
mockLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
12+
}))
13+
14+
vi.mock('@sim/logger', () => ({
15+
createLogger: () => mockLogger,
1016
}))
1117

1218
vi.mock('@/lib/core/security/encryption', () => ({
@@ -536,3 +542,138 @@ describe('projectExecutionDataForDisplay provenance handling', () => {
536542
expect(displayData.traceSpans).toEqual([])
537543
})
538544
})
545+
546+
describe('stored provenance display reporting', () => {
547+
const REGISTRY_SUMMARY_MESSAGES = [
548+
'Resolved secret registry marked incomplete',
549+
'Resolved secret input path marked incomplete',
550+
]
551+
552+
function registrySummaryLines(): unknown[] {
553+
return [...mockLogger.warn.mock.calls, ...mockLogger.error.mock.calls].filter(([message]) =>
554+
REGISTRY_SUMMARY_MESSAGES.includes(message as string)
555+
)
556+
}
557+
558+
/**
559+
* The stored state was recorded when the run wrote it; a view re-deriving it must say which
560+
* execution it served, once — not restate the latch through registry summaries that name none.
561+
*/
562+
it('reports an incomplete stored envelope once, naming the execution and the parts', async () => {
563+
const displayData = await projectExecutionDataForDisplay(
564+
{
565+
finalOutput: { result: 'value' },
566+
executionState: {
567+
resolvedSecretTraceProvenance: { version: 1, complete: false, entries: [] },
568+
finalOutputResolvedSecretTraceProvenance: { version: 1, complete: false, entries: [] },
569+
},
570+
},
571+
CONTEXT
572+
)
573+
574+
expect(displayData).not.toHaveProperty('finalOutput')
575+
expect(registrySummaryLines()).toHaveLength(0)
576+
expect(mockLogger.warn).toHaveBeenCalledWith(
577+
'Stored execution provenance cannot vouch for display content',
578+
expect.objectContaining({
579+
site: 'traceStore.displayProjection',
580+
executionId: 'execution-1',
581+
workflowId: 'workflow-1',
582+
workspaceId: 'workspace-1',
583+
parts: ['traceSpans', 'finalOutput'],
584+
partCount: 2,
585+
})
586+
)
587+
expect(mockLogger.error).not.toHaveBeenCalled()
588+
})
589+
590+
it('reports a malformed stored envelope at error, keeping the value withheld', async () => {
591+
const displayData = await projectExecutionDataForDisplay(
592+
{
593+
finalOutput: { result: 'value' },
594+
executionState: {
595+
resolvedSecretTraceProvenance: {
596+
version: 1,
597+
complete: true,
598+
entries: [],
599+
scope: { userId: 'user-1', workspaceId: 'workspace-1' },
600+
},
601+
finalOutputResolvedSecretTraceProvenance: 'garbage',
602+
},
603+
},
604+
CONTEXT
605+
)
606+
607+
expect(displayData).not.toHaveProperty('finalOutput')
608+
expect(registrySummaryLines()).toHaveLength(0)
609+
expect(mockLogger.error).toHaveBeenCalledWith(
610+
'Stored execution provenance is malformed',
611+
expect.objectContaining({
612+
site: 'traceStore.displayProjection',
613+
executionId: 'execution-1',
614+
parts: ['finalOutput'],
615+
})
616+
)
617+
})
618+
619+
it('stays silent when every stored envelope is complete', async () => {
620+
const displayData = await projectExecutionDataForDisplay(
621+
{
622+
finalOutput: { result: 'direct-literal' },
623+
executionState: {
624+
resolvedSecretTraceProvenance: {
625+
version: 1,
626+
complete: true,
627+
entries: [],
628+
scope: { userId: 'user-1', workspaceId: 'workspace-1' },
629+
},
630+
finalOutputResolvedSecretTraceProvenance: {
631+
version: 1,
632+
complete: true,
633+
entries: [],
634+
scope: { userId: 'user-1', workspaceId: 'workspace-1' },
635+
},
636+
},
637+
},
638+
CONTEXT
639+
)
640+
641+
expect(displayData.finalOutput).toEqual({ result: 'direct-literal' })
642+
expect(mockLogger.warn).not.toHaveBeenCalled()
643+
expect(mockLogger.error).not.toHaveBeenCalled()
644+
})
645+
646+
it('reports incomplete block-output envelopes once for the whole block read', async () => {
647+
const result = await materializeExecutionDataForDisplayWithBlockOutputs(
648+
{
649+
executionState: {
650+
resolvedSecretTraceProvenance: {
651+
version: 1,
652+
complete: true,
653+
entries: [],
654+
scope: { userId: 'user-1', workspaceId: 'workspace-1' },
655+
},
656+
blockStates: {
657+
'block-1': {
658+
output: { value: 1 },
659+
resolvedSecretTraceProvenance: { version: 1, complete: false, entries: [] },
660+
},
661+
},
662+
},
663+
},
664+
CONTEXT,
665+
['block-1']
666+
)
667+
668+
expect(result.blockOutputs.has('block-1')).toBe(false)
669+
expect(registrySummaryLines()).toHaveLength(0)
670+
expect(mockLogger.warn).toHaveBeenCalledWith(
671+
'Stored execution provenance cannot vouch for display content',
672+
expect.objectContaining({
673+
site: 'traceStore.blockOutputs',
674+
executionId: 'execution-1',
675+
parts: ['blockOutput:block-1'],
676+
})
677+
)
678+
})
679+
})

apps/sim/lib/logs/execution/trace-store.ts

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -300,11 +300,16 @@ export async function materializeExecutionDataForDisplayWithBlockOutputs(
300300
return { executionData: displayData, blockOutputs: new Map() }
301301
}
302302

303+
const runProvenance =
304+
materialized[RESOLVED_SECRET_PROVENANCE_KEY] ?? executionState?.[RESOLVED_SECRET_PROVENANCE_KEY]
303305
const runRegistry = await importResolvedSecretTraceRegistry(
304-
materialized[RESOLVED_SECRET_PROVENANCE_KEY] ??
305-
executionState?.[RESOLVED_SECRET_PROVENANCE_KEY],
306+
runProvenance,
306307
'traceStore.blockOutputRunProvenance'
307308
)
309+
const provenanceFaults = new Map<string, StoredDisplayProvenanceFault>()
310+
if (isResolvedSecretTraceProvenanceV1(runProvenance) && !runProvenance.complete) {
311+
provenanceFaults.set('run', 'incomplete')
312+
}
308313
const blockOutputs = new Map<string, unknown>()
309314
const projectionStore = createReadOnlyProjectionStore(context)
310315

@@ -313,6 +318,12 @@ export async function materializeExecutionDataForDisplayWithBlockOutputs(
313318
if (!blockState || blockState.output === undefined) continue
314319

315320
const hasExactProvenance = Object.hasOwn(blockState, RESOLVED_SECRET_PROVENANCE_KEY)
321+
if (hasExactProvenance) {
322+
const blockProvenance = blockState[RESOLVED_SECRET_PROVENANCE_KEY]
323+
if (isResolvedSecretTraceProvenanceV1(blockProvenance) && !blockProvenance.complete) {
324+
provenanceFaults.set(`blockOutput:${blockId}`, 'incomplete')
325+
}
326+
}
316327
const registry = hasExactProvenance
317328
? await importResolvedSecretTraceRegistry(
318329
blockState[RESOLVED_SECRET_PROVENANCE_KEY],
@@ -338,6 +349,7 @@ export async function materializeExecutionDataForDisplayWithBlockOutputs(
338349
blockOutputs.set(blockId, projected.output.value)
339350
}
340351
}
352+
reportStoredDisplayProvenanceFaults('traceStore.blockOutputs', context, provenanceFaults)
341353

342354
return { executionData: displayData, blockOutputs }
343355
}
@@ -346,17 +358,67 @@ function readRecord(value: unknown): Record<string, unknown> | undefined {
346358
return isRecordLike(value) ? (value as Record<string, unknown>) : undefined
347359
}
348360

361+
/**
362+
* Staged: display registries filter stored values for one materialization and are discarded, and
363+
* their own mark-time summaries name no execution — the read boundary reports instead, through
364+
* {@link reportStoredDisplayProvenanceFaults}. A stored envelope's incompleteness is not an event
365+
* on this path; it was recorded when the run wrote it, and every later view re-derives it.
366+
*/
349367
async function importResolvedSecretTraceRegistry(
350368
provenance: unknown,
351369
origin: string
352370
): Promise<ResolvedSecretTraceRegistry | undefined> {
353371
if (!isResolvedSecretTraceProvenanceV1(provenance)) return undefined
354372

355-
const registry = new ResolvedSecretTraceRegistry([], provenance.scope)
373+
const registry = new ResolvedSecretTraceRegistry([], provenance.scope, { staged: true })
356374
await registry.importProvenance(provenance, { trusted: true, origin })
357375
return registry
358376
}
359377

378+
type StoredDisplayProvenanceFault = 'incomplete' | 'malformed'
379+
380+
const MAX_REPORTED_PROVENANCE_FAULT_PARTS = 20
381+
382+
/**
383+
* One attributed line per display materialization, in place of one registry summary per envelope
384+
* per view.
385+
*
386+
* The registry summaries these replace carried counts and a workspace but no execution id, so a
387+
* reader repeatedly materializing the same stored rows produced an unattributable stream — the
388+
* lines could not say which executions to go look at. Incomplete stays at warn (a stored state
389+
* being re-read); malformed stays at error (a stored envelope that cannot be parsed is a fault
390+
* wherever it is met, matching the level its registry reason carries elsewhere).
391+
*/
392+
function reportStoredDisplayProvenanceFaults(
393+
site: string,
394+
context: TraceStoreReadContext,
395+
faults: ReadonlyMap<string, StoredDisplayProvenanceFault>
396+
): void {
397+
if (faults.size === 0) return
398+
const partsByFault = { incomplete: [] as string[], malformed: [] as string[] }
399+
for (const [part, fault] of faults) partsByFault[fault].push(part)
400+
const details = {
401+
site,
402+
executionId: context.executionId,
403+
...(context.workflowId ? { workflowId: context.workflowId } : {}),
404+
...(context.workspaceId ? { workspaceId: context.workspaceId } : {}),
405+
}
406+
if (partsByFault.incomplete.length > 0) {
407+
logger.warn('Stored execution provenance cannot vouch for display content', {
408+
...details,
409+
parts: partsByFault.incomplete.slice(0, MAX_REPORTED_PROVENANCE_FAULT_PARTS),
410+
partCount: partsByFault.incomplete.length,
411+
})
412+
}
413+
if (partsByFault.malformed.length > 0) {
414+
logger.error('Stored execution provenance is malformed', {
415+
...details,
416+
parts: partsByFault.malformed.slice(0, MAX_REPORTED_PROVENANCE_FAULT_PARTS),
417+
partCount: partsByFault.malformed.length,
418+
})
419+
}
420+
}
421+
360422
function createReadOnlyProjectionStore(context: TraceStoreReadContext) {
361423
return {
362424
workspaceId: context.workspaceId ?? undefined,
@@ -426,6 +488,11 @@ export async function projectExecutionDataForDisplay(
426488

427489
const projectionStore = createReadOnlyProjectionStore(context)
428490

491+
const provenanceFaults = new Map<string, StoredDisplayProvenanceFault>()
492+
if (isResolvedSecretTraceProvenanceV1(provenance) && !provenance.complete) {
493+
provenanceFaults.set('traceSpans', 'incomplete')
494+
}
495+
429496
const exactValueProjections = new Map<string, unknown>()
430497
for (const [valueKey, provenanceKey] of Object.entries(EXACT_LOG_VALUE_PROVENANCE_KEYS)) {
431498
if (
@@ -438,14 +505,16 @@ export async function projectExecutionDataForDisplay(
438505

439506
const exactProvenance = executionState[provenanceKey]
440507
const exactRegistry = isResolvedSecretTraceProvenanceV1(exactProvenance)
441-
? new ResolvedSecretTraceRegistry([], exactProvenance.scope)
442-
: new ResolvedSecretTraceRegistry()
508+
? new ResolvedSecretTraceRegistry([], exactProvenance.scope, { staged: true })
509+
: new ResolvedSecretTraceRegistry([], undefined, { staged: true })
443510
if (isResolvedSecretTraceProvenanceV1(exactProvenance)) {
511+
if (!exactProvenance.complete) provenanceFaults.set(valueKey, 'incomplete')
444512
await exactRegistry.importProvenance(exactProvenance, {
445513
trusted: true,
446514
origin: 'traceStore.exactProvenance',
447515
})
448516
} else {
517+
provenanceFaults.set(valueKey, 'malformed')
449518
exactRegistry.markIncomplete('untrusted-provenance', { origin: 'traceStore.exactProvenance' })
450519
}
451520

@@ -467,6 +536,7 @@ export async function projectExecutionDataForDisplay(
467536
exactValueProjections.set(valueKey, projected.output.value)
468537
}
469538
}
539+
reportStoredDisplayProvenanceFaults('traceStore.displayProjection', context, provenanceFaults)
470540

471541
const envelope: Record<string, unknown> = {}
472542
for (const key of LOG_DISPLAY_CONTENT_KEYS) {

0 commit comments

Comments
 (0)