Files
Agent-Native/runtime/session/jsonl.go
T
weichen 7710be2db4 Add session compaction, metadata, and crash-safe transcript loading
Why: long multi-turn executions grow the JSONL transcript until they
hit provider context limits (parity-matrix #11), and a crashed append
left a partial line that made the whole session unloadable
(parity-matrix #12 resume semantics).

Evidence: go vet ./... clean; go test ./... 13 packages ok. Session
cases: crash-residue tail skipped while mid-file corruption still
errors, Stats line/byte counts, meta sidecar roundtrip (missing meta
nil), Compact rewriting system+summary+keepLastN atomically with no
tmp residue, no-op on short transcripts, summarizer error leaving the
file untouched. Adapter case: threshold-triggered compaction runs
before the loop and the summary marker reaches both the transcript
and the model's context.

Impact: session gains Stats/SaveMeta/ReadMeta/Compact; native.Options
gains Compact *CompactConfig (default summarizer is one tool-less
stream call; compaction failure logs and continues - optimization,
never a silent failure); session meta sidecar records model/scope/
exec id per execution.

Refs: docs/parity-matrix.md #11/#12 (Batch3 scope).
2026-10-01 05:51:46 +00:00

87 lines
1.9 KiB
Go

package session
import (
"bufio"
"encoding/json"
"os"
"path/filepath"
"strings"
"sync"
"agent-native/runtime/llm"
)
// Store is an append-only JSONL transcript. Each line is one llm.Message.
type Store struct {
path string
mu sync.Mutex
}
func Open(path string) (*Store, error) {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return nil, err
}
f, err := os.OpenFile(path, os.O_CREATE, 0o644)
if err != nil {
return nil, err
}
_ = f.Close()
return &Store{path: path}, nil
}
func (s *Store) Path() string { return s.path }
func (s *Store) Append(msg llm.Message) error {
s.mu.Lock()
defer s.mu.Unlock()
raw, err := json.Marshal(msg)
if err != nil {
return err
}
f, err := os.OpenFile(s.path, os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
raw = append(raw, '\n')
_, err = f.Write(raw)
return err
}
// Load 全量读取转录。崩溃残留容错:Append 恒以换行收尾,因此无换行结尾的
// 尾部若不是完整 JSON 即为崩溃半行,丢弃而不是让整个会话不可用;
// 行中间的坏行仍报错(那是文件损坏,不是崩溃残留)。
func (s *Store) Load() ([]llm.Message, error) {
s.mu.Lock()
defer s.mu.Unlock()
raw, err := os.ReadFile(s.path)
if err != nil {
return nil, err
}
text := string(raw)
if text != "" && !strings.HasSuffix(text, "\n") {
if idx := strings.LastIndexByte(text, '\n'); idx >= 0 {
if !json.Valid([]byte(text[idx+1:])) {
text = text[:idx+1]
}
} else if !json.Valid([]byte(text)) {
text = ""
}
}
var out []llm.Message
sc := bufio.NewScanner(strings.NewReader(text))
sc.Buffer(make([]byte, 0, 64*1024), 8*1024*1024)
for sc.Scan() {
line := sc.Bytes()
if len(line) == 0 {
continue
}
var msg llm.Message
if err := json.Unmarshal(line, &msg); err != nil {
return nil, err
}
out = append(out, msg)
}
return out, sc.Err()
}