Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,15 @@ This bounds extra maintenance work but does not bypass capacity, a busy lifecycl
gate or multi-page scheduling, and does not guarantee a resume deadline.

A recovered or uncertain running installation fails and uses existing cleanup, without replaying writes.
A hosted provisioning failure is terminal for its Session. The allocation's cleanup
transaction stores a safe reason with the failed Environment and records
`environment.failed`, `error` and one `agent.session.failed`; Session reads derive
`failed`, that reason and the failure time from the same record, and live streams end
after the failed event. The initializer reports only the integer exit status of a
failed sandboxed step. Core composes the reason from a fixed step label and that status,
never from command, package-manager or file output; unknown effects, timeouts and
receipts without a status keep a generic reason. New input then gets the observed 409
`conflict_error`; expiry and pending-input settlement keep their behavior.
Completed environments never reinstall initial files on reconnect or native recovery.
Provider RunCommand carries bounded stdin, not confidential argv. Only fixed trusted
initializers may run with Runtime authority. User setup and package install hooks
Expand Down Expand Up @@ -1266,8 +1275,9 @@ admission deadline plus response grace. Request/observer disconnect stops waitin
not the durable reservation or execution; retries keep the original identity and
deadline. The Worker remains the readiness, promotion and Start owner. Local failure
mapping uses 409 `environment_input_expired` / `environment_input_cancelled`, 503
`execution_unavailable` for ownership loss, and existing 404 for deletion. Exact
hosted failure status/body and pending-input crash recovery remain unverified.
`execution_unavailable` for ownership loss, and existing 404 for deletion. New input
after a hosted provisioning failure returns the observed 409 `conflict_error`; other
exact hosted failure statuses/bodies and pending-input crash recovery remain unverified.
Principal acceptance must use public Session creation and input against the built
standalone service, including a wait exceeding its ordinary 30-second write timeout,
real remote commands/files and a second native-history Turn. Private provisioning
Expand Down Expand Up @@ -2544,7 +2554,10 @@ replaced; do not carry obsolete compatibility code forward to satisfy this secti
connection comment and ends at once, admitting nothing and following no work,
because official same-key requests create distinct Sessions. Retry the same
request/key with `stream=false`, or use the GET events stream, to recover. GET
event streams keep their live-only start and never end on settlement.
event streams keep their live-only start and never end on settlement or a Turn
failure; the only server-side end is the terminal `agent.session.failed` of a
hosted provisioning failure (and Session deletion), since that Session can
never run again.
Disconnect never cancels admitted work. Official observations cover `none`
creation; self-hosted, hosted and no-input stream lifetimes and the retry
behavior are local choices, and the separate SDK one-Turn helper does not
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ build-agents-api-release:

check-agents-api: build-agents-api
go test ./services/agents-api/... ./packages/agents-client/... -count=1
PYTHONDONTWRITEBYTECODE=1 python3 services/agents-api/deploy/runtime/initialize_receipt_test.py

docker-build-agents-api:
./scripts/build-agents-api-image.sh
Expand Down
2 changes: 1 addition & 1 deletion apps/web/e2e/agents-lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1224,7 +1224,7 @@ test("keeps managed Environment resource and terminal event states fail-closed",
});
await page.reload();
await expect(page.getByText("Session cannot continue", { exact: true })).toBeVisible();
await expect(page.getByText("The environment is no longer available for this input.", { exact: true })).toBeVisible();
await expect(page.getByText('Failed to provision environment: script "setup_commands[0]" failed with exit code 3', { exact: true })).toBeVisible();
await expect(page.getByLabel("Message the Agent")).toBeDisabled();
opened = await openEnvironmentDialog(page);
await expect(opened.panel).toContainText("Managed Environment failed");
Expand Down
3 changes: 2 additions & 1 deletion apps/web/e2e/fixture-core.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,8 @@ function applyEnvironmentScenario(value) {
skills: [],
};
session.status = "failed";
session.error = "The environment is no longer available for this input.";
// A hosted provisioning failure reports the failed step and exit status only.
session.error = 'Failed to provision environment: script "setup_commands[0]" failed with exit code 3';
session.required_actions = [];
return;
}
Expand Down
5 changes: 4 additions & 1 deletion contracts/agents-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -821,7 +821,10 @@ reservation expires or fails. A creation that admitted nothing ends right after
`created`. A settlement that records no event ends the stream after the events
up to the cursor read with a settled projection in one snapshot; another client's
work drained before that read can still be sent. Observe later Turns with the GET
event stream, which never ends on its own. Terminal Turn events carry the Turn
event stream, which does not end on settlement or a Turn failure; it ends only
after the terminal `agent.session.failed` of a hosted provisioning failure, as
officially observed ([initialization failure](environment-templates.md#initialization-failure--september-23)),
or when the Session is deleted. Terminal Turn events carry the Turn
snapshot's `usage` at the top level, null when unknown.

The local `Idempotency-Key` creation extension shares identity across response
Expand Down
41 changes: 40 additions & 1 deletion contracts/agents-api/environment-templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,10 @@ version-1 JSON operation on stdin. It configures read-only tool env under
and runs ordered commands through distro bubblewrap. The fixed mount/process map
excludes daemon credentials, native history and staging. User values are applied
inside isolation, never to the launcher. Receipt and process exit must both confirm
completion; child output is discarded because it can contain secrets.
completion; child output is discarded because it can contain secrets. A failed step
that ran inside that isolation (a setup command or a package manager) adds only its
integer `exit_code` to the failed receipt; see
[Initialization failure](#initialization-failure--september-23).

Runtime receives a `tool_environment` execution flag, without template identity or
provider information. Adapters validate the common files and apply them in their
Expand All @@ -430,6 +433,42 @@ The adapter verifies the required trusted managed hook before preparation and
stops the Turn on an observed failed hook. Earlier command effects may already
exist; this is not an atomic hook-failure prevention guarantee.

## Initialization failure — September 23

When a hosted Environment fails to provision, Core now reports it the way the
official service does (evidence and rows H1–H8 in
[official semantics](official-semantics-alignment.md#hosted-initialization-failure--september-23)).
In the allocation's cleanup transaction, Core marks the Environment failed and
records, in order, `agent.session.environment.failed`, an `error` event and one
`agent.session.failed`. The Session reads `failed` with the safe reason as `error`
and the failure time as `last_active_at`; live streams end after the failed event.
New input on that Session returns 409 `conflict_error` "the hosted environment
failed to provision". Pending input settles as failed exactly as before.

The reason names only the failed step and its exit status:

| Step | Reason |
| --- | --- |
| Setup command `i` | `Failed to provision environment: script "setup_commands[i]" failed with exit code N` (observed) |
| Python packages | `... script "Python package installation" failed with exit code N` (observed label; the official reason appends raw pip output, Core never does) |
| npm or system packages | `... script "npm package installation"` / `"System package installation"` `failed with exit code N` (unverified) |
| Initial file or Skill with a confirmed failed write | `Failed to provision environment: initial file installation failed` / `Skill installation failed` (unverified) |
| Anything else | `Failed to provision environment: initialization did not complete` |

"Anything else" covers timeouts, the thirty-minute budget, unknown effects,
missing or malformed receipts, receipts without `exit_code` from Runtime images
built before this change, Plugin and capability installation, bootstrap
rejection and Core restart during initialization. Core treats a step as
confirmed failed only when the process exits 1 with empty stderr and stdout
decodes as a version-1 receipt whose `outcome` is `failed`. The decoder is
deliberately lenient so older images keep working: other receipt fields are
ignored and never read. The only value ever taken from the receipt is
`exit_code`, used only when it is an integer from 1 to 255. The Store composes the
reason from a fixed label and integers, so commands, env values, package names,
paths and any process output never reach the reason, events, logs or responses.
The failed step is
not retried and later steps do not run.

## System packages

`packages.system` accepts package names for the Runtime's Debian apt repositories,
Expand Down
34 changes: 34 additions & 0 deletions contracts/agents-api/history-events-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,37 @@ the TypeScript client and Web unit tests. `make openapi` adds only `x-nullable`
Item `phase` and event `output_index`; `make sqlc-generate` changes
`SessionTokenUsage` and adds the internal `SessionMeasuredTokenUsage`. The native pinned-SDK scripts updated for these shapes run
only with a native daemon, and live model acceptance is recorded separately.

## Hosted initialization failure events, 2026-09-23

Evidence: campaign scan 6 HI-01..04 (private
`~/.parsar/remediation/20260923/campaign-scan-6/hosted-init/`, raw frames
`official/007-S2-events.json` and `009-S3-events.json`); rows H1–H8 are in
[official semantics](official-semantics-alignment.md#hosted-initialization-failure--september-23).

- **Order.** A hosted Environment that fails to provision records
`agent.session.environment.failed`, `error` and `agent.session.failed` in one
transaction, as officially observed. The official streams showed no
`environment.pending` event; Core records none either.
- **Payloads.** `environment.error` is `{type: environment_error, code:
environment_connection_failed, message: "The environment failed to connect."}`.
The `error` event carries the pinned `SessionError`: `{type: environment_error,
code: sandbox_error, message: <safe reason>, param: null}`. Core's own
`stream_interrupted` frame keeps its three-field error without `param`, which
released clients validate exactly. The `agent.session.failed` snapshot has `status: failed`,
the reason as `error`, `required_actions: []` and the failure time as
`last_active_at`, identical to later retrieve and list reads. Pending input
settled by the failure is captured in the same snapshot.
- **Stream lifetime.** GET and creation streams end right after that
`agent.session.failed`, as the official GET stream did. This changes the
earlier rule that GET streams never end on their own, for this terminal case
only: a Turn failure leaves GET streams open because the Session can continue,
and a GET stream opened after the failure stays open (not observed officially).
- **Client.** The TypeScript client still raises `stream_interrupted` as an
`AgentCoreError`, and now delivers other `error` events to `onEvent` as
`AgentSessionErrorEvent`, before the failed snapshot. It accepts an optional
nullable `param` on stream errors. Core Web renders the failed Session and its
error from the snapshot and ignores the error event.

Environments that failed before migration `000062` have no recorded reason; they
keep their earlier projection and events.
68 changes: 68 additions & 0 deletions contracts/agents-api/official-semantics-alignment.md
Original file line number Diff line number Diff line change
Expand Up @@ -725,3 +725,71 @@ creation-stream and live events, deletion and retries. The pinned-SDK scripts
`official_mcp.py` and `official_mcp_credentials.py` assert the omitted origin,
the projection and the error fields. Real Core acceptance is recorded separately
by the coordinator.

## Hosted initialization failure — September 23

Hosted Environments that fail to provision now surface the failure as the
official service does, from Core main `e1970fd7`. Evidence is HI-01..04 in the
campaign scan recorded privately in
`~/.parsar/remediation/20260923/campaign-scan-6/hosted-init/findings.json`, with
raw official records under `official/`: `006-S2-create-setup-exit3`,
`007-S2-events`, `009-S3-events`, `021-S2-env-after-failed`,
`024-S2-session-after-failed`, `025-S3-session-after-failed`,
`026-S2-input-after-failure`, `027-S3-input-after-failure`, `038-S2-delete` and
`041-S3-delete`. Two owned `openai_hosted` Sessions without a Turn failed, one on a
setup command that echoed a value and exited 3, one on a nonexistent Python
package; both were deleted. The batch plan is
`~/.parsar/remediation/20260924/hosted-init-failure/PLAN.md`.

| Row | Case | Core behavior |
| --- | --- | --- |
| H1 | Hosted initialization fails in any step | One transaction records the Environment failure, `agent.session.environment.failed`, `error` and one `agent.session.failed`. The Session reads `status: failed`, the stored safe reason as `error`, `required_actions: []` and the failure time as `last_active_at`; retrieve, list and the event snapshot agree. Pending input reserved for the Environment settles as failed exactly as before, captured in the same snapshot. |
| H2 | `environment.failed` payload | `error` is `{type: environment_error, code: environment_connection_failed, message: "The environment failed to connect."}`. |
| H3 | `error` event | `{type: environment_error, code: sandbox_error, message: <reason>, param: null}`. |
| H4 | Reason | `Failed to provision environment: script "setup_commands[i]" failed with exit code N`, and `script "Python package installation"` for Python packages; the official Python reason also appends raw pip output, which Core never copies. npm, system package, initial file and Skill labels are unverified. Other failures use `Failed to provision environment: initialization did not complete`; see the [initialization lifecycle](environment-templates.md#initialization-failure--september-23). |
| H5 | Live SSE | GET and creation streams end right after that `agent.session.failed`. |
| H6 | Later `events.create` | 409 `conflict_error`/`conflict_error` "the hosted environment failed to provision", param null. Expired Environments, and input already waiting when the Environment failed, keep 409 `environment_unavailable`. |
| H7 | Delete | 200 `agent.session.deleted`, as officially, then 404. Deletion while provisioning is unchanged (HI-05 awaits a decision). |
| H8 | Unchanged | `self_hosted` and `none` Environments, successful initialization and its timing, the two-minute step limit (HI-06), expiry and tenant isolation. |

Decisions:

- **No output.** The shared Runtime initializer adds only an integer `exit_code`
to its failed receipt, and only for a step run inside its bwrap isolation;
Runtime helpers, signals and other errors keep the generic receipt, and the
exception is never serialized. Core confirms a failed step only when the
process exits 1 with empty stderr and a version-1 `failed` receipt. The
decoder is deliberately lenient for older images and ignores other fields; the
only value ever taken from the receipt is `exit_code`, and only as an integer
from 1 to 255. The Store composes the reason from a fixed step label and
integers, so commands, env values, package names, paths and process output
cannot reach the reason, events, logs or responses.
- **Storage.** The additive migration `000062_environment_failure.sql` adds the
nullable `environments.failure_reason` and `failed_at`; a check ties them to
`status = failed` and bounds the reason to 256 characters. Environments that
failed earlier keep NULL and their previous projection and events; new input
on them gets the H6 409.
- **Runtime images.** A Runtime image built before this change reports no
`exit_code`; its failures use the generic reason and otherwise follow H1–H7.
The Codex, Claude and MiniMax Code images must be rebuilt for exit statuses.
- **Scope of the terminal state.** Only a recorded hosted provisioning failure
makes the Session terminal. A Turn failure still leaves GET streams open, and
a GET stream opened after the failure stays open; that case was not observed.
- **Clients.** Official-shaped `error` events carry `param: null`; Core's own
`stream_interrupted` frame keeps its three-field error without `param`, so
released clients still report it as an interruption. The TypeScript client
delivers error events other than Core's `stream_interrupted` to `onEvent`
before the failed snapshot, instead of raising them. Core Web already renders
the failed Session, its error and the blocked input.

Go store tests on a dedicated PostgreSQL database drive the managed Worker with a
controlled Provider through setup exit statuses (first and later command),
Python packages, a receipt without `exit_code`, an unknown effect, raw output
instead of a receipt and a failed initial file write. They check the Session
read, list, the exact three events and snapshot, the H6 rejection, pending-input
settlement, tenant B and the absence of a canary. A real-PostgreSQL HTTP test
checks retrieve, list, the live GET stream and its end, the exact 409, tenant B
404s, delete and the canary in every body. Go API and contract tests pin the
projection, stream lifetime, wire shapes and error mapping; Python tests pin the
initializer receipt, and the TypeScript client and Web unit tests pass. Live
Docker acceptance is recorded separately by the coordinator.
Loading
Loading