From 10f891a21d42107ceccb525a1936e459a13ce9ff Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:52:24 -0400 Subject: [PATCH] =?UTF-8?q?fix(llmrec):=20=E4=BB=8E=20ctx=20=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=20session=EF=BC=8C=E4=BF=AE=E5=A4=8D=20task/worker=20?= =?UTF-8?q?=E5=88=97=E4=B8=BA=E7=A9=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原实现依赖 SetSession() 写入共享的 sessionID 字段,但该方法从未被调用, 导致 session/task/worker 恒为空、落库为 NULL,前端两列无数据。且共享可变字段 在 planner 与多 worker 并发调用同一 Recorder 时会互相覆盖、串号。 norma 已通过 transcript.WithSessionID 把 session 注入 ctx(格式 exp-), 改为在 Stream 中按 ctx 逐调用读取,天然 per-call、无竞争。删除未使用的 SetSession/getContext 及共享字段。 Co-Authored-By: Claude Opus 4.8 --- llmrec/llmrec.go | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/llmrec/llmrec.go b/llmrec/llmrec.go index 8101a54..2d6a9fa 100644 --- a/llmrec/llmrec.go +++ b/llmrec/llmrec.go @@ -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-...". -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)