Skip to content
Skip to article
Project SynapseEnterprise agent guide
Open demo

Run evidence and permanent audit records

Project Synapse keeps the handoff from a user to an agent visible and diagnosable. A submitted run receives a UUID immediately, and that UUID opens a permanent owner-scoped record at /runs/<run-id>. Text, documents, screenshots, model evidence, cost evidence, lifecycle timestamps, safe diagnostics, and the output are tied to that one run.

This capability is intended for synthetic, non-sensitive M1 demonstrations. It does not by itself authorize company-confidential, regulated, patient, or production data. Enterprise data use still requires the provider, retention, region, classification, and company approvals in plan.md.

What a user sees

  1. The composer accepts ordinary text blocks and screenshot cards. Pasting an image at the cursor splits the active text block, inserts the image at that exact position, and leaves a text block after it. Drop and file selection use the same ordered model.
  2. Each screenshot can have a plain-text caption and can be replaced, removed, or moved with visible keyboard-operable controls.
  3. Submission creates the run before screenshot transfer begins. The receipt displays the complete run UUID, Accepted time, current state, Copy action, and permanent-record link. A transfer or browser-network failure does not erase that handoff.
  4. The UI distinguishes Preparing from Streaming. An evidence-backed run is not shown as executing until finalization has activated the run or a durable run event proves that execution started.
  5. A transient screenshot-transfer failure can retry the same run while the browser still holds its short-lived in-memory capability. Retry never creates a second run. Cancel closes the original waiting run. After finalization, upload authority is destroyed and a dropped response follows the original run through its authenticated event stream.

The permanent record shows the submitted input in authored order, Accepted, Started, and Ended times in the viewer's browser timezone, duration, execution mode, attempt, pinned agent version, support trace ID, the latest allowlisted provider diagnostic, document names, private screenshots, model/cost evidence, and output. Its Steps view also holds an Advisor consults block when the run consulted an advisor model, and an Answer check section when the run's answer was checked. It never renders a Storage path, signed token, unrestricted event payload, raw execution snapshot, authorization header, or service credential.

Composer narrative field (merged writing surface)

Some agents mark one type:'string' input as format:'composer' (for example issue-analyzer.symptom). That field renders AS the fluid evidence surface at its position in the run form, so a user writes the narrative and drops screenshots in one place. The named input contract is unchanged: workflows, prompt assembly, and the permanent record still see the field. Agents without a composer field (for example incident-classifier) keep the standalone "Evidence & context" section. A schema that marks more than one composer field fails closed — the agent page returns 404 and the run POST returns HTTP 500.

A composer run travels in one of two shapes:

  • Text-only draft (no screenshots). The browser sends NO evidence manifest and puts the joined narrative directly into input[field]. This is the ordinary plain-input path — zero evidence rows are created, and the prompt renders the narrative in full under ## Inputs.
  • Image draft (one or more screenshots). The browser sends the evidence manifest and the SERVER derives input[field] from the manifest's ordered text blocks via the shared pure helper composerTextFromBlocks (client and route call the same function, so they agree on the stored value). The client's own value for the composer field is always ignored when a manifest is present. A non-empty derived narrative is stored on runs.input[field]; an images-only draft (derived text empty after trim) drops the key, so a required composer field fails Ajv validation with the exact Missing required field: <name> (HTTP 422) — identical to an empty required textarea. The client pre-flights the same required-empty rule with a friendly inline error before creating a run.

Single-send guarantee. The model receives the narrative exactly once. When a run has ordered evidence rows, the engine renders the composer field in the prompt as a placeholder line (- **<field>**: provided as the ordered walkthrough below) and the narrative arrives once through the ordered evidence blocks (content[0] carries the placeholder prompt, keyed off the actual presence of evidence rows, not off the schema alone). A text-only run has no evidence rows, so it renders the full narrative in ## Inputs and no placeholder is used. Pre-merge runs whose pinned snapshot has no composer field, and all workflow agent steps, keep the full renderInput behavior unchanged.

60,000-character note. serializeEvidenceDraft caps the combined evidence text at 60,000 characters. Because the shared helper joins text blocks with a blank line (\n\n), the derived narrative stored in runs.input[field] may exceed 60,000 by at most 40 joiner characters (two per join, at most 20 joins for 21 blocks). This is deliberate: runs.input has no per-field length cap, so NO second cap is added — the small joiner overflow is accepted rather than introducing a divergent limit.

Supported evidence limits

ControlM1 limit
Screenshot count5 per run
Accepted source formatsPNG, JPEG, WebP
Declared source size5 MiB per screenshot
Ordered evidence blocks21
Combined evidence text60,000 characters
Caption500 characters per screenshot
Complete multipart run requestConservatively estimated at no more than 4,400,000 bytes
Source safetyStatic images only; animated and oversized decoded inputs fail
Normalized resultLossless WebP, metadata removed, longest edge at most 1,568 px

Document bytes retain their 4 MiB combined limit, and the complete multipart request has a second stricter estimator that includes UTF-8 structured input, evidence JSON, multibyte filenames, MIME values, document bytes, and conservative per-part/framing overhead. Requests above 4,400,000 estimated bytes fail before run creation, leaving margin below Vercel's 4.5 MB edge limit. Screenshot bytes do not cross the Vercel Function request body. The browser uploads them directly to exact private paths using short-lived signed upload capabilities.

Private upload and activation lifecycle

The client first sends a versioned manifest containing only ordered blocks and declared image metadata. The server creates a waiting run and immutable evidence rows, then returns exact-path upload capabilities. The browser validates that the response has the same run UUID, exact asset count, asset identifiers, and canonical paths before uploading.

