sessions: omp's local sessions are in Usage › Sessions (#303), and the GLM Coding Plans take Codex's Responses as they are (#306) — omp (oh-my-pi) keeps Pi's session jsonl (v3) under ~/.omp/agent/sessions (and profiles' and $XDG_DATA_HOME/omp's) with a title line, title changes, model_usage for calls outside the chat, provider/model changes and subagent/advisor transcripts beside the file; they are read through Pi's parser, subagents counted in their session, a fork's copied entries in the original only, resumed by omp --resume <id>. The zhipu and zai Coding Plan regions get their Responses base (open.bigmodel.cn/api/v1, api.z.ai/api/v1, per their Codex docs), so Codex's requests go there as they are instead of rebuilt for Chat; Chat and Claude Code routes are unchanged. TestOmp, TestDirsMore and TestGLMCodingPlanResponses fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.

This commit is contained in:
yetone committed 2026-09-30 18:07:32 +08:00
1 parent 9ec237d4e6
commit cd7b7a042f
15 files changed
+374 -16

No files matched your search

+125
View File
@@ -0,0 +1,125 @@
package gateway
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"github.com/yetone/magpie/internal/provider"
)
// glmPlan plays a GLM Coding Plan: chat completions at /api/coding/paas/v4,
// the Responses API at /api/v1, Anthropic's messages at /api/anthropic.
type glmPlan struct {
mu sync.Mutex
paths []string
model []string
}
func (g *glmPlan) ServeHTTP(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var v struct {
Model string `json:"model"`
}
json.Unmarshal(body, &v)
g.mu.Lock()
g.paths, g.model = append(g.paths, r.URL.Path), append(g.model, v.Model)
g.mu.Unlock()
w.Header().Set("Content-Type", "text/event-stream")
switch r.URL.Path {
case "/api/v1/responses":
io.WriteString(w, sse(
`event: response.created`+"\n"+`data: {"type":"response.created","response":{"id":"resp_1","model":"`+v.Model+`","status":"in_progress","output":[]}}`,
`event: response.output_text.delta`+"\n"+`data: {"type":"response.output_text.delta","item_id":"m1","output_index":0,"content_index":0,"delta":"from responses"}`,
`event: response.completed`+"\n"+`data: {"type":"response.completed","response":{"id":"resp_1","model":"`+v.Model+`","status":"completed","output":[{"type":"message","id":"m1","role":"assistant","content":[{"type":"output_text","text":"from responses"}]}],"usage":{"input_tokens":5,"output_tokens":2,"total_tokens":7}}}`))
case "/api/coding/paas/v4/chat/completions":
io.WriteString(w, sse(
`data: {"id":"c1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":"from chat"}}]}`,
`data: {"id":"c1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":2}}`,
`data: [DONE]`))
case "/api/anthropic/v1/messages":
io.WriteString(w, sse(
`event: message_start`+"\n"+`data: {"type":"message_start","message":{"id":"msg_1","model":"`+v.Model+`","usage":{"input_tokens":5}}}`,
`data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`,
`data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"from messages"}}`,
`data: {"type":"content_block_stop","index":0}`,
`data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":2}}`,
`data: {"type":"message_stop"}`))
default:
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
io.WriteString(w, `{"error":{"message":"no such route"}}`)
}
}
// The GLM Coding Plan's Responses API (#306): the zhipu and zai presets'
// Coding Plan regions give it at /api/v1, as the plans' Codex pages set
// base_url with wire_api = "responses", so Codex's request is relayed there
// as it came rather than translated to chat; chat clients stay on the plan's
// chat completions and Claude Code on its messages.
func TestGLMCodingPlanResponses(t *testing.T) {
for _, tc := range []struct{ preset, host string }{
{"zhipu", "https://open.bigmodel.cn"},
{"zai", "https://api.z.ai"},
} {
t.Run(tc.preset, func(t *testing.T) {
pr := provider.Preset(tc.preset)
var coding, payg provider.Region
for _, r := range pr.Regions {
switch r.ID {
case "coding":
coding = r
case "api":
payg = r
}
}
if coding.Responses != tc.host+"/api/v1" || coding.Chat != tc.host+"/api/coding/paas/v4" || coding.Anthropic != tc.host+"/api/anthropic" {
t.Fatalf("coding plan: %+v", coding)
}
// pay as you go is left as it was: the docs give /api/v1 for the plan
if payg.Responses != "" || pr.Responses != "" {
t.Fatalf("pay as you go: %+v / %q", payg, pr.Responses)
}
fresh(t)
up := &glmPlan{}
srv := httptest.NewServer(up)
t.Cleanup(srv.Close)
p, err := provider.FromPreset(tc.preset)
if err != nil {
t.Fatal(err)
}
at := func(u string) string { return srv.URL + strings.TrimPrefix(u, tc.host) }
p.Key = "glm-k"
p.Chat, p.Responses, p.Anthropic = at(coding.Chat), at(coding.Responses), at(coding.Anthropic)
p.Models = []string{"glm-5.3"}
if err := provider.Save(p); err != nil {
t.Fatal(err)
}
m := tc.preset + "/glm-5.3"
for _, c := range []struct{ path, body, upstream, reply string }{
{"/v1/responses", `{"model":"` + m + `","input":"hi","stream":true}`, "/api/v1/responses", "from responses"},
{"/v1/chat/completions", `{"model":"` + m + `","messages":[{"role":"user","content":"hi"}],"stream":true}`, "/api/coding/paas/v4/chat/completions", "from chat"},
{"/v1/messages", `{"model":"` + m + `","max_tokens":20,"messages":[{"role":"user","content":"hi"}],"stream":true}`, "/api/anthropic/v1/messages", "from messages"},
} {
up.mu.Lock()
up.paths, up.model = nil, nil
up.mu.Unlock()
code, body := post(t, c.path, c.body)
if code != 200 || !strings.Contains(body, c.reply) {
t.Fatalf("%s: status %d: %s", c.path, code, body)
}
up.mu.Lock()
paths, models := up.paths, up.model
up.mu.Unlock()
if len(paths) != 1 || paths[0] != c.upstream || models[0] != "glm-5.3" {
t.Fatalf("%s asked upstream at %v %v, want %s", c.path, paths, models, c.upstream)
}
}
})
}
}
+5 -3
View File
@@ -109,17 +109,19 @@ var presets = []PresetDef{
Chat: "https://open.bigmodel.cn/api/paas/v4", Anthropic: "https://open.bigmodel.cn/api/anthropic",
Website: "https://open.bigmodel.cn", KeysURL: "https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys",
// a GLM Coding Plan is served at its own OpenAI endpoint: a plan's key
// sent to the pay-as-you-go one is told it has no balance
// sent to the pay-as-you-go one is told it has no balance. The plan
// serves the Responses API at /api/v1 as well: its tool pages give
// it for Codex, wire_api = "responses" (#306)
RegionLabel: "Plan", Regions: []Region{
{ID: "api", Name: "Pay as you go", Chat: "https://open.bigmodel.cn/api/paas/v4", Anthropic: "https://open.bigmodel.cn/api/anthropic"},
{ID: "coding", Name: "Coding Plan", Chat: "https://open.bigmodel.cn/api/coding/paas/v4", Anthropic: "https://open.bigmodel.cn/api/anthropic"},
{ID: "coding", Name: "Coding Plan", Chat: "https://open.bigmodel.cn/api/coding/paas/v4", Responses: "https://open.bigmodel.cn/api/v1", Anthropic: "https://open.bigmodel.cn/api/anthropic"},
}},
{ID: "zai", Name: "Z.ai", Icon: "zai", Kind: KindVendor, Catalog: "zhipuai",
Chat: "https://api.z.ai/api/paas/v4", Anthropic: "https://api.z.ai/api/anthropic",
Website: "https://z.ai", KeysURL: "https://z.ai/manage-apikey/apikey-list",
RegionLabel: "Plan", Regions: []Region{
{ID: "api", Name: "Pay as you go", Chat: "https://api.z.ai/api/paas/v4", Anthropic: "https://api.z.ai/api/anthropic"},
{ID: "coding", Name: "Coding Plan", Chat: "https://api.z.ai/api/coding/paas/v4", Anthropic: "https://api.z.ai/api/anthropic"},
{ID: "coding", Name: "Coding Plan", Chat: "https://api.z.ai/api/coding/paas/v4", Responses: "https://api.z.ai/api/v1", Anthropic: "https://api.z.ai/api/anthropic"},
}},
{ID: "minimax", Name: "MiniMax", Icon: "minimax-color", Kind: KindVendor, Catalog: "minimax",
Chat: "https://api.minimax.io/v1", Anthropic: "https://api.minimax.io/anthropic",
+2 -1
View File
@@ -29,6 +29,7 @@ func setupAgents(t *testing.T) (home string) {
copyTree(t, "testdata/qoder-cn", filepath.Join(home, ".qoder-cn"))
copyTree(t, "testdata/grok", filepath.Join(home, ".grok"))
copyTree(t, "testdata/workbuddy", filepath.Join(home, ".workbuddy"))
copyTree(t, "testdata/omp", filepath.Join(home, ".omp"))
zcMakeDB(t, filepath.Join(home, ".zcode", "cli", "db", "db.sqlite"))
return
}
@@ -272,7 +273,7 @@ func TestDirsMore(t *testing.T) {
home := setupAgents(t)
d := Dirs()
want := []string{filepath.Join(home, ".zcode"), filepath.Join(home, ".dsh"), filepath.Join(home, ".cline", "data", "sessions"), filepath.Join(home, ".qoder"), filepath.Join(home, ".qoder-cn"),
filepath.Join(home, ".grok"), filepath.Join(home, ".workbuddy")}
filepath.Join(home, ".grok"), filepath.Join(home, ".workbuddy"), filepath.Join(home, ".omp", "agent")}
if len(d) != 2+len(want) {
t.Fatalf("dirs %v", d)
}
+86
View File
@@ -0,0 +1,86 @@
package sessions
import (
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
)
// omp (oh-my-pi, a fork of Pi) keeps its sessions as Pi does, a file per
// session, <time>_<session id>.jsonl, in a folder per working directory
// under its agent folder's sessions/, and they are read as Pi's are (piParse).
// What omp adds: a fixed-width "title" line before the header, which it
// rewrites in place (the title_change entry appended with each new title is
// read instead), the header's own title, "model_usage" entries for model
// calls outside the conversation, and its subagents' and advisor's sessions
// in the session's artifacts folder beside its file (<time>_<id>/<agent
// id>.jsonl, theirs in turn one folder deeper), which count in the session
// they ran in. `omp --resume <id>` picks one up again.
// OmpDir is omp's agent folder, ~/.omp/agent, as magpie's omp agent has it.
func OmpDir() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".omp", "agent")
}
// ompSessionRoots are the folders omp keeps sessions in: its agent folder's
// sessions/, each profile's, and $XDG_DATA_HOME/omp/sessions once omp has
// moved its data there (Linux and macOS).
func ompSessionRoots() []string {
roots := []string{filepath.Join(OmpDir(), "sessions")}
profiles, _ := filepath.Glob(filepath.Join(filepath.Dir(OmpDir()), "profiles", "*", "agent", "sessions"))
roots = append(roots, profiles...)
if d := os.Getenv("XDG_DATA_HOME"); d != "" && runtime.GOOS != "windows" {
roots = append(roots, filepath.Join(d, "omp", "sessions"))
}
return roots
}
func ompFiles() []file {
var out []file
seen := map[string]bool{}
for _, root := range ompSessionRoots() {
paths, _ := filepath.Glob(filepath.Join(root, "*", "*.jsonl"))
for _, p := range paths {
if seen[p] {
continue
}
seen[p] = true
name := strings.TrimSuffix(filepath.Base(p), ".jsonl")
_, id, ok := strings.Cut(name, "_")
if !ok || id == "" {
continue
}
key := "omp:" + id
f := file{agent: "omp", key: key, path: p, main: true}
if !stat(&f) {
continue
}
out = append(out, f)
// its subagents and advisor, at any depth
filepath.WalkDir(strings.TrimSuffix(p, ".jsonl"), func(q string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
if d.IsDir() {
// side questions (/btw) are kept apart, as .json
if d.Name() == "btw-history" {
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(q, ".jsonl") {
return nil
}
sub := file{agent: "omp", key: key, path: q}
if stat(&sub) {
out = append(out, sub)
}
return nil
})
}
}
return out
}
+90
View File
@@ -0,0 +1,90 @@
package sessions
import (
"os"
"path/filepath"
"runtime"
"testing"
)
const (
ompMain = "019a0000-0000-7000-8000-00000000000a"
ompFork = "019a0000-0000-7000-8000-00000000000c"
ompNamed = "019a0000-0000-7000-8000-00000000000d"
)
// omp's sessions (#303): Pi's kind, with its title line and title changes,
// model_usage entries, and its subagents' and advisor's sessions beside it.
func TestOmp(t *testing.T) {
inZone(t, 0)
home := setupAgents(t)
ss := List(0)
if n := count(ss, "omp"); n != 3 {
t.Fatalf("want 3 omp sessions (the subagents and advisor in theirs), got %d", n)
}
m := find(t, ss, "omp", ompMain)
if m.Cwd != "/work/omp" || m.Title != "Port the parser" {
t.Fatalf("omp: %+v", m)
}
// the task tool's summed usage not counted again
for _, c := range []struct {
model string
want Tokens
}{
{"claude-opus-5-5", Tokens{110, 55, 1500, 200}},
{"claude-haiku-5", Tokens{20, 5, 0, 0}}, // a model_usage entry
{"claude-sonnet-5", Tokens{340, 34, 100, 0}}, // the subagent and its own
{"gpt-6-astra", Tokens{50, 10, 0, 0}}, // the advisor
} {
if got := model(m, c.model); got.Tokens != c.want {
t.Fatalf("%s: %+v, want %+v", c.model, got, c.want)
}
}
if len(m.Models) != 4 || !model(m, "claude-opus-5-5").Priced {
t.Fatalf("models %+v", m.Models)
}
if !m.Start.Equal(at("2026-09-28T08:00:00Z")) || !m.Last.Equal(at("2026-09-28T08:02:00Z")) {
t.Fatalf("omp times %s %s", m.Start, m.Last)
}
if runtime.GOOS != "windows" {
if want := "cd '/work/omp' && omp --resume " + ompMain; m.Resume != want {
t.Fatalf("resume %q", m.Resume)
}
}
// the fork: what it copied counted in the session it came from
f := find(t, ss, "omp", ompFork)
if f.Tokens != (Tokens{7, 3, 0, 0}) || f.Title != "Port the parser" {
t.Fatalf("fork: %+v", f)
}
if !f.Start.Equal(at("2026-09-29T09:00:00Z")) || !f.Last.Equal(at("2026-09-29T09:00:20Z")) {
t.Fatalf("fork times %s %s", f.Start, f.Last)
}
// begun from a template: the title it was last given
if n := find(t, ss, "omp", ompNamed); n.Title != "Tidy the tests" {
t.Fatalf("named: %+v", n)
}
// a profile's sessions, and those in $XDG_DATA_HOME/omp
from := filepath.Join(home, ".omp", "agent", "sessions", "--work-omp--")
moves := map[string]string{ompNamed: filepath.Join(home, ".omp", "profiles", "work", "agent", "sessions", "--work-omp--")}
if runtime.GOOS != "windows" {
moves[ompFork] = filepath.Join(os.Getenv("XDG_DATA_HOME"), "omp", "sessions", "--work-omp--")
}
for id, to := range moves {
names, _ := filepath.Glob(filepath.Join(from, "*_"+id+".jsonl"))
if len(names) != 1 {
t.Fatalf("fixture for %s: %v", id, names)
}
os.MkdirAll(to, 0o755)
if err := os.Rename(names[0], filepath.Join(to, filepath.Base(names[0]))); err != nil {
t.Fatal(err)
}
}
Reset()
ss = List(0)
for id := range moves {
find(t, ss, "omp", id)
}
}
+18 -4
View File
@@ -18,7 +18,9 @@ import (
// tool's result may carry the usage of model work it did, and "usage",
// "compaction" and "branch_summary" entries theirs. A session forked from
// another starts with a copy of that one's entries, times and all, which
// are counted where they were first written.
// are counted where they were first written. omp's sessions are the same
// kind (omp.go), with a title of the header's or a title_change entry's, and
// "model_usage" entries as Pi's "usage" ones.
// PiDir is Pi's agent folder: $PI_CODING_AGENT_DIR, else ~/.pi/agent.
func PiDir() string {
@@ -102,8 +104,9 @@ type piLine struct {
Cwd string `json:"cwd"`
ParentSession string `json:"parentSession"`
Name string `json:"name"` // session_info
Title string `json:"title"` // omp's header, title_change
ModelID string `json:"modelId"` // model_change
Model string `json:"model"` // usage
Model string `json:"model"` // usage; omp's model_change, as provider/model
Usage *piUsage `json:"usage"` // usage, compaction, branch_summary
Message *struct {
Role string `json:"role"`
@@ -117,6 +120,7 @@ var (
piHeader = []byte(`"type":"session"`)
piInfo = []byte(`"type":"session_info"`)
piModel = []byte(`"type":"model_change"`)
piRetitle = []byte(`"type":"title_change"`)
piUsed = []byte(`"usage":{`)
piUserMsg = []byte(`"role":"user"`)
)
@@ -129,7 +133,7 @@ func piParse(s *state, b []byte, main bool) {
if !copied {
s.saw(at, main)
}
want := header || bytes.Contains(b, piInfo) || bytes.Contains(b, piModel) ||
want := header || bytes.Contains(b, piInfo) || bytes.Contains(b, piModel) || bytes.Contains(b, piRetitle) ||
!copied && bytes.Contains(b, piUsed) ||
s.Title == "" && bytes.Contains(b, piUserMsg)
if !want {
@@ -146,14 +150,24 @@ func piParse(s *state, b []byte, main bool) {
if l.ParentSession != "" {
s.Since = at
}
if l.Title != "" {
s.Named = title(l.Title)
}
}
case "session_info":
s.Named = title(l.Name)
case "title_change":
// omp's, one with each new title
if l.Title != "" {
s.Named = title(l.Title)
}
case "model_change":
if l.ModelID != "" {
s.Model = l.ModelID
} else if _, id, ok := strings.Cut(l.Model, "/"); ok && id != "" {
s.Model = id
}
case "usage":
case "usage", "model_usage":
if !copied && l.Model != "" {
s.use(dateOf(at), l.Model, l.Usage.tokens())
}
+9 -6
View File
@@ -1,8 +1,8 @@
// Package sessions lists the agents' recent sessions from their own session
// files — Claude Code's projects/*/<id>.jsonl (and Qoder's, the same kind),
// Codex's rollout files, OpenCode's database (or its older JSON files) and
// ZCode's, Pi's session files, DeepSeek Harness's, Cline's, Grok Build's and
// WorkBuddy's — with the
// ZCode's, Pi's session files and omp's, DeepSeek Harness's, Cline's, Grok
// Build's and WorkBuddy's — with the
// tokens each spent, what that cost at list price, and the command that
// resumes it. It only ever reads the agents' folders.
//
@@ -67,7 +67,7 @@ type Model struct {
// Session is one agent session.
type Session struct {
Agent string `json:"agent"` // magpie agent id: claude, codex, opencode, pi, zcode, dsh, cline, qoder, qoder-cn, grok, workbuddy
Agent string `json:"agent"` // magpie agent id: claude, codex, opencode, pi, omp, zcode, dsh, cline, qoder, qoder-cn, grok, workbuddy
ID string `json:"id"`
Cwd string `json:"cwd"`
Title string `json:"title"` // the first prompt, else the agent's own title
@@ -340,7 +340,7 @@ func allFiles() []file {
var out []file
for _, fs := range [][]file{claudeFiles(), codexFiles(), openCodeFiles(), piFiles(),
zcodeFiles(), dshFiles(), clineFiles(), ccFiles("qoder", QoderDir("qoder")), ccFiles("qoder-cn", QoderDir("qoder-cn")),
grokFiles(), workbuddyFiles()} {
grokFiles(), workbuddyFiles(), ompFiles()} {
out = append(out, fs...)
}
return out
@@ -360,6 +360,7 @@ func Dirs() []string {
{QoderDir("qoder-cn"), filepath.Join(QoderDir("qoder-cn"), "projects")},
{GrokDir(), filepath.Join(GrokDir(), "sessions")},
{WorkBuddyDir(), filepath.Join(WorkBuddyDir(), "projects")},
{OmpDir(), filepath.Join(OmpDir(), "sessions")},
} {
if _, err := os.Stat(d.sessions); err == nil {
out = append(out, d.dir)
@@ -740,7 +741,7 @@ func assemble(fs []file, price func(string) *catalog.Price) (Session, bool) {
if first == "" {
first = st.First
}
if (s.Agent == "codex" || s.Agent == "pi" || s.Agent == "dsh" || s.Agent == "grok") && st.ID != "" && f.path == fs[0].path {
if (s.Agent == "codex" || s.Agent == "pi" || s.Agent == "omp" || s.Agent == "dsh" || s.Agent == "grok") && st.ID != "" && f.path == fs[0].path {
s.ID = st.ID
}
}
@@ -815,7 +816,7 @@ func parse(f file, old *state) *state {
switch f.agent {
case "codex":
line = codexLine
case "pi":
case "pi", "omp":
line = piParse
case "workbuddy":
line = workbuddyLine
@@ -1016,6 +1017,8 @@ func ResumeCommand(agent, id, cwd string) string {
run = "qoderclicn --resume " + id
case "grok":
run = "grok --resume " + id
case "omp":
run = "omp --resume " + id
default:
return ""
}
+2 -2
View File
@@ -47,8 +47,8 @@ func setup(t *testing.T) (claude, codex string) {
t.Setenv("PI_CODING_AGENT_DIR", filepath.Join(dir, "pi"))
t.Setenv("OPENCODE_DB", "")
t.Setenv("PI_CODING_AGENT_SESSION_DIR", "")
// ZCode, dsh, Cline, Qoder, Grok Build and WorkBuddy keep theirs in the
// home folder: never the real one's
// ZCode, dsh, Cline, Qoder, Grok Build, WorkBuddy and omp keep theirs in
// the home folder: never the real one's
t.Setenv("HOME", filepath.Join(dir, "home"))
t.Setenv("USERPROFILE", filepath.Join(dir, "home"))
for _, env := range []string{"DSH_HOME", "CLINE_DIR", "CLINE_DATA_DIR", "CLINE_SESSION_DATA_DIR", "QODER_CONFIG_DIR", "QODERCN_CONFIG_DIR",
@@ -0,0 +1,11 @@
{"type":"title","v":1,"title":"Port the parser to omp","source":"auto","updatedAt":"2026-09-28T08:01:10.000Z","pad":" "}
{"type":"session","version":3,"id":"019a0000-0000-7000-8000-00000000000a","title":"Port the parser","titleSource":"auto","timestamp":"2026-09-28T08:00:00.000Z","cwd":"/work/omp"}
{"type":"model_change","id":"a0000001","parentId":null,"timestamp":"2026-09-28T08:00:00.100Z","model":"anthropic/claude-opus-5-5","role":"default"}
{"type":"thinking_level_change","id":"a0000002","parentId":"a0000001","timestamp":"2026-09-28T08:00:00.100Z","thinkingLevel":"high"}
{"type":"message","id":"a0000003","parentId":"a0000002","timestamp":"2026-09-28T08:00:01.000Z","message":{"role":"user","content":[{"type":"text","text":"Port the parser"}],"attribution":"user","timestamp":1790582401000}}
{"type":"message","id":"a0000004","parentId":"a0000003","timestamp":"2026-09-28T08:00:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ok"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-opus-5-5","usage":{"input":100,"output":50,"cacheRead":1000,"cacheWrite":200,"totalTokens":1350,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1790582405000}}
{"type":"model_usage","id":"a0000005","parentId":"a0000004","timestamp":"2026-09-28T08:00:06.000Z","purpose":"title","role":"smol","api":"anthropic-messages","provider":"anthropic","model":"claude-haiku-5","usage":{"input":20,"output":5,"cacheRead":0,"cacheWrite":0,"totalTokens":25,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop"}
{"type":"message","id":"a0000006","parentId":"a0000004","timestamp":"2026-09-28T08:00:50.000Z","message":{"role":"toolResult","toolCallId":"toolu_1","toolName":"task","content":[{"type":"text","text":"done"}],"details":{"usage":{"input":340,"output":34,"cacheRead":100,"cacheWrite":0,"totalTokens":474,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}}},"isError":false,"timestamp":1790582450000}}
{"type":"message","id":"a0000007","parentId":"a0000006","timestamp":"2026-09-28T08:01:00.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ok"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-opus-5-5","usage":{"input":10,"output":5,"cacheRead":500,"cacheWrite":0,"totalTokens":515,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1790582460000}}
{"type":"title_change","id":"a0000008","parentId":"a0000007","timestamp":"2026-09-28T08:01:10.000Z","title":"Port the parser to omp","previousTitle":"Port the parser","source":"auto","trigger":"auto"}
{"type":"compaction","id":"a0000009","parentId":"a0000008","timestamp":"2026-09-28T08:02:00.000Z","summary":"Ported.","firstKeptEntryId":"a0000007","tokensBefore":1800}
@@ -0,0 +1,4 @@
{"type":"session","version":3,"id":"019a0000-0000-7000-8000-00000000001a","timestamp":"2026-09-28T08:00:10.000Z","cwd":"/work/omp","parentSession":"/home/me/.omp/agent/sessions/--work-omp--/2026-09-28T08-00-00-000Z_019a0000-0000-7000-8000-00000000000a.jsonl"}
{"type":"model_change","id":"b0000001","parentId":null,"timestamp":"2026-09-28T08:00:10.100Z","model":"anthropic/claude-sonnet-5","role":"default"}
{"type":"message","id":"b0000002","parentId":"b0000001","timestamp":"2026-09-28T08:00:11.000Z","message":{"role":"user","content":[{"type":"text","text":"Find the parser"}],"attribution":"user","timestamp":1790582411000}}
{"type":"message","id":"b0000003","parentId":"b0000002","timestamp":"2026-09-28T08:00:20.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ok"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-5","usage":{"input":300,"output":30,"cacheRead":100,"cacheWrite":0,"totalTokens":430,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1790582420000}}
@@ -0,0 +1,3 @@
{"type":"session","version":3,"id":"019a0000-0000-7000-8000-00000000002a","timestamp":"2026-09-28T08:00:25.000Z","cwd":"/work/omp","parentSession":"/home/me/.omp/agent/sessions/--work-omp--/2026-09-28T08-00-00-000Z_019a0000-0000-7000-8000-00000000000a/0-Explore.jsonl"}
{"type":"message","id":"c0000001","parentId":null,"timestamp":"2026-09-28T08:00:26.000Z","message":{"role":"user","content":[{"type":"text","text":"Grep for parse"}],"attribution":"user","timestamp":1790582426000}}
{"type":"message","id":"c0000002","parentId":"c0000001","timestamp":"2026-09-28T08:00:30.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ok"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-5","usage":{"input":40,"output":4,"cacheRead":0,"cacheWrite":0,"totalTokens":44,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1790582430000}}
@@ -0,0 +1,3 @@
{"type":"session","version":3,"id":"019a0000-0000-7000-8000-00000000003a","timestamp":"2026-09-28T08:00:35.000Z","cwd":"/work/omp","parentSession":"/home/me/.omp/agent/sessions/--work-omp--/2026-09-28T08-00-00-000Z_019a0000-0000-7000-8000-00000000000a.jsonl"}
{"type":"message","id":"d0000001","parentId":null,"timestamp":"2026-09-28T08:00:36.000Z","message":{"role":"user","content":[{"type":"text","text":"Review the plan"}],"attribution":"user","timestamp":1790582436000}}
{"type":"message","id":"d0000002","parentId":"d0000001","timestamp":"2026-09-28T08:00:40.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ok"}],"api":"openai-responses","provider":"openai","model":"gpt-6-astra","usage":{"input":50,"output":10,"cacheRead":0,"cacheWrite":0,"totalTokens":60,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1790582440000}}
@@ -0,0 +1,9 @@
{"type":"title","v":1,"title":"Port the parser","source":"auto","updatedAt":"2026-09-29T09:00:00.000Z","pad":" "}
{"type":"session","version":3,"id":"019a0000-0000-7000-8000-00000000000c","title":"Port the parser","titleSource":"auto","timestamp":"2026-09-29T09:00:00.000Z","cwd":"/work/omp","parentSession":"/home/me/.omp/agent/sessions/--work-omp--/2026-09-28T08-00-00-000Z_019a0000-0000-7000-8000-00000000000a.jsonl"}
{"type":"model_change","id":"a0000001","parentId":null,"timestamp":"2026-09-28T08:00:00.100Z","model":"anthropic/claude-opus-5-5","role":"default"}
{"type":"thinking_level_change","id":"a0000002","parentId":"a0000001","timestamp":"2026-09-28T08:00:00.100Z","thinkingLevel":"high"}
{"type":"message","id":"a0000003","parentId":"a0000002","timestamp":"2026-09-28T08:00:01.000Z","message":{"role":"user","content":[{"type":"text","text":"Port the parser"}],"attribution":"user","timestamp":1790582401000}}
{"type":"message","id":"a0000004","parentId":"a0000003","timestamp":"2026-09-28T08:00:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ok"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-opus-5-5","usage":{"input":100,"output":50,"cacheRead":1000,"cacheWrite":200,"totalTokens":1350,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1790582405000}}
{"type":"model_usage","id":"a0000005","parentId":"a0000004","timestamp":"2026-09-28T08:00:06.000Z","purpose":"title","role":"smol","api":"anthropic-messages","provider":"anthropic","model":"claude-haiku-5","usage":{"input":20,"output":5,"cacheRead":0,"cacheWrite":0,"totalTokens":25,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop"}
{"type":"message","id":"e0000001","parentId":"a0000005","timestamp":"2026-09-29T09:00:10.000Z","message":{"role":"user","content":[{"type":"text","text":"Try another way"}],"attribution":"user","timestamp":1790672410000}}
{"type":"message","id":"e0000002","parentId":"e0000001","timestamp":"2026-09-29T09:00:20.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ok"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-opus-5-5","usage":{"input":7,"output":3,"cacheRead":0,"cacheWrite":0,"totalTokens":10,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1790672420000}}
@@ -0,0 +1,6 @@
{"type":"title","v":1,"title":"Tidy the tests","source":"user","updatedAt":"2026-09-29T10:00:30.000Z","pad":" "}
{"type":"session","version":3,"id":"019a0000-0000-7000-8000-00000000000d","title":"Refactor","titleSource":"auto","timestamp":"2026-09-29T10:00:00.000Z","cwd":"/work/omp"}
{"type":"message","id":"f0000001","parentId":null,"timestamp":"2026-09-29T10:00:01.000Z","message":{"role":"user","content":[{"type":"text","text":"<template>Refactor the helpers</template>"}],"attribution":"user","timestamp":1790676001000}}
{"type":"message","id":"f0000002","parentId":"f0000001","timestamp":"2026-09-29T10:00:10.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ok"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-opus-5-5","usage":{"input":1,"output":1,"cacheRead":0,"cacheWrite":0,"totalTokens":2,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1790676010000}}
{"type":"title_change","id":"f0000003","parentId":"f0000002","timestamp":"2026-09-29T10:00:20.000Z","title":"Refactor the helpers","previousTitle":"Refactor","source":"auto","trigger":"auto"}
{"type":"title_change","id":"f0000004","parentId":"f0000003","timestamp":"2026-09-29T10:00:30.000Z","title":"Tidy the tests","previousTitle":"Refactor the helpers","source":"user","trigger":"rename"}