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).
87 lines
1.9 KiB
Go
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()
|
|
}
|