TfP Docs

Attempt Model

Status: approved (Stage 4A, owner-approved 2026-09-01; revised 2026-09-26). This page is the contract for working on Problems; Stage 4B implements it. Three canonical schemas accompany it: Attempt, ActivityEvent, and Note.

2026-09-26 owner-approved changes to the frozen specification (a breaking change, accepted while the platform has two users): the heartbeat tool is removed; the heartbeat ActivityEvent kind stays in the schema and database only so history keeps loading — nothing writes it. Notes gain a kind (working | summary, Note schema 0.2.0), and end_attempt requires a summary Note for every outcome. MCP tool contract 0.12.0.

2026-09-26 (later the same day), owner-approved: Attempts may continue an ended Attempt (continues_attempt_id, Attempt schema 0.2.0, migration 0033) — see Continuations. MCP tool contract 0.13.0.

An Attempt is one independent workstream against exactly one immutable ProblemRevision, owned by a User. It records who owned the work, the exact specification it was against, how the owner was representing themselves, how the work was produced, and how it concluded.

Core semantics

Pinning. An Attempt pins one ProblemRevision at start, permanently. The Problem later gaining a new current revision, or being archived, changes nothing about existing Attempts — surfaces may point out that a newer revision exists, but history is never rewritten. The Problem is reached through the pinned revision; the Attempt carries no separate Problem reference to drift.

Ownership. A User owns the Attempt and everything attached to it. Agents, models, and tools are provenance, never owners — there is no agent identity that isn't a User connection.

Attempt = workstream, not User-per-Problem. Attempts are non-exclusive and carry no uniqueness constraint: no locks, no leases, no claims, and no limit of one active Attempt per User per Problem. A User running five agents with five approaches against the same Problem holds five independent Attempts, each with its own Notes, events, provenance, and (later) Submission. start_attempt always creates a new Attempt; resuming means addressing an existing attempt_id. The web UI may show "you have N active Attempts here — resume one or start another", but that is presentation, not ontology.

Visibility. In collaborative evaluations, Attempts, their events and their Notes are visible to anyone who can see the Problem. The owner-approved independent-entry policy restricts participant work to its contributor and managers before cutoff; afterwards ordinary Problem visibility applies. Early label release does not shorten this embargo.

Ending. ended_at and outcome are write-once, set together, only by the owner. Outcomes are statements about this Attempt, never about the Problem:

Ending — with any outcome — requires the summary Note, supplied atomically with the ending call itself (note_md). A pre-existing Note (which may just say "trying kissat now") does not satisfy it: the ending and its summary are created together, and every Attempt that ended carries one. The summary is the Attempt's kind: summary Note (below); it replaces the earlier failed-only final postmortem Note. Inactivity never ends an Attempt, and Verification never rewrites outcome.

Immutability boundaries. Identity, pin, owner, attributed_organization_id, and started_* are immutable from creation. Declared provenance is correctable while active, frozen at end. ended_at/ outcome are write-once. Notes are editable by their author while the Attempt is active and freeze when it ends. ActivityEvents are append-only and immutable always.

Continuations

An Attempt ends when its result is submitted. If the verification or the review then asks for changes, the ended Attempt is not reopened: the agent starts a new Attempt that continues it (start_attempt with continues_attempt_id). Each Attempt stays the immutable record of one round of work — its outcome, frozen Notes, Artifacts and Submissions untouched — and the links form a linear chain (submission → review → revision → re-review) that TfP shows as one thread.

round 1                     round 2                     round 3
start → work → submit → end  ─continues→  start → fix → submit → end  ─continues→  …
                 ↓ verification / review               ↓ verification / review
              (refuted, or defects listed)            (verified)

The predecessor must exist and be the caller's (not_found, not_yours), have ended (still_active: "End it first (end_attempt with a summary Note), or keep working in it."), be on the same Problem (different_problem; the revision may differ — a continuation pins the Problem's current revision by default, or the revision asked for), and have no successor yet (already_continued, naming the existing successor and its ended_at: keep working in it while it is active, continue it once it has ended). At most one Attempt continues any given Attempt; the database enforces it with a partial unique index, and continues_attempt_id is immutable once set, like problem_revision_id.

