Implement platform management features
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-14
|
||||
@@ -0,0 +1,75 @@
|
||||
## Context
|
||||
|
||||
Run currently persists a supervised process identity and forwards plugin-declared `process.stdout` and `process.stderr` through its durable spool. Autonomous lifecycle stream IDs are stable per Run endpoint, server, and stream key, so Platform cannot separate a later process start from the previous generation. The terminal then lists and replays all server streams, including old jobs, before it listens for SSE appends.
|
||||
|
||||
Platform remains the authorization and persistence boundary. Run remains the sole authority for the supervised process. The design must expose only a logical session identifier and timestamps, never output paths, PIDs, command lines, credentials, or direct process handles.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Follow only the current plugin-declared supervised process output in the live terminal.
|
||||
- Preserve the same session when Run restarts and resumes an already-running supervised process.
|
||||
- Switch an open terminal atomically when the supervised process starts a new generation.
|
||||
- Keep bounded recent replay for the selected session and keep all older streams readable as explicit history.
|
||||
- Keep RCON and any future plugin command transport independent of the log data path.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Expose browser shell access, host paths, raw sockets, or process stdin by default.
|
||||
- Infer game logs from arbitrary files or special-case SCUM, Minecraft, or another game.
|
||||
- Delete, migrate, or reinterpret legacy log bodies.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Process-generation session is generated and persisted by Run
|
||||
|
||||
`ProcessIdentity` receives a random/logical session ID and its start timestamp when a new managed process is started. The identity is journaled, so a Run restart resumes tailing and continues using the identical session for the still-running process. Starting a replacement process generates a new session.
|
||||
|
||||
The session ID is added to process log batches and autonomous process stream IDs. This avoids sequence collisions and gives Platform a durable grouping key. A stable stream ID with an inferred time was rejected because delayed spool upload could make an old process appear newer.
|
||||
|
||||
### Platform selects the current session only from supervised process streams
|
||||
|
||||
Platform persists the logical session ID and process-start timestamp with each stream. Run identifies an observed generation as `log-session:<session-id>`, and a persisted session is eligible as current only while the bound Run endpoint is online, Platform's latest Run-reported supervised-process fact is `running`, and that fact names the same session. This generation binding prevents a delayed batch from an older or not-yet-observed process from becoming current merely because it has a newer timestamp. Among the matching session's process streams, timestamp and deterministic ID ordering remain useful only for stable replay ordering. Only `source=process` streams with a non-empty session ID participate. Legacy, job, file-tail, and management-program logs remain historical and cannot displace a current terminal session.
|
||||
|
||||
Run reports observations for every process it supervises, including servers handled by a general worker rather than only a generated single-server Run. Platform publishes process-state changes into the existing server log subscription. A stopped, exited, or failed fact emits an empty session boundary; a later running fact re-evaluates the persisted streams. Run endpoint disconnect alone does not end a session because the game process may survive a Run restart, but a newly opened terminal treats an offline endpoint as having no proven current session until Run reconnects and reports the process fact.
|
||||
|
||||
Dispatching a new start clears Platform's previous managed-process binding before the job is queued. Historical output from the prior generation therefore cannot reappear during the interval between desired start intent and Run's first observation for the replacement generation.
|
||||
|
||||
Using the newest arbitrary stream was rejected because a file backfill or command result is not evidence of the process that the terminal must follow.
|
||||
|
||||
### SSE carries an explicit session boundary
|
||||
|
||||
The server log event endpoint begins with a `session` event, then exposes only the selected session's streams and a bounded replay. On each incoming process event Platform re-evaluates the active session. When it changed, Platform emits a new `session` event, its stream metadata, and a new selected-session replay before normal appends. The client clears the live buffer on the session boundary and remains connected; it never has to guess based on timestamps.
|
||||
|
||||
Re-opening a new EventSource on each restart was rejected because it races with output and leaves the old terminal contents visible while reconnecting.
|
||||
|
||||
### Durable source cursors make resumed file reads idempotent
|
||||
|
||||
Each process-output append carries its generation-local source-file cursor into the durable spool. The spool commits sequence allocation and the source cursor with the batch, restores both from pending segments, and treats a replayed cursor as already committed. This closes the crash window between a durable spool append and the process journal offset checkpoint.
|
||||
|
||||
The process journal retains superseded generations until both stdout and stderr files are fully drained. Starting generation B therefore cannot erase generation A recovery state if Run exits before A's final output is spooled. Existing journals without a session ID are upgraded in place for a still-running process so deployment of the new Run does not require restarting the game process.
|
||||
|
||||
### History is explicitly requested
|
||||
|
||||
The existing log-stream list/cursor contracts remain the history source. The terminal includes a deliberate history mode that reads selected historical stream cursors; it is not fed by the live SSE endpoint. This retains operator access without contaminating the live view.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Old Run versions emit no session ID] → Platform keeps their logs accessible in history but excludes them from live-follow selection; deployment is backward-safe and becomes live when Run is updated.
|
||||
- [A process emits before the initial SSE replay completes] → line identity is stream ID plus sequence and client-side merge de-duplicates replay/live overlap.
|
||||
- [Run exits after spooling a line but before checkpointing its file offset] → the durable source cursor makes the repeated read idempotent.
|
||||
- [A new process starts before the old output files finish draining] → Run journals the retired generation until both channels are complete.
|
||||
- [Session metadata tampering] → Platform accepts it only through the existing authenticated Run ingest channel and requires it to match the stream's immutable metadata.
|
||||
- [Unbounded current logs] → live replay remains bounded while the full selected stream remains available through explicit history cursors.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Deploy Platform so it accepts session metadata and treats old streams as history.
|
||||
2. Deploy Run so new managed process starts generate session-scoped stream IDs and resumptions preserve the persisted session.
|
||||
3. Deploy the frontend terminal that understands `session` SSE boundaries and offers explicit history.
|
||||
4. Rollback is safe: historical streams and batches remain immutable; an older frontend ignores the additional SSE event and Run can continue uploading session-scoped streams to the compatible Platform.
|
||||
|
||||
## Open Questions
|
||||
|
||||
None. The session scope is the plugin-declared process output, and the terminal's default is the current session.
|
||||
@@ -0,0 +1,31 @@
|
||||
## Why
|
||||
|
||||
The server terminal currently replays recent entries across every log stream ever created for an instance. That makes an open terminal appear stuck on stale startup output and leaves it unable to distinguish the process currently supervised by Run from a prior Run or process generation.
|
||||
|
||||
Operators need a terminal that continuously follows the current managed process even when Run or the process restarts, while keeping historical logs available deliberately and keeping game command execution separate from log transport.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Define a current supervised-log session identity for each Run-managed process generation and attach it to process stdout/stderr log streams.
|
||||
- Make the live terminal subscribe to the active supervised-log session by default, including a bounded recent replay from that session only.
|
||||
- Notify terminal subscribers when the active session changes so an already-open terminal automatically replaces the old session output with the new process generation and continues following it.
|
||||
- Clear the live terminal when Run reports that the supervised process stopped or exited, and restore the same session when a restarted Run confirms that the process survived.
|
||||
- Retain historical logs as an explicit history view instead of mixing them into the live terminal.
|
||||
- Preserve RCON and other plugin-declared command transports solely for command dispatch; they are not log sources.
|
||||
- Extend the local end-to-end smoke coverage with a process/Run generation switch and output markers proving that the terminal feed follows the new generation.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `current-supervised-log-session`: identifies and follows the current plugin-declared supervised process stdout/stderr session, including generation changes and separate explicit history access.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
<!-- No existing OpenSpec capability covers runtime log sessions. -->
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected code: Run process supervision and log metadata, Platform log ingest/domain/API/SSE handling, the server-management terminal API client and drawer, and the local debug smoke path.
|
||||
- Affected API: Run log ingest, supervised-process observations, and server log SSE gain a bounded session identity/filtering contract; a history-only query is exposed or retained separately.
|
||||
- Compatibility: existing persisted log streams remain readable through history, but no longer appear by default in the live terminal.
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Live terminal follows the current supervised process session
|
||||
The system SHALL identify each newly started Run-supervised process generation with a durable logical session ID, and SHALL associate its plugin-declared `process.stdout` and `process.stderr` streams with that session. A Run restart that resumes the same running process SHALL retain that session ID.
|
||||
|
||||
#### Scenario: Process output is collected independently of the terminal
|
||||
- **WHEN** a plugin-declared supervised process writes stdout or stderr while no browser terminal is open
|
||||
- **THEN** Run SHALL durably spool and upload that output under the process's logical session without opening an RCON connection or depending on browser state
|
||||
|
||||
#### Scenario: Run resumes a running process after restart
|
||||
- **WHEN** Run restarts while its persisted supervised process is still running
|
||||
- **THEN** Run SHALL resume collecting output with the persisted process session ID and the terminal SHALL continue following that session
|
||||
|
||||
#### Scenario: Run crashes between durable append and output checkpoint
|
||||
- **WHEN** a process line is already durable in the local spool but Run restarts before its output-file offset checkpoint is persisted
|
||||
- **THEN** Run SHALL recognize the repeated source cursor and SHALL NOT allocate or upload a duplicate log entry
|
||||
|
||||
#### Scenario: A replacement starts before the prior output drain completes
|
||||
- **WHEN** Run starts a replacement process while the prior generation still has unread stdout or stderr
|
||||
- **THEN** Run SHALL retain and resume the prior generation's drain state independently of the replacement generation
|
||||
|
||||
#### Scenario: Existing process journal predates session metadata
|
||||
- **WHEN** an upgraded Run loads a legacy journal for a supervised process that is still alive
|
||||
- **THEN** Run SHALL assign and persist a session for that same process without requiring the game process to restart
|
||||
|
||||
### Requirement: Live SSE exposes only the active supervised session
|
||||
The server log SSE endpoint SHALL select the supervised-process session named by the current `running` process fact from an online bound Run endpoint, publish an explicit session boundary, and replay only a bounded set of entries from that selected session. The process fact SHALL identify the generation as `log-session:<session-id>`. The endpoint SHALL exclude legacy, completed-job, file-tail, management-program, unbound, and older supervised-session entries from the default live feed.
|
||||
|
||||
#### Scenario: Terminal opens after prior process generations
|
||||
- **WHEN** an instance has historical job streams and older process sessions and an operator opens the terminal
|
||||
- **THEN** the endpoint SHALL publish only the active session metadata and its bounded recent output before live appends
|
||||
|
||||
#### Scenario: No current session exists
|
||||
- **WHEN** no session-scoped supervised process stream has been accepted for an instance
|
||||
- **THEN** the endpoint SHALL publish a ready empty live session and SHALL NOT replay unrelated historical streams
|
||||
|
||||
#### Scenario: Persisted session belongs to a stopped process
|
||||
- **WHEN** the newest persisted session belongs to a process that Run reported stopped, exited, or failed
|
||||
- **THEN** a newly opened terminal SHALL receive an empty live session and SHALL expose the persisted output only through history
|
||||
|
||||
#### Scenario: Output arrives before its generation observation
|
||||
- **WHEN** session-scoped process output is accepted before Run reports a matching `log-session:<session-id>` running observation
|
||||
- **THEN** the endpoint SHALL retain the output as history and SHALL NOT select it as the current live session until that matching observation arrives
|
||||
|
||||
#### Scenario: A new start is dispatched from a stopped instance
|
||||
- **WHEN** Platform accepts a new start request for an instance with a previous process-session binding
|
||||
- **THEN** Platform SHALL clear the previous binding before dispatch so historical output cannot become current while the replacement generation is unobserved
|
||||
|
||||
### Requirement: Open terminal changes session without reconnection
|
||||
The SSE endpoint SHALL detect a newer supervised-process session on accepted process output and SHALL emit a new session boundary, its stream metadata, and the new session replay to existing subscribers before sending subsequent output for that session.
|
||||
|
||||
#### Scenario: Process restarts while terminal is open
|
||||
- **WHEN** a replacement supervised process writes its first stdout or stderr entry while an operator's terminal is open
|
||||
- **THEN** the terminal SHALL discard the old live buffer, label the new current session, and display the new generation's output without the operator reopening the terminal
|
||||
|
||||
#### Scenario: Current process exits while terminal is open
|
||||
- **WHEN** Run reports that the current supervised process stopped, exited, or failed
|
||||
- **THEN** the same SSE connection SHALL emit an empty session boundary and the terminal SHALL remove the ended session from its live buffer
|
||||
|
||||
#### Scenario: Run reconnects to a surviving process
|
||||
- **WHEN** a Run endpoint reconnects and reports that its persisted supervised process is still running
|
||||
- **THEN** the same SSE connection SHALL reselect that process's existing session without creating a replacement session
|
||||
|
||||
### Requirement: Historical logs remain explicit and separate
|
||||
The system SHALL retain persisted log streams and cursor queries for historical inspection. The terminal SHALL expose history only through an explicit operator action or view and SHALL NOT merge it into the current live buffer.
|
||||
|
||||
#### Scenario: Operator reviews an older stream
|
||||
- **WHEN** an operator explicitly selects a historical log stream
|
||||
- **THEN** the terminal SHALL fetch and display that stream's retained cursor entries separately from the live session
|
||||
|
||||
### Requirement: Command transports are not log sources
|
||||
The system SHALL use RCON or another plugin-declared command transport only to execute a requested command. It SHALL collect live terminal output only through plugin-declared log sources, prioritizing the supervised process stdout/stderr channels.
|
||||
|
||||
#### Scenario: Operator sends an RCON command
|
||||
- **WHEN** an operator submits a SCUM management command
|
||||
- **THEN** the command SHALL use the protected RCON command path while live terminal output continues to arrive independently from the supervised process stream
|
||||
@@ -0,0 +1,28 @@
|
||||
## 1. Run process-session protocol
|
||||
|
||||
- [x] 1.1 Persist a new logical session ID and start timestamp for each newly supervised process while retaining it during Run output resumption.
|
||||
- [x] 1.2 Include session metadata in durable process log batches and make autonomous process stream IDs generation-scoped without changing non-process/job log behavior.
|
||||
- [x] 1.3 Add Run tests for new process generations and Run restart/resumption retaining the session.
|
||||
- [x] 1.4 Make source-file replay idempotent across crashes, retain undrained retired generations, atomically replace aggregated spool segments, and upgrade live legacy journal entries.
|
||||
|
||||
## 2. Platform active-session feed
|
||||
|
||||
- [x] 2.1 Persist and validate immutable session metadata on log streams, including session-scoped Run stream recognition.
|
||||
- [x] 2.2 Select active supervised-process streams and add session-boundary SSE events with selected-session-only replay and live filtering.
|
||||
- [x] 2.3 Add Platform service/API tests covering stale history exclusion, new-session switches, and command/log separation.
|
||||
- [x] 2.4 Gate initial current-session replay on online Run process facts and publish empty/recovered session boundaries on supervised process observations.
|
||||
|
||||
## 3. Terminal live and history views
|
||||
|
||||
- [x] 3.1 Extend frontend log contracts and SSE parsing for session boundaries.
|
||||
- [x] 3.2 Make the terminal clear and follow a switched session automatically, and provide an explicit historical-stream view.
|
||||
- [x] 3.3 Add frontend tests for current-session replay, live session switch, and separated history.
|
||||
|
||||
## 4. End-to-end verification
|
||||
|
||||
- [x] 4.1 Extend local debug smoke to prove first-generation output, process/Run generation switch, and post-switch terminal output.
|
||||
- [ ] 4.2 Run OpenSpec validation, focused unit tests, structural validation, and the complete local smoke; record any external-environment blocker precisely.
|
||||
- 2026-08-15 verification passed: `openspec validate follow-current-supervised-log-session --strict`; `go test ./api ./service ./domain ./dto ./validator` in `platform/`; `go test ./runtime ./spool ./protocol` in `run/`; `npm --prefix platform_web test -- ServerManagementTerminalDrawer client schemas/serverManagement`; `scripts/check-structure.sh`; `bash -n scripts/local-debug/smoke.sh`.
|
||||
- 2026-08-15 complete local smoke command attempted: `LOCAL_DEBUG_ROOT=/private/tmp/browser-local-debug-current-session-smoke LOCAL_DEBUG_PLATFORM_PORT=18198 LOCAL_DEBUG_WEB_PORT=5192 LOCAL_DEBUG_SELF_START=true scripts/dev-smoke.sh`.
|
||||
- Current supervised log-session proof passed inside that smoke, including generation A replay, Run restart/resumption, generation B switch on the same SSE connection, and explicit-history exclusion; evidence file: `/private/tmp/browser-local-debug-current-session-smoke/smoke/current-log-session-verification.json`.
|
||||
- External blocker: the complete smoke later failed in the platform Docker distribution builder at the host-native Run generation step because `go mod download` timed out fetching `github.com/dustin/go-humanize@v1.0.1` from `proxy.golang.org` (`dial tcp 142.251.33.209:443: i/o timeout`).
|
||||
Reference in New Issue
Block a user