d52f60c54e
- Add CalendarEvents method to recordingAPI in auth_test.go - Add CalendarEvents method to fakeCore in handlers_test.go Co-Authored-By: opencode <opencode@anthropic.com>
124 lines
4.6 KiB
Go
124 lines
4.6 KiB
Go
// 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}
|
|
}
|