feat(daemon): Subscribe command stream + CompleteCommand — the vendor loop closes
The HCACK-4 contract, implemented end to end. For every YAML-declared command the service registers a forwarding handler (new HostCommandRegistry names()/spec() accessors): with a subscribed tool client the command is queued onto the Subscribe stream (id + name + params via from_item) and the host is answered S2F42 HCACK=4 immediately — never blocking the io thread or the T3 window; with NO subscriber the command takes its declarative YAML ack (the honest pre-daemon behaviour). Settled + documented in the proto: v1 is a firehose with no buffering/replay. CompleteCommand correlates the pending id (audit; unknown id => PARAMETER_INVALID). Side effects stay suppressed on HCACK-4 (router applies them only on Accept), so the completion event the TOOL fires is the host's real signal — exactly E30's intent. Tests (daemon suite 101 -> 124 assertions): a real S2F41 dispatched through the full default-handler router ON the io thread under run_async — HCACK 4 with subscriber + params on the stream, declarative Accept without, CompleteCommand known/unknown, fallback restored after unsubscribe. Interop (now 20 checks, all green): the complete conformant loop against the secsgem-py reference host — S2F41 START -> S2F42 HCACK=4 -> tool receives Command(name=START, id=1) -> CompleteCommand -> FireEvent -> host receives S6F11. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+19
-13
@@ -28,7 +28,7 @@ host and stays conformant while the tool software restarts/upgrades/crashes.
|
||||
| `secs_gemd`: `SetVariables` / `FireEvent` / `GetControlState` / `GetVariables` | ✅ | **format-aware** both directions (declared SECS-II formats on write, `from_item` on read) and thread-safe (snapshot maps + posted writes + `read_sync` reads). In-process gRPC tests incl. run_async production mode (`test_daemon_service.cpp`, 61 assertions) |
|
||||
| Daemon interop vs **secsgem-py** reference host | ✅ | `interop/daemon_interop.py` (via `gemd` compose service): gRPC `SetVariables(ChamberPressure=2.5)` + `FireEvent` → host receives `S6F11 CEID 300` carrying `<F4 2.5>` — value *and declared format* flow gRPC→engine→HSMS→host |
|
||||
| Daemon interop vs **secs4j** (Java) | ⬜ | mirror the secsgem-py harness against `interop/secs4j` |
|
||||
| `Subscribe` host→tool command stream | ⬜ | design settled (HCACK-4, see below); not implemented |
|
||||
| `Subscribe` host→tool command stream + `CompleteCommand` | ✅ | HCACK-4 contract implemented + tested in-process AND live vs secsgem-py (full loop: S2F41 → stream → complete → S6F11) |
|
||||
| Universal RPC surface complete (vars/events/alarms/control-state/health) | ✅ | Phase A done; daemon tests 101 assertions, interop 15 checks |
|
||||
| Python client package (the "beautiful API") | ⬜ | thin wrapper over generated stubs |
|
||||
|
||||
@@ -72,11 +72,11 @@ your command" from "the work finished". The conformant, non-blocking flow:
|
||||
goes out) requires a deferred-reply mechanism in the engine — explicitly a
|
||||
v2 refinement, not needed for conformance.
|
||||
|
||||
Open sub-decisions to settle while implementing:
|
||||
- Per-command routing (subscribe to specific RCMDs?) or one firehose? (v1: firehose.)
|
||||
- Reconnect semantics: buffer commands while no subscriber (bounded queue +
|
||||
declarative fallback after timeout) or reject with HCACK 2? Must be decided
|
||||
and TESTED before calling the stream production-ready.
|
||||
Sub-decisions (settled 2026-06-10, implemented + tested):
|
||||
- v1 is a firehose: every subscriber receives every host request.
|
||||
- NO buffering: with no subscriber a command takes its declarative YAML ack
|
||||
and is not replayed on reconnect — never "will finish later" for work no
|
||||
tool will do. Documented in the proto's Subscribe contract.
|
||||
|
||||
## Plan — ordered next steps
|
||||
|
||||
@@ -147,13 +147,19 @@ debts tax every later phase, and the most valuable tests aren't automated.
|
||||
4. ✅ Done per-item above (daemon suite at 101 assertions; interop at 15 checks).
|
||||
|
||||
### Phase B — the command stream (the big one)
|
||||
5. ⬜ Implement `Subscribe`/`CompleteCommand` per the design above, including
|
||||
the no-subscriber fallback and bounded buffering. In-process gRPC tests:
|
||||
command arrives on stream; HCACK 4 on the wire; declarative fallback when
|
||||
unsubscribed.
|
||||
6. ⬜ Extend `daemon_interop.py`: secsgem-py host sends `S2F41 START` → gRPC
|
||||
tool receives it on the stream → tool fires completion event → host sees
|
||||
`S6F11`. (The full conformant loop against the reference implementation.)
|
||||
5. ✅ `Subscribe`/`CompleteCommand` implemented per the HCACK-4 design.
|
||||
Reconnect decision settled and documented in the proto: **no buffering** —
|
||||
a command with no subscriber takes its declarative YAML ack (the honest
|
||||
pre-daemon behaviour) and is not replayed. Firehose fan-out; per-command
|
||||
forwarding handlers registered from the registry (new `names()`/`spec()`
|
||||
accessors); pending-id audit map. In-process tests drive a REAL S2F41
|
||||
through the default-handler router on the io thread: HCACK 4 with a
|
||||
subscriber (params arrive on the stream), declarative Accept without,
|
||||
CompleteCommand known/unknown ids, fallback restored after unsubscribe.
|
||||
6. ✅ The full conformant loop runs against secsgem-py live: host `S2F41
|
||||
START` → `S2F42 HCACK=4` → tool receives Command(name=START, id) on the
|
||||
stream → `CompleteCommand` → tool fires the event → host receives `S6F11`.
|
||||
(interop now 20 checks.)
|
||||
7. ⬜ Java interop: `secs4j` host variant of the same scenario.
|
||||
|
||||
### Phase C — the beautiful Python client
|
||||
|
||||
Reference in New Issue
Block a user