# Decision: freeze a Package candidate before publication

Status: accepted for the local authoring runner, 2026-09-24.
Action: implement and validate locally; no remote deployment or client-policy promotion.

## Context and selection

The product objective is continuous agent-authored content through the existing
authoritative Package boundary. The previous one-step authoring wrapper could
seal and publish, but an interrupted preparation required manual inspection and
a different request ID. A caller could not separately inspect a prepared candidate
and require publication of those exact bytes.

Baseline: `e1dc2c0d5f38231054325dba67a9ea3344ea7ea5` plus the pre-existing dirty
working tree. The authoring runtime owns filesystem orchestration; Rust owns cold
Package validation; Edge and the shard own admission, generation CAS, composite
checkpoints and tick activation. No new world mutation API is introduced.

| Alternative | Benefit and cost | Decision |
| --- | --- | --- |
| Keep one-step publication | No interface change; cannot inspect and pin a candidate separately or resume interrupted preparation | Retain as a compatibility convenience only |
| Freeze input and output in the current runner | Small ownership change; both MCP and durable jobs use the same implementation and existing publication path | Implement |
| New authoring database/service or agent-owned world editor | Larger deployment, migration and authority surface without resolving a measured requirement | Defer |

## Authoring contract

The registry generates seven authoring tool schemas. The Cloudflare job bridge
accepts the same three new tool names; its queue, grants and claims are unchanged.

```mermaid
flowchart LR
  A[Source and expected generation] --> B[Prepare: freeze input and cold-stage]
  B --> C[Immutable candidate and digest]
  C --> D[Inspect candidate]
  D --> E[Publish exact digest]
  E --> F[Existing admission and generation CAS]
  F --> G[Composite checkpoint and tick activation]
  G --> H[Reconcile authoritative status]
```

1. Call `authoring_prepare_package` with the same source fields as
   `authoring_publish_package`: `request_id`, decimal-string
   `expected_generation`, `files`, optional `assets` and `character_job_id`.
   It returns `candidate_sha256`, the exact Package reference, and counts of
   prefabs, behaviors, dependencies and sealed bytes.
2. Inspect with `authoring_candidate_status({"request_id":"outpost-v1"})`.
   It rehashes the artifact inventory without compiling, polling generation,
   uploading or creating an unknown request.
3. Publish with `authoring_publish_prepared_package`:

   ```json
   {"request_id":"outpost-v1","candidate_sha256":"<64 lowercase hex characters from prepare>"}
   ```

4. Poll `authoring_release_status`. A submission receipt is not activation.
   `active` requires the exact Package closure, expected successor generation,
   no pending publication and no busy checkpoint in the authoritative server.

Preparation does **cold Package staging only**. It does not run a world preview,
prove world admission, simulate physics, estimate visual quality or authorize a
release. The summary reports this explicitly. Actual admission can still reject
a cold-staged candidate. Generation remains frozen even when the world advances;
`needs_rebase` requires a new request against a newly observed generation.

`authoring_publish_package` now calls prepare and exact-candidate publication.
Existing legacy journals retain their previous publication/status semantics;
an interrupted legacy preparation is not silently adopted or repaired.

## Identity, interruption and limits

`request.json` binds canonical authored input, compiler SHA-256, generated
character snapshot, and world/CDN/Edge target. Each attempt writes into a fresh
directory. A completed seal includes a hashed artifact inventory and is installed
without replacing an existing sealed directory. Concurrent preparations must
agree on the same result. The publisher verifies the inventory and candidate
digest before any upload; it never recompiles a reviewed candidate.

| State / event | Caller behavior |
| --- | --- |
| `preparing` | Repeat identical prepare input; at most four attempts per request. Interrupted attempts remain diagnostic evidence. |
| `prepared` | Inspect summary and candidate identity, then publish that digest. |
| `submission_unconfirmed` | Submission began but its durable local receipt is absent. Retry the same digest; do not assume rejection or invent a new request. |
| `submitted` | Local release receipt is recorded; poll authoritative release status. |
| `active` | Exact release is the current authoritative generation. |
| `activation_not_current` | Edge recorded activation, but that exact generation/closure is no longer current. This does not establish that old instances disappeared. |
| `needs_rebase`, `rejected`, `failed` | Surface the release reason; never change the frozen request behind its ID. |