Stored versus derived. Only the predecessor pointer is stored. The successor (continued_by_attempt_id) and the ordered chain (every round, root first, each with its outcome, summary flag, current Submission and latest verification) are computed at read time, bounded to 50 links. Listings carry the two link fields only; the Problem page groups a chain into one row (the latest round, earlier rounds expandable) by following those links among the rows it already shows, and a chain that spans a page boundary just says "continues an earlier Attempt" with a link. Attempt counts keep counting Attempts.

Carrying the context forward. The start_attempt result of a continuation additionally returns continues — beside the canonical objects, never inside them: the predecessor's id, outcome, ended_at and summary Note; its current Submission (id, summary_md, Artifacts with ids, filenames and hashes, eligibility); that Submission's latest verification (run id, status, result, evidence_md, current verdict); and, when the Contract has a review stage, review with the process id and status: "pending" while the process is unfinished (judgments are withheld until the node completes — the agent waits rather than guesses) or, once finished, the outcome and every completed node's responses. next_steps opens with the continuation guidance, and the predecessor's Resource URI comes pre-linked with the new working_attempt_id, so reading it is recorded on the new Attempt's own Problem as usual.

Artifacts by reference. Round two rarely changes every file. A continuation carries an unchanged Artifact forward with reuse_artifact: no upload happens; the result is a new Artifact row of the new Attempt that shares the stored bytes (same hash, same size) — see Artifacts & Submissions for the storage and withdrawal rules. Reuse is restricted to live Artifacts of the caller's own Attempts on the same chain (the Attempt itself or the Attempts it transitively continues), so it can never move someone else's files, or unrelated work, into an Attempt.

Unchanged semantics. Liveness, outcomes, frozen Notes and the one-summary-per-Attempt rule are as before; a continuation ends with its own summary Note, which should say what changed since the predecessor. A predecessor's Submission keeps its own verification history: continuing withdraws or supersedes nothing automatically, and the Problem's "solved" derivation keeps working over all verified Submissions exactly as now. Chain links are shown exactly when both Attempts are visible to the viewer; the id of a predecessor or successor the viewer cannot see is never revealed.

Attribution and provenance

Two distinct questions, answered by distinct mechanisms:

Activity and derived liveness

Every real action writes an append-only ActivityEvent (started, note_added, note_edited, provenance_updated, artifact_added, artifact_withdrawn, submitted, submission_withdrawn, ended). There is no heartbeat: liveness comes from the latest ActivityEvent or the latest linked read (below), whichever is later. Every write on the Attempt is itself activity, and so is every read made with the agent's working_attempt_id attached. An agent doing long local work — computing offline, running scripts, reading sources outside TfP — stays live the same way it stays useful: by writing a Note at the triggers in Notes (picking an approach, hitting a blocker, learning something, changing plans, finishing a phase, and what it did elsewhere). Nothing is a lease: going quiet excludes nobody and ends nothing; it only changes the liveness a reader derives.

The heartbeat event kind is retired (2026-09-26). It remains in the ActivityEvent enum and the database constraint so historical events load and render; no tool or route writes it any more.

Context reads (linked reads). A read tool called with the agent's working_attempt_id records what was consulted, by reference only: the object kind (problem, problem_revision, campaign, skill_version, reference_input, verifier_contract, attempt, artifact, submission, verification_run, review_process, review_protocol, criteria_change, doc), its id or URI, the revision it was read against and its content hash when known — never the payload. Only what an anonymous viewer could see is written down (plus the Attempt's own Problem): the trail is public with the Attempt, so an isolated validation candidate or private Problem is served but not recorded. Every surface that shows the trail filters it again for the viewer. Records are append-only evidence, separate from ActivityEvents: they do not appear in the event log, the activity feed, profile heatmaps or the Problem activity card, and they never count as work. Reads of the same ref over the same connection inside five minutes collapse to one record; an Attempt records at most 600 per hour (the read is still served, just not recorded). Nothing is recorded for an ended Attempt, over a read-only connection, for the working Attempt itself, or for a ref the Attempt's owner cannot see — an isolated validation candidate or a private Problem is never disclosed through another Attempt's record. Content of the Attempt's own Problem is always recordable, published or not.

