Table-drive the IPC dispatcher instead of a 42-arm switch

dispatch() replaces the hand-written switch with a package-level
map[Method]handlerFunc built once at init. Each entry is one
withParams/withParamsVoid/withoutParams call closing only over the
CoreAPI method it invokes — adding a method is now one table line
instead of a new arm.

Check still runs once at the top before any unmarshal, unchanged. The
three non-CoreAPI methods (assert_stepup, store_encryption_key, unlock)
are special-cased before the table lookup since they drive Server
fields (StepUp/WrapKeyFn/UnlockFn), not store state. The current
CoreAPI is loaded once per dispatch and passed into the handler as an
argument, so SetAPI's runtime swap (the unlock transition) still takes
effect on the next request — the table itself never captures an api
value. No wire-format change; existing round-trip and unknown-method
tests in ipc_test.go pass unmodified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
This commit is contained in:
kami
2026-07-31 23:36:46 +04:00
parent ac36216f5d
commit 029449eefa
+241 -306
View File
@@ -501,6 +501,239 @@ func (s *Server) safeDispatch(ctx context.Context, req Request) (result json.Raw
return s.dispatch(ctx, req)
}
// handlerFunc — one table entry's shape: unmarshal req.Params (if it wants
// any), call the matching CoreAPI method against the api passed in, marshal
// the result. api is a parameter, not a closed-over field, precisely so a
// table built once at package init never pins a stale CoreAPI — see the note
// on methodTable below about SetAPI.
type handlerFunc func(ctx context.Context, api CoreAPI, raw json.RawMessage) (json.RawMessage, error)
// withParams adapts a (typed params, typed result) CoreAPI call into a
// handlerFunc: unmarshal into P, call fn, marshal R. On error the result is
// dropped (marshalResult's output is never read when err != nil — see
// serveConn) so every entry can uniformly return early on error without
// re-deriving what the pre-table per-arm code used to return in that case.
func withParams[P any, R any](fn func(ctx context.Context, api CoreAPI, p P) (R, error)) handlerFunc {
return func(ctx context.Context, api CoreAPI, raw json.RawMessage) (json.RawMessage, error) {
var p P
if err := unmarshalParams(raw, &p); err != nil {
return nil, err
}
r, err := fn(ctx, api, p)
if err != nil {
return nil, err
}
return marshalResult(r), nil
}
}
// withParamsVoid is withParams for the error-only methods (mark/resolve/
// enable/disable/...): params in, no result out, wire reply is always null.
func withParamsVoid[P any](fn func(ctx context.Context, api CoreAPI, p P) error) handlerFunc {
return func(ctx context.Context, api CoreAPI, raw json.RawMessage) (json.RawMessage, error) {
var p P
if err := unmarshalParams(raw, &p); err != nil {
return nil, err
}
return marshalResult(nil), fn(ctx, api, p)
}
}
// withoutParams is withParams for the handful of methods that take no
// params at all (Presence, TickTrace, MorningStatus, ListProposedRoutines).
// It does NOT call unmarshalParams — matching the pre-table arms, which
// never touched req.Params for these four methods.
func withoutParams[R any](fn func(ctx context.Context, api CoreAPI) (R, error)) handlerFunc {
return func(ctx context.Context, api CoreAPI, _ json.RawMessage) (json.RawMessage, error) {
r, err := fn(ctx, api)
if err != nil {
return nil, err
}
return marshalResult(r), nil
}
}
// methodTable — one entry per CoreAPI-backed method. Built once at package
// init, not per-Server and not per-dispatch: entries close over nothing but
// the CoreAPI method being called, and dispatch passes in the *current*
// api (loaded fresh via s.api.Load() every call, same as before the table
// existed) as an argument — so SetAPI's runtime swap (the unlock transition)
// is still honored on the very next request with no extra plumbing here.
//
// MethodAssertStepUp, MethodStoreEncryptionKey and MethodUnlock are NOT in
// this table: they bypass CoreAPI entirely (s.StepUp / s.WrapKeyFn /
// s.UnlockFn), so dispatch special-cases them before consulting the table.
var methodTable = map[Method]handlerFunc{
MethodWriteFact: withParams(func(ctx context.Context, api CoreAPI, p WriteFactReq) (idResp, error) {
id, err := api.WriteFact(ctx, p)
return idResp{ID: id}, err
}),
MethodLatestFact: withParams(func(ctx context.Context, api CoreAPI, p keyReq) (Fact, error) {
return api.LatestFact(ctx, p.Key)
}),
MethodLatestFactBySource: withParams(func(ctx context.Context, api CoreAPI, p keySourceReq) (Fact, error) {
return api.LatestFactBySource(ctx, p.Key, p.Source)
}),
MethodSince: withParams(func(ctx context.Context, api CoreAPI, p sinceReq) (sinceResp, error) {
d, err := api.Since(ctx, p.Key, p.Now)
return sinceResp{Dur: d}, err
}),
MethodPresence: withoutParams(func(ctx context.Context, api CoreAPI) (Presence, error) {
return api.Presence(ctx)
}),
MethodCreateReminder: withParams(func(ctx context.Context, api CoreAPI, p createReminderReq) (idResp, error) {
id, err := api.CreateReminder(ctx, p.Fire, p.Payload, p.Cron)
return idResp{ID: id}, err
}),
MethodMarkReminder: withParamsVoid(func(ctx context.Context, api CoreAPI, p markReminderReq) error {
return api.MarkReminder(ctx, p.ID, p.Status)
}),
MethodListReminders: withParams(func(ctx context.Context, api CoreAPI, p nReq) ([]Reminder, error) {
out, err := api.ListReminders(ctx, p.N)
if err != nil {
return nil, err
}
if out == nil {
out = []Reminder{}
}
return out, nil
}),
MethodRecordNudge: withParams(func(ctx context.Context, api CoreAPI, p recordNudgeReq) (idResp, error) {
id, err := api.RecordNudge(ctx, p.Rule, p.Channel, p.Message, p.Ts)
return idResp{ID: id}, err
}),
MethodResolveNudge: withParamsVoid(func(ctx context.Context, api CoreAPI, p resolveNudgeReq) error {
return api.ResolveNudge(ctx, p.ID, p.Outcome, p.Ts)
}),
MethodRecentOutcomes: withParams(func(ctx context.Context, api CoreAPI, p outcomesReq) ([]string, error) {
out, err := api.RecentOutcomes(ctx, p.Rule, p.N)
if err != nil {
return nil, err
}
if out == nil {
out = []string{} // stable non-null on the wire
}
return out, nil
}),
MethodRecentFacts: withParams(func(ctx context.Context, api CoreAPI, p nReq) ([]Fact, error) {
out, err := api.RecentFacts(ctx, p.N)
if err != nil {
return nil, err
}
if out == nil {
out = []Fact{}
}
return out, nil
}),
MethodCalendarEvents: withParams(func(ctx context.Context, api CoreAPI, p calendarEventsReq) ([]Fact, error) {
out, err := api.CalendarEvents(ctx, p.From, p.To)
if err != nil {
return nil, err
}
if out == nil {
out = []Fact{}
}
return out, nil
}),
MethodRecentNudges: withParams(func(ctx context.Context, api CoreAPI, p nReq) ([]Nudge, error) {
out, err := api.RecentNudges(ctx, p.N)
if err != nil {
return nil, err
}
if out == nil {
out = []Nudge{}
}
return out, nil
}),
MethodWriteNote: withParams(func(ctx context.Context, api CoreAPI, p writeNoteReq) (idResp, error) {
id, err := api.WriteNote(ctx, p.Ts, p.Text, p.Embedding, p.Source)
return idResp{ID: id}, err
}),
MethodQueryNotes: withParams(func(ctx context.Context, api CoreAPI, p queryNotesReq) ([]Note, error) {
out, err := api.QueryNotes(ctx, p.Embedding, p.K)
if err != nil {
return nil, err
}
if out == nil {
out = []Note{}
}
return out, nil
}),
MethodRecentNotes: withParams(func(ctx context.Context, api CoreAPI, p nReq) ([]Note, error) {
out, err := api.RecentNotes(ctx, p.N)
if err != nil {
return nil, err
}
if out == nil {
out = []Note{}
}
return out, nil
}),
MethodProposeTool: withParams(func(ctx context.Context, api CoreAPI, p proposeToolReq) (proposeToolResp, error) {
ok, err := api.ProposeTool(ctx, p.Name, p.Utterance, p.Scope, p.Ts)
return proposeToolResp{Proposed: ok}, err
}),
MethodEnableTool: withParamsVoid(func(ctx context.Context, api CoreAPI, p enableToolReq) error {
return api.EnableTool(ctx, p.Name, p.Cmd, p.Destructive, p.Scope, p.Ts)
}),
MethodDisableTool: withParamsVoid(func(ctx context.Context, api CoreAPI, p disableToolReq) error {
return api.DisableTool(ctx, p.Name)
}),
MethodLookupTool: withParams(func(ctx context.Context, api CoreAPI, p lookupToolReq) (Tool, error) {
return api.LookupTool(ctx, p.Name)
}),
MethodListTools: withParams(func(ctx context.Context, api CoreAPI, p listToolsReq) (listToolsResp, error) {
out, err := api.ListTools(ctx, p.Status)
if err != nil {
return listToolsResp{}, err
}
if out == nil {
out = []Tool{}
}
return listToolsResp{Tools: out}, nil
}),
// MethodDeleteTool shares disableToolReq — both take just a tool name.
MethodDeleteTool: withParamsVoid(func(ctx context.Context, api CoreAPI, p disableToolReq) error {
return api.DeleteTool(ctx, p.Name)
}),
MethodListProposedRoutines: withoutParams(func(ctx context.Context, api CoreAPI) (listProposedRoutinesResp, error) {
out, err := api.ListProposedRoutines(ctx)
if err != nil {
return listProposedRoutinesResp{}, err
}
if out == nil {
out = []ProposedRoutine{}
}
return listProposedRoutinesResp{Routines: out}, nil
}),
MethodDismissProposedRoutine: withParamsVoid(func(ctx context.Context, api CoreAPI, p dismissProposedRoutineReq) error {
return api.DismissProposedRoutine(ctx, p.ID)
}),
MethodAcceptProposedRoutine: withParamsVoid(func(ctx context.Context, api CoreAPI, p acceptProposedRoutineReq) error {
return api.AcceptProposedRoutine(ctx, p.ID)
}),
MethodRevertFact: withParams(func(ctx context.Context, api CoreAPI, p revertReq) (map[string]int64, error) {
newID, err := api.RevertFact(ctx, p.Key)
if err != nil {
return nil, err
}
return map[string]int64{"new_id": newID}, nil
}),
MethodChat: withParams(func(ctx context.Context, api CoreAPI, p chatReq) (chatResp, error) {
reply, err := api.Chat(ctx, p.Text)
return chatResp{Reply: reply}, err
}),
MethodTickTrace: withoutParams(func(ctx context.Context, api CoreAPI) (TickTrace, error) {
return api.TickTrace(ctx)
}),
// MorningStatus intentionally has no nil→[]T{} normalization here — the
// pre-table arm marshaled api.MorningStatus's result as-is (a nil slice
// serializes as JSON null), and this preserves that exact wire shape.
MethodMorningStatus: withoutParams(func(ctx context.Context, api CoreAPI) ([]MorningRoutineStatus, error) {
return api.MorningStatus(ctx)
}),
}
// dispatch unmarshals params for req.Method and calls the matching CoreAPI
// method. Unknown method ⇒ ErrUnknownMethod; a malformed params payload ⇒
// ErrBadParams with the underlying text (local, server-side, not shipped to
@@ -517,312 +750,11 @@ func (s *Server) dispatch(ctx context.Context, req Request) (json.RawMessage, er
return nil, err
}
}
// These three bypass CoreAPI entirely — they drive Server fields set
// directly by the daemon (StepUp / WrapKeyFn / UnlockFn), not store
// state, so they can never be table entries keyed on a CoreAPI method.
switch req.Method {
case MethodWriteFact:
var p WriteFactReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
id, err := api.WriteFact(ctx, p)
return marshalResult(idResp{ID: id}), err
case MethodLatestFact:
var p keyReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
f, err := api.LatestFact(ctx, p.Key)
if err != nil {
return nil, err
}
return marshalResult(f), nil
case MethodLatestFactBySource:
var p keySourceReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
f, err := api.LatestFactBySource(ctx, p.Key, p.Source)
if err != nil {
return nil, err
}
return marshalResult(f), nil
case MethodSince:
var p sinceReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
d, err := api.Since(ctx, p.Key, p.Now)
if err != nil {
return nil, err
}
return marshalResult(sinceResp{Dur: d}), nil
case MethodPresence:
pres, err := api.Presence(ctx)
if err != nil {
return nil, err
}
return marshalResult(pres), nil
case MethodCreateReminder:
var p createReminderReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
id, err := api.CreateReminder(ctx, p.Fire, p.Payload, p.Cron)
return marshalResult(idResp{ID: id}), err
case MethodMarkReminder:
var p markReminderReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
err := api.MarkReminder(ctx, p.ID, p.Status)
return marshalResult(nil), err
case MethodListReminders:
var p nReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
out, err := api.ListReminders(ctx, p.N)
if err != nil {
return nil, err
}
if out == nil {
out = []Reminder{}
}
return marshalResult(out), nil
case MethodRecordNudge:
var p recordNudgeReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
id, err := api.RecordNudge(ctx, p.Rule, p.Channel, p.Message, p.Ts)
return marshalResult(idResp{ID: id}), err
case MethodResolveNudge:
var p resolveNudgeReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
err := api.ResolveNudge(ctx, p.ID, p.Outcome, p.Ts)
return marshalResult(nil), err
case MethodRecentOutcomes:
var p outcomesReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
out, err := api.RecentOutcomes(ctx, p.Rule, p.N)
if err != nil {
return nil, err
}
if out == nil {
out = []string{} // stable non-null on the wire
}
return marshalResult(out), nil
case MethodRecentFacts:
var p nReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
out, err := api.RecentFacts(ctx, p.N)
if err != nil {
return nil, err
}
if out == nil {
out = []Fact{}
}
return marshalResult(out), nil
case MethodCalendarEvents:
var p calendarEventsReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
out, err := api.CalendarEvents(ctx, p.From, p.To)
if err != nil {
return nil, err
}
if out == nil {
out = []Fact{}
}
return marshalResult(out), nil
case MethodRecentNudges:
var p nReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
out, err := api.RecentNudges(ctx, p.N)
if err != nil {
return nil, err
}
if out == nil {
out = []Nudge{}
}
return marshalResult(out), nil
case MethodWriteNote:
var p writeNoteReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
id, err := api.WriteNote(ctx, p.Ts, p.Text, p.Embedding, p.Source)
return marshalResult(idResp{ID: id}), err
case MethodQueryNotes:
var p queryNotesReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
out, err := api.QueryNotes(ctx, p.Embedding, p.K)
if err != nil {
return nil, err
}
if out == nil {
out = []Note{}
}
return marshalResult(out), nil
case MethodRecentNotes:
var p nReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
out, err := api.RecentNotes(ctx, p.N)
if err != nil {
return nil, err
}
if out == nil {
out = []Note{}
}
return marshalResult(out), nil
case MethodProposeTool:
var p proposeToolReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
ok, err := api.ProposeTool(ctx, p.Name, p.Utterance, p.Scope, p.Ts)
if err != nil {
return nil, err
}
return marshalResult(proposeToolResp{Proposed: ok}), nil
case MethodEnableTool:
var p enableToolReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
return marshalResult(nil), api.EnableTool(ctx, p.Name, p.Cmd, p.Destructive, p.Scope, p.Ts)
case MethodDisableTool:
var p disableToolReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
return marshalResult(nil), api.DisableTool(ctx, p.Name)
case MethodLookupTool:
var p lookupToolReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
t, err := api.LookupTool(ctx, p.Name)
if err != nil {
return nil, err
}
return marshalResult(t), nil
case MethodListTools:
var p listToolsReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
out, err := api.ListTools(ctx, p.Status)
if err != nil {
return nil, err
}
if out == nil {
out = []Tool{}
}
return marshalResult(listToolsResp{Tools: out}), nil
case MethodDeleteTool:
var p disableToolReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
return marshalResult(nil), api.DeleteTool(ctx, p.Name)
case MethodListProposedRoutines:
out, err := api.ListProposedRoutines(ctx)
if err != nil {
return nil, err
}
if out == nil {
out = []ProposedRoutine{}
}
return marshalResult(listProposedRoutinesResp{Routines: out}), nil
case MethodDismissProposedRoutine:
var p dismissProposedRoutineReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
return marshalResult(nil), api.DismissProposedRoutine(ctx, p.ID)
case MethodAcceptProposedRoutine:
var p acceptProposedRoutineReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
return marshalResult(nil), api.AcceptProposedRoutine(ctx, p.ID)
case MethodRevertFact:
var p struct {
Key string `json:"key"`
}
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
newID, err := api.RevertFact(ctx, p.Key)
if err != nil {
return nil, err
}
return marshalResult(map[string]int64{"new_id": newID}), nil
case MethodChat:
var p chatReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
reply, err := api.Chat(ctx, p.Text)
if err != nil {
return nil, err
}
return marshalResult(chatResp{Reply: reply}), nil
case MethodTickTrace:
t, err := api.TickTrace(ctx)
if err != nil {
return nil, err
}
return marshalResult(t), nil
case MethodMorningStatus:
s, err := api.MorningStatus(ctx)
if err != nil {
return nil, err
}
return marshalResult(s), nil
case MethodAssertStepUp:
if s.StepUp != nil {
return marshalResult(nil), s.StepUp(ctx)
@@ -848,10 +780,13 @@ func (s *Server) dispatch(ctx context.Context, req Request) (json.RawMessage, er
return marshalResult(nil), s.UnlockFn(ctx, p.PublicKey)
}
return nil, fmt.Errorf("%w: %s", ErrUnknownMethod, req.Method)
}
default:
h, ok := methodTable[req.Method]
if !ok {
return nil, fmt.Errorf("%w: %s", ErrUnknownMethod, req.Method)
}
return h(ctx, api, req.Params)
}
func unmarshalParams(raw json.RawMessage, v any) error {