Repository navigation
feat(gateway): name the role and call behind each change to an env's state - #240
Merged
Merged
Conversation
…state Every write the gateway serves is now a window of changelog ids in its trajectory: a tool call's `tool_call_result` carries `changelog_id_before` next to `changelog_id`, a trigger's tool action already carried both, and a request that may write through the REST proxy (`/svc` and `/website`, any method but GET/HEAD/OPTIONS/TRACE) is logged as an `http_request` with its role, route, service, method, path, status and the trigger fires it was made for. The request's window closes when the response has ended, so a streamed answer (an MCP call's SSE) that writes after its headers is inside it. `GET /changes` (urn:agentenv:changes/v1) joins the env's changelog rows onto those windows: each row lists the actors whose window holds it, one for a row one call wrote, several for a row written while calls overlapped (performance mode), none for a write the gateway did not serve. Paged by `after_id` and `limit`. `snapshot_env` gains `include_env_changes`, which records the joined rows as JSONL in the object store under `metadata["env_changes"][env_id]`, next to the existing `include_env_trajectory` capture. An empty changelog now reads 0 instead of null, so the first write after the changelog triggers go in has a window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e rows; one-line comments - `_query_changelog_id` is back to MAX(id), so `/state`, `/step` and the trajectory still read null for an empty changelog; the attribution reads a null `before` as an empty changelog instead. - An `http_request` trajectory event is written only for a request that wrote changelog rows, so reads by POST, MCP handshakes and loads before the changelog triggers go in add nothing to the trajectory. - New tests: the stream is not held back by a write window, a mutating request that wrote nothing is not logged, and a first write on an empty changelog is still attributed. - Every added comment and docstring is one line; pre-existing ones are as they were. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… keeps its write, a request keeps its cause - A tool result with no `changelog_id_before` (a gateway-native tool) opens no write window, so a /step read after a write is no longer an actor on every earlier row; only an explicit null still reads as an empty changelog. - A child call that raises (after committing a write, say) is logged as a `tool_call_failed` event with its changelog ids, so its rows name it as a failed call; the original error still reaches the caller unchanged. - A proxied request's trigger cause is read when it starts, not when its response ends, so a fire that settles mid-stream is not lost or replaced. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
earakely-scale
approved these changes
Oct 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Name the role and the call behind each change to an env's state, so a verifier can tell the agent's changes from a trigger's, a user sim's or an executor's.
Today the before/after state diff says what changed but not who changed it. A task with triggers or a user sim can't separate "the agent sent this email" from "a trigger delivered this email".
changelog_id_beforenext to the existingchangelog_id. A call that fails is logged astool_call_failedwith both ids, so a write it committed before failing still names it (failed: true)./svcand/websiteproxy: newhttp_requesttrajectory events, with role, route, service, method, path, status, the trigger fires the request was made for, and the two ids.GET /changes(urn:agentenv:changes/v1, on the card): the env's changelog rows, each listing its actors. Paged withafter_idandlimit.performancemode runs them concurrently)snapshot_envgetsinclude_env_changes, next toinclude_env_trajectory. It saves the attributed rows as JSONL and records them undermetadata["env_changes"][env_id].Why at the gateway: it's the one component that sees both the caller (role, tool or request, trigger fire) and the changelog. Writing the actor into the changelog table itself would need every service, including ones outside this repo, to tag its own database connections.
Worth a look
nullin/state,/stepresponses and the trajectory. The attribution reads anullid before a call as an empty changelog; if that one read failed while the one after succeeded, earlier rows would also list this call, as an extra actor.http_requestevent is written only when a proxied non-GET request actually wrote changelog rows, so POST reads, MCP handshakes and loads before the changelog triggers go in add nothing. The trajectory's consumers here and in the hub count onlytool_callevents or copy it verbatim.consistentmode serializes tool calls, so their windows are exact. It doesn't serialize proxied writes. Should it?Tests
tst/unit/env/gateway/changes_test.py(new): the join, the recorded windows, a stream not held back by its window, a write-less request not logged, null kept for an empty changelog, and the three Greptile cases (a gateway-native read claims no earlier row, a failed call keeps its write and its error, a request keeps the trigger fire it started under).test_snapshot_env.py,gateway_card_test.pyandtest_env.pyupdated./svc/mcp-email/mcp(the SSE case).ruff, plugin-api (no break) anduv lock --checkpass/changestests, consistent mode, gateway trigger tests,register_env_triggerslocal tests)🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge; all three previous findings are addressed and no new actionable issues were found.Summary
The PR adds
/changesto show who may have written each changelog row, plus optional JSONL capture insnapshot_env.Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR A[Tool call or proxied request] --> B[Record starting changelog ID] B --> C[Run call or stream response] C --> D[Record ending ID and actor] D --> E[Join actors to changelog rows] E --> F[GET /changes] F --> G[Optional snapshot JSONL]Reviews (2) · Last reviewed commit: "fix(gateway): a result without a window ..." · Reviewed by Greptile