ed9bdd5e09
The plan answers "какие планы на сегодня?" by putting one day in order: calendar events (with #126's ambient provenance carried through and hedged), pending reminders, and one line per morning routine that still has items outstanding. "что дальше?" trims what has already passed. It lives in internal/morning, not in a parallel system, because it is the same question the checklist asks at a different scale — the routine knows what is missing from a window, the plan knows what the whole day holds, and both read the same facts and the same idea of "today". BuildPlan is pure; tickLoop.dayPlan is the impure half that reads the store. It is not a nag. Nothing here fires, schedules or announces: the plan is built only when asked, over IPC (day_plan) or on the existing /morning page. Unprompted delivery stays with the morning nudge and the dispatcher's policy. The query source sits before "calendar" in querySources because both match "…на сегодня" and the plan's matcher is the more specific one; IsDayPlanQuery matches whole words so "планёрка" (a meeting) is not read as a request for the plan, and refuses any utterance naming another day, since the plan is built for the clock's own day only. Verified: make build and make test both exit 0; new tests cover plan ordering, the checklist-only-what-is-left rule, other-day rejection, the RU rendering against the persona checks, rest-of-day trimming, the source ordering, and the matcher's refusals.
146 lines
5.4 KiB
Go
146 lines
5.4 KiB
Go
package ipc
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// Method — one RPC verb. The set is intentionally small: it mirrors exactly
|
|
// what a module legitimately needs from core state, and nothing more. Adding
|
|
// a method is a core-authority change (every method is a new thing a module
|
|
// can ask for); do it deliberately.
|
|
type Method string
|
|
|
|
const (
|
|
MethodWriteFact Method = "write_fact"
|
|
MethodLatestFact Method = "latest_fact"
|
|
MethodLatestFactBySource Method = "latest_fact_by_source"
|
|
MethodSince Method = "since"
|
|
MethodPresence Method = "presence"
|
|
MethodCreateReminder Method = "create_reminder"
|
|
MethodMarkReminder Method = "mark_reminder"
|
|
MethodListReminders Method = "list_reminders"
|
|
MethodRecordNudge Method = "record_nudge"
|
|
MethodResolveNudge Method = "resolve_nudge"
|
|
MethodRecentOutcomes Method = "recent_outcomes"
|
|
MethodRecentFacts Method = "recent_facts"
|
|
MethodCalendarEvents Method = "calendar_events"
|
|
MethodRecentNudges Method = "recent_nudges"
|
|
MethodWriteNote Method = "write_note"
|
|
MethodQueryNotes Method = "query_notes"
|
|
MethodRecentNotes Method = "recent_notes"
|
|
MethodProposeTool Method = "propose_tool"
|
|
MethodEnableTool Method = "enable_tool"
|
|
MethodDisableTool Method = "disable_tool"
|
|
MethodAssertStepUp Method = "assert_stepup"
|
|
MethodStoreEncryptionKey Method = "store_encryption_key"
|
|
MethodUnlock Method = "unlock"
|
|
MethodLookupTool Method = "lookup_tool"
|
|
MethodListTools Method = "list_tools"
|
|
MethodDeleteTool Method = "delete_tool"
|
|
MethodListProposedRoutines Method = "list_proposed_routines"
|
|
MethodDismissProposedRoutine Method = "dismiss_proposed_routine"
|
|
MethodAcceptProposedRoutine Method = "accept_proposed_routine"
|
|
MethodRevertFact Method = "revert_fact"
|
|
MethodTickTrace Method = "tick_trace"
|
|
MethodMorningStatus Method = "morning_status"
|
|
MethodDayPlan Method = "day_plan"
|
|
MethodChat Method = "chat"
|
|
)
|
|
|
|
// Request — one frame from module to core. Params is the JSON-encoded argument
|
|
// struct for Method (see api.go for the per-method shapes). The server
|
|
// unmarshals Params based on Method; an unknown Method ⇒ ErrUnknownMethod.
|
|
type Request struct {
|
|
Method Method `json:"m"`
|
|
Params json.RawMessage `json:"p,omitempty"`
|
|
}
|
|
|
|
// Response — one frame from core back to module. Exactly one of Result/Error
|
|
// is set. Result is the JSON-encoded return value of the method (might be a
|
|
// scalar, a struct, or null for void methods).
|
|
type Response struct {
|
|
Result json.RawMessage `json:"r,omitempty"`
|
|
Error *RpcError `json:"e,omitempty"`
|
|
}
|
|
|
|
// RpcError — a typed wire error. Code is one of the sentinel codes below;
|
|
// the client rehydrates it into the matching package sentinel so callers can
|
|
// use errors.Is like they would in-process (core's contract is the same on
|
|
// both sides of the wire — the boundary shouldn't change error semantics).
|
|
type RpcError struct {
|
|
Code string `json:"c"`
|
|
Message string `json:"m,omitempty"`
|
|
}
|
|
|
|
func (e *RpcError) Error() string {
|
|
if e.Message != "" {
|
|
return fmt.Sprintf("ipc: %s: %s", e.Code, e.Message)
|
|
}
|
|
return fmt.Sprintf("ipc: %s", e.Code)
|
|
}
|
|
|
|
// Sentinel codes. Stable over the wire — do not rename. Mirror the package
|
|
// sentinels in api.go 1:1. The string is the contract.
|
|
const (
|
|
codeNoFact = "no_fact"
|
|
codeConfidence = "confidence"
|
|
codeVoidsMissing = "voids_missing"
|
|
codeNudgeNotFound = "nudge_not_found"
|
|
codeNudgeOutcome = "nudge_outcome"
|
|
codeReminderMissing = "reminder_not_found"
|
|
codeReminderState = "reminder_state"
|
|
codeToolNotFound = "tool_not_found"
|
|
codeUnknownMethod = "unknown_method"
|
|
codeBadParams = "bad_params"
|
|
codeForbidden = "forbidden"
|
|
codeInternal = "internal"
|
|
)
|
|
|
|
// codeOf maps a server-side sentinel to its wire code. Anything not matched
|
|
// is codeInternal — we never leak internal Go error text to a module; it
|
|
// gets a generic "internal" and the daemon logs the real error server-side.
|
|
func codeOf(err error) string {
|
|
switch {
|
|
case err == nil:
|
|
return ""
|
|
case errors.Is(err, ErrNoFact):
|
|
return codeNoFact
|
|
case errors.Is(err, ErrConfidence):
|
|
return codeConfidence
|
|
case errors.Is(err, ErrVoidsMissing):
|
|
return codeVoidsMissing
|
|
case errors.Is(err, ErrNudgeNotFound):
|
|
return codeNudgeNotFound
|
|
case errors.Is(err, ErrNudgeOutcome):
|
|
return codeNudgeOutcome
|
|
case errors.Is(err, ErrReminderNotFound):
|
|
return codeReminderMissing
|
|
case errors.Is(err, ErrReminderState):
|
|
return codeReminderState
|
|
case errors.Is(err, ErrToolNotFound):
|
|
return codeToolNotFound
|
|
case errors.Is(err, ErrUnknownMethod):
|
|
return codeUnknownMethod
|
|
case errors.Is(err, ErrBadParams):
|
|
return codeBadParams
|
|
case errors.Is(err, ErrForbidden):
|
|
return codeForbidden
|
|
default:
|
|
return codeInternal
|
|
}
|
|
}
|
|
|
|
// rpcErr builds the wire error for a server-side error. message is omitted
|
|
// for sentinel codes (the Code carries the meaning; no need to echo text the
|
|
// caller can re-derive from errors.Is) and included for internal/bad-params
|
|
// where the text is the actual 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}
|
|
}
|