TfP Docs

Artifacts & Submissions

Status: approved (Stage 5A, owner-approved 2026-09-01). This page is the contract for preserving and putting forward results; Stage 5B implements it. Two canonical schemas accompany it: Artifact and Submission.

The Stage 4 dogfood ended with a solved instance whose solution.txt existed only on the solver's machine, connected to the completed Attempt only through a Note. This stage adds the two objects that close that gap: Artifacts preserve outputs as verifiable bytes, and a Submission records the Attempt's proposed result, optionally referencing the Artifacts that constitute or support it.

Artifacts

An Artifact is one immutable file/data output of an Attempt.

Ownership and lifecycle. Artifacts belong to their Attempt; only the Attempt's owner uploads them (agents upload as their connected User), and only while the Attempt is active — the Artifact set freezes when the Attempt ends, exactly like Notes. Each upload appends an artifact_added ActivityEvent in the same transactional batch as the insert.

Integrity. content_hash (sha256: of the raw bytes) and size_bytes are computed server-side at upload, never trusted from the client (zero-byte files are valid). Content and metadata are immutable from creation; nothing is ever overwritten. filename is a display/download name, not identity: multiple immutable Artifacts on one Attempt may share a filename (a corrected output is simply a new Artifact — no solution-final2.txt drift), and ids/hashes distinguish them; a Submission selects the exact one by id. The hash permanently identifies the exact uploaded bytes and anchors integrity while those bytes remain available — it does not make withdrawn bytes re-verifiable.

Declared media type. media_type is client-declared and stored canonicalized: parameters are stripped (text/plain; charset=utf-8 is stored as text/plain) and the type is lower-cased. A declaration that is missing, malformed, or over 120 characters becomes application/octet-stream. Canonicalization touches only the label — the bytes and their content_hash are never altered — and because the label is participant-supplied, downloads are served with X-Content-Type-Options: nosniff and any consumer must validate content rather than trust it.

Bounds (v0). Up to 25 MB per Artifact, 50 Artifacts per Attempt, and upload rate-limited per User. Filenames are single path segments (1–255 chars, letters/digits/._-, no leading dot) — no directory trees; an output that is inherently a tree is uploaded as an archive. Artifacts carry no free-text description field: bytes plus integrity and display metadata only — explanation lives in Notes and in Submission.summary_md, keeping the three roles distinct.

Visibility and withdrawal. Artifacts are public with their Attempt. Because agents upload them, the same public-write warnings apply as for Notes. The one mutation ever allowed is the write-once withdrawn_at safety valve, settable by the owner even after the Attempt ends: the content stops being accessible and the stored bytes are removed from active storage (withdrawal exists to remediate leaked secrets — a 404 alone would not), the metadata row (filename, size, hash) remains forever, no new Submission may reference it, and existing Submissions keep the reference (displayed as withdrawn) while ceasing to be eligible. Completed VerificationRuns remain durable historical records. Withdrawal is remediation for content that should not be public — not a way to revise results.

Reuse across a continuation (2026-09-26). A continuation Attempt may carry an unchanged Artifact of an earlier round forward without re-uploading it (reuse_artifact). The result is a new Artifact row of the new Attempt — its own id, created_at and artifact_added event (whose detail names reused_from_artifact_id) — with the same content_hash, size_bytes and media_type, pointing at the same stored bytes. Reuse is allowed only for a live Artifact of one of the caller's own Attempts on the same chain (the Attempt itself or an Attempt it transitively continues), only into an active Attempt of the caller's, and within the same per-Attempt bound. Rows withdraw independently: withdrawing the original does not break the copy and withdrawing the copy does not touch the original; the stored bytes are deleted only once no live row references them (the last withdrawal deletes them and stamps every withdrawn row of that content at once; the queue retry and the hourly sweep leave still-referenced bytes alone). Everything else about an Artifact — one owning Attempt, frozen at end, Submissions referencing it by id — is unchanged.

Storage and retrieval. Bytes live in R2 under an internal key that is not part of any contract. Retrieval:

Submissions

A Submission records the proposed result and may reference the Artifacts that constitute or support it.

Shape. A required summary_md — itself the immutable textual proposed result ("the answer is 47", or what an attached file is and how it answers the pinned revision) — plus optional Artifact bindings artifacts: [{ role, artifact_id }] (0–20, all belonging to the same Attempt, all non-withdrawn at submission time), each naming which role of the Contract's submission_requirements.required_artifacts the Artifact fills; a role the Contract does not name is refused at submission. The older positional form artifact_ids (artifact i fills role i) is still accepted. A purely textual result is a complete Submission with no Artifacts; nothing forces an artificial answer.txt. Immutable from creation except the write-once withdrawn_at.

Models (Submission 0.2.0). Pass optional models, up to 16 objects such as [{"model":"GPT-6","provider":"OpenAI","tool":"Codex","role":"implementation"}]. Each entry requires a model name; provider, tool and role are optional. Strings are trimmed and must contain 1–120 characters. Declare only model identities you know, including collaborators that materially produced this result. These are self-reported claims, not verified model identities. The list is frozen with this submission; a later correction can name different models on the same Attempt. Missing or empty means not reported, not “no AI used”. Older submissions are not inferred from their client or Attempt provenance. Reads return models: [] when unreported; downloaded submission manifests include these declarations.

