Skip to content

Commit 4bafd14

Browse files
improvement(provenance): name every guard that can latch a registry (#6513)
* fix(provenance): name every guard that can latch a registry A production latch reported `reason: "unspecified"` because 44 call sites took the default. The reason is the only thing that names which guard tripped, and a refusal surfaces many frames later as one fixed sentence, so an unnamed latch is undiagnosable — that is what left an incident's origin unidentified for a day. Give each call site a literal that names its guard, add the 20 new literals to the reason union, and sort them into the existing error/warn split: a guard that should not trip on a healthy run reports at error, everything else stays at warn. `log-creation-skipped` joins the by-design set since it fires on every run that does not persist a log. Make `reason` required on both `markIncomplete` and `markInputPathIncomplete`, so omission is a compile error rather than a silent `unspecified`. A caller with genuinely nothing to say now passes `'unspecified'` where a reviewer can see it. The three remaining bare calls are on ResolvedSecretTraceProvenanceAccumulator, a different class with no reason concept. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(provenance): pin the new reasons and the guard that latched in production Cover what the reason set is for rather than only that it compiles: the non-enumerable tool-params branch now asserts it names `tool-input-not-enumerable`, which is the guard the production logs showed reporting `unspecified`, and every new literal asserts which stream it reports on — error for a guard that cannot trip on a healthy run, warn for one reachable without a fault, silent for the by-design log-less session. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(provenance): make reason the only locator, not one of two Two fields had grown into competing answers to the same question. `origin` is a free-form label for which importer accepted an already-incomplete bundle; four latches had started passing `markIncomplete('unspecified', { origin })`, using it to stand in for a reason that did not exist yet. That splits one fact across a closed enum and an open string, leaving neither worth alerting on. Give those four the literal they were reaching for — none needed a new one — and split the five reasons that covered genuinely different guards, so the reason alone locates the site rather than needing an origin beside it. `origin` keeps its narrow job, now documented: it disambiguates importers that share one guard, and a latch that wants an origin because no reason fits should add a reason instead. No production call site passes 'unspecified' any more. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(provenance): stop two expected states from reporting as faults Cursor Bugbot caught `backfill-checkpoint-*`: the guard covered four conditions under one reason classified as an originating fault, and one of them — a state persisted before the checkpoint contract existed — is what essentially every legacy row looks like. A backfill over historical rows would have put one error line per row into the stream the error/warn split exists to protect. Auditing the rest of the error-level reasons for the same shape found a second: a client tool invoked without a run id has no binding to unseal against, so it took the `[null, null]` path and reported `client-tool-seal-failed` at error on an ordinary configuration. Split both along the line that matters — absent versus unusable, not attempted versus failed — and classify each half: expected states warn, genuine faults keep error. `backfill-scope-mismatch` is retired; it named one of its four conditions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 5f4dc19 commit 4bafd14

33 files changed

Lines changed: 253 additions & 76 deletions

apps/sim/app/api/knowledge/search/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
381381
results,
382382
})
383383
if (!resultProvenanceSnapshot.imported) {
384-
resultSecretRegistry.markIncomplete()
384+
resultSecretRegistry.markIncomplete('knowledge-result-provenance-unavailable')
385385
if (useReranker) {
386386
return NextResponse.json(
387387
{ error: 'Knowledge result secret provenance is unavailable' },
@@ -608,7 +608,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
608608
'knowledge'
609609
))
610610
) {
611-
resultSecretRegistry.markIncomplete()
611+
resultSecretRegistry.markIncomplete('knowledge-result-provenance-unavailable')
612612
}
613613
}
614614

