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:
15 files changed
+374
-16
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
+11
@@ -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}
|
||||
+4
@@ -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}}
|
||||
+3
@@ -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}}
|
||||
+3
@@ -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}}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"question":"why?","answer":"because"}
|
||||
+9
@@ -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}}
|
||||
+6
@@ -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"}
|
||||
Reference in new issue
Block a user