Skip to content

Commit f89176e

Browse files
committed
feat: emit exiting{reason:"requested"} on clean shutdown (Aegis Engine Protocol v1)
Aegis-AI's daemon (aegis-daemon, W7 Rust rewrite) needs to tell a clean, planned shutdown apart from a real crash. Today SwiftLM's SIGTERM/SIGINT handlers print plain text and exit(0) with no machine-readable signal at all. Adds a small shared emitEvent(_:) helper (used by both the existing ready event and this new one, replacing ready's inline JSONSerialization call) and emits {"event":"exiting","reason":"requested"} on both signal handlers before exiting. Consumers should ignore unrecognized future reason values rather than error on them. This is Phase 1 of a larger plan (see Aegis-AI's daemon/spec/engine-protocol.md §3/§4) — self-reporting a SPECIFIC failure reason on a real crash (OOM, model load failure, etc.) is separate, larger follow-up work, since none of those paths have any error handling to build on today. Verified: swift build --target SwiftLM (debug) and swift build -c release --product SwiftLM both green. Smoke-tested against a real Aegis-AI daemon build in an isolated sandbox: reached ready normally, and a real SIGTERM via the daemon's stop endpoint exited the process cleanly with the new event observed, without the daemon misclassifying it as a crash.
1 parent bb11618 commit f89176e

2 files changed

Lines changed: 37 additions & 5 deletions

File tree

‎Sources/SwiftLM/Server.swift‎

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,21 @@ final class ProgressTracker {
189189
}
190190
}
191191

192+
/// Emit one machine-readable JSON-lines event on stdout for the Aegis-AI
193+
/// daemon to consume (`docs/AEGIS_INTEGRATION.md`, and the daemon's own
194+
/// `daemon/spec/engine-protocol.md`). Shared by the `ready` event and the
195+
/// `exiting` event so the JSON-encode-and-flush boilerplate isn't
196+
/// duplicated at each call site — this is deliberately the SAME manual
197+
/// `JSONSerialization` shape the pre-existing `ready` event already used,
198+
/// not a new encoding convention.
199+
func emitEvent(_ payload: [String: Any]) {
200+
if let data = try? JSONSerialization.data(withJSONObject: payload),
201+
let json = String(data: data, encoding: .utf8) {
202+
print(json)
203+
fflush(stdout)
204+
}
205+
}
206+
192207
@main
193208
struct MLXServer: AsyncParsableCommand {
194209
static let configuration = CommandConfiguration(
@@ -1026,24 +1041,29 @@ struct MLXServer: AsyncParsableCommand {
10261041
}
10271042
readyEvent["partition"] = info
10281043
}
1029-
if let data = try? JSONSerialization.data(withJSONObject: readyEvent),
1030-
let json = String(data: data, encoding: .utf8) {
1031-
print(json)
1032-
fflush(stdout)
1033-
}
1044+
emitEvent(readyEvent)
10341045

10351046
// ── Graceful shutdown on SIGTERM/SIGINT ──
1047+
// Engine Protocol v1 (`daemon/spec/engine-protocol.md` §3, TD-7):
1048+
// emit `exiting{reason:"requested"}` before exiting so the daemon
1049+
// can tell a planned, no-error stop apart from a real failure —
1050+
// `requested` has no `ExitClassification` equivalent on the daemon
1051+
// side (a daemon-requested stop never reaches that classifier at
1052+
// all), it exists purely for the daemon to positively confirm this
1053+
// was a clean shutdown, not infer it from absence of other signals.
10361054
let shutdownSource = DispatchSource.makeSignalSource(signal: SIGTERM, queue: .main)
10371055
let interruptSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main)
10381056
signal(SIGTERM, SIG_IGN)
10391057
signal(SIGINT, SIG_IGN)
10401058

10411059
shutdownSource.setEventHandler {
10421060
print("\n[SwiftLM] Received SIGTERM, shutting down gracefully...")
1061+
emitEvent(["event": "exiting", "reason": "requested"])
10431062
Darwin.exit(0)
10441063
}
10451064
interruptSource.setEventHandler {
10461065
print("\n[SwiftLM] Received SIGINT, shutting down gracefully...")
1066+
emitEvent(["event": "exiting", "reason": "requested"])
10471067
Darwin.exit(0)
10481068
}
10491069
shutdownSource.resume()

‎docs/AEGIS_INTEGRATION.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ The server will emit a machine-readable JSON ready event on stdout when it is re
4747

4848
Aegis-AI should **wait for this event** before routing any requests to the server.
4949

50+
The server also emits a machine-readable JSON `exiting` event on stdout
51+
before a clean, planned shutdown (SIGTERM/SIGINT):
52+
53+
```json
54+
{"event":"exiting","reason":"requested"}
55+
```
56+
57+
`reason: "requested"` means this shutdown was expected (e.g. Aegis-AI
58+
called `stop`) — not a crash. This is the first of what may grow into a
59+
small set of self-reported exit reasons; consumers should ignore any
60+
`reason` value they don't recognize rather than treat it as an error.
61+
5062
---
5163

5264
## 🧠 Running 122B+ MoE Models (Critical)

0 commit comments

Comments
 (0)