496559c9dd
Migration #22 adds done_when and blocked_on to tasks, both NOT NULL DEFAULT ''. "He has not written one" and "there is nothing to write" are the same state here, so no caller has to tell NULL from empty. blocked_on is a canonical Nexus entity id, never a name. It names a person and identity lives in Nexus, so free text here would be a second answer to a question Nexus already owns. The caller resolves before it writes. Both columns round-trip through ipc.TaskAPI: on ipc.Task, settable at intake through CaptureTaskReq, and writable afterwards through the new SetTaskFields, which is deliberately not one-way — he may sharpen a criterion, and a blocker clears when the person answers. SetTaskStatus now refuses candidate → open when done_when is empty (ErrTaskNoDoneWhen, mapped across the wire), the same refusal ParseTaskCapture makes for a capture marker with nothing after it: confirming work whose finish line nobody wrote is how a board fills with rows that can never leave it. Dropping such a candidate stays legal, and the /tasks confirm button now says what is missing instead of surfacing a not-found. One caller skips the gate. CaptureTask promoting a candidate he stated out loud would otherwise be denied intake rather than asked for a criterion, and a direct open capture never carried one either. The gate belongs to the deliberate promotion on /tasks, where V-511 puts a form.
176 lines
7.0 KiB
Go
176 lines
7.0 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"
|
|
MethodRecentActiveFacts Method = "recent_active_facts_by_kind"
|
|
MethodCalendarEvents Method = "calendar_events"
|
|
MethodRecentNudges Method = "recent_nudges"
|
|
MethodDeliveryAttempts Method = "delivery_attempts"
|
|
MethodRecentEcoTraces Method = "recent_ecosystem_traces"
|
|
MethodWriteNote Method = "write_note"
|
|
MethodQueryNotes Method = "query_notes"
|
|
MethodRecentNotes Method = "recent_notes"
|
|
MethodRecentNotesFromSource Method = "recent_notes_source"
|
|
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"
|
|
MethodMCPServers Method = "mcp_servers"
|
|
MethodDayPlan Method = "day_plan"
|
|
MethodChat Method = "chat"
|
|
MethodCaptureTask Method = "capture_task"
|
|
MethodListTasks Method = "list_tasks"
|
|
MethodSetTaskStatus Method = "set_task_status"
|
|
MethodSetTaskFields Method = "set_task_fields"
|
|
MethodIngestMail Method = "ingest_mail"
|
|
MethodSwapModel Method = "swap_model"
|
|
MethodModelStatus Method = "model_status"
|
|
MethodDescribeImage Method = "describe_image"
|
|
MethodCaptureStart Method = "capture_start"
|
|
MethodCaptureAppend Method = "capture_append"
|
|
MethodCaptureStop Method = "capture_stop"
|
|
MethodCaptureStatus Method = "capture_status"
|
|
MethodEnrollSpeaker Method = "enroll_speaker"
|
|
MethodListSpeakers Method = "list_speakers"
|
|
MethodForgetSpeaker Method = "forget_speaker"
|
|
MethodRecentEvents Method = "recent_events"
|
|
MethodSeedEvent Method = "seed_event"
|
|
|
|
// MethodPing — liveness, and the only method that answers in locked mode
|
|
// without a passkey assertion. It reaches no store, takes no arguments and
|
|
// returns whether the daemon is locked, so an operator tool can tell "she is
|
|
// up and waiting for a passkey" apart from "she is not there at all".
|
|
// Everything else about her state needs the store, and the store needs the
|
|
// key.
|
|
MethodPing Method = "ping"
|
|
)
|
|
|
|
// 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}
|
|
}
|