fix(llmrec): 从 ctx 读取 session,修复 task/worker 列为空

原实现依赖 SetSession() 写入共享的 sessionID 字段,但该方法从未被调用,
导致 session/task/worker 恒为空、落库为 NULL,前端两列无数据。且共享可变字段
在 planner 与多 worker 并发调用同一 Recorder 时会互相覆盖、串号。

norma 已通过 transcript.WithSessionID 把 session 注入 ctx(格式 exp<N>-<role>),
改为在 Stream 中按 ctx 逐调用读取,天然 per-call、无竞争。删除未使用的
SetSession/getContext 及共享字段。

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Autumn-27andClaude Opus 4.8 committed 2026-08-07 08:52:24 -04:00
1 parent 5b58330f01
commit 10f891a21d
1 file changed
+7 -22
+7 -22
View File
@@ -9,10 +9,10 @@ import (
"iter"
"log"
"strings"
"sync"
"time"
"github.com/Autumn-27/norma/llm"
"github.com/Autumn-27/norma/transcript"
"github.com/Autumn-27/artex/db"
)
@@ -22,11 +22,6 @@ type Recorder struct {
pg *db.DB
model string // model name (from config, not in CompletionRequest)
prof string // LLM profile name (from llm_profiles)
mu sync.Mutex
sessionID string // current session context (set by caller)
taskID string // exploration/task identifier
worker string // worker name (planner, work#1, mainagent, etc.)
}
// Wrap returns a Provider that records all calls to pg.
@@ -35,15 +30,6 @@ func Wrap(inner llm.Provider, pg *db.DB, model, profName string) *Recorder {
return &Recorder{inner: inner, pg: pg, model: model, prof: profName}
}
// SetSession sets the session identifier for subsequent calls (e.g. "exp1-worker-i3").
// It also derives taskID and worker from the session format "exp<N>-<role>...".
func (r *Recorder) SetSession(id string) {
r.mu.Lock()
r.sessionID = id
r.taskID, r.worker = parseSession(id)
r.mu.Unlock()
}
// parseSession extracts task id and worker role from session strings like
// "exp1-worker-i3" → ("1", "worker") or "exp2-planner" → ("2", "planner").
func parseSession(s string) (taskID, worker string) {
@@ -68,17 +54,16 @@ func parseSession(s string) (taskID, worker string) {
return taskID, worker
}
func (r *Recorder) getContext() (session, taskID, worker string) {
r.mu.Lock()
defer r.mu.Unlock()
return r.sessionID, r.taskID, r.worker
}
// Stream implements llm.Provider. It delegates to the inner provider, accumulates
// the streamed events to reconstruct the response, and records the full exchange.
//
// The session identifier (e.g. "exp1-worker-i3") is carried on ctx by the norma
// harness via transcript.WithSessionID; reading it per-call is race-free even when
// planner and multiple workers share one Recorder instance.
func (r *Recorder) Stream(ctx context.Context, req llm.CompletionRequest) iter.Seq2[llm.StreamEvent, error] {
start := time.Now()
session, taskID, worker := r.getContext()
session := transcript.SessionIDFrom(ctx)
taskID, worker := parseSession(session)
// Serialize the request for storage.
reqBody := serializeRequest(req)