diff --git a/cmd/mavend/main.go b/cmd/mavend/main.go index 05d23f8..7968479 100644 --- a/cmd/mavend/main.go +++ b/cmd/mavend/main.go @@ -127,8 +127,10 @@ func run(args []string) error { cfgPath := flag.String("config", defaultConfigPath(), "path to mavend JSON config") wrappedKeyPath := flag.String("wrapped-key-file", "", "path to wrapped encryption key blob (enables cold-start unlock)") reembed := flag.Bool("reembed", false, "re-embed every stored note and fact with the configured embedder, then serve normally (run once after an embedder swap; the daemon does not answer until it finishes)") + allowSeed := flag.Bool("allow-seed", false, "enable the backdated seed_event write path (QA only: it lets a caller place a fact in the past and mint a routine the tick loop will then act on; off means the method has nothing to write with)") flag.CommandLine.Parse(args) reembedOnStart = *reembed + allowSeedOnStart = *allowSeed cfg, err := config.Load(*cfgPath) if err != nil { return err @@ -338,6 +340,7 @@ func run(args []string) error { getMorningStatus: func(ctx context.Context) []ipc.MorningRoutineStatus { return tl.morningStatus(ctx, time.Now()) }, getDayPlan: func(ctx context.Context) ipc.DayPlan { return tl.dayPlan(ctx, time.Now()) }, getEvents: intakeEventsFn(evBus), + seedStore: seedStoreIfAllowed(st), } if voiceW != nil && voiceW.handler != nil { api := coreAPI.(*daemonAPI) @@ -605,6 +608,7 @@ func run(args []string) error { getMorningStatus: func(ctx context.Context) []ipc.MorningRoutineStatus { return tl.morningStatus(ctx, time.Now()) }, getDayPlan: func(ctx context.Context) ipc.DayPlan { return tl.dayPlan(ctx, time.Now()) }, getEvents: intakeEventsFn(evBus), + seedStore: seedStoreIfAllowed(st), } if voiceW != nil && voiceW.handler != nil { newAPI.chatFn = voiceW.handler.handleText diff --git a/cmd/mavend/seed.go b/cmd/mavend/seed.go new file mode 100644 index 0000000..0740bbe --- /dev/null +++ b/cmd/mavend/seed.go @@ -0,0 +1,107 @@ +// mavend/seed.go — the backdated-fact seam (Vikunja #518). +// +// The pattern detector needs four events for one action+object, spread by at +// least pattern.MinIntervalDays, before it proposes a routine. Nothing could +// produce that against a running daemon in one sitting: the only writer is a +// fact write at time.Now(), so V-43, V-46, V-247 and V-254 all stopped at the +// same missing step and had been stopped there since they were filed. +// +// This is the write path that unblocks them, and it is deliberately the narrow +// one. It takes a fact, not an event, so pattern.Extract runs for real and a +// key the extractor ignores seeds nothing. It runs detectAndPropose, so what a +// seed proves is the daemon's own wiring rather than the detector in isolation +// — which is what an eval-lab fixture would have proved, and is not what those +// four tasks doubt. +// +// It is off unless mavend was started with -allow-seed, and AuthStepUp in the +// authority table besides. See ipc.SeedEventReq and auth.Requirement. +package main + +import ( + "context" + "database/sql" + "errors" + "fmt" + "log" + + "github.com/kami/maven/internal/ipc" + "github.com/kami/maven/internal/pattern" + "github.com/kami/maven/internal/store" +) + +// errSeedDisabled — what a caller gets on an ordinary box. Named rather than +// inline so the mavweb route can tell "not allowed here" apart from "the seed +// ran and the extractor declined", which look the same to a reader otherwise. +var errSeedDisabled = errors.New("mavend: seeding is off (start with -allow-seed)") + +// seedSource — every seeded fact carries this, and no other writer uses it. +// The point is that seeded data stays identifiable forever: a fact that came +// from a QA sitting must never be mistaken for something he said, either by a +// person reading /history or by the wipe in V-494 when it lands. +const seedSource = "seed:qa" + +// seedStoreIfAllowed returns st only when -allow-seed was passed, and logs the +// fact loudly when it does. A box that can rewrite its own past should say so +// in its boot log, so nobody reads a seeded routine months later as evidence of +// something he actually did. +func seedStoreIfAllowed(st *store.Store) *store.Store { + if !allowSeedOnStart { + return nil + } + log.Printf("seed: -allow-seed is ON — backdated fact writes are permitted under source %q (Vikunja #518)", seedSource) + return st +} + +// SeedEvent writes the fact at the caller's timestamp, extracts an event from +// it, and runs the same detect-and-propose step the voice path runs. +// +// Best-effort is NOT the shape here, unlike detectPattern: a seed that half +// worked is a QA result nobody can trust, so every step reports its own +// failure. Extraction declining is not a failure — it is the extractor's +// documented answer for a value outside its lexicon, and Extracted says so. +func (d *daemonAPI) SeedEvent(ctx context.Context, req ipc.SeedEventReq) (ipc.SeedEventResp, error) { + if d.seedStore == nil { + return ipc.SeedEventResp{}, errSeedDisabled + } + if req.Key == "" || req.Value == "" { + return ipc.SeedEventResp{}, errors.New("mavend: seed needs a key and a value") + } + if req.Ts.IsZero() { + return ipc.SeedEventResp{}, errors.New("mavend: seed needs an explicit timestamp") + } + + // No Subject, unlike the voice path: a seeded key must not queue a Nexus + // resolution. QA data has no business reaching the ecosystem. + factID, err := d.seedStore.WriteFact(ctx, req.Ts, store.KindSelf, req.Key, req.Value, seedSource, 1.0, sql.NullInt64{}) + if err != nil { + return ipc.SeedEventResp{}, fmt.Errorf("seed write fact: %w", err) + } + resp := ipc.SeedEventResp{FactID: factID} + + ev := pattern.Extract(factID, req.Key, req.Value, req.Ts) + if ev == nil { + // The fact is written and stays written. Saying so matters: a caller + // that assumed a seed always produces an event would otherwise read + // four silent successes and conclude the detector is broken. + log.Printf("seed: %s=%s wrote fact %d, no event (value outside the action lexicon)", req.Key, req.Value, factID) + return resp, nil + } + resp.Extracted, resp.Action, resp.Object = true, ev.Action, ev.Object + + eventID, err := d.seedStore.CreateEvent(ctx, factID, ev.Action, ev.Object, req.Ts) + if err != nil { + return resp, fmt.Errorf("seed create event: %w", err) + } + resp.EventID = eventID + + r, routineID, err := detectAndPropose(ctx, d.seedStore, ev.Action, ev.Object, req.Ts) + if err != nil { + return resp, fmt.Errorf("seed detect: %w", err) + } + if r == nil { + return resp, nil // too few events yet, too irregular, or already decided + } + resp.Proposed, resp.RoutineID, resp.IntervalDays = true, routineID, r.IntervalDays + log.Printf("seed: proposed routine %d — %s/%s every %.1f days", routineID, r.Action, r.Object, r.IntervalDays) + return resp, nil +} diff --git a/cmd/mavend/seed_test.go b/cmd/mavend/seed_test.go new file mode 100644 index 0000000..33786f0 --- /dev/null +++ b/cmd/mavend/seed_test.go @@ -0,0 +1,105 @@ +package main + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/kami/maven/internal/ipc" +) + +// Off is the default and it must mean "nothing to write with", not "permission +// to refuse later". A daemonAPI with no seedStore writes no fact at all. +func TestSeedRefusedWithoutTheFlag(t *testing.T) { + d := &daemonAPI{CoreAPI: ipc.UnimplementedCoreAPI{}} + _, err := d.SeedEvent(context.Background(), ipc.SeedEventReq{ + Key: "cat_water_fountain", Value: "заправил", Ts: time.Now(), + }) + if err == nil { + t.Fatal("seed succeeded with no seedStore") + } + if !strings.Contains(err.Error(), "-allow-seed") { + t.Errorf("error does not name the flag: %v", err) + } +} + +// The whole point of the task: four seeds spread past the detector's floor +// produce a proposal against the real daemon path, which is what nobody could +// do before (Vikunja #518). Three seeds must NOT propose — MinEvents is four, +// and a test that only checked the happy end would pass on an off-by-one. +func TestSeedFourEventsProposesARoutine(t *testing.T) { + ctx := context.Background() + d := &daemonAPI{CoreAPI: ipc.UnimplementedCoreAPI{}, seedStore: newTestStore(t)} + now := time.Now() + + var last ipc.SeedEventResp + // Oldest first, three hours apart — past MinIntervalDays (two hours). + for i := 3; i >= 0; i-- { + var err error + last, err = d.SeedEvent(ctx, ipc.SeedEventReq{ + Key: "cat_water_fountain", + Value: "заправил", + Ts: now.Add(-time.Duration(i) * 3 * time.Hour), + }) + if err != nil { + t.Fatalf("seed %d: %v", i, err) + } + if !last.Extracted { + t.Fatalf("seed %d: no event extracted from a lexicon verb", i) + } + if i > 0 && last.Proposed { + t.Fatalf("proposed after only %d events, MinEvents is 4", 4-i) + } + } + if !last.Proposed { + t.Fatal("four spaced events did not propose a routine") + } + if last.Action != "refill" || last.Object != "cat_water_fountain" { + t.Errorf("wrong pair: %s/%s", last.Action, last.Object) + } + if last.IntervalDays < 0.1 { + t.Errorf("interval %v — the detector saw a burst, not a rhythm", last.IntervalDays) + } + + // The proposal is readable through the same list the /routines page uses, + // which is the wiring an eval-lab fixture would not have proved. + proposed, err := d.seedStore.ListProposedRoutines(ctx) + if err != nil { + t.Fatalf("list: %v", err) + } + if len(proposed) != 1 { + t.Fatalf("expected 1 proposed routine, got %d", len(proposed)) + } +} + +// A value outside the action lexicon writes the fact and says it seeded +// nothing. Silence here would read as four working seeds and a broken +// detector. +func TestSeedReportsWhenExtractionDeclines(t *testing.T) { + d := &daemonAPI{CoreAPI: ipc.UnimplementedCoreAPI{}, seedStore: newTestStore(t)} + resp, err := d.SeedEvent(context.Background(), ipc.SeedEventReq{ + Key: "mood", Value: "ok", Ts: time.Now(), + }) + if err != nil { + t.Fatalf("seed: %v", err) + } + if resp.FactID == 0 { + t.Error("fact was not written") + } + if resp.Extracted || resp.EventID != 0 || resp.Proposed { + t.Errorf("claimed an event for a non-action value: %+v", resp) + } +} + +// A seed with no timestamp is refused rather than defaulting to now: the only +// reason this seam exists is the caller choosing when, so a zero Ts is a bug in +// the caller and must not silently write a fact at the wrong time. +func TestSeedRequiresAnExplicitTimestamp(t *testing.T) { + d := &daemonAPI{CoreAPI: ipc.UnimplementedCoreAPI{}, seedStore: newTestStore(t)} + if _, err := d.SeedEvent(context.Background(), ipc.SeedEventReq{ + Key: "cat_water_fountain", Value: "заправил", + }); err == nil { + t.Fatal("seed accepted a zero timestamp") + } +} diff --git a/cmd/mavend/tick_api.go b/cmd/mavend/tick_api.go index d1c50e3..ad733ba 100644 --- a/cmd/mavend/tick_api.go +++ b/cmd/mavend/tick_api.go @@ -10,6 +10,7 @@ import ( "github.com/kami/maven/internal/ipc" "github.com/kami/maven/internal/loop" + "github.com/kami/maven/internal/store" ) // daemonAPI wraps a store-backed CoreAPI and overrides TickTrace with the @@ -22,6 +23,11 @@ type daemonAPI struct { chatFn func(ctx context.Context, conversation, text string) string getMCPServers func() []ipc.MCPServerStatus getEvents func(n int) []ipc.IntakeEvent + // seedStore — non-nil ONLY when mavend was started with -allow-seed. It is + // the whole off-switch for the backdated write path (Vikunja #518), and it + // is a store rather than a bool so that leaving the flag off means the + // method has nothing to write with, not merely permission to refuse. + seedStore *store.Store } // RecentEvents — the unified intake journal (Vikunja #283). Empty, not an diff --git a/cmd/mavend/voicewire.go b/cmd/mavend/voicewire.go index 2a48dfd..fbbd7c4 100644 --- a/cmd/mavend/voicewire.go +++ b/cmd/mavend/voicewire.go @@ -552,6 +552,11 @@ func repairFactVectors(dataStore *store.Store, emb router.Embedder) { // runReembed. var reembedOnStart bool +// allowSeedOnStart is the -allow-seed flag (set in run()). Opt-in, and the +// default is the one that matters: a box nobody is testing has no live path to +// write a fact into the past. See seed.go and Vikunja #518. +var allowSeedOnStart bool + // checkStoredEmbedder compares the embedder we just loaded with the one that // wrote the vectors already in the DB (Vikunja #378). // diff --git a/cmd/mavweb/main.go b/cmd/mavweb/main.go index 09aa456..57e4480 100644 --- a/cmd/mavweb/main.go +++ b/cmd/mavweb/main.go @@ -1094,34 +1094,53 @@ func handleRoutines(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI, se var msg string if r.Method == http.MethodPost { action := r.FormValue("action") - idStr := r.FormValue("id") - var rid int64 - if n, _ := fmt.Sscanf(idStr, "%d", &rid); n != 1 { - http.Error(w, "invalid id", http.StatusBadRequest) - return - } - switch action { - case "accept": + // "seed" is the one action with no routine to act on — it is what + // MAKES a routine (Vikunja #518), so it runs before the id parse. It + // lives on this route rather than a page of its own because it is + // already the step-up-gated surface for this table, and a second gated + // surface is a second thing to get wrong. + if action == "seed" { if !stepUpOK(session, requireStepUp) { http.Error(w, "step-up required: assert a passkey first", http.StatusForbidden) return } - if err := acceptRoutine(ctx, core, rid); err != nil { - log.Printf("routines: accept %d: %v", rid, err) - http.Error(w, "accept failed: "+err.Error(), http.StatusBadGateway) + out, err := seedRoutineEvent(ctx, core, r) + if err != nil { + log.Printf("routines: seed: %v", err) + http.Error(w, "seed failed: "+err.Error(), http.StatusBadGateway) return } - msg = "accepted routine — maven will remind you" - case "dismiss": - if err := core.DismissProposedRoutine(ctx, rid); err != nil { - log.Printf("routines: dismiss %d: %v", rid, err) - http.Error(w, "dismiss failed: "+err.Error(), http.StatusBadGateway) + msg = out + } else { + idStr := r.FormValue("id") + var rid int64 + if n, _ := fmt.Sscanf(idStr, "%d", &rid); n != 1 { + http.Error(w, "invalid id", http.StatusBadRequest) + return + } + switch action { + case "accept": + if !stepUpOK(session, requireStepUp) { + http.Error(w, "step-up required: assert a passkey first", http.StatusForbidden) + return + } + if err := acceptRoutine(ctx, core, rid); err != nil { + log.Printf("routines: accept %d: %v", rid, err) + http.Error(w, "accept failed: "+err.Error(), http.StatusBadGateway) + return + } + msg = "accepted routine — maven will remind you" + case "dismiss": + if err := core.DismissProposedRoutine(ctx, rid); err != nil { + log.Printf("routines: dismiss %d: %v", rid, err) + http.Error(w, "dismiss failed: "+err.Error(), http.StatusBadGateway) + return + } + msg = "dismissed routine" + default: + http.Error(w, "unknown action", http.StatusBadRequest) return } - msg = "dismissed routine" - default: - http.Error(w, "unknown action", http.StatusBadRequest) - return } } proposed, err := core.ListProposedRoutines(ctx) @@ -1158,6 +1177,45 @@ func toRoutineViews(rs []ipc.ProposedRoutine) []routineView { // may do it (Vikunja #367): accepting gives the tick loop a standing new // reason to speak, which DESIGN.md puts at layer 3, and the button here is // behind step-up. Voice can park the question and dismiss, never accept. +// seedRoutineEvent drives one backdated fact write through core (Vikunja #518), +// so the pattern detector can be exercised against a running daemon instead of +// over real days. Refused unless mavend was started with -allow-seed; on an +// ordinary box the error says so and nothing is written. +// +// Takes "ago" rather than an absolute timestamp — hours before now, as a float +// so a QA sitting can space four seeds three hours apart without doing clock +// arithmetic. The detector's floor is two hours, and "0" is a legal answer +// meaning now. +func seedRoutineEvent(ctx context.Context, core ipc.CoreAPI, r *http.Request) (string, error) { + key := strings.TrimSpace(r.FormValue("key")) + value := strings.TrimSpace(r.FormValue("value")) + if key == "" || value == "" { + return "", errors.New("seed needs a key and a value") + } + agoHours, err := strconv.ParseFloat(strings.TrimSpace(r.FormValue("ago")), 64) + if err != nil { + return "", fmt.Errorf("seed: bad ago (hours before now): %w", err) + } + if agoHours < 0 { + return "", errors.New("seed: ago is hours BEFORE now, so it cannot be negative") + } + resp, err := core.SeedEvent(ctx, ipc.SeedEventReq{ + Key: key, + Value: value, + Ts: time.Now().Add(-time.Duration(agoHours * float64(time.Hour))), + }) + if err != nil { + return "", err + } + if !resp.Extracted { + return fmt.Sprintf("wrote fact %d, but %q is not in the action lexicon — no event, no pattern", resp.FactID, value), nil + } + if !resp.Proposed { + return fmt.Sprintf("seeded %s/%s (fact %d, event %d) — not enough yet to propose", resp.Action, resp.Object, resp.FactID, resp.EventID), nil + } + return fmt.Sprintf("seeded %s/%s and PROPOSED routine %d, every %.1f days", resp.Action, resp.Object, resp.RoutineID, resp.IntervalDays), nil +} + func acceptRoutine(ctx context.Context, core ipc.CoreAPI, id int64) error { proposed, err := core.ListProposedRoutines(ctx) if err != nil { diff --git a/internal/auth/policy.go b/internal/auth/policy.go index ede1e77..510a1f1 100644 --- a/internal/auth/policy.go +++ b/internal/auth/policy.go @@ -91,6 +91,19 @@ func Requirement(m ipc.Method) Authority { // can do is make Maven stop recognising someone, which is the state the // box ships in anyway. return AuthWrite + case ipc.MethodSeedEvent: + // The one backdating write path in the tree (Vikunja #518). AuthStepUp, + // the same rung as mutating the tool allowlist, and for a reason that is + // not about privilege: every other write records when something actually + // happened, and this one asserts it. A caller who can place a fact in the + // past can manufacture a routine Maven will then act on forever, which is + // the tick loop obeying evidence nobody produced. + // + // Step-up is not the real gate and is not meant to be. mavend refuses the + // method entirely unless started with -allow-seed, so the ordinary state + // of the box is that no gesture reaches it. This rung is what stops a + // module from calling it on a box where QA left the flag on. + return AuthStepUp case ipc.MethodWriteFact: return AuthWrite case ipc.MethodIngestMail: diff --git a/internal/ipc/api.go b/internal/ipc/api.go index 6e8d572..2726bd0 100644 --- a/internal/ipc/api.go +++ b/internal/ipc/api.go @@ -184,6 +184,48 @@ type CaptureTaskResp struct { Promoted bool `json:"promoted,omitempty"` } +// SeedEventReq — write one fact at a caller-supplied timestamp and run the +// pattern path over it, so a recurring routine can be produced on demand +// instead of over real days (Vikunja #518). +// +// This is the ONLY backdating write path in the tree, and it exists for one +// reason: the detector needs four events spread over hours before it proposes +// anything, so V-43, V-46, V-247 and V-254 could not be verified against a +// running daemon at all. A store fixture would have exercised the detector +// without the wiring those tasks doubt. +// +// Two things hold it shut. It is AuthStepUp in the authority table, the same +// rung as mutating the tool allowlist. And mavend refuses it outright unless +// started with -allow-seed, so a box nobody is testing carries no live +// backdating path even for a caller who cleared the gate. +// +// Key and Value are a fact, not an event: extraction runs for real, so a key +// the extractor ignores seeds nothing and says so. That is deliberate — a +// seam that accepted action and object directly would let QA prove a detector +// against events no utterance could ever produce. +type SeedEventReq struct { + Key string `json:"key"` + Value string `json:"value"` + Ts time.Time `json:"ts"` +} + +// SeedEventResp — what the seed produced. Extracted is false when the fact was +// written but yielded no event, which is the extractor declining rather than a +// failure. Proposed is true only when this seed completed a pattern; the first +// three seeds of a run return false with no routine. +type SeedEventResp struct { + FactID int64 `json:"fact_id"` + EventID int64 `json:"event_id,omitempty"` + Extracted bool `json:"extracted"` + Action string `json:"action,omitempty"` + Object string `json:"object,omitempty"` + Proposed bool `json:"proposed"` + RoutineID int64 `json:"routine_id,omitempty"` + // IntervalDays — the median the detector settled on, echoed so QA can + // check it against the spacing it asked for. + IntervalDays float64 `json:"interval_days,omitempty"` +} + // IngestMailReq — one message a mail reader has fetched, handed to core for // extraction (Vikunja #246). // diff --git a/internal/ipc/client.go b/internal/ipc/client.go index 4a36322..6029f84 100644 --- a/internal/ipc/client.go +++ b/internal/ipc/client.go @@ -487,6 +487,14 @@ func (c *Client) ListProposedRoutines(ctx context.Context) ([]ProposedRoutine, e return r.Routines, nil } +func (c *Client) SeedEvent(ctx context.Context, req SeedEventReq) (SeedEventResp, error) { + var r SeedEventResp + if err := c.call(ctx, MethodSeedEvent, req, &r); err != nil { + return SeedEventResp{}, err + } + return r, nil +} + func (c *Client) CaptureTask(ctx context.Context, req CaptureTaskReq) (CaptureTaskResp, error) { var r CaptureTaskResp if err := c.call(ctx, MethodCaptureTask, req, &r); err != nil { diff --git a/internal/ipc/coreapi.go b/internal/ipc/coreapi.go index 481c517..8cd65ab 100644 --- a/internal/ipc/coreapi.go +++ b/internal/ipc/coreapi.go @@ -110,6 +110,13 @@ type RoutineAPI interface { // loop takes the schedule from there — no reminder is created (Vikunja #366). AcceptProposedRoutine(ctx context.Context, id int64) error + // SeedEvent writes a backdated fact and runs extraction and detection over + // it, so a proposal can be produced in one sitting rather than over real + // days (Vikunja #518). See SeedEventReq for why this exists and what keeps + // it shut. Daemon-computed, like MorningStatus — the store adapter refuses + // it, because the detect-and-propose step lives in mavend. + SeedEvent(ctx context.Context, req SeedEventReq) (SeedEventResp, error) + // MorningStatus returns each configured morning routine's current // checklist state (see internal/morning): active today/now, which items // are done, which are still missing. diff --git a/internal/ipc/server.go b/internal/ipc/server.go index d055274..bd6df2b 100644 --- a/internal/ipc/server.go +++ b/internal/ipc/server.go @@ -524,6 +524,9 @@ var methodTable = map[Method]handlerFunc{ MethodCaptureTask: withParams(func(ctx context.Context, api CoreAPI, p CaptureTaskReq) (CaptureTaskResp, error) { return api.CaptureTask(ctx, p) }), + MethodSeedEvent: withParams(func(ctx context.Context, api CoreAPI, p SeedEventReq) (SeedEventResp, error) { + return api.SeedEvent(ctx, p) + }), MethodListTasks: withParams(func(ctx context.Context, api CoreAPI, p listTasksReq) (listTasksResp, error) { out, err := api.ListTasks(ctx, p.Status) if err != nil { diff --git a/internal/ipc/storeapi.go b/internal/ipc/storeapi.go index f67ac90..ec99b7d 100644 --- a/internal/ipc/storeapi.go +++ b/internal/ipc/storeapi.go @@ -265,6 +265,14 @@ func (a *storeAPI) TickTrace(ctx context.Context) (TickTrace, error) { return TickTrace{}, errors.New("store: tick trace not available via direct store API") } +// SeedEvent — same shape as MorningStatus: writing the fact is a store call, +// but extraction and detect-and-propose live in mavend, and a seed that wrote +// the fact without running them would be the one thing this seam must not be, +// a way to prove a detector that never ran (Vikunja #518). +func (a *storeAPI) SeedEvent(ctx context.Context, req SeedEventReq) (SeedEventResp, error) { + return SeedEventResp{}, errors.New("store: seed event not available via direct store API") +} + func (a *storeAPI) MorningStatus(ctx context.Context) ([]MorningRoutineStatus, error) { return nil, errors.New("store: morning status not available via direct store API") } diff --git a/internal/ipc/unimplemented.go b/internal/ipc/unimplemented.go index 4279c67..4c245f2 100644 --- a/internal/ipc/unimplemented.go +++ b/internal/ipc/unimplemented.go @@ -105,6 +105,9 @@ func (UnimplementedCoreAPI) DeleteTool(ctx context.Context, name string) error { func (UnimplementedCoreAPI) CaptureTask(ctx context.Context, req CaptureTaskReq) (CaptureTaskResp, error) { return CaptureTaskResp{}, ErrNotImplemented } +func (UnimplementedCoreAPI) SeedEvent(ctx context.Context, req SeedEventReq) (SeedEventResp, error) { + return SeedEventResp{}, ErrNotImplemented +} func (UnimplementedCoreAPI) ListTasks(ctx context.Context, status string) ([]Task, error) { return nil, ErrNotImplemented } diff --git a/internal/ipc/wire.go b/internal/ipc/wire.go index 50e2f15..2e12df7 100644 --- a/internal/ipc/wire.go +++ b/internal/ipc/wire.go @@ -67,6 +67,7 @@ const ( 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