feat(context): reasoning-thread continuity, L3 doc filtering, and gate hardening

Threads reasoning_content across inference calls and journal renders, filters
markdown noise out of L3 repo-knowledge retrieval, adds PlanLinter H3 checks,
and tightens filesystem tool output/dir-listing behavior surfaced by prior
live QA (see project_readloop_campaign memory).
This commit is contained in:
2026-07-10 11:13:43 +04:00
parent f51a8dada4
commit 3d5e05c1fb
32 changed files with 742 additions and 85 deletions
+18
View File
@@ -89,6 +89,22 @@ type EventEntry struct {
Detail string
}
// PlanStageStatus tracks a stage's lifecycle in a locked execution plan.
type PlanStageStatus int
const (
PlanPending PlanStageStatus = iota
PlanRunning
PlanCompleted
PlanFailed
)
// PlanStage is one stage in a locked execution plan, with live execution status.
type PlanStage struct {
ID string
Status PlanStageStatus
}
// ToolStatus mirrors the Kotlin ToolDisplayStatus.
type ToolStatus int
@@ -194,6 +210,8 @@ type Session struct {
Name string
LastEventAt int64
CurrentStage string
PlanGoal string
PlanStages []PlanStage
WorkspaceRoot string // bound cwd, from session.workspace_bound
LastOutput string
LastResponse string
@@ -612,9 +612,11 @@ var nonRenderingEventTypes = map[string]string{
protocol.TypeSnapshotComplete: "control frame: ends the snapshot-buffering phase; no render",
protocol.TypeRouterResponse: "recognized no-op: superseded by chat.turn on the global stream",
protocol.TypeProtocolError: "transport diagnostic: explicit no-op, not surfaced",
protocol.TypeFileList: "query reply: populates the @ file picker overlay, not the transcript",
protocol.TypeGrantList: "query reply: populates the standing-grants overlay, not the transcript",
protocol.TypeHealthChecks: "query reply: populates the health-checks overlay, not the transcript",
protocol.TypeFileList: "query reply: populates the @ file picker overlay, not the transcript",
protocol.TypeGrantList: "query reply: populates the standing-grants overlay, not the transcript",
protocol.TypeHealthChecks: "query reply: populates the health-checks overlay, not the transcript",
protocol.TypeProjectProfile: "query reply: populates the project profile overlay, not the transcript",
protocol.TypeOperatorProfile: "query reply: populates the operator profile overlay, not the transcript",
}
// TestRenderMatrixCoversEveryEventType is the load-bearing matrix guarantee: it scans
+23
View File
@@ -153,16 +153,34 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
s.Tools = nil
s.StageTokensUsed = 0
s.addEvent(msg.OccurredAt, "StageStarted", msg.StageID)
for i := range s.PlanStages {
if s.PlanStages[i].ID == msg.StageID {
s.PlanStages[i].Status = PlanRunning
break
}
}
}
case protocol.TypeStageCompleted:
if s := m.session(msg.SessionID); s != nil {
s.CurrentStage = ""
s.addEvent(msg.OccurredAt, "StageCompleted", msg.StageID)
for i := range s.PlanStages {
if s.PlanStages[i].ID == msg.StageID {
s.PlanStages[i].Status = PlanCompleted
break
}
}
}
case protocol.TypeStageFailed:
if s := m.session(msg.SessionID); s != nil {
s.CurrentStage = ""
s.addEvent(msg.OccurredAt, "StageFailed", msg.StageID)
for i := range s.PlanStages {
if s.PlanStages[i].ID == msg.StageID {
s.PlanStages[i].Status = PlanFailed
break
}
}
}
case protocol.TypeInferenceStarted:
if s := m.session(msg.SessionID); s != nil {
@@ -303,6 +321,11 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
if s := m.session(msg.SessionID); s != nil {
s.addEvent(nowMillis(), "PlanLocked", strings.Join(msg.StageIDs, " → "))
s.LastOutput = "plan locked: " + strings.Join(msg.StageIDs, " → ")
s.PlanGoal = msg.Content
s.PlanStages = make([]PlanStage, 0, len(msg.StageIDs))
for _, id := range msg.StageIDs {
s.PlanStages = append(s.PlanStages, PlanStage{ID: id, Status: PlanPending})
}
}
case protocol.TypeWorkspaceBound:
if s := m.session(msg.SessionID); s != nil {
+51
View File
@@ -790,6 +790,10 @@ func (m Model) buildTranscriptRows(w int) ([]string, []int) {
t := m.theme
msgs := m.routerMessages[m.selectedID]
var rows []string
// Prepend the live execution plan card when the session has locked plan stages.
if planRows := m.planCardLines(); len(planRows) > 0 {
rows = append(rows, planRows...)
}
msgStart := make([]int, len(msgs)) // first row index of each message, for scroll-to-selection
for mi, e := range msgs {
msgStart[mi] = len(rows)
@@ -847,6 +851,53 @@ func (m Model) buildTranscriptRows(w int) ([]string, []int) {
return rows, msgStart
}
// planCardLines renders the live execution plan card for the selected session.
func (m Model) planCardLines() []string {
t := m.theme
s := m.session(m.selectedID)
if s == nil || len(s.PlanStages) == 0 {
return nil
}
goal := strings.TrimSpace(s.PlanGoal)
var out []string
if goal != "" {
out = append(out, t.span(goal, t.P.Faint))
}
stageLabel := "stage"
if len(s.PlanStages) != 1 {
stageLabel = "stages"
}
out = append(out, t.span("Plan \u00b7 "+itoa(len(s.PlanStages))+" "+stageLabel, t.P.Accent))
idW := 0
for _, ps := range s.PlanStages {
if len(ps.ID) > idW {
idW = len(ps.ID)
}
}
for _, ps := range s.PlanStages {
var badge, badgeStyle string
var badgeColor color.Color
switch ps.Status {
case PlanPending:
badge = "\u25cb"
badgeColor = t.P.Faint
case PlanRunning:
badge = "\u25cf"
badgeColor = t.P.Accent
case PlanCompleted:
badge = "\u2713"
badgeColor = t.P.OK
case PlanFailed:
badge = "\u2717"
badgeColor = t.P.Bad
}
badgeStyle = t.span(badge, badgeColor)
rest := t.span(" "+padRaw(ps.ID, idW), t.P.Fg)
out = append(out, " "+badgeStyle+rest)
}
return out
}
func (m Model) eventRows(w, h int) []string {
t := m.theme
header := lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Bold(true).Render("event stream") +