Reliable job creation and continuation¶
External MCP hosts can lose a response after Sonaloop has already committed a write. Sonaloop's job/run contract makes a retry resume the same intent rather than creating another project, run or journal row.
Create once, retry safely¶
When Sonaloop Cloud exposes begin_research_job, that is the only project-creation front door for
normal interactive workspace clients. The lower-level start_project primitive remains an
operator/hosted-automation compatibility surface, but is deliberately absent from the normal Cloud
tool profile. Call the front door exactly
once per user intent with the user's verbatim request, a stable opaque operation_id, and an explicit
methodology choice:
job = begin_research_job(
user_request=<the user's verbatim request>,
operation_id="<stable key for this one request>",
methodology="auto" | "Reaction Test" | "freeform",
provider="mistral" | "openai" | "anthropic" | ...,
model="<declared client model, if known>",
)
The call creates or repairs the project and its first run as a retry-safe saga, then returns exactly
one valid next action. For a ready research task this is run_step. For a Reaction Test that still
needs setup it is the current evidence or cohort remediation instead; the raw journal may already
exist, but state="needs_setup" takes presentation and orchestration precedence. Reuse the exact
same call and key after a timeout, reconnect,
permission prompt or client error such as 6101. Never recover by creating a replacement project or
run. If a weak host nevertheless assigns a fresh attempt id, Sonaloop matches the same logical
request inside the bounded SONALOOP_FRONT_DOOR_RETRY_WINDOW_SECONDS window (default 30 minutes).
A workspace-bound logical generation is derived before the project write, so simultaneous retries
with different attempt ids also converge through one atomic project claim. The window is anchored
at the first accepted request; later aliases are recorded with their provider/model/session/consent
metadata but do not extend it. Exact operation-id replay remains valid after the window expires.
A genuinely new request gets a new operation key. To run an intentionally separate but otherwise
identical study inside the window, pass new_job_intent=true with a fresh operation key; reusing a
previous key fails closed. If several intentional identical studies are still recent, an alias-only
retry is ambiguous and must use its original operation key. Thus four
attempt-scoped retries of one request followed by a distinct second request produce two jobs, not
ten. Project, plan and run are replay-repairable; they are not claimed to be one physical database
transaction.
Raw-content consent on an alias applies to that observed call but does not silently upgrade the canonical job's persisted consent for later steps. Workspace owner policy and deployment kill switches remain independently required.
Authenticated creator attribution¶
For Cloud jobs created after creator attribution is enabled, the creator is the authenticated user
behind the active workspace membership at the first successful project commit. It is server-derived
provenance: a caller cannot supply or override a creator through user_request, provider/model
metadata, MCP _meta or an operation key.
The first creator is immutable. An idempotent retry by the same user returns the existing attribution; a retry or continuation by another authorized workspace member may act on the job but cannot rewrite who created it. This keeps created by distinct from last acted on by and from the external MCP connector family. The inspector shows the bounded display label on both the Jobs overview and the Job detail. It never renders the opaque actor id.
When the first begin_research_job request also carries a recognized, server-observed MCP client,
Cloud freezes a separate closed connector snapshot. The UI may then render, for example,
Created by Ann-Kristin · via Mistral or … · via ChatGPT. The label comes from a fixed
family-to-product mapping, not caller text. Unknown or conflicting connector observations are omitted,
and retries through a different client cannot rewrite the first snapshot. This remains a connector
claim only: the tooltip states that it does not prove the hidden model or inference provider. The
caller-authored provider and model trace fields never power the byline.
Jobs that predate the field remain explicitly unknown during ordinary edits, retries and support inspection. Sonaloop does not infer their creator from current ownership, project text, connector metadata, audit timing or the person viewing the workspace, and it does not silently backfill a plausible name. A privileged Cloud support operation may fill a legacy gap only when a current workspace owner explicitly attests an exact project-to-current-member mapping. That operation is dry-run-first, tenant-bound and audit-receipted; ambiguous, missing or conflicting assignments fail closed. The inspector still renders only the member's bounded display label, never the attestation receipt or opaque identities.
methodology="auto" is the normal choice when the user has not selected a framework. Sonaloop
ranks routing signals authored on the live methodology registry; framework vocabulary is data,
not a hardcoded provider prompt. An explicitly named methodology or Job preset always wins. A
unique automatic candidate must clear its registry-declared threshold and ambiguity margin. If no
candidate clears that bar, or two remain too close, begin_research_job returns one short
clarification with no_mutation=true. Ask it once and repeat the call with the chosen exact
methodology. It never silently falls back to freeform.
freeform remains a valid deliberate choice. Every accepted job persists a
methodology_decision.v1 trace with the requested/resolved value, source, deterministic routing
confidence, ranked candidates, matched signals, rationale and any explicit override. Confidence is
a routing score, not a calibrated research-quality probability; the governed evidence and critic
gates still decide whether the run is complete.
Hosts without that Cloud tool use the two-step Core contract below.
Use one opaque, non-sensitive key per mutation intent and reuse it only for transport retries of that same payload:
project = start_project(
title=...,
goal=<the user's request>,
methodology="Reaction Test",
operation_id="<stable project-create key>",
)
run = start_run(
project_id=project.id,
operation_id="<separate stable run-create key>",
)
The keys are workspace-scoped. Identical replays return the original object with
idempotent_replay=true; conflicting payloads fail without mutation. Do not put prompts, names,
email addresses, API keys or other customer data into an operation key. Once a run_id is known,
resume an interrupted run with start_run(project_id, run_id=...).
Methodology keys and display names are resolved before the first write. Reaction Test,
reaction-test and reaction_test all resolve to the durable reaction_test key. Unknown or
ambiguous names leave no orphan project.
Drive the server-owned state machine¶
Repeatedly call run_step(run_id) and execute exactly the returned dispatch. Newer dispatches carry a
dispatch_token that binds every mutation to the active project, run and task before the mutation is
written. The persisted claim also freezes workspace, dispatch cursor, expected output kind and a
content-free input fingerprint, and permits one primary output kind. Supported evidence writers
auto-link their output, complete the task when its evidence gate passes, and checkpoint the exact
dispatch. Replaying the same token and authored payload returns the original primitive and receipt.
An authored repair while the gate is still open gets an explicit payload revision; changed content
after completion, another primary output kind, another project or another task fails before mutation.
This closes the common failure window where a weak host writes an artifact and disconnects before it
links or checkpoints it.
Primitive storage, evidence linking and the journal checkpoint are a replay-repairable sequence of commits, not one physical database transaction. Retrying the same token repairs any interrupted seam without minting a second primitive. This distinction matters in incident analysis.
Compatibility paths still expose the deterministic key/checkpoint_step protocol. Follow the
dispatch response rather than inventing a second orchestration path.
The inspector presents a genuine needs_setup gate as Waiting for input / Wartet auf
Eingabe, with one concrete next action. It is an input request, not an error state. Archived test,
duplicate and superseded Jobs never contribute to the global Run indicator.
Resumable report hand-off¶
The final project report is a governed multi-write dispatch, not a side effect of reaching the end of
the plan. run_step returns step_id="__report_handoff__" with one stable report_id, its exact
dispatch_token and the remaining section ids. A scaffold and each partial section are progress only.
The host authors each section through brief_synthesis_section → record_synthesis_section against
that same report/token. Only a non-empty lead plus at least one section with every section authored
links and checkpoints the report.
If the host, connector or provider stops partway through, the next run_step returns the same report,
same dispatch and first unfinished section. A partial hand-off does not create another Job or report,
and the completeness critic cannot begin until the hand-off is complete. A capped run also does not
manufacture an empty report.
The same report is resumed only while it remains the correct frozen hand-off. Once the plan has a current verify synthesis, that exact synthesis must exist in the report snapshot and be named by at least one section as an input. A fully authored report that predates the terminal synthesis is stale: it remains immutable and inspectable, but it no longer satisfies project completion. The next governed scaffold derives one stable refresh intent from the original operation and required synthesis, then creates one current replacement report. Transport retries converge on that replacement instead of creating more reports.
Cloud's report-render analytics follows this same content-derived boundary: a legacy report marked
done but still carrying an empty section remains running/partial and has no ready-age. The stored
status flag alone never upgrades an unfinished report.
That structural hand-off is not a shortcut around research provenance. Project reports use their native section citations rather than a duplicate generic claim envelope. New outlines freeze their graph sources when the report is scaffolded. If a structural section has no phase-local nodes, it receives the graph-wide source ids from that frozen snapshot, so even structural prose has an explicit, bounded citation set.
For a current section with a non-empty source_study_ids, validation requires at least one citation
from that exact section list as its phase anchor. Additional cross-phase citations are allowed, but
every supplied citation must still exist in the report's immutable graph snapshot. An older persisted section whose
source_study_ids is empty gets a compatibility fallback, but only to study ids already present in the
frozen graph snapshot's build order or nodes. The empty legacy list never admits a newly added live
project node or an arbitrary external study. Missing or foreign citations keep the report readable but
mark project health unverified. In other words, complete bodies answer “was a report delivered?”,
while valid section citations answer “is its prose evidence-backed?”.
The inspector uses the same provenance to connect the hand-off visibly. Deduplicated section source
ids become based_on edges into the terminal report node; for historical source-less sections only,
snapshot-valid citations provide the bounded fallback. It never infers an input from timestamps or
from merely being present in the snapshot. The contributing synthesis is therefore marked as used,
the report is the endpoint, and both the project outline and report detail expose the relationship.
The section-authoring brief resolves typed council:<id>, synthesis:<id> and note:<id> sources to
their real summary, voices, convergence and observation content while preserving those typed ids for
citations and edges. Large source payloads are bounded with an in-band truncation record, so omitted
material cannot silently read as an absent finding. Large outline graphs return an aligned bounded
source/build-order slice plus full totals and a stable digest; edge and open-question omissions are
counted in the same response.
That graph snapshot remains unchanged during an in-place lead repair of an already authored report. Evidence added to the live project afterward cannot retroactively legitimize an earlier citation; use a new report outline when the evidence boundary genuinely needs to change. Governed final hand-off does this automatically when the current terminal synthesis is missing from an older report. Report sources are validated against the frozen/current graph's build order and nodes, not the legacy project study-id projection.
Reaction Test evidence contract¶
A Reaction Test cannot start from a model's assumptions about the application. Its first governed step is a versioned Product Understanding preflight:
- attach or capture the real product stimulus (screens, a defined flow, a captured artifact or a grounded observed session);
- inspect it and record target, revision, observed routes/flows/states, explicit unknowns and evidence-cited capability claims; and
- pass the dispatch token so the record is linked and checkpointed through the replay-repairable dispatch sequence.
A public URL in the request identifies the target only. It is not a screenshot, a captured route,
an observed state, behaviour evidence, or permission for Sonaloop Cloud to fetch the page. A URL-only
job therefore returns stimulus_required and one direct-byte screenshot admission template. If the
host cannot supply real PNG/JPEG/WebP bytes, it asks the user for a screenshot; it must not invent an
inventory from the URL.
After each admitted screen, an exact capture review asks the host to name missing routes/states or deliberately finalize the inventory. A one-action host therefore cannot freeze its first screenshot as a complete application by accident. A later screen/revision invalidates the older review and flow for current setup.
For a finalized remote flow, weak clients do not have to author the nested integrity artifact.
inspect_reaction_test_screen delivers one exact screen at a time and writes a dispatch-bound receipt
whose honest status is served_to_host; it does not pretend to prove model cognition.
record_manifest_product_understanding accepts one flat {step_index, visible_observation} per
delivered screen plus explicit unknown_capabilities. It refuses missing/mixed receipts. The server
supplies the immutable manifest binding, revision, evidence references and coverage checklist and
reports an exact field plus a safe retry on malformed input. Honest unknowns are valid; the URL can
never manufacture an observed pass.
If the project has no cohort, the next frame dispatch includes bounded workspace-local candidate
summaries (id/name/role/segment) next to select_reaction_test_cohort; the model chooses at least two
real IDs instead of guessing them. If too few exist, the sole action is catalog discovery first.
Selection does not claim that Cohort Integrity passed. The normal server-owned depth, leakage and
countervoice gate still runs after the research frame. Each remediation advances the same project
and run under its original operation/dispatch identity.
After Product Understanding, the initial frame and the final cohort-integrity gate, Reaction Test
exposes two concrete Council dispatches in order: first-impression/comprehension and
trust/information-gaps/action-readiness. These are methodology data, not prompt folklore. The host
does not call add_task or invent its own breadth; both distinct evidence-producing tasks must
complete before the verify/synthesis gate can pass.
Every later council and synthesis carries explicit claim posture:
| Posture | Meaning |
|---|---|
observed |
real behaviour backed by a grounded session and exact step: anchor |
memory_grounded |
supported by citable persona memory/evidence |
simulated |
a synthetic persona reaction |
inferred |
an evidence-linked analyst inference |
unsupported |
not currently supported; blocks a verified Reaction Test artifact |
A screenshot proves a product state, never observed user behaviour. Synthetic councils cannot be relabeled as observation. Uninventoried prose and unsupported claims remain visible as an unverified hypothesis draft instead of silently becoming findings.
After Product Understanding, the initial research frame commits the questions/hypotheses and a final Cohort Integrity preflight checks them against independent pre-project persona depth/provenance. Thin circular profiles require real deepening/reselection work and a declared countervoice. Cohort ids, Product Understanding revision and frame-hypothesis digests bind the pass together with project goal/description; changing any of them makes it stale. Countervoices must quote cited independent pre-project persona context rather than exist as bare host labels, and deep memory does not waive high stimulus/profile overlap. An explicit override remains a report limitation.
For a critic dispatch:
- call
record_completeness_critic(project_id, verdict, run_id, operation_id=dispatch.operation_id); - call
record_critic_round(run_id, critic_report_id=report.id, key=dispatch.key); and - continue with
run_step.
Those links are mandatory. Retrying one verdict cannot count as two independent critic rounds.
What “finished” means¶
Only the engine owns successful completion. A run can become finished only when its plan/result
contract is complete, required organization/conclusion/handoff artifacts exist, and two distinct,
persisted, run-bound completeness critics pass with no missing work. Read failures are unknown
evidence and therefore fail closed. The only terminal status values are finished, stopped and
capped. Repeating run_step(run_id) after any terminal status is a read-only terminal replay: it
returns kind="done", the persisted status and idempotent_replay=true, and cannot issue another
dispatch or mutate the plan.
Normal clients should not call finish_run to force success. Continue until run_step returns
kind="done". The web inspector's run state is derived from these underlying facts, not from a
model's prose claim that it is finished.
Diagnose and recover without making a duplicate¶
project_health(project_id) is the canonical support view shared by MCP, CLI and the inspector. It
separates needs setup, running, stalled, expired but resumable, engine-finished and
output unverified. An active journal becomes stalled after six quiet hours and is projected as
expired after 24 hours. Expiry is not persisted as a terminal status: the run stays active, its
journal is preserved and the same run remains the safe continuation. It never implies unverified
evidence; unverified is reserved for an output that fails the evidence or hand-off contract. A
missing current Reaction-Test prerequisite takes precedence over a raw active journal, so Jobs, Job
detail, /runs and the global attention indicator cannot claim background research is progressing.
Only the current prerequisite is actionable; later dependent gates remain hidden/locked until their
inputs exist. The normal project
canvas keeps its compact run chip and a human-readable state. Open Technical diagnostics in the
chip or in /runs when support detail is actually needed: there Sonaloop names the first unmet
invariant, last successful operation, Product Understanding coverage, claim/source counts,
repairable orphaned evidence, one safe next action and a stable sltrace_* workflow reference.
Completed Product Understanding/cohort evidence remains inspectable behind one closed, borderless
setup-details row instead of interrupting the Job canvas with engineering cards.
In Sonaloop Cloud, a user can simply ask their MCP host to continue an existing job. With an exact
project id the host calls continue_research_job directly. Without one it first calls
list_unfinished_research_jobs(query=...): one match is selected; several matches must be shown to
the user for an explicit choice. Titles are never guessed or silently merged.
continue_research_job(project_id=...) follows an explicit state table. It resumes the sole active
run; starts one deterministic governed run when the job never started or its previous run was
stopped/capped; and starts a separate repair run only when an engine-finished job has unverified
output plus concrete ready plan work. One legacy exception is deliberately fail-closed: if an old
front-door project has an operation id but no persisted run budget, continuation asks the host to
retry the original begin_research_job call with exactly the same arguments instead of guessing
that budget. It never creates a project, replaces an active run, or continues an
archived/superseded or clean terminal job. Multiple active legacy runs fail closed and name the
competing ids. When setup is incomplete, continuation returns that one current remediation instead
of a misleading run_step; otherwise its returned run_step(run_id) must be repeated on that same
id until the engine returns kind="done".
Core/local clients can use the lower-level equivalent returned by project_health: call
resume_project_run(project_id, run_id, operation_id?), then run_step(run_id). Resume checks the
explicit project/run identity and never creates a replacement, reopens a terminal run or marks it
finished.
Run creation also has a hard single-active-owner invariant per workspace and project. Concurrent or accidental second starts fail with the existing run id and an exact resume instruction. Legacy stores that already contain multiple active rows remain inspectable, but recovery fails closed until an operator explicitly reconciles them instead of guessing. Run creation, deletion, archiving and superseding also share one project-scoped cross-process lifecycle lock: a close/delete operation cannot race a new active journal onto an archived, superseded or vanished project. A project with an active run must be explicitly stopped or recovered before it is preserved as closed. Hard delete is limited to never-started containers without any run history; once a governed journal exists, archive the job and retain its evidence.
The support view is honest about blind spots: it cannot prove that an external MCP host disconnected and cannot see hidden provider prompts, reasoning, permission dialogs or host-internal retries. Cloud can join its tenant-bound audit replay through the returned project/run/operation query.
The inspector distinguishes a job whose run never started, an active run that went quiet, and a job whose previous run stopped or hit its cap. All three need attention. A quiet active run resumes its existing journal; a stopped/capped job starts one deterministic successor run; and a never-started job normally starts its one missing governed run (subject to the legacy missing-budget exception above). The global counter therefore says that jobs need attention instead of pretending every row is a currently stalled worker. Attention takes display priority over an unrelated active-run count, so one progressing job cannot mask eight that need intervention.
Duplicate cleanup preserves history. supersede_project(new_id, old_id, operation_id, reason) records
an explicit old→new relationship and marks the old job obsolete without deleting any artifact.
archive_project(project_id, operation_id, reason) is also non-destructive and refuses active runs.
Similarity of names or prose never chooses a canonical job. Archived jobs leave the normal Jobs
overview, ⌘K/Ctrl+K project results, methodology-related job discovery, the Runs overview,
global run-status widget and GET /api/runs, but their exact /jobs/{project_id} detail links
continue to resolve with the retained evidence.
Provider quality versus contract quality¶
Stronger models still plan, route tools and write better. The server-side safety invariants do not depend on model strength: transaction order, idempotency, executable tool profiles, evidence links and completion gates are server-enforced. Host routing, orchestration and research quality remain model-dependent and must be qualified separately. This lets Mistral, OpenAI, Anthropic, MiniMax and future hosts use the same guarded protocol while their quality differences remain measurable rather than becoming duplicate jobs or false completion states.
Cloud support can reconstruct the Sonaloop-observed side with
cloud_get_research_job_trace(project_id=...) or the stable
cloud_get_research_job_trace(workflow_trace_id="sltrace_…"). A W3C/HTTP transport trace identifies
one request; the content-free workflow trace follows one job across creation retries, later MCP
calls, its run, sessions, assets, reports and exports. Legacy projects derive the same value without
a migration. The local replay includes retries, project/run state,
the front door's caller-supplied user_request, release metadata and deterministic quality scores. The
request stays workspace-owned domain data; metadata-only audit/PostHog projections retain only
privacy-reduced summaries. Replay labels unprovable facts unknown; it never
pretends to contain an external host's hidden prompt, reasoning, provider retry or model response.
Live provider qualification is a separate gate¶
qualification-run replays captured submissions through the deterministic harness. It proves the
Sonaloop contract, not that a live Mistral/OpenAI/Anthropic host chose the right tool from natural
language. A provider canary must use an isolated workspace and an ephemeral private MCP connector,
then separately assert routing safety (same project, one governed run, idempotent repetition) and
research completion/quality. A programmatic connector test still does not exercise Le Chat's own
permission and retry UI, so that surface needs a small manual smoke as well. Provider credentials
belong on the canary/operator side; they are not required in the Sonaloop production service merely
to test an external host.