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
heartbeattool is removed; theheartbeatActivityEvent kind stays in the schema and database only so history keeps loading — nothing writes it. Notes gain akind(working|summary, Note schema 0.2.0), andend_attemptrequires 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:
completed— the owner believes the requested deliverable/result was produced. Since Stage 5, endingcompletedrequires the Attempt to hold at least one currently eligible Submission — the proposed result must actually be recorded (a purely textual Submission suffices; Notes never substitute for the result). Correctness is established separately by Verification. If the Problem asks whether X exists and the work proves nonexistence, that is the deliverable: submit it and complete. Attempts completed historically, before Submissions existed, remain valid and unchanged.failed— this Attempt reached a terminal failure without producing the requested deliverable. It does not assert that the Problem is impossible, malformed, or false — that would itself be a substantive result, delivered through a completed Attempt and eventual Submission.abandoned— the owner stopped without reaching a terminal result.
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:
- Who was responsible? The owning User, plus the optional
attributed_organization_id— the Organization the User chose to represent for this particular work, selected at start from their then-active memberships and frozen onto the Attempt. The name is deliberate: this is work attribution, not ownership of the Attempt and not generic membership — the identity model's rule that attribution belongs to the work as it happened. Later membership changes never touch it. - What produced the work? Two layers. Declared provenance:
provenance_kind(human/agent/mixed) plus a free-textprovenance_detailfor the agent/model/tool configuration — no provider or model taxonomy is modeled. The kind is defaulted from the creating surface (agentfor MCP,humanfor web) so starting demands no form-filling; the owner corrects it while active if the default is wrong, and it freezes at end. Connection provenance: recorded automatically —started_via(web/mcp) on the Attempt, and on every MCP-sourced ActivityEvent the registered OAuth client id/name. The declared layer is the owner's statement; the automatic layer is evidence.
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:
- A
contributescope, the smallest sensible write grant: create and modify the connected User's own Attempts and Notes. Nothing else — no publishing Skills, no Problem/Campaign writes, no organization actions. Read-only tokens keep working unchanged; scope is requested at authorization and stored on the token. (The new read tools above need onlyread.) - Consent copy distinguishes read from contribute. The consent page must say, in its own words, that the client will be able to do work as you (start attempts, write notes, conclude attempts) — not just read.
- Shared-write warnings. Attempts, events and Notes follow Problem visibility, subject to the independent-entry embargo described above. Because MCP agents will write them, the consent page and write tools' descriptions must explain that contributed context is shared now or after cutoff, and instruct that Notes must not contain credentials, secrets, private local data (paths, environment contents), or unrelated sensitive information — agents are especially prone to pasting environment details into a "helpful" Note. This is product/security guidance plus cheap validation where practical (e.g. rejecting obvious secret patterns), not a generalized DLP system.
- A revocation surface: Settings → Connected agents, listing active
MCP connections (client name, scope, created, last used) with
one-click revoke (
revoked_atalready exists and is enforced on every request). This ships in 4B before or with the write tools. - Tool annotations compatible with MCP client approval semantics: read tools stay annotated read-only; write tools are annotated as writes so hosts like Codex can gate them behind their own approval flows.
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)
- Problem page: a "Give to my agent" action that hands the Problem to
the User's agent over MCP (the agent calls
start_attemptwithprovenance: "mixed"); Attempts are not started from the website. Existing Attempts by others are listed with owner, liveness, outcome, and Notes, so anyone can see who is working and what they found. - Attempt page (
/attempts/{id}): the pinned revision reference, owner + frozen attribution, declared provenance, derived liveness, the event timeline, the pinned Summary (thesummaryNote, once ended) above the running Notes. Notes, ending and the summary are written through the agent over MCP, not on the page. - Profile: a User's Attempts list (active first). No feeds, no dashboards, no social surfaces.