feat(daemon): Phase D — GEM300 in-the-loop (jobs, recipes, EC changes)

Semantics settled and documented: v1 is observe-and-report. The engine keeps
acking S16/S3/S7/S2F15 from its FSM tables — exactly the behaviour both
reference implementations validated — while the tool observes lifecycle
events on the Subscribe stream and reports physical progress back. Gating
stays the documented v2 deferred-reply item.

Engine: two new store observers (HandlerSlot pattern) — RecipeStore fires
(ppid, body) after an add (S7F3 downloads), EquipmentConstantStore fires
(id, value) on ACCEPTED S2F15 writes only. Unit-tested.

Daemon: the service registers PJ/recipe/EC observers (io thread; add_
observers coexist with register_default_handlers' primaries) and fans the
new HostRequest variants out via push_request (fire-and-forget, no-
buffering contract). ProcessJob carries action (Start->START, Resume->
RESUME, Paused->PAUSE, Stopping->STOP, Aborting->ABORT) + recipe + material
bindings read store-side on the io thread. ReportProcessJob maps SETTING_UP
->SetupComplete, COMPLETE->ProcessComplete, ABORTED->AbortComplete via
read_sync; PROCESSING is informational; unknown job => INVALID_OBJECT,
table-rejected transition => CANNOT_DO_NOW. Carriers deferred (CarrierStore
has no observer machinery; ReportCarrier stays UNIMPLEMENTED) — roadmap.

Python client: on_process_job / on_recipe / on_constant_change decorators +
report_job(job_id, state); ProcessJob dataclass exported.

Tests: daemon suite 141 -> 175 assertions — the full in-process loop
(S16F11 create -> tool setup -> S16F5 PJSTART -> stream ProcessJob with
recipe+carriers -> ReportProcessJob(COMPLETE) -> FSM at ProcessComplete),
rejection paths, S7F3 -> ProcessProgram, S2F15 -> ConstantChange with the
configured name. Core 475/3097 (observer units). Live regression: daemon
interop 20 checks + pyclient 13 checks still green against the running
daemon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 23:53:45 +02:00
parent b30443089f
commit b1772cfefd
10 changed files with 333 additions and 10 deletions
+23 -6
View File
@@ -202,12 +202,29 @@ debts tax every later phase, and the most valuable tests aren't automated.
`cpp_mini_tool` is the worked example.
### Phase D — GEM300 in-the-loop (process/carrier tools)
10. Settle job/carrier semantics (who acks S16F5/S3F17, gate vs observe —
see proto comments), then wire `ProcessJob`/`CarrierAction` onto the
stream + `ReportProcessJob`/`ReportCarrier` into the PJ/CJ/carrier stores.
11. ⬜ Recipe download (`ProcessProgram` on the stream when S7F3 lands) and
EC-change notification (`ConstantChange` when S2F15 lands).
12. ⬜ Interop scenarios for jobs/carriers vs secsgem-py + secs4j.
10. 🚧 Semantics SETTLED: v1 is **observe-and-report** — the engine keeps
acking S16/S3/S7/S2F15 from its FSM tables (what both references
validated); the tool observes lifecycle events on the stream and reports
physical progress back. Gating (tool decides the ack) = the documented
v2 deferred-reply item. ✅ `ProcessJob` on the stream (PJ store observer:
→Processing/Start=START, /Resume=RESUME, →Paused=PAUSE, →Stopping=STOP,
→Aborting=ABORT; carries recipe + material bindings) +
`ReportProcessJob` (SETTING_UP→SetupComplete, COMPLETE→ProcessComplete,
ABORTED→AbortComplete; PROCESSING informational; INVALID_OBJECT /
CANNOT_DO_NOW on unknown job / illegal transition). ⬜ Carriers deferred:
`CarrierStore` has no observer machinery yet — apply the HandlerSlot
pattern, then mirror the job wiring; `ReportCarrier` stays gRPC
UNIMPLEMENTED (the stub default) until then.
11. ✅ `ProcessProgram` on the stream when S7F3 lands (new RecipeStore
added-observer) and `ConstantChange` when S2F15 is ACCEPTED (new
EquipmentConstantStore changed-observer; rejected writes never fire).
Python client: `on_process_job`/`on_recipe`/`on_constant_change` +
`report_job`.
12. 🚧 In-process E2E covers the full job loop (S16F11→S16F5→stream→report→
FSM, 175-assertion daemon suite); secs4j's 55 checks run against the
daemon (declarative path). ⬜ A live host-driven job loop with a
subscribed tool needs raw S16 frames host-side (secsgem-py 0.3.0 lacks
S16 builders — see interop/raw_gem300_harness.py for the frame source).
### Phase E — hardening & operations
13. ⬜ gRPC exposure: default to localhost + document UDS; optional TLS creds.