// Package worker is the audio-job boundary between core and its stt/tts // modules. // // It is a SIBLING to internal/ipc, not a reuse of it: // // - internal/ipc is the core↔module *state* boundary. Modules call INTO // core to write facts / read presence. core is the server; the module // is the client. The verbs (WriteFact, Since, ...) are authority-bound; // the auth layer scopes them per Caller. // - internal/worker is the core↔module *job* boundary. Core calls OUT to // the stt/tts module: "transcribe these bytes", "synthesize this text." // Core is the client here; the worker module is the server. The verbs // are not authority-bound — they are pure compute over the payload core // already owns (audio bytes / reply text). Nothing about authority moves // across this boundary because the worker has no key, no facts, nothing // stateful to read or write. // // The reversal matters: a module that ships audio work to core would have // to hold a Caller identity, an enrolled daemon surface, etc.; core that // ships work to a module ships only the work. Same unix-socket floor (0600 // dir + socket perms carry "same user") but the flow is reversed, and the // surface stays out of the auth cascade — the worker never reaches into // core's state, so it has nothing to be capped against. // // Transport: unix domain socket, local-only, same-host as core. Same wire // shape as ipc (4-byte big-endian length + JSON body) so `socat`/`nc` can // debug it identically. Frame cap is 64 MiB (vs ipc's 4) so audio blobs fit // — a minute of 16k mono int16 is ~1.9 MiB, comfortably under; an hour's // sleep-clip transcription is implausible to send as one frame but the cap // permits long-enough live clips without framing the worker into chunks. package worker import ( "encoding/json" "errors" "fmt" ) // maxFrame — 64 MiB. Audio up to ~3 minutes @ 16k mono int16 fits one frame // with headroom; longer audio is chunked by the caller (meeting-record mode // is post-MVP) or rejected with ErrFrameTooLarge. Sized for stt inputs and // tts outputs at single-utterance scale. const maxFrame = 64 << 20 // Method — one RPC verb. Adding one is a worker-API change, not an authority // change (worker has no authority surface — see package doc). The two verbs // mirror the stt/tts interfaces the daemon wires; new verbs go with a new // module kind (e.g. translation), not a new feature of an existing one. type Method string const ( MethodTranscribe Method = "transcribe" MethodSynthesize Method = "synthesize" ) // Request — one frame from core to a worker module. type Request struct { Method Method `json:"m"` Params json.RawMessage `json:"p,omitempty"` } // Response — one frame back from the worker. Exactly one of Result/Error is set. type Response struct { Result json.RawMessage `json:"r,omitempty"` Error *RpcError `json:"e,omitempty"` } // RpcError — a typed wire error. Mirrors internal/ipc's shape for tooling // parity (same args to socat, same `errors.Is` rehydration pattern); worker // sentinels are independent since this boundary is independent. type RpcError struct { Code string `json:"c"` Message string `json:"m,omitempty"` } func (e *RpcError) Error() string { if e.Message != "" { return fmt.Sprintf("worker: %s: %s", e.Code, e.Message) } return fmt.Sprintf("worker: %s", e.Code) } // Sentinel codes. Stable over the wire — do not rename. Mirror the package // sentinels 1:1. const ( codeUnknownMethod = "unknown_method" codeBadParams = "bad_params" codeInternal = "internal" ) // Sentinel errors. The client rehydrates a wire RpcError into one of these so // callers use errors.Is the same way they would in-process. var ( ErrUnknownMethod = errors.New("worker: unknown method") ErrBadParams = errors.New("worker: bad params") ) // codeOf maps a server-side sentinel to its wire code. Anything not matched // is codeInternal — internal Go error text never ships to the caller; the // server logs the real text and the client sees a generic code. func codeOf(err error) string { switch { case err == nil: return "" case errors.Is(err, ErrUnknownMethod): return codeUnknownMethod case errors.Is(err, ErrBadParams): return codeBadParams default: return codeInternal } } // rpcErr builds the wire error for a server-side error. message is omitted // for sentinel codes (the Code carries the meaning). internal errors carry // the message text — it's not authority-bearing text, just a diagnostic. func rpcErr(err error) *RpcError { c := codeOf(err) if c == codeInternal || c == codeBadParams { return &RpcError{Code: c, Message: err.Error()} } return &RpcError{Code: c} }