Capabilities exist only in memory. They are not persisted in local storage, rendered into the page, logged, or placed in navigation URLs. They are zeroized as soon as finalization proves a same-run execution handoff. The Storage bucket is private and browser users have no direct table-write permission.

Finalization downloads each staged object through the service boundary and checks declared byte length, decoded MIME/magic, animation, source dimensions, and decoded pixel count. It auto-orients, constrains the image, strips metadata, creates a deterministic lossless WebP, and records its byte length, dimensions, and SHA-256. Only normalized ready images reach a model or the authenticated evidence-delivery route. A run, its ready evidence, and the queued event activate atomically.

The preparation window is five minutes. Staging deletion waits until two hours after that deadline so a late signed upload cannot recreate an object after it was cleaned. The worker and normal run intake perform bounded cleanup batches. The database lists candidates; the application removes Storage objects and only then acknowledges cleanup. Ambiguous finalization never request-deletes a normalized object that may already be referenced; terminal unreferenced orphans are handled by the aged cleanup path.

Model ordering and cost control

The model receives one ordered user message whose text and normalized images match the authored block order. A top-level system instruction declares evidence to be untrusted data, not executable instruction. Refinement reuses the same immutable evidence sequence.

Permanent run record showing numbered text and image evidence blocks in authored order

A completed multimodal run's ordered evidence — numbered text and image blocks preserved in the exact order the user authored them.

Binary bytes are replaced by a fixed sentinel only in the accounting copy used for UTF-8 envelope estimation; they are never serialized as prompt text. The four approved multimodal profiles, anthropic/claude-haiku-5.5, anthropic/claude-sonnet-5.5, anthropic/claude-opus-5.5 and anthropic/claude-fable-5.1, each reserve 3,136 visual input tokens per image for every provider attempt and retry. All four models read images in 28 px patches in the high-resolution tier, and a normalized image is at most 1,568 px on each edge, so one image costs at most 56 x 56 = 3,136 tokens. It also caps the complete encoded multimodal envelope at 24 MiB—counting system/text/tools plus exact base64 expansion—before reservation or provider invocation, leaving an 8 MiB margin below the upstream 32 MiB request boundary. Provider-reported usage settles the existing input-token meter. AI SDK step results explicitly do not retain the provider request body. An unknown real multimodal model profile fails before model construction or paid provider work; text-only and mock paths keep their existing behavior.

Diagnostics and support workflow

Users receive a generic provider failure message. The permanent record may show only these provider categories:

  • authentication
  • billing_or_quota
  • policy
  • invalid_request
  • invalid_response
  • model_unavailable
  • dependency_unavailable
  • rate_limited
  • timeout
  • upstream_unavailable
  • unknown

The diagnostic also contains only a valid HTTP status or null, a retryable boolean, and gateway, api_call, sdk_retry, or unknown as the error type. Provider bodies, messages, URLs, headers, causes, stacks, and retry history are not copied into run records or terminal events. Evidence query, Storage, decode, and byte-read failures similarly collapse to one cause-free readiness message.

The diagnostic query filters to events that contain provider evidence before it orders and limits results, so ordinary refinement failures cannot hide an older valid provider diagnostic. A deterministic screenshot validation rejection is not retryable with the same immutable upload; the UI offers cancel/start-new guidance instead. Missing or ambiguous transfers remain same-run retryable while their in-memory capability is valid.

For support, ask the user for the run UUID or the displayed 32-hex trace ID. Operators may correlate those identifiers with structured server logs and the append-only run event sequence. Do not ask the user to share signed upload tokens, Storage paths, credentials, or raw provider responses. Do not copy screenshot contents into tickets or Obsidian unless the company's data-handling policy explicitly permits it.

Authorization and caching

Run records, evidence rows, and the image-delivery route require the run owner or an administrator. Unknown, unauthorized, pending, or inconsistent evidence uses a generic not-found/not-ready boundary. Images are served with private, no-store, nosniff, and cross-origin-resource-policy headers from the same-origin authenticated route. No public Storage policy or signed read URL is used.

The permanent run page and image response are not a public asset cache. Any future CDN, thumbnail, export, or sharing feature must receive its own data classification, authorization, retention, revocation, and audit design before implementation.

Audit evidence

PlatformAdmin and Auditor can open a run from the audit explorer at /admin/audit to see its evidence. The view shows identifiers, statuses, counts, timestamps, allowlisted diagnostics, and the run's events in the tamper-evident audit ledger. It never shows raw input, output, retrieved text, or tool arguments or results; those stay on this permanent run record. Estimated cost appears for PlatformAdmin only. The auditor, never the administrator, can export a bounded window of ledger events, and each export is itself recorded in the ledger. Signed checkpoints of the ledger's chain make later tampering detectable. They are stored in the same Supabase project and signed with a key the platform holds, so they are not proof against the database owner. An external write-once copy is future work.

Operator checks

  • Confirm migration 0015_run_evidence.sql is applied forward-only before the application commit is promoted.
  • Run evidence behavior integration only on CI's disposable Supabase stack (or an explicitly confirmed disposable local stack). Never reset the shared local development database.
  • Verify run-evidence remains private and browser roles cannot insert, update, or delete evidence rows or execute lifecycle RPCs.
  • Monitor the fixed, path-free evidence.cleanup_failed and evidence.cleanup_unavailable events, worker maintenance error counters, waiting-run expiry, provider failure categories, usage_unknown, and run cost reservations without logging input or screenshot content.
  • For rollback, disable new runtime exposure first. Do not roll migration 0015 backward or delete evidence rows/objects as an application rollback shortcut.