the ring reads out over ipc as turn decisions (V-564)

Same shape as TickTrace and RecentEvents: a bounded daemon ring, so the store
adapter refuses rather than pretending a table exists. No voice wiring means an
empty list and not an error, because a box with no voice path has had no turns
to arbitrate.
This commit is contained in:
2026-08-06 00:52:48 +04:00
parent 3e6a427e85
commit a5b245dbf5
9 changed files with 70 additions and 0 deletions
+2
View File
@@ -350,6 +350,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),
getDecisions: turnDecisionsFn(voiceW),
seedStore: seedStoreIfAllowed(st),
nexus: nexusOf(voiceW),
}
@@ -619,6 +620,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),
getDecisions: turnDecisionsFn(voiceW),
seedStore: seedStoreIfAllowed(st),
}
if voiceW != nil && voiceW.handler != nil {
+12
View File
@@ -23,6 +23,7 @@ type daemonAPI struct {
chatFn func(ctx context.Context, conversation, text string) string
getMCPServers func() []ipc.MCPServerStatus
getEvents func(n int) []ipc.IntakeEvent
getDecisions func(n int) []ipc.TurnDecision
// nexus — the identity client, nil when no nexus block is configured. It
// is what makes ResolveEntity answerable at all; without it the store
// adapter's refusal stands, and a surface that wanted an entity id says so
@@ -118,6 +119,17 @@ func (d *daemonAPI) TickTrace(ctx context.Context) (ipc.TickTrace, error) {
return toIPCTickTrace(*trace), nil
}
// TurnDecisions — the arbitration records of the last few turns (V-564). Nil
// getter means voice was never wired, and that is an empty list rather than an
// error: a box with no voice path has had no turns to arbitrate, which is not a
// fault and renders as an empty table.
func (d *daemonAPI) TurnDecisions(ctx context.Context, n int) ([]ipc.TurnDecision, error) {
if d.getDecisions == nil {
return nil, nil
}
return d.getDecisions(n), nil
}
func (d *daemonAPI) MorningStatus(ctx context.Context) ([]ipc.MorningRoutineStatus, error) {
if d.getMorningStatus == nil {
return nil, errors.New("mavend: morning status not available")
+24
View File
@@ -836,6 +836,30 @@ type TickTrace struct {
Rules []RuleTrace `json:"rules"`
}
// --- Turn decision trace DTOs (V-564) ---
// TurnClaim — one claimant's say on one turn: who, at which stage, what it
// would have made the turn, the score it reported if it has one, and what
// happened to the claim. Same shape as RuleTrace above and for the same reason:
// a winner alone does not explain an arbitration, the losers do.
type TurnClaim struct {
Stage string `json:"stage"`
Claimant string `json:"claimant"`
Intent string `json:"intent,omitempty"`
Score float64 `json:"score,omitempty"`
HasScore bool `json:"has_score,omitempty"`
Outcome string `json:"outcome"`
Reason string `json:"reason,omitempty"`
}
// TurnDecision — one turn's arbitration, newest first when read as a list.
type TurnDecision struct {
Ts time.Time `json:"ts"`
Utterance string `json:"utterance"`
Winner string `json:"winner"`
Claims []TurnClaim `json:"claims"`
}
// MorningRoutineItem — one checklist entry's current state.
type MorningRoutineItem struct {
Key string `json:"key"`
+8
View File
@@ -672,6 +672,14 @@ func (c *Client) TickTrace(ctx context.Context) (TickTrace, error) {
return t, nil
}
func (c *Client) TurnDecisions(ctx context.Context, n int) ([]TurnDecision, error) {
var d []TurnDecision
if err := c.call(ctx, MethodTurnDecisions, nReq{N: n}, &d); err != nil {
return nil, err
}
return d, nil
}
func (c *Client) RecentEvents(ctx context.Context, n int) ([]IntakeEvent, error) {
var e []IntakeEvent
if err := c.call(ctx, MethodRecentEvents, nReq{N: n}, &e); err != nil {
+7
View File
@@ -169,6 +169,13 @@ type SystemAPI interface {
// persisted — it's a daemon-level cache).
TickTrace(ctx context.Context) (TickTrace, error)
// TurnDecisions returns the newest turn arbitration records, newest first
// (V-564). Same shape as TickTrace and RecentEvents: a bounded in-memory
// ring on the daemon, so the store adapter returns an error rather than
// pretending a table exists. Empty is a normal answer — it means no turn
// has run since the daemon started.
TurnDecisions(ctx context.Context, n int) ([]TurnDecision, error)
// RecentEcosystemTraces reads the ecosystem call log, which lives in its
// own table so machine-rate traces never crowd out human-rate facts.
RecentEcosystemTraces(ctx context.Context, n int) ([]EcosystemTrace, error)
+7
View File
@@ -583,6 +583,13 @@ var methodTable = map[Method]handlerFunc{
MethodTickTrace: withoutParams(func(ctx context.Context, api CoreAPI) (TickTrace, error) {
return api.TickTrace(ctx)
}),
MethodTurnDecisions: withParams(func(ctx context.Context, api CoreAPI, p nReq) ([]TurnDecision, error) {
d, err := api.TurnDecisions(ctx, p.N)
if d == nil {
d = []TurnDecision{}
}
return d, err
}),
// 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.
+6
View File
@@ -265,6 +265,12 @@ func (a *storeAPI) TickTrace(ctx context.Context) (TickTrace, error) {
return TickTrace{}, errors.New("store: tick trace not available via direct store API")
}
// TurnDecisions — same story as TickTrace: the arbitration record is a daemon
// ring, not a table, so there is nothing here to read it from (V-564).
func (a *storeAPI) TurnDecisions(ctx context.Context, n int) ([]TurnDecision, error) {
return nil, errors.New("store: turn decisions 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,
+3
View File
@@ -144,6 +144,9 @@ func (UnimplementedCoreAPI) RevertFact(ctx context.Context, key string) (int64,
func (UnimplementedCoreAPI) TickTrace(ctx context.Context) (TickTrace, error) {
return TickTrace{}, ErrNotImplemented
}
func (UnimplementedCoreAPI) TurnDecisions(ctx context.Context, n int) ([]TurnDecision, error) {
return nil, ErrNotImplemented
}
func (UnimplementedCoreAPI) MorningStatus(ctx context.Context) ([]MorningRoutineStatus, error) {
return nil, ErrNotImplemented
}
+1
View File
@@ -48,6 +48,7 @@ const (
MethodAcceptProposedRoutine Method = "accept_proposed_routine"
MethodRevertFact Method = "revert_fact"
MethodTickTrace Method = "tick_trace"
MethodTurnDecisions Method = "turn_decisions"
MethodMorningStatus Method = "morning_status"
MethodMCPServers Method = "mcp_servers"
MethodDayPlan Method = "day_plan"