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:
2026-06-10 20:27:18 +02:00
parent 1da56f973f
commit e6ee927900
6 changed files with 288 additions and 13 deletions
+9
View File
@@ -57,6 +57,15 @@ service Equipment {
// Subscribe to everything the host asks of this equipment. The daemon streams
// HostRequest messages for as long as the call stays open.
//
// Delivery contract (v1):
// - firehose: every subscriber receives every host request;
// - NO buffering: a command arriving while no client is subscribed is
// answered with its declarative ack from the equipment config (the
// pre-daemon behaviour) and is NOT replayed on reconnect — the daemon
// never tells the host "will finish later" for work no tool will do;
// - when a Command does arrive here, the host has ALREADY been answered
// with S2F42 HCACK=4; report the real outcome via FireEvent/SetAlarm.
rpc Subscribe(SubscribeRequest) returns (stream HostRequest);
// Report the outcome of a Command delivered on the stream, quoting its `id`.