An immutable submission intent is written before network activity. Losing an HTTP
reply therefore remains an unknown outcome. Resubmitting uses the same Edge
idempotency key and exact candidate; it does not mint a second logical release.
Two publishers can issue duplicate transport requests, so the guarantee relies
on Edge's existing exact-identity deduplication, not exactly-once HTTP delivery.

If a durable job is already marked failed, use a new **queue job ID** to retry
the same **engine request ID and input**. These are different identities.

Limits: 64 text members, 512 KiB UTF-8 each and 2 MiB aggregate; 32 imported GLB
member paths, 4 MiB each and 64 MiB aggregate; 3 MiB serialized frozen JSON;
256 output blobs and 256 MiB total sealed inventory. JSON escaping and the
existing 3 MiB MCP/remote transport limit can reject input below the raw-text
ceiling. A generated character is separately hash-bound and capped at 64 MiB.

Preparation tolerates process interruption. Synced files and immutable
installation are not a tested power-loss durability guarantee. Local storage
quotas, abandoned-job garbage collection, deadlines/cancellation, portable
artifact transfer and signed provenance remain work to qualify separately.

## Executable acceptance

[Recorded results and sanitized receipts](evidence/prepared-authoring-20260924/README.md)
include the real publication/recovery run, source and binary identities, and
the passing fault/regression suite. The shard recovered all 24 entities across
three Package revisions and continued 94 authority ticks without replaying builds.

Fault tests use an injected compiler/transport to cover interrupted output,
concurrent preparation/publication, changed compiler/input/target, artifact
tampering, generated-character mutation, bounded attempts, JSON expansion,
lost replies and one-step compatibility:

```powershell
node --test scripts/authoring/candidate-store.test.mjs scripts/authoring/authoring.test.mjs
```

The real rehearsal uses the Rust compiler, generated schemas through a separate
stdio MCP process, real local Edge D1/R2/DO, an authoritative shard and two
protocol clients. It establishes an empty delivery Package first: a bare genesis
serves Catalog V1, while the current Jev GameClient requires V2. No default
client/server compatibility policy changes as a side effect of this fixture.

```powershell
$env:CARGO_TARGET_DIR = 'target/agent-authoring-validation'
cargo build -p pajama-server --bin pajama-package --bin pajama-server
node scripts/authoring/demo-host.mjs target/prepared-host target/agent-authoring-validation/debug --natural-only
# Keep the isolated host running; use another terminal:
node scripts/authoring/verify-prepared-publication.mjs target/prepared-host/config.json target/prepared-proof
```

Do not pass `--recording-edge`: the rehearsal needs actual local Edge behavior.
It checks cold preparation without upload/activation, wrong-digest rejection,
MCP process restart, one deliberately lost reply after actual Edge acceptance,
same-release retry, three ABI2 updates, 8 -> 16 -> 24 exact replicated entities,
retained old IDs/transforms, and rejection of a stale-generation candidate.

After stopping **only the owned shard**, the existing recovery launcher uses
the same checkpoint without genesis seeding. An independent read-only observer
checks recovery and continued ticks:

```powershell
node scripts/authoring/recover-demo.mjs target/prepared-host
node scripts/authoring/verify-prepared-recovery.mjs target/prepared-host/config.json target/prepared-proof/proof.json target/prepared-recovery.json
```

Stop owned test processes before cleaning the exact task-specific Cargo target.
Retain only sanitized proof receipts in `docs/evidence`; host configuration,
credentials, certificate keys and local Edge storage stay in ignored `target/`.

This gate concerns primitive prefabs and protocol-client observations. It does
not establish native rendering, arbitrary historical GLB retention, universal
DSL correctness, remote production publication, or autonomous Jev content design.
The larger [architecture plan](AGENT_NATIVE_AUTHORING_PLAN.md) remains separate.
