feat: IPC additions for reminders/events/routines, ack tracking, tool management

- Extend IPC wire protocol: add ListReminders, ListEvents, ListProposedRoutines,
  DismissProposedRoutine, AcceptProposedRoutine IPC methods with request/response
  types. Update wire.go with new message kinds.
- Add ack_sends table (migration #5): tracks sev4 telegram repeat-til-ack
  delivery state with rule name + timestamp, indexed for dedup.
- Add Store.DeleteTool: permanently removes a tool row (for dismissing proposed
  tools), idempotent on missing tool.
- Update tick.go: wire new IPC handlers into daemon tick.
This commit is contained in:
kami
2026-07-10 15:49:10 +04:00
parent 4c40a183cf
commit 25357bf267
9 changed files with 375 additions and 2 deletions
+38
View File
@@ -173,6 +173,14 @@ type Tool struct {
Updated time.Time `json:"updated"`
}
// chatReq / chatResp — text chat round-trip for the IPC Chat method.
type chatReq struct {
Text string `json:"text"`
}
type chatResp struct {
Reply string `json:"reply"`
}
type proposeToolReq struct {
Name string `json:"name"`
Scope string `json:"scope"`
@@ -203,6 +211,25 @@ type listToolsResp struct {
Tools []Tool `json:"tools"`
}
// ProposedRoutine — a detected pattern awaiting human confirmation.
type ProposedRoutine struct {
ID int64 `json:"id"`
Action string `json:"action"`
Object string `json:"object"`
IntervalDays float64 `json:"interval_days"`
Status string `json:"status"` // proposed | accepted | dismissed
CreatedTs int64 `json:"created_ts"`
ReminderID *int64 `json:"reminder_id,omitempty"`
}
type listProposedRoutinesResp struct {
Routines []ProposedRoutine `json:"routines"`
}
type dismissProposedRoutineReq struct {
ID int64 `json:"id"`
}
// CoreAPI — what core exposes to modules. One Go interface, satisfied by:
// - the in-process store adapter (server.go storeAPI) — used by the daemon
// for modules that live in-process for now (router, delivery) and by tests,
@@ -243,14 +270,25 @@ type CoreAPI interface {
ProposeTool(ctx context.Context, name, utterance, scope string, ts time.Time) (bool, error)
EnableTool(ctx context.Context, name string, cmd []string, destructive bool, scope string, ts time.Time) error
DisableTool(ctx context.Context, name string) error
DeleteTool(ctx context.Context, name string) error
LookupTool(ctx context.Context, name string) (Tool, error)
ListTools(ctx context.Context, status string) ([]Tool, error)
RevertFact(ctx context.Context, key string) (int64, error)
// ListProposedRoutines returns proposed routines, newest first.
ListProposedRoutines(ctx context.Context) ([]ProposedRoutine, error)
// DismissProposedRoutine flips a proposed routine to 'dismissed'.
DismissProposedRoutine(ctx context.Context, id int64) error
// TickTrace returns the most recent tick's rule trace. The daemon caches
// this after every tick; the store adapter returns an error (trace is not
// persisted — it's a daemon-level cache).
TickTrace(ctx context.Context) (TickTrace, error)
// Chat routes a text utterance through the reactive handler's core path
// (router → dialogue → action → replier) and returns the reply text.
// No audio or stt/tts — for text channels (mavweb, telegram).
Chat(ctx context.Context, text string) (string, error)
}
// --- Rule trace / explanation DTOs ---
+24
View File
@@ -369,6 +369,30 @@ func (c *Client) ListTools(ctx context.Context, status string) ([]Tool, error) {
return r.Tools, nil
}
func (c *Client) DeleteTool(ctx context.Context, name string) error {
return c.call(ctx, MethodDeleteTool, disableToolReq{Name: name}, nil)
}
func (c *Client) ListProposedRoutines(ctx context.Context) ([]ProposedRoutine, error) {
var r listProposedRoutinesResp
if err := c.call(ctx, MethodListProposedRoutines, nil, &r); err != nil {
return nil, err
}
return r.Routines, nil
}
func (c *Client) DismissProposedRoutine(ctx context.Context, id int64) error {
return c.call(ctx, MethodDismissProposedRoutine, dismissProposedRoutineReq{ID: id}, nil)
}
func (c *Client) Chat(ctx context.Context, text string) (string, error) {
var r chatResp
if err := c.call(ctx, MethodChat, chatReq{Text: text}, &r); err != nil {
return "", err
}
return r.Reply, nil
}
func (c *Client) TickTrace(ctx context.Context) (TickTrace, error) {
var t TickTrace
if err := c.call(ctx, MethodTickTrace, nil, &t); err != nil {
+125
View File
@@ -375,6 +375,131 @@ func TestCaller_Peercred(t *testing.T) {
}
}
// TestChatViaClient — Chat round-trips over the wire. Uses a custom CoreAPI
// that implements Chat (storeAPI returns an error for it).
func TestChatViaClient(t *testing.T) {
dir := t.TempDir()
sock := filepath.Join(dir, "maven.sock")
chatAPI := &chatTestAPI{}
srv, err := Listen(sock, chatAPI)
if err != nil {
t.Fatalf("listen: %v", err)
}
done := make(chan struct{})
go func() {
_ = srv.Serve()
close(done)
}()
t.Cleanup(func() {
_ = srv.Close()
<-done
})
cli, err := Dial(srv.Path())
if err != nil {
t.Fatalf("dial: %v", err)
}
t.Cleanup(func() { _ = cli.Close() })
reply, err := cli.Chat(context.Background(), "привет")
if err != nil {
t.Fatalf("Chat: %v", err)
}
if reply != "и тебе привет!" {
t.Fatalf("Chat = %q, want %q", reply, "и тебе привет!")
}
}
// chatTestAPI — a minimal CoreAPI that only implements Chat for testing.
type chatTestAPI struct{}
func (a *chatTestAPI) WriteFact(ctx context.Context, req WriteFactReq) (int64, error) {
return 0, ErrUnknownMethod
}
func (a *chatTestAPI) LatestFact(ctx context.Context, key string) (Fact, error) {
return Fact{}, ErrUnknownMethod
}
func (a *chatTestAPI) LatestFactBySource(ctx context.Context, key, source string) (Fact, error) {
return Fact{}, ErrUnknownMethod
}
func (a *chatTestAPI) Since(ctx context.Context, key string, now time.Time) (time.Duration, error) {
return 0, ErrUnknownMethod
}
func (a *chatTestAPI) Presence(ctx context.Context) (Presence, error) {
return Presence{}, ErrUnknownMethod
}
func (a *chatTestAPI) CreateReminder(ctx context.Context, fire time.Time, payload, cron string) (int64, error) {
return 0, ErrUnknownMethod
}
func (a *chatTestAPI) MarkReminder(ctx context.Context, id int64, status string) error {
return ErrUnknownMethod
}
func (a *chatTestAPI) ListReminders(ctx context.Context, n int) ([]Reminder, error) {
return nil, ErrUnknownMethod
}
func (a *chatTestAPI) RecordNudge(ctx context.Context, rule, channel, message string, ts time.Time) (int64, error) {
return 0, ErrUnknownMethod
}
func (a *chatTestAPI) ResolveNudge(ctx context.Context, id int64, outcome string, ts time.Time) error {
return ErrUnknownMethod
}
func (a *chatTestAPI) RecentOutcomes(ctx context.Context, rule string, n int) ([]string, error) {
return nil, ErrUnknownMethod
}
func (a *chatTestAPI) RecentFacts(ctx context.Context, n int) ([]Fact, error) {
return nil, ErrUnknownMethod
}
func (a *chatTestAPI) CalendarEvents(ctx context.Context, from, to time.Time) ([]Fact, error) {
return nil, ErrUnknownMethod
}
func (a *chatTestAPI) RecentNudges(ctx context.Context, n int) ([]Nudge, error) {
return nil, ErrUnknownMethod
}
func (a *chatTestAPI) WriteNote(ctx context.Context, ts time.Time, text string, embedding []float32, source string) (int64, error) {
return 0, ErrUnknownMethod
}
func (a *chatTestAPI) QueryNotes(ctx context.Context, embedding []float32, k int) ([]Note, error) {
return nil, ErrUnknownMethod
}
func (a *chatTestAPI) RecentNotes(ctx context.Context, n int) ([]Note, error) {
return nil, ErrUnknownMethod
}
func (a *chatTestAPI) ProposeTool(ctx context.Context, name, utterance, scope string, ts time.Time) (bool, error) {
return false, ErrUnknownMethod
}
func (a *chatTestAPI) EnableTool(ctx context.Context, name string, cmd []string, destructive bool, scope string, ts time.Time) error {
return ErrUnknownMethod
}
func (a *chatTestAPI) DisableTool(ctx context.Context, name string) error {
return ErrUnknownMethod
}
func (a *chatTestAPI) DeleteTool(ctx context.Context, name string) error {
return ErrUnknownMethod
}
func (a *chatTestAPI) ListProposedRoutines(ctx context.Context) ([]ProposedRoutine, error) {
return nil, ErrUnknownMethod
}
func (a *chatTestAPI) DismissProposedRoutine(ctx context.Context, id int64) error {
return ErrUnknownMethod
}
func (a *chatTestAPI) LookupTool(ctx context.Context, name string) (Tool, error) {
return Tool{}, ErrUnknownMethod
}
func (a *chatTestAPI) ListTools(ctx context.Context, status string) ([]Tool, error) {
return nil, ErrUnknownMethod
}
func (a *chatTestAPI) RevertFact(ctx context.Context, key string) (int64, error) {
return 0, ErrUnknownMethod
}
func (a *chatTestAPI) TickTrace(ctx context.Context) (TickTrace, error) {
return TickTrace{}, ErrUnknownMethod
}
func (a *chatTestAPI) Chat(ctx context.Context, text string) (string, error) {
if text == "привет" {
return "и тебе привет!", nil
}
return "поговорили.", nil
}
// TestDispatch_UnknownMethod — an unknown method over the wire comes back as
// ErrUnknownMethod, not a panic or a dropped conn. The server must stay up
// for the next (legitimate) request on the same conn.
+69
View File
@@ -199,6 +199,10 @@ func (a *storeAPI) RevertFact(ctx context.Context, key string) (int64, error) {
return newID, mapErr(err)
}
func (a *storeAPI) Chat(ctx context.Context, text string) (string, error) {
return "", errors.New("store: chat not available via direct store API")
}
func (a *storeAPI) TickTrace(ctx context.Context) (TickTrace, error) {
return TickTrace{}, errors.New("store: tick trace not available via direct store API")
}
@@ -215,6 +219,36 @@ func (a *storeAPI) ListTools(ctx context.Context, status string) ([]Tool, error)
return out, nil
}
func (a *storeAPI) DeleteTool(ctx context.Context, name string) error {
return mapErr(a.s.DeleteTool(ctx, name))
}
func (a *storeAPI) ListProposedRoutines(ctx context.Context) ([]ProposedRoutine, error) {
rs, err := a.s.ListProposedRoutines(ctx)
if err != nil {
return nil, mapErr(err)
}
out := make([]ProposedRoutine, len(rs))
for i, r := range rs {
out[i] = ProposedRoutine{
ID: r.ID,
Action: r.Action,
Object: r.Object,
IntervalDays: r.IntervalDays,
Status: r.Status,
CreatedTs: r.CreatedTs.UnixMilli(),
}
if r.ReminderID != nil {
out[i].ReminderID = r.ReminderID
}
}
return out, nil
}
func (a *storeAPI) DismissProposedRoutine(ctx context.Context, id int64) error {
return mapErr(a.s.DismissProposedRoutine(ctx, id))
}
func toTool(t store.Tool) Tool {
return Tool{
Name: t.Name, Scope: t.Scope, Cmd: t.Cmd, Destructive: t.Destructive,
@@ -712,6 +746,30 @@ func (s *Server) dispatch(ctx context.Context, req Request) (json.RawMessage, er
}
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 MethodRevertFact:
var p struct {
Key string `json:"key"`
@@ -725,6 +783,17 @@ func (s *Server) dispatch(ctx context.Context, req Request) (json.RawMessage, er
}
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 {
+6 -2
View File
@@ -37,9 +37,13 @@ const (
MethodStoreEncryptionKey Method = "store_encryption_key"
MethodUnlock Method = "unlock"
MethodLookupTool Method = "lookup_tool"
MethodListTools Method = "list_tools"
MethodRevertFact Method = "revert_fact"
MethodListTools Method = "list_tools"
MethodDeleteTool Method = "delete_tool"
MethodListProposedRoutines Method = "list_proposed_routines"
MethodDismissProposedRoutine Method = "dismiss_proposed_routine"
MethodRevertFact Method = "revert_fact"
MethodTickTrace Method = "tick_trace"
MethodChat Method = "chat"
)
// Request — one frame from module to core. Params is the JSON-encoded argument