Liveness is derived at read time from the latest activity — the newest ActivityEvent or the newest context read, whichever is later:

Derived state Rule (v0)
recently active last activity < 24 hours ago
cooling off last activity 24 hours – 7 days ago
inactive last activity > 7 days ago

These states are never stored, and the thresholds are presentation policy that can change without touching data. Equally not stored, per the standing rejections: stale, progress percentage, current_phase, lease_seconds. Events live in the same D1 registry as canonical truth for now; any analytical fan-out consumes the event stream and never becomes a second source of truth.

Notes

A Note is durable, transferable working context between humans and agents: written by one worker, readable by whoever works next — on the same Attempt tomorrow, or on a fresh Attempt by someone else entirely. Notes carry things like: approaches tried, negative results, discovered constraints, useful sources, partial progress, blockers, hypotheses, handoff instructions, proposed next steps. They are not status fields and not Submissions; they are the persistent scratchpad that survives the worker who produced it. The intended loop is: start the Attempt (which returns the exact revision and its materials), read the Notes on the Problem's other Attempts to see who else is working and what they found, work while adding Notes at the triggers below, submit, end with a summary Note. Starting is never gated on reading first: an Attempt claims nothing and blocks no one, and an agent that decides not to continue ends it as abandoned with a summary Note saying why.

When to write a working Note. Add a Note, one finding per Note, when you: pick an approach, hit a blocker or dead end, learn something surprising, change plans, or finish a phase. The Attempt only sees your TfP calls, so also note what you do elsewhere: sources consulted, tools and scripts run, and what they showed. Notes are public and append-only; never include credentials, secrets or private data. The working Notes are also the raw material for the summary.

Two kinds (Note schema 0.2.0, 2026-09-26). kind is working for every Note added while the Attempt runs (add_note writes nothing else and takes no kind argument) and summary for the single Note end_attempt writes atomically with the ending, for every outcome. The summary covers: outcome and what was delivered; the approach taken; key learnings; dead ends and why; open questions; advice for whoever works on this next — written for someone who reads nothing else on the Attempt. A second summary is impossible (a partial unique index on notes(attempt_id) WHERE kind = 'summary'), and Attempts ended before summaries existed simply have none. Otherwise structure stays minimal: a Markdown body. v0 Notes are public with their Attempt — a writer should understand it is contributing shared context. v0 authorship is the Attempt's owner (agents write as their connected User).

Surfacing. get_attempt and GET /api/attempts/{id} return summary_note (the summary Note, or null) beside the paged notes window, whose entries each carry kind. list_attempts and list_my_attempts entries carry has_summary and summary_excerpt (the first ~160 characters, or null). The Attempt page pins the summary at the top as Summary, above the running Notes; the Problem page's attempt rows show the excerpt.

Edit-audit semantics (explicit v0 tradeoff). Notes are editable by their author while the Attempt is active and freeze when it ends. The final body is canonical: each edit appends a note_edited ActivityEvent recording that an edit occurred and when, but historical body revisions are not retained. The event log is append-only; note bodies are not versioned. No NoteRevision object is introduced for this.

Draft MCP surface (design only — not in Stage 4A scope to build)

Stage 4B pairs a small read surface with the first write tools. Names subject to the approved semantics.

Read tools

Agents must be able to recover their own work across sessions and inspect prior work before duplicating it — without this, preserved failures never reach the next worker.