apps/sim/app/api/knowledge/secret-provenance.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export async function createKnowledgeProvenanceResponse(options: {
154154
})
155155
for (const provenance of options.provenances) {
156156
if (provenance.status === 'unknown') {
157-
registry.markIncomplete()
157+
registry.markIncomplete('durable-provenance-unknown')
158158
break
159159
}
160160
const sourceRegistry = await createDurableSecretProvenanceRegistry(provenance, {

apps/sim/app/api/memory/secret-provenance.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export async function createMemoryResponse(options: {
9898
workspaceId: options.workspaceId,
9999
})
100100
if (options.memories.length > MAX_PRIVATE_MEMORY_CROSSINGS) {
101-
registry.markIncomplete()
101+
registry.markIncomplete('memory-crossing-capacity-exceeded')
102102
} else {
103103
const ids = [...new Set(options.memories.map((record) => record.id))]
104104
const memoriesById = new Map<string, MemoryCrossing[]>()
@@ -123,7 +123,7 @@ export async function createMemoryResponse(options: {
123123
provenanceEntryCount > MAX_PRIVATE_MEMORY_PROVENANCE_ENTRIES ||
124124
provenanceBytes > MAX_PRIVATE_MEMORY_PROVENANCE_BYTES
125125
) {
126-
registry.markIncomplete()
126+
registry.markIncomplete('memory-crossing-capacity-exceeded')
127127
break
128128
}
129129
const sidecarById = new Map(sidecars.map((sidecar) => [sidecar.memoryId, sidecar]))

apps/sim/app/api/workflows/[id]/log/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export const POST = withRouteHandler(
142142
: undefined
143143
const trustedProvenance = trustedExecutionState?.resolvedSecretTraceProvenance
144144
if (trustedProvenance === undefined) {
145-
resolvedSecretTraceRegistry.markIncomplete()
145+
resolvedSecretTraceRegistry.markIncomplete('restored-provenance-untrusted')
146146
} else {
147147
await resolvedSecretTraceRegistry.importProvenance(trustedProvenance, {
148148
trusted: true,

apps/sim/executor/handlers/agent/memory.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ describe('Memory', () => {
278278

279279
it('persists raw memory with unknown lineage when provenance is unavailable', async () => {
280280
const registry = new ResolvedSecretTraceRegistry()
281-
registry.markIncomplete()
281+
registry.markIncomplete('unspecified')
282282
const appendMessage = vi
283283
.spyOn(memoryService as any, 'appendMessage')
284284
.mockResolvedValue(undefined)
@@ -293,7 +293,7 @@ describe('Memory', () => {
293293

294294
it('seeds raw memory with unknown lineage when provenance is unavailable', async () => {
295295
const registry = new ResolvedSecretTraceRegistry()
296-
registry.markIncomplete()
296+
registry.markIncomplete('unspecified')
297297
const seedMemoryRecord = vi
298298
.spyOn(memoryService as any, 'seedMemoryRecord')
299299
.mockResolvedValue(undefined)

apps/sim/executor/handlers/generic/generic-handler.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@ export class GenericBlockHandler implements BlockHandler {
199199
boundary && boundary.paths.length > 0 && registry?.hasResolvedInputProjections()
200200
? registry.projectResolvedInputSelections(inputs)
201201
: undefined
202-
if (projectedInputs?.complete === false) registry?.markIncomplete()
202+
if (projectedInputs?.complete === false)
203+
registry?.markIncomplete('structural-input-projection-incomplete')
203204

204205
if (projectedInputs?.complete && boundary && tool && registry) {
205206
for (const projection of projectedInputs.values) {
@@ -233,7 +234,7 @@ export class GenericBlockHandler implements BlockHandler {
233234
continue
234235
}
235236
if (boundary.requiredProjectionRoots.has(projection.path[0])) {
236-
registry.markIncomplete()
237+
registry.markIncomplete('structural-input-root-unprojected')
237238
}
238239
continue
239240
}

apps/sim/executor/handlers/mothership/mothership-handler.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ async function consumeMothershipProvenance(
372372
return false
373373
}
374374
if (inspection.status === 'invalid') {
375-
registry?.markIncomplete()
375+
registry?.markIncomplete('mothership-provenance-invalid')
376376
throw new Error('Mothership response provenance metadata is invalid')
377377
}
378378

@@ -399,7 +399,7 @@ function inspectMothershipResponseCapability(
399399
return false
400400
}
401401

402-
registry?.markIncomplete()
402+
registry?.markIncomplete('mothership-provenance-invalid')
403403
throw new Error('Mothership response provenance metadata is invalid')
404404
}
405405

@@ -464,7 +464,7 @@ async function readMothershipExecuteResponse(
464464
result = (await response.json()) as MothershipExecuteResult
465465
} catch (error) {
466466
if (expectsProvenance) {
467-
registry?.markIncomplete()
467+
registry?.markIncomplete('mothership-response-unreadable')
468468
throw new Error('Mothership response provenance metadata is invalid')
469469
}
470470
throw error
@@ -528,7 +528,7 @@ async function readMothershipExecuteResponse(
528528
return finalResult
529529
} finally {
530530
if (expectsProvenance && !finalResult && !receivedTerminalProvenance) {
531-
registry?.markIncomplete()
531+
registry?.markIncomplete('mothership-provenance-missing')
532532
}
533533
reader.releaseLock()
534534
}
@@ -630,7 +630,7 @@ function createMothershipStreamingExecution(
630630
}
631631
} finally {
632632
if (expectsProvenance && !sawFinal && !receivedTerminalProvenance) {
633-
options.registry?.markIncomplete()
633+
options.registry?.markIncomplete('mothership-provenance-missing')
634634
}
635635
cleanup()
636636
reader?.releaseLock()
@@ -948,7 +948,7 @@ export class MothershipBlockHandler implements BlockHandler {
948948
try {
949949
payload = (await response.clone().json()) as MothershipExecuteResult
950950
} catch {
951-
resultRegistry?.markIncomplete()
951+
resultRegistry?.markIncomplete('mothership-response-unreadable')
952952
throw new Error('Mothership response provenance metadata is invalid')
953953
}
954954
await consumeMothershipProvenance(payload, response, resultRegistry)

apps/sim/executor/handlers/pi/local/sim-tools.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ describe('buildSimToolSpecs', () => {
320320
output: { result: 'untrusted output' },
321321
})
322322
const registry = new ResolvedSecretTraceRegistry()
323-
registry.markIncomplete()
323+
registry.markIncomplete('unspecified')
324324
const [spec] = await buildSimToolSpecs(executionContext(registry), toolInput)
325325

326326
const result = await spec.execute({})

apps/sim/executor/handlers/pi/pi-handler.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ describe('PiBlockHandler', () => {
227227

228228
it('fails closed when task provenance is incomplete', async () => {
229229
const registry = new ResolvedSecretTraceRegistry()
230-
registry.markIncomplete()
230+
registry.markIncomplete('unspecified')
231231

232232
await expect(
233233
handler.execute(

apps/sim/executor/handlers/pi/search/tool.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ describe('buildPiSearchToolSpec', () => {
288288

289289
it('fails closed before search when provenance is incomplete', async () => {
290290
const registry = new ResolvedSecretTraceRegistry()
291-
registry.markIncomplete()
291+
registry.markIncomplete('unspecified')
292292

293293
const result = await buildTool('exa', executionContext(registry)).execute({ query: 'pi' })
294294

@@ -303,7 +303,7 @@ describe('buildPiSearchToolSpec', () => {
303303
const registry = new ResolvedSecretTraceRegistry()
304304
const mergeSpy = vi.spyOn(registry, 'mergeToolCallRegistry')
305305
mockExecuteTool.mockImplementation(async (_toolId, _params, options) => {
306-
options.resolvedSecretTraceRegistry.markIncomplete()
306+
options.resolvedSecretTraceRegistry.markIncomplete('unspecified')
307307
return {
308308
success: true,
309309
output: {

0 commit comments

Comments
 (0)