Skip to content

feat(api): wire ContainerEvent + events() streaming API (CHAOS-1323)#14

Open
chrisgeo wants to merge 1 commit into
mainfrom
feat/chaos-1323-events
Open

feat(api): wire ContainerEvent + events() streaming API (CHAOS-1323)#14
chrisgeo wants to merge 1 commit into
mainfrom
feat/chaos-1323-events

Conversation

@chrisgeo

@chrisgeo chrisgeo commented May 2, 2026

Copy link
Copy Markdown

Staging branch — fork-internal review before any apple/container upstream filing.

Linear: CHAOS-1323

Type of Change

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

Motivation and Context

External orchestrators that drive the API server (the canonical use case is a Compose-spec orchestrator implementing compose events) today have no daemon-side event signal — they have to poll ContainerClient.list on a 1-second cadence and diff snapshots to synthesize lifecycle events. That has three problems:

  1. 1s latency floor on event delivery.
  2. Events that happen between polls (a quick start → exit → restart) are lost.
  3. Polling cost grows linearly with project size.

Wiring real events at the daemon side replaces the polling-fallback with first-party signal.

What this PR changes

  • Sources/ContainerResource/Container/ContainerEvent.swift (new): ContainerEvent { containerId, action: { create, start, stop, die, destroy }, timestamp } as a Codable + Sendable + Equatable struct.
  • Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift: a private eventBuffer: [ContainerEvent] ring (capped at 1000) and a private recordEvent(_:action:) helper. Lifecycle methods (handleCreate, handleStart, handleStop, handleDelete-running, handleDelete-default) record the matching action. A new public recentEvents(since:) returns the buffered events to the harness.
  • Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift: new events(_:) XPC handler that JSON-encodes recentEvents() into the reply via the existing XPCKeys.containerEvent payload key.
  • Sources/Services/ContainerAPIService/Client/ContainerClient.swift: new events() method — sends the XPCRoute.containerEvent message and decodes [ContainerEvent] from the response.
  • Sources/APIServer/APIServer+Start.swift: register the harness' events handler for XPCRoute.containerEvent.

Total: 5 files (1 new + 4 modified), +99/−0.

Note on placeholder reuse

XPCRoute.containerEvent AND XPCKeys.containerEvent both already exist in main as reserved placeholders (no implementation behind them). This PR is purely the implementation that fills them in — no enum changes needed.

Wire compatibility

Pure additive at the API surface. Older clients ignore the new events() method. Older servers receiving a containerEvent route respond with whatever the unwired placeholder did before (typically an empty reply, which decodes as "no events").

Known limitations (intentional follow-ups)

  • Snapshot-style API, not push. Clients call events() and get the current buffer; long-lived subscribers still have to poll. A push-style AsyncStream<ContainerEvent> is a natural follow-up.
  • No per-client cursor. The buffer is global; clients deduplicate via recentEvents(since:) on the daemon side.
  • No persistence across daemon restarts. The buffer is in-memory.
  • Buffer cap hardcoded at 1000 events. Sufficient for typical Compose project sizes; large projects under chatty orchestration will see rollover. A configurable cap is a follow-up.

Testing

  • Tested locally (full swift build clean on macOS 26 / Apple silicon, all targets including downstream consumers of ContainerClient).
  • Added/updated tests — none yet; the recordEvent → recentEvents → harness round-trip is straightforward to test if maintainers want it.
  • Added/updated docs — public API doc comments added on the new type, each lifecycle action case, and the events() method (including the ring-buffer / no-persistence caveats).

Status

Draft, fork-staged. Routing to full-chaos/container:main first so we can review the surface internally before opening the apple/container companion issue + upstream PR (same pattern as CHAOS-1319 / CHAOS-1320 / CHAOS-1321 / CHAOS-1322 / CHAOS-1324).

@linear

linear Bot commented May 2, 2026

Copy link
Copy Markdown

@chrisgeo chrisgeo marked this pull request as ready for review May 14, 2026 19:49
Wires the placeholder 'XPCRoute.containerEvent' and 'XPCKeys.containerEvent'
that already existed in main into a working lifecycle-event stream:
the daemon now records create / start / stop / die / destroy events in
a bounded ring buffer, and 'ContainerClient.events()' returns the
buffered events to the client.

Motivation
----------

External orchestrators that drive the API server (the canonical use
case is a Compose-spec orchestrator implementing 'compose events')
today have no daemon-side event signal — they have to poll
'ContainerClient.list' on a 1-second cadence and diff snapshots to
synthesize lifecycle events. That has three problems:

  1. 1s latency floor on event delivery.
  2. Events that happen between polls (a quick start->exit->restart)
     are lost.
  3. Polling cost grows linearly with project size.

Wiring real events at the daemon side replaces the polling-fallback
with first-party signal.

What this PR changes
--------------------

- Sources/ContainerResource/Container/ContainerEvent.swift (new):
  ContainerEvent { containerId, action: { create, start, stop, die,
  destroy }, timestamp } as a Codable Sendable Equatable struct.
- Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift:
  a private 'eventBuffer: [ContainerEvent]' ring (capped at 1000) and
  a private 'recordEvent(_:action:)' helper. Lifecycle methods
  (handleCreate, handleStart, handleStop, handleDelete) record the
  matching action. A new public 'recentEvents(since:)' method returns
  the buffered events to the harness.
- Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift:
  new 'events(_:)' XPC handler that JSON-encodes recentEvents() into
  the reply via the existing 'XPCKeys.containerEvent' payload key.
- Sources/Services/ContainerAPIService/Client/ContainerClient.swift:
  new 'events()' method on ContainerClient — sends the
  'XPCRoute.containerEvent' message and decodes
  '[ContainerEvent]' from the response.
- Sources/APIServer/APIServer+Start.swift: register the harness'
  events handler for 'XPCRoute.containerEvent' (the route case
  already existed in main as an unwired placeholder).

Note: 'XPCRoute.containerEvent' and 'XPCKeys.containerEvent' both
already existed in main as reserved placeholders — no enum changes
needed in this PR. This is purely the implementation that fills them
in.

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

Pure additive at the API surface. Older clients ignore the new
events() method. Older servers receiving a containerEvent route
respond with whatever the unwired placeholder did before (typically
an empty reply, which decodes as 'no events').

Known limitations (intentional follow-ups)
------------------------------------------

- Snapshot-style API, not push. Clients call events() and get the
  current buffer; long-lived subscribers still have to poll. A
  push-style AsyncStream is a natural follow-up.
- No per-client cursor. The buffer is global; clients deduplicate
  via 'recentEvents(since:)' on the daemon side.
- No persistence across daemon restarts. The buffer is in-memory.
- Buffer cap is hardcoded at 1000 events. Sufficient for typical
  Compose project sizes; large projects under chatty orchestration
  will see rollover. A configurable cap is a follow-up.

Verification
------------

Full 'swift build' clean on macOS 26 / Apple silicon (release config,
all targets including downstream consumers of ContainerClient).
@chrisgeo chrisgeo force-pushed the feat/chaos-1323-events branch from 0fdd2e0 to 13495fe Compare May 24, 2026 13:29
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.

1 participant