Skip to content

feat(api): surface lastExitCode on ContainerSnapshot#1503

Open
chrisgeo wants to merge 4 commits into
apple:mainfrom
full-chaos:feat/chaos-1320-last-exit-code
Open

feat(api): surface lastExitCode on ContainerSnapshot#1503
chrisgeo wants to merge 4 commits into
apple:mainfrom
full-chaos:feat/chaos-1320-last-exit-code

Conversation

@chrisgeo
Copy link
Copy Markdown
Contributor

@chrisgeo chrisgeo commented May 2, 2026

Companion issue: #1501

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Motivation and Context

ContainerSnapshot today exposes RuntimeStatus.stopped but not the underlying exit code. External orchestrators, such as a Compose-spec orchestrator implementing depends_on: condition: service_completed_successfully, need to distinguish clean exit (exit code 0) from failed exit (non-zero) of a one-shot container. Without this, they have to treat .stopped as success and can silently misinterpret non-zero exits.

The data already exists internally: ContainersService.handleContainerExit(id:code:context:) receives ExitStatus? from the existing ExitMonitor callback wiring when a container exits via the runtime. This PR stamps that value onto the container snapshot.

See #1501 for the full motivation and design notes.

What this PR changes

  • Adds optional lastExitCode: Int32? to ContainerSnapshot, with a defaulted initializer parameter so existing construction sites continue to compile unchanged.
  • Records the exit status in the stopped container snapshot when ContainersService handles a container exit.
  • Adds focused coverage for:
    • ContainerSnapshot.lastExitCode defaulting to nil.
    • Codable round-trip behavior for non-nil lastExitCode.
    • Decoding older JSON payloads that do not contain lastExitCode.
    • The container exit snapshot mutation that sets .stopped, stamps lastExitCode, and clears network attachments.

Wire compatibility

ContainerSnapshot is marshaled as Codable JSON over XPC. This change is an additive optional scalar field, not a replacement of an existing Codable field's type or representation.

That distinction matters: replacing an existing persisted String path field with a synthesized FilePath field would change JSON from a plain string such as "/mnt/data" into swift-system's synthesized object representation, which would break persisted RuntimeConfiguration payloads on upgrade. This PR does not make that kind of wire-format change.

For this change:

  • Older clients against a newer server ignore the new lastExitCode key.
  • Newer clients against an older server decode the missing optional lastExitCode key as nil, matching the documented "never exited or exit not captured" case.
  • ContainerSnapshotTests includes an explicit missing-key decode test for this compatibility path.

Scope

This is intentionally in-memory only. The exit code lives in the in-memory ContainerState snapshot for API-server uptime. A daemon restart resets snapshots to .stopped without exit codes, matching existing restart behavior. Bundle persistence, such as writing an exit_status.json, is a deliberate out-of-scope follow-up.

Testing

  • swift build
  • swift test --filter 'ContainerSnapshotTests|ContainerExitStatusTests'
  • Added/updated tests for Codable compatibility and exit-status snapshot handling.
  • Added inline API documentation on the new ContainerSnapshot.lastExitCode field. No separate docs page appears to describe this model field today.

Adds an optional lastExitCode: Int32? field to ContainerSnapshot,
populated in ContainersService.handleContainerExit when the container
transitions to .stopped. The exit code is taken from the existing
ExitMonitor callback's ExitStatus value.

Motivation
----------

External orchestrators that drive the API server (Docker-Compose-style
service ordering is the canonical use case) need to distinguish between
'container exited cleanly' and 'container exited but the host gave up
on it'. The compose-spec depends_on: condition: service_completed_successfully
gate is exactly this distinction. Today, ContainerSnapshot exposes
.stopped without an exit code, so consumers can only fall back to a
stopped-on-time check and silently misinterpret a non-zero exit as
success.

Wire compatibility
------------------

ContainerSnapshot is marshaled as Codable JSON over XPC. Adding an
optional field is forward-compatible:

  - Older clients reading from a newer server: ignore the new key.
  - Newer clients reading from an older server: decode lastExitCode
    as nil (which the doc comment already documents as 'never exited
    or exit not captured').

Scope
-----

In-memory only. The exit code lives in the in-memory ContainerState
snapshot for the duration of apiserver uptime. A daemon restart resets
all snapshots to .stopped without exit codes (existing behavior). A
follow-up patch can add bundle persistence (exit_status.json) if the
daemon-restart case becomes operationally relevant.

Files
-----

- Sources/ContainerResource/Container/ContainerSnapshot.swift: new
  field + init parameter (default nil for backward compat at all
  existing construction sites; SandboxService.swift's snapshot
  constructor is unchanged).
- Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift:
  one assignment in handleContainerExit's terminal state block, taking
  exitCode from the existing 'code: ExitStatus?' parameter.
chrisgeo and others added 3 commits May 23, 2026 16:17
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Resolve ContainersService exit-state conflict by preserving upstream runtime-client refactor and applying lastExitCode snapshot handling.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants