* feat(environment): extract the initial state a capture's reads observed
Add the environment-state analysis view. From the parents the environment
manifest accepts, it extracts the data the capture's reads observed before
the first related write: entity types with an identity field (proved by a
path parameter or inferred from an identity name), entities with their
recorded values and provenance, documents for written non-entity paths, and
a render plan per read tool that is kept only when it reproduces the tool's
latest initial recordings from the state; every other read tool is a
recorded view with the reason. The Evidence is always inferred.
Response-body decoding moves to a domain owner shared with replay, exact
JSON parsing moves to the owner of the exact-number policy, the manifest and
state views share parent admission and record grouping, and
projectAnalysisView takes its accumulators as one object.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): bound state at redacted writes; infer identity only from items
A scenario write whose URL was redacted has no request line, so it was
dropped and reads after it counted as initial state. Such a successful
write now joins the timeline with an empty path, whose root relates to
every type, so it ends every type's state; a limitation counts them.
A list whose recordings were all empty passed the "every listed item
carries it" check vacuously and published an identity field nothing
showed; it now needs at least one item, else the view stays recorded
as no-identity.
A value that differed between two request lines of one item tool, such
as a query variant, was called volatile, and the gate then substituted
each recording's own value; only repeats of one request line now show
volatility, so differing lines go through fields-differ.
List membership is compared only while a list can still be the whole
collection, and merged entities are indexed by type and collection once,
so paged captures no longer cost pages times entities (8,000 pages of
40,000 entities: 5.7 s to 0.23 s).
Docs and the tool contract say scenario state keeps the capture's
redaction markers and how redacted writes bound it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): keep first sightings, one list pointer and linear field unions
Entities took the order of the first plan observed, and item plans run
before list plans, so a complete list read before its detail reads came
out reordered and lost "all" membership; each entity now keeps its
earliest recorded sighting.
A list plan reused the first recording's pointer for later recordings,
so a later response that was itself an entity wrapping a list was read
as that list; every recording must now locate the same list.
The field union scanned its ordered list for every key, quadratic in an
object's width; a set now answers membership.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): keep post-write values and partial lists out of the initial state
A value that changed between repeats of one request line was named
volatile whenever no write under the type's own root lay between, so a
write under another root (for example POST /api/dashboards/db before a
second GET /api/search) left its effect in the initial state. Any
recorded write between the two observations now leaves every tool of the
type recorded with reason fields-differ instead.
A list was published with membership "all" even when a total or count
envelope member exceeded the number it listed. Such a counter now makes
the membership "recorded".
Also drop a stale doc comment on ObservedEntity.order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(environment): scope item-shaped and deleted written paths out of documents
State documents are held only for list-shaped paths a POST, PUT or PATCH
tool writes. Reads of item-shaped written paths and of paths that are only
deleted stay recorded views; the guide now says so and the spec defers them
to a later increment of #4933.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): count writes in flight when a value changes between reads
The volatility check compared write start positions with the two reads,
so a write already in flight when the earlier read started was missed and
its effect was named volatile. A write now counts when its interval
overlaps the span from the earlier read's start to the later read's end.
The check also ran once per changed field and scanned every write. It now
runs once per pair of observations, answered by a binary search over the
writes in timeline order and the latest end among each prefix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): catch cross-tool changes across writes; read only item totals
A value that changed across a write was caught only between repeats of
one tool's request line, so a list read before a write and an item read
after it put the post-write value into the initial state. Changes are
now classified per entity in timeline order after observation: across
any recorded write, a change between repeats of a line or between two
lines of the type rewrites the type (every tool recorded as
fields-differ); with no write between, a repeat's change is volatile.
The classification lives in environmentStateMerge.ts.
Any numeric member whose name contained "total" or "count" counted as
an item total, so total_pages, account_id or page_count made complete
lists partial. Item totals are now members whose name tokens include
total or count and none of page, pages or per; a string of digits counts
as a number; and a set has_more, next or next-page token member also
marks the list partial.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): keep post-write observations out of initial state
* fix(environment): retain field provenance and bound state inference work
* feat(environment): serve tool calls from the environment state
`environment-serve --state <state.json>` serves an environment from the
initial state an environment-state view extracted. Read tools whose views
are state-backed answer from a live state store: item views render the held
entity into the recorded envelope, list views render the collection or the
recorded members, and document views return the held value; a missing entity
or one lacking a required field is refused, and requests the state cannot
answer fall back to their recordings with the reason. The /agentenv data
plane implements data/reset, data/add (a rea-state/v1 document as a data
part, base64 bytes, a file:// path or an https URL, validated and replaced
atomically) and data/get, and the card advertises those operations and an
intake declaration. Stage 1 is read-only: write tools are not served in
state mode. Without --state nothing changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(environment): record verification after merging the state view's review fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(environment)!: serve the environment state by default
environment-serve derives the environment state at startup by running the
environment-state view in-process on the capture with the manifest's
template rules, so an environment comes with its state and a user supplies
nothing extra. The derived Evidence is the same one inspect-analysis-view
produces by hand. --state is removed; --replay-only serves exactly the
stateless replay of main. When the state cannot be derived, or its catalog
does not fit the discovery budget, the server starts in replay mode, logs
the reason and reports it in the stop summary's `state`. Exported bundles
serve the state by default too: export derives it the same way, so the
verifier's tool count and checked answer match what is served, and the
README describes the served mode.
BREAKING CHANGE: environment-serve answers read tools from the derived
state and does not serve write tools unless --replay-only is given; the
stop summary reports `state` instead of `state_evidence_id`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): bound data-plane replies, keep batch order, cancel intake on stop
- One /agentenv response is held to 64 MiB: replies are measured as they
are produced and one past the budget becomes a -32000 error stating it.
- Every message of a batch enters the arrival-order queue on arrival, so
another request's reset never lands between them.
- Stopping the server, or a caller leaving, cancels the active download
and answers queued calls `cancelled` without running them.
- file:// URIs name only this host: another host or a network share is
refused before anything is opened.
- A loaded view value may not give an entity another identity.
- List answers refuse a member lacking a field every recording carried,
as item answers do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): keep bodies too deep for the state's nesting out of the state
A recorded body at the JSON depth limit was accepted by extraction, then
nested a few levels deeper as a document or entity value, so sealing the
state Evidence threw and environment-serve and environment-export stopped
instead of falling back. stateBodyJson, the one rule extraction and
serving share, now treats a body nesting deeper than the limit minus 16
levels as unusable, so its tool stays a recorded view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): measure data-plane replies before building them
83fcc739 bounded one /agentenv response to 64 MiB, but it still
serialized each reply whole before measuring it, so a batch of data/get
calls on a large state built every refused reply in full only to drop
it. Each reply is now measured in bounded JSON parts (the shared
jsonParts encoder), its parts kept only while it fits; a reply past the
budget is counted for its exact reply_bytes but never assembled. A test
checks that a refused read is never handed to JSON.stringify whole and
that its reply_bytes is exact.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): release batch replies as they are encoded; judge exact numbers by value
answerBatch kept every settled reply in its pending array until the
batch ended, so a batch alternating data/add and data/get held each
loaded state alive; each reply now leaves the queue as it is encoded.
The exact-number policy judged a number by its recorded spelling, so a
state loaded with 1e20 came back from data/get as
100000000000000000000, which data/add refused, breaking the promise
that data/get output loads back unchanged. A number is exact only when
both its recorded spelling and the one serialization writes are safe,
so integers from 2^53 below 1e21 are refused in any spelling; replay
keeps such bodies as recorded text. A domain test checks that every
accepted number is accepted again once written back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): keep room for refusals and framing in the reply budget
The response budget counted accepted replies only: a budget error that
replaced a reply, and a batch's brackets and commas, were not charged,
so near 64 MiB later refusals pushed the body past the advertised
limit. Every reply still to come now keeps room for its own budget
error, and a batch keeps room for its framing, so a refusal always fits
and the body never passes the budget. Reply ids and which messages are
answered come from one place shared by the replies and the reserve.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): refuse a loaded state that data/get would write past the intake limit
data/add measured only the document it was given, while data/get
writes the state back with the members a document may omit (types, the
state Evidence id, view fields and record citations). A document just
under 32 MiB could therefore load and be reported larger than any intake
accepts, breaking the data/get then data/add round trip. replace now
measures the state as data/get writes it, part by part, and refuses it
past the limit, leaving the state unchanged. The limit moves to the
state document module, so the application store can read it; the
intake re-exports it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): keep merged export checks on the shared test graph
Main's export tests installed module mocks for the whole services
project, which reuses one module graph. A tarball read failure then
missed its importer, and a one-shot heap mock could be taken by another
file. Read those through bindings the tests replace for one case.
Co-authored-by: morluto <morluto@users.noreply.github.com>
* fix(environment): keep a state file whose close failed owned until it closes
A file:// data/add that could not close its descriptor dropped the handle,
and an admission failure whose cleanup close failed dropped its owner, so
nothing could retry the close or report it.
The read now runs through withOwnedRegularFile, and a failed close is
retained in a server-lifetime ArtifactResourceScope. The next file read
retries it first and answers -32000 while it is still open; inline and
https documents still load. Closing the served environment retries it again
and rejects with the unfinished close.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
13 KiB
Serve the environment from its state by default
Issue: https://github.com/morluto/rea/issues/4933
Outcome
rea environment-serve <manifest> --evidence <capture> serves the environment
from the initial state the capture observed, with nothing else to supply: at
startup it derives the environment-state view of the capture in-process with
the manifest's template_rules. Read tools whose views are state-backed answer
from one live state, the /agentenv data plane implements data/reset,
data/add and data/get against it, and the card advertises those operations
and an intake declaration. --replay-only serves exactly the stateless replay
main serves: every manifest tool replays its recordings, /agentenv answers every method as not found and the card is
byte-identical. A state that cannot be derived, or whose catalog exceeds the
discovery budget, starts the server in replay mode, logs why and reports it. Exported bundles serve the state by default,
and their verifier and README match what they serve. Serving is read-only in
this increment: write tools are not served from the state.
Requirements
- Derive the state in the application layer, shared by serve and export:
deriveEnvironmentState(source, capture, operation)authenticates the capture value the command already read (an owned snapshot authenticated by the source load is reused, not parsed again), projects theenvironment-stateview with the manifest'stemplate_rulesthroughinspectAnalysisViewValidatedwith the capture as retained Evidence (the Evidence id equals that of a projection by hand, and the capture's strings are not charged again as inline input, which refuses captures of a few MiB), then appliesloadEnvironmentState:inspect_analysis_viewEvidence of kindenvironment-state, itsparent_evidence_idequal to the capture's, itstemplate_rulesequal to the manifest's in the same order, and every view naming a manifest tool, refusing otherwise withinvalid_requestwhose issue path starts withstate. The source validates a HAR capture with the owned borrowing parse, so the view reuses that validation. prepareServedState(source, capture, {name, operation})first builds the state catalog under the served name, then derives the state, and reports{served: true, evidence_id}or{served: false, reason: "unavailable", code, message}with the refusal's projected code and message: a state catalog over the discovery budget is unavailable withresource_constraint, and no state is derived for it. A derived state whose catalog fits always serves from the state. The CLI reports{served: false, reason: "replay-only"}itself for--replay-onlyand derives nothing.- Keep one state store per server process, shared by every MCP session and the
data plane: entities keyed by type, collection and id with per-collection
order taken from the entities array, documents keyed by tool.
resetrestores the Evidence state,replaceinstalls a validated document whole, and mutations are synchronous. - Answer item views by rendering the entity the path arguments name into the
recorded envelope of
envelope_ordinal. Match arguments as replay does: a held collection segment equal to the argument as written or after percent-decoding, preferring verbatim spellings, and the identity as the last argument percent-decoded, then as written, never re-split on/. Any held entity answers, and query arguments only setreplay.exact_match. Refuse an entity the state does not hold, or one lacking a required field, as a tool error and never invent a body. - Answer list views from the state only when the arguments name one of the
view's
checked_record_ordinalsexactly (every path and query value equal and no recorded query parameter unsupplied), choosing among them as replay chooses: every collection entity in state order formembership: "all", otherwise the recorded members the state still holds. A member lacking one of the view'srequired_fieldsmakes the call a tool error stating that the state holds no complete member, never a partial body, as for items. Lines recorded after a write or with other filters were never checked and answer from recordings. - Answer document views with the state's value when the arguments name the document's checked line exactly.
- Answer recorded views, tools without a view and unrecorded requests from the
recording, labelled
state: {source: "recording", reason}; label state answersstate: {source: "state", view, …}with the base record's replay provenance andtruncated: false; refuse a state answer over 256 KiB as a tool error rather than cut it. - Serve
data/reset({}),data/addwith exactly onedataorfilepart (bytes, afile://regular file or anhttps://download with a 30 s timeout covering the body, no redirects and a streamed 32 MiB cap;mimeTypewhen given isapplication/json) anddata/get({parts: [{kind: "data", data: rea_state}]}). Afile://uri whose host is neither empty norlocalhost, or whose path is a UNC path, is-32602before anything is opened. A document thatdata/getwould write back past 32 MiB, once it adds the members a document may omit, is-32602, so a reported state always loads again. Anything else is-32602, including emptyparts; a failed download is-32000; errors never echo a uri or content and name a coarse failure category inerror.data.reason. Calls run one at a time in arrival order across requests; every message of a batch is queued when the batch arrives, so no other request's call lands between them, and notifications run without a reply. - Stopping the server (
closeorprepareShutdown) aborts the data plane's stop signal, and a request's own signal aborts when its client leaves: the download in progress ends with reasoncancelled, queued calls are answered-32000with reasoncancelledwithout running, and adata/addwhose file read finished after the cancellation does not replace the state. - A
file://read whose descriptor close fails answers-32000and keeps the descriptor in a server-lifetime owner. The next file read retries the close first and, while it still fails, answers-32000without opening the file.closeretries it again and rejects while it is still open. - Bound one
/agentenvresponse to 64 MiB of replies (twice the intake limit). Each reply is measured in bounded JSON parts as it is produced, in message order, and assembled only when it fits, so a reply past the budget is counted but never serialized whole. Room is kept for every later reply's budget error and for a batch's brackets and commas, so the body never passes the budget; a reply that does not fit is replaced by-32000withdata: {max_response_bytes, reply_bytes}, unless that error is no smaller than the reply, so the body is never built past the budget and small write replies are kept. - Validate
rea-state/v1documents against the served types and views: known types, collections that instantiate their template, ids equal to the identity field, unique keys,view_fieldsnaming item or list views of the type without setting the type's identity field to a value whose scalar text differs from the entity's id (refused at that view'sfields), documents naming document views once at the initial path, and the optionalstate_evidence_idandtypesthatdata/getreports equal to the served ones. Refuse numbers without an exact native value and__proto__members. Name the first problem by JSON pointer, never deeper than a caller-data member (fields,value,types), and leave the state unchanged. - Keep record citations Evidence provenance: refuse
record_ordinalsor arecord_ordinalother than the Evidence's for that entity or document, and hold the Evidence's citations only for loaded values equal to the Evidence's; edited or added values cite none. - In state mode, declare
operations: ["data/reset", "data/add", "data/get"]and theurn:agentenv:intake/v1extension, describe tools as answering from the state where it backs them, and leave outPOST,PUT,PATCHandDELETEtools other than queryPOSTs. The discovery budget reserves the card and descriptions actually served (servedCatalogreturns the tools and the data plane the card declares). A state catalog can exceed the budget a replay catalog fits, so such a state is unavailable and the environment and its export serve replay: everything main served and exported is still served and exported. - The CLI has no
--state;--replay-only(default false) serves replay without deriving. Otherwise it prepares the state under the name it serves (ENVIRONMENT_NAMEor the manifest's). When the state is unavailable the command logs a warning with the code and message through its listener logger before binding, and serves replay. The stopped document'sstatemember is the report.serveEnvironmentkeeps taking an optional prepared state and serves replay without one; the served catalog (servedCatalog) and the answer function (environmentToolAnswer) are shared with export. environment-exportkeeps the bundle commandrea environment-serve /env/manifest.json --evidence /env/evidence.jsonand, after its name checks, prepares the state under the bundle name as that server will, to predict what it serves, so a refused--namederives nothing; its report is served or unavailable, never replay-only. The verifier'sTOOL_COUNTand the command'stool_countare the served tool count, and its check tool and expected status come from calling the server's answer function with{}on the first servedGETtool without path parameters whose answer is not an error. The verifier and smoke task use the idserve. The README's "What the environment serves" section describes serving from the state, the replay fallback and--replay-only, says which applies to the bundle (with the code and message when the state is unavailable), and keeps the not-emulation and untrusted-content caveats. The export output addsstate. The hosted-lane verifier script expects the served count.
Acceptance and limits
Services tests cover derivation (the by-hand Evidence id and state, a capture
the manifest was not projected from, a capture inline admission refuses),
the prepared reports (served, unavailable for a capture_browser_scenario
capture with header values but no response bodies, unavailable with
resource_constraint for a 350-tool read-only manifest whose replay catalog
fits), bundle plans served from the state and in replay (tool count, check tool
and status, a check whose state answer differs from replay's in record and
status, a check that skips a tool the server cannot answer, the replay fallback
of that 350-tool manifest, name refusals before any preparation, a manifest
neither mode admits, verifier, smoke task and README wording), the loader
refusals, store reset, order, round trip and
atomic replace, the first-problem pointers, citation provenance after a load and
every answer path: item (including percent-encoded collection segments and
ids), list all, list recorded, lines recorded after a write or with other
filters, a line replay's tie-break would not pick, document, recorded view, tool
without a view, unrecorded request, missing entity, missing required field and
the body limit, a view value that changes an entity's identity, and a list
member missing a required field. Adapters tests cover the data-plane shapes,
-32602 for empty and malformed parts and invalid documents, inexact numbers,
bytes, file:// and its failure categories, file:// uris naming another
host or a UNC path, injected https downloads (status, declared and streamed
size, the timeout whether or not the fetcher honours its signal), the default
fetch's refused redirects, arrival-order application across requests, a batch
another request's reset cannot split, cancellation by the server's stop and by
a departed caller, a server that closes while a download is stalled, a
retained state-file close that the next file read and close retry, the
response budget, batches and notifications, the card declaration, absent write
tools, a served query POST, the per-mode discovery reservation and a
data/add that changes the next tool answer over MCP. The mcp-boundary tests serve the state the
built CLI derives from only a manifest and a capture, and --replay-only with
every manifest tool, -32601 data-plane answers and a card whose SHA-256
equals that of the card main serves for the fixture; the process-boundary test checks the stopped document's
state for a derived state, --replay-only and an unavailable state, and the
warning logged before binding; the cli-boundary export test checks the served
tool count, the verifier and the README of an exported bundle. No test reaches
an external network. Write effects, list filters beyond recorded lines and
tables in the intake declaration are out of scope.
Deferred issue criteria
Write effects are a later increment of #4933. This PR must reference rather than close the issue.