Sync thread names from Codex
This commit is contained in:
@@ -203,6 +203,22 @@ func (c *Client) ResumeThread(ctx context.Context, threadID string) (Thread, err
|
||||
return result.Thread, nil
|
||||
}
|
||||
|
||||
func (c *Client) ReadThread(ctx context.Context, threadID string) (Thread, error) {
|
||||
if err := c.EnsureConnected(ctx); err != nil {
|
||||
return Thread{}, err
|
||||
}
|
||||
var result struct {
|
||||
Thread Thread `json:"thread"`
|
||||
}
|
||||
if err := c.call(ctx, "thread/read", map[string]any{
|
||||
"threadId": threadID,
|
||||
"includeTurns": false,
|
||||
}, &result); err != nil {
|
||||
return Thread{}, err
|
||||
}
|
||||
return result.Thread, nil
|
||||
}
|
||||
|
||||
func (c *Client) ForkThread(ctx context.Context, threadID string) (Thread, error) {
|
||||
if err := c.EnsureConnected(ctx); err != nil {
|
||||
return Thread{}, err
|
||||
|
||||
@@ -86,6 +86,31 @@ func TestClientWebSocketUnixJSONRPC(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
var readThread map[string]any
|
||||
if err := conn.ReadJSON(&readThread); err != nil {
|
||||
serverDone <- err
|
||||
return
|
||||
}
|
||||
if readThread["method"] != "thread/read" {
|
||||
serverDone <- unexpectedMessage("thread/read", readThread["method"])
|
||||
return
|
||||
}
|
||||
readParams := readThread["params"].(map[string]any)
|
||||
if readParams["threadId"] != "thr_1" || readParams["includeTurns"] != false {
|
||||
payload, _ := json.Marshal(readParams)
|
||||
serverDone <- unexpectedMessage("thread/read params", string(payload))
|
||||
return
|
||||
}
|
||||
if err := conn.WriteJSON(map[string]any{
|
||||
"id": readThread["id"],
|
||||
"result": map[string]any{
|
||||
"thread": map[string]any{"id": "thr_1", "name": "Read title", "preview": "test"},
|
||||
},
|
||||
}); err != nil {
|
||||
serverDone <- err
|
||||
return
|
||||
}
|
||||
|
||||
var setName map[string]any
|
||||
if err := conn.ReadJSON(&setName); err != nil {
|
||||
serverDone <- err
|
||||
@@ -145,6 +170,13 @@ func TestClientWebSocketUnixJSONRPC(t *testing.T) {
|
||||
if thread.ID != "thr_1" {
|
||||
t.Fatalf("unexpected thread: %+v", thread)
|
||||
}
|
||||
readThread, err := client.ReadThread(ctx, "thr_1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if readThread.ID != "thr_1" || readThread.Name != "Read title" {
|
||||
t.Fatalf("unexpected read thread: %+v", readThread)
|
||||
}
|
||||
if err := client.SetThreadName(ctx, "thr_1", "Short title"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -433,6 +433,18 @@ func (s *Store) RenameThreadByCodexID(ctx context.Context, codexThreadID, title
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) SyncThreadTitle(ctx context.Context, telegramUserID, id int64, title string) error {
|
||||
_, err := s.db.ExecContext(ctx, `
|
||||
UPDATE threads SET title = ?
|
||||
WHERE telegram_user_id = ? AND id = ?`, title, telegramUserID, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) SyncThreadTitleByCodexID(ctx context.Context, codexThreadID, title string) error {
|
||||
_, err := s.db.ExecContext(ctx, "UPDATE threads SET title = ? WHERE codex_thread_id = ?", title, codexThreadID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) UpsertPendingApproval(ctx context.Context, approval PendingApproval) (PendingApproval, error) {
|
||||
_, err := s.db.ExecContext(ctx, `
|
||||
INSERT INTO pending_approvals (
|
||||
|
||||
@@ -156,6 +156,26 @@ func TestRenameThread(t *testing.T) {
|
||||
if thread.Title != "codex title" {
|
||||
t.Fatalf("title = %q", thread.Title)
|
||||
}
|
||||
if err := st.SyncThreadTitle(ctx, 42, thread.ID, "synced title"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
thread, err = st.GetThreadByID(ctx, 42, thread.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if thread.Title != "synced title" {
|
||||
t.Fatalf("title = %q", thread.Title)
|
||||
}
|
||||
if err := st.SyncThreadTitleByCodexID(ctx, "codex-thread", "synced codex title"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
thread, err = st.GetThreadByID(ctx, 42, thread.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if thread.Title != "synced codex title" {
|
||||
t.Fatalf("title = %q", thread.Title)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkspacePath(t *testing.T) {
|
||||
|
||||
@@ -270,6 +270,7 @@ func (b *Bot) sendResumeChoices(ctx context.Context, userID, chatID int64, page
|
||||
_, err := b.tg.SendMessage(ctx, chatID, text, SendMessageOptions{})
|
||||
return err
|
||||
}
|
||||
threads = b.syncThreadTitles(ctx, threads)
|
||||
hasNext := len(threads) > resumeThreadPageSize
|
||||
if hasNext {
|
||||
threads = threads[:resumeThreadPageSize]
|
||||
@@ -302,11 +303,9 @@ func (b *Bot) resumeThreadByID(ctx context.Context, userID, chatID int64, id int
|
||||
if err != nil {
|
||||
return b.sendError(ctx, chatID, "Could not resume Codex thread", err)
|
||||
}
|
||||
if resumed.Name != "" && normalizeThreadTitle(resumed.Name) != thread.Title {
|
||||
thread.Title = normalizeThreadTitle(resumed.Name)
|
||||
if err := b.store.RenameThread(ctx, userID, thread.ID, thread.Title); err != nil {
|
||||
return err
|
||||
}
|
||||
thread, err = b.applyCodexThreadTitle(ctx, thread, resumed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.store.SetActiveThread(ctx, userID, thread.ID); err != nil {
|
||||
return err
|
||||
@@ -357,8 +356,17 @@ func (b *Bot) renameThread(ctx context.Context, userID, chatID int64, session st
|
||||
if err := b.codex.SetThreadName(ctx, thread.CodexThreadID, title); err != nil {
|
||||
return b.sendError(ctx, chatID, "Could not rename Codex thread", err)
|
||||
}
|
||||
if err := b.store.RenameThread(ctx, userID, thread.ID, title); err != nil {
|
||||
return err
|
||||
if codexThread, readErr := b.codex.ReadThread(ctx, thread.CodexThreadID); readErr == nil {
|
||||
thread, err = b.applyCodexThreadTitle(ctx, thread, codexThread)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
title = thread.Title
|
||||
} else {
|
||||
b.logger.Printf("read renamed thread %s: %v", thread.CodexThreadID, readErr)
|
||||
if err := b.store.SyncThreadTitle(ctx, userID, thread.ID, title); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = b.tg.SendMessage(ctx, chatID, fmt.Sprintf("Renamed thread ID %d: %s", thread.ID, title), SendMessageOptions{})
|
||||
return err
|
||||
@@ -373,10 +381,7 @@ func (b *Bot) forkThread(ctx context.Context, userID, chatID int64, session stor
|
||||
if err != nil {
|
||||
return b.sendError(ctx, chatID, "Could not fork Codex thread", err)
|
||||
}
|
||||
title := forked.Name
|
||||
if title == "" {
|
||||
title = "fork of ID " + strconv.FormatInt(thread.ID, 10)
|
||||
}
|
||||
title := codexThreadTitle(forked, "fork of ID "+strconv.FormatInt(thread.ID, 10))
|
||||
local, err := b.store.CreateThread(ctx, userID, forked.ID, thread.WorkspaceID, title)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -423,7 +428,15 @@ func (b *Bot) sendStatus(ctx context.Context, userID, chatID int64, session stor
|
||||
}
|
||||
thread := "(none)"
|
||||
if session.ActiveThreadID != 0 {
|
||||
thread = fmt.Sprintf("#%d", session.ActiveThreadID)
|
||||
thread = fmt.Sprintf("ID %d", session.ActiveThreadID)
|
||||
if active, err := b.store.GetThreadByID(ctx, userID, session.ActiveThreadID); err == nil {
|
||||
if synced, syncErr := b.syncThreadTitle(ctx, active); syncErr == nil {
|
||||
active = synced
|
||||
} else {
|
||||
b.logger.Printf("sync status thread title %s: %v", active.CodexThreadID, syncErr)
|
||||
}
|
||||
thread = fmt.Sprintf("ID %d: %s", active.ID, threadDisplayTitle(active))
|
||||
}
|
||||
}
|
||||
model := session.Model
|
||||
if model == "" {
|
||||
@@ -703,6 +716,51 @@ func (b *Bot) continueThread(ctx context.Context, message *Message, session stor
|
||||
return nil
|
||||
}
|
||||
|
||||
func codexThreadTitle(thread codexapp.Thread, fallback string) string {
|
||||
if title := normalizeThreadTitle(thread.Name); title != "" {
|
||||
return title
|
||||
}
|
||||
if title := normalizeThreadTitle(thread.Preview); title != "" {
|
||||
return title
|
||||
}
|
||||
return normalizeThreadTitle(fallback)
|
||||
}
|
||||
|
||||
func (b *Bot) applyCodexThreadTitle(ctx context.Context, thread store.Thread, codexThread codexapp.Thread) (store.Thread, error) {
|
||||
title := codexThreadTitle(codexThread, "")
|
||||
if title == thread.Title {
|
||||
return thread, nil
|
||||
}
|
||||
if err := b.store.SyncThreadTitle(ctx, thread.TelegramUserID, thread.ID, title); err != nil {
|
||||
return thread, err
|
||||
}
|
||||
thread.Title = title
|
||||
return thread, nil
|
||||
}
|
||||
|
||||
func (b *Bot) syncThreadTitle(ctx context.Context, thread store.Thread) (store.Thread, error) {
|
||||
if thread.CodexThreadID == "" {
|
||||
return thread, nil
|
||||
}
|
||||
codexThread, err := b.codex.ReadThread(ctx, thread.CodexThreadID)
|
||||
if err != nil {
|
||||
return thread, err
|
||||
}
|
||||
return b.applyCodexThreadTitle(ctx, thread, codexThread)
|
||||
}
|
||||
|
||||
func (b *Bot) syncThreadTitles(ctx context.Context, threads []store.Thread) []store.Thread {
|
||||
for i := range threads {
|
||||
synced, err := b.syncThreadTitle(ctx, threads[i])
|
||||
if err != nil {
|
||||
b.logger.Printf("sync thread title %s: %v", threads[i].CodexThreadID, err)
|
||||
continue
|
||||
}
|
||||
threads[i] = synced
|
||||
}
|
||||
return threads
|
||||
}
|
||||
|
||||
func (b *Bot) createNewThread(ctx context.Context, userID, chatID int64, session store.Session) (store.Thread, store.Workspace, error) {
|
||||
workspace, err := b.resolveWorkspace(ctx, userID, session)
|
||||
if err != nil {
|
||||
@@ -712,13 +770,7 @@ func (b *Bot) createNewThread(ctx context.Context, userID, chatID int64, session
|
||||
if err != nil {
|
||||
return store.Thread{}, store.Workspace{}, b.sendError(ctx, chatID, "Could not start Codex thread", err)
|
||||
}
|
||||
title := codexThread.Name
|
||||
if title == "" {
|
||||
title = codexThread.Preview
|
||||
}
|
||||
if title == "" {
|
||||
title = workspace.Label
|
||||
}
|
||||
title := codexThreadTitle(codexThread, workspace.Label)
|
||||
thread, err := b.store.CreateThread(ctx, userID, codexThread.ID, workspace.ID, title)
|
||||
if err != nil {
|
||||
return store.Thread{}, store.Workspace{}, err
|
||||
@@ -734,6 +786,11 @@ func (b *Bot) ensureThread(ctx context.Context, userID, chatID int64, session st
|
||||
if session.ActiveThreadID != 0 {
|
||||
thread, err := b.store.GetThreadByID(ctx, userID, session.ActiveThreadID)
|
||||
if err == nil && !thread.Archived {
|
||||
if synced, syncErr := b.syncThreadTitle(ctx, thread); syncErr == nil {
|
||||
thread = synced
|
||||
} else {
|
||||
b.logger.Printf("sync active thread title %s: %v", thread.CodexThreadID, syncErr)
|
||||
}
|
||||
workspace, err := b.store.GetWorkspaceByID(ctx, thread.WorkspaceID)
|
||||
return thread, workspace, err
|
||||
}
|
||||
@@ -1236,8 +1293,12 @@ func (b *Bot) handleCodexNotification(ctx context.Context, event codexapp.Event)
|
||||
if err := json.Unmarshal(event.Params, ¶ms); err != nil {
|
||||
return err
|
||||
}
|
||||
if params.ThreadID != "" && params.ThreadName != nil {
|
||||
return b.store.RenameThreadByCodexID(ctx, params.ThreadID, normalizeThreadTitle(*params.ThreadName))
|
||||
if params.ThreadID != "" {
|
||||
title := ""
|
||||
if params.ThreadName != nil {
|
||||
title = normalizeThreadTitle(*params.ThreadName)
|
||||
}
|
||||
return b.store.SyncThreadTitleByCodexID(ctx, params.ThreadID, title)
|
||||
}
|
||||
case "serverRequest/resolved":
|
||||
var params struct {
|
||||
|
||||
Reference in New Issue
Block a user