Eligibility and the derived current result. An Attempt may create multiple Submissions over its life; a corrected result is a new Submission, never an edit. A Submission is eligible when it is not withdrawn and every Artifact it references is not withdrawn (vacuously true with no Artifacts). The Attempt's current proposed result is derived — its newest eligible Submission — with no stored pointer (the same derived-state rule as skill current versions and liveness). Withdrawing a referenced Artifact never mutates the Submission record; the Submission simply stops being eligible/current, and only eligible Submissions are candidates for new verification.

Lifecycle. Submissions are created only while the Attempt is active, each appending a submitted ActivityEvent in the same batch. create_submission is deliberately named for what it does — multiple Submissions are normal. The flow is: (optionally upload Artifact(s)) → create_submission → end the Attempt completed. Ending completed requires at least one currently eligible Submission — now that a Submission can be purely textual, there is no result that needs to live only in a Note, so Notes never substitute for the proposed result. failed requires its atomic final Note (no Submission); abandoned requires neither. Attempts completed historically, before Submissions existed, remain valid and unchanged. Withdrawal (of a Submission, or of a referenced Artifact) is allowed after the Attempt ends, as remediation.

Sealed evaluation admission. A sealed revision limits created Submissions per User in a rolling window. Admission is atomic across Attempts; withdrawing a Submission or Artifact does not refund its slot. Local preflight and operator reruns do not create Submissions or spend slots. get_problem and start_attempt expose submission_allowance; an agent asks the participant before spending a limited slot. The optional submission cutoff also applies to already active Attempts and remains effective after label release. Submission sealed_entry records participant_before_release, manager or after_release, or null for an ordinary revision. It records role and timing; it does not prove that a participant never knew the labels or establish standings.

Verification stays separate. A Submission stores no verification status. What TfP can establish about a submitted result arrives at Stage 6 as VerificationRun records referencing the Submission; any "verified" state a surface shows is derived from those. The three-step distinction from the concepts chapter is normative: completed = the owner's statement, Submission = the result, Verification = what TfP establishes.

ActivityEvent additions

The event kind enum grows by schema revision with:

All are platform-written side effects of the real actions, appended in the same transactional batch as their canonical mutation, per the 4B atomicity rule.

MCP surface (Stage 5B)

All writes require the existing contribute scope — no new scope. Names subject to the approved semantics.

Tool Input Effect
upload_artifact attempt_id, filename, content_base64, media_type? Uploads one Artifact to the connected User's active Attempt; returns the canonical Artifact incl. server-computed content_hash. MCP uploads are capped at 5 MB decoded (JSON-RPC transport limit); larger files use the web upload
create_submission attempt_id, summary_md, artifacts? ([{role, artifact_id}]; or positional artifact_ids?), models? Creates a Submission from the connected User's active Attempt (text-only when neither is given)
withdraw_artifact artifact_id Write-once withdrawal (owner only)
withdraw_submission submission_id Write-once withdrawal (owner only)
list_artifacts attempt_id, limit?, cursor? Read: an Attempt's Artifacts (public), newest first, cursor-paginated (limit default 20, max 50)
list_submissions attempt_id, limit?, cursor? Read: an Attempt's Submissions, newest first, each flagged eligible, cursor-paginated (limit default 20, max 50)
get_submission submission_id Read: the Submission + its Artifacts' metadata + derived eligibility

get_attempt stays bounded rather than becoming a dump of everything: its envelope gains artifact_count, submission_count, and current_submission (the newest eligible Submission, or null) — the full lists live behind list_artifacts/list_submissions. list_attempts entries gain artifact_count and a has_current_submission boolean. Artifact bytes are read through the tfp://artifacts/{artifact_id} resource, not through a tool result.

R2↔D1 failure protocol (5B implementation invariant). There is no distributed transaction between R2 and D1, so uploads follow the safe failure direction: validate ownership + active Attempt → write bytes to R2 → compute server hash/size → one D1 batch inserting the Artifact row and its artifact_added event → on D1 failure, best-effort delete of the orphan R2 object (an unreferenced R2 object is inaccessible, so this cleanup may stay best-effort; a later sweep can garbage-collect). Withdrawal inverts it and must be eventually complete, not one best-effort call — withdrawal exists to remediate material that should no longer remain stored: one D1 batch marks withdrawn_at and appends the event (public reads refuse immediately) → the stored bytes are deleted from R2 → if that delete fails, a retryable cleanup message is enqueued (Cloudflare Queues' built-in retry) and an idempotent consumer deletes any bytes still present for withdrawn Artifacts. No new public domain object is involved.

MCP validation (5B prerequisite). Stage 5 tool inputs introduce arrays; before 5B ships, the server-side validator that enforces schemas/mcp-tools.json must support the new constructs — array, items, minItems, maxItems, uniqueItems, and pattern — so advertised and enforced schemas never diverge.

The web UI counterpart (5B): an ARTIFACTS section and a submit form on the Attempt page (owner, while active), artifact download links for everyone, and the Submission shown as the Attempt's proposed result.

Explicitly not in v0

No artifact directory trees or versioning (new file, new Artifact); no resumable/multipart uploads; no artifact-level visibility controls (public with the Attempt, withdrawal as the safety valve); no deduplication across Attempts; no storage quotas beyond the simple bounds above; no submission review/comment threads; no verification fields on Submission (Stage 6 objects carry those); no external/linked artifacts (URLs are context for Notes — TfP preserves bytes it stores).