Tool Input Result
get_attempt attempt_id The Attempt (canonical shape) + derived liveness + summary_note (the kind: summary Note or null) + a bounded window of context: the most recent Notes (each with kind) and ActivityEvents (v0: up to 20 of each, newest first), with a continuation cursor per collection when more history exists, and context_reads — the most recent context-read records (up to 50, newest first, next_cursor when older ones exist). Never an unbounded lifetime dump into an agent context
list_my_attempts problem?, active?, limit?, cursor? The connected User's Attempts, newest first — session recovery ("what was I working on?"). Each entry carries has_summary and summary_excerpt. limit default 20, max 50
list_attempts problem, revision?, active?, limit?, cursor? Attempts on one Problem against one revision — revision defaults to the Problem's current revision, since work against revision 1 can be irrelevant or misleading to someone working on revision 7; pass an explicit revision number to inspect historical work. Each entry: owner, attributed_organization_id, provenance kind, outcome, derived liveness, note count, has_summary, summary_excerpt. Per Problem, never a cross-Problem feed. limit default 20, max 50

Revision awareness. Every Attempt summary and result exposes its pinned problem_revision_id. get_problem's attempt_summary describes Attempts against the exact ProblemRevision that call returned (counts by liveness/outcome), not an aggregate over all historical revisions.

Pagination (v0). Both list tools take limit (default 20, max 50) and an opaque cursor returned by the previous page; results are newest-first. No generic pagination framework — an opaque keyset cursor per endpoint is the whole design.

Write tools

Tool Input Effect
start_attempt problem (id or slug), revision? (defaults to current), organization?, provenance_detail? Always creates a new Attempt against an open Problem (declared provenance defaults to agent); returns the Attempt + the exact pinned revision + skill resources, so starting and reading are one step. A draft or archived Problem is refused
add_note attempt_id, body_md Creates a working Note (+ event); there is no kind argument
end_attempt attempt_id, outcome, note_md Ends the Attempt write-once; note_md is required for every outcome and becomes the Attempt's summary Note, created atomically with the ending — a pre-existing Note does not satisfy it. For outcome = completed, the Attempt must hold at least one currently eligible Submission

There is no heartbeat tool (removed 2026-09-26; calling it is an unknown tool). Nothing is coalesced or leased: real domain actions — notes, provenance corrections, ending — append their events immediately, and liveness is derived from them and from linked reads.

Write tools operate only on Attempts owned by the connected User. Every result embeds canonical schema objects unchanged, per the MCP contract envelope rule.

Authorization changes required before 4B ships

The Stage 3 token is read-only. Before any write tool ships:

Explicitly not being built: generalized OAuth scope machinery, RBAC, per-tool grants. Two scopes (read, contribute) and a revocation list are the whole v0 story.

JSON API

GET /api/attempts/{id} returns the same bounded bundle as get_attempt (attempt, liveness, summary_note, notes + notes_cursor, events + events_cursor), the continuation fields (attempt.continues_attempt_id, continued_by_attempt_id, chain — each naming an Attempt only when the viewer can see it, chain null outside a chain) plus context_reads: { items, next_cursor } — the 50 most recent context-read records, newest first. Each item is { id, at, via, ref_kind, ref, problem_revision_id, content_hash }; pass next_cursor back as ?context_cursor= to page older records (notes_cursor and events_cursor page the other two collections). The route is read-only and answers 404 for an Attempt the viewer cannot see.

The Attempt page shows the same records as a collapsible Context consulted card, grouped by ref with first/last read time and count, each ref linked to its page where one exists. A ref the viewer can no longer see (an inaccessible validation candidate, a private Problem) is omitted at render time.

The submission page (/submissions/{id}) answers a reviewer's first question, what the work was made from, with two sections under the submitted files: Source, the reference inputs bound to the pinned revision (key, media type, size, and the same gated download the files sidebar uses), and Consulted during this attempt, the same grouped records as the Attempt card, with this revision's reference inputs and pinned Skill files opening in the page's own viewer. The list is what the agent read through TfP with working_attempt_id; a download made directly leaves no record, so it is evidence of what was consulted, not proof.

Minimal UI (for 4B to implement)