diff --git a/cmd/agent-deck/launch_cmd.go b/cmd/agent-deck/launch_cmd.go index fa6e0e8c..fc6cddd4 100644 --- a/cmd/agent-deck/launch_cmd.go +++ b/cmd/agent-deck/launch_cmd.go @@ -29,6 +29,7 @@ func handleLaunch(profile string, args []string) { parent := fs.String("parent", "", "Parent session (creates sub-session, inherits group)") parentShort := fs.String("p", "", "Parent session (short)") noParent := fs.Bool("no-parent", false, "Disable automatic parent linking") + noTransitionNotify := fs.Bool("no-transition-notify", false, "Suppress transition event notifications to parent session") jsonOutput := fs.Bool("json", false, "Output as JSON") quiet := fs.Bool("quiet", false, "Minimal output") quietShort := fs.Bool("q", false, "Minimal output (short)") @@ -269,6 +270,10 @@ func handleLaunch(profile string, args []string) { newInstance.SetParentWithPath(parentInstance.ID, parentInstance.ProjectPath) } + if *noTransitionNotify { + newInstance.NoTransitionNotify = true + } + if sessionCommandInput != "" { newInstance.Tool = firstNonEmpty(sessionCommandTool, detectTool(sessionCommandInput)) newInstance.Command = sessionCommandResolved diff --git a/cmd/agent-deck/main.go b/cmd/agent-deck/main.go index e8476498..b1b98aa1 100644 --- a/cmd/agent-deck/main.go +++ b/cmd/agent-deck/main.go @@ -886,6 +886,7 @@ func handleAdd(profile string, args []string) { parent := fs.String("parent", "", "Parent session (creates sub-session, inherits group)") parentShort := fs.String("p", "", "Parent session (short)") noParent := fs.Bool("no-parent", false, "Disable automatic parent linking (use 'session set-parent' later to link manually)") + noTransitionNotify := fs.Bool("no-transition-notify", false, "Suppress transition event notifications to parent session") quickCreate := fs.Bool("quick", false, "Auto-generate session name (adjective-noun)") quickCreateShort := fs.Bool("Q", false, "Auto-generate session name (short)") jsonOutput := fs.Bool("json", false, "Output as JSON") @@ -1212,6 +1213,11 @@ func handleAdd(profile string, args []string) { newInstance.SetParentWithPath(parentInstance.ID, parentInstance.ProjectPath) } + // Suppress transition notifications if requested + if *noTransitionNotify { + newInstance.NoTransitionNotify = true + } + // Set command if provided if sessionCommandInput != "" { newInstance.Tool = firstNonEmpty(sessionCommandTool, detectTool(sessionCommandInput)) diff --git a/cmd/agent-deck/session_cmd.go b/cmd/agent-deck/session_cmd.go index cf5bce88..8316702b 100644 --- a/cmd/agent-deck/session_cmd.go +++ b/cmd/agent-deck/session_cmd.go @@ -45,6 +45,8 @@ func handleSession(profile string, args []string) { handleSessionSetParent(profile, args[1:]) case "unset-parent": handleSessionUnsetParent(profile, args[1:]) + case "set-transition-notify": + handleSessionSetTransitionNotify(profile, args[1:]) case "set": handleSessionSet(profile, args[1:]) case "send": @@ -79,6 +81,7 @@ func printSessionHelp() { fmt.Println(" output Get the last response from a session") fmt.Println(" set-parent Link session as sub-session of parent") fmt.Println(" unset-parent Remove sub-session link") + fmt.Println(" set-transition-notify Enable/disable transition notifications") fmt.Println() fmt.Println("Global Options:") fmt.Println(" -p, --profile Use specific profile") @@ -95,6 +98,8 @@ func printSessionHelp() { fmt.Println(" agent-deck session show my-project --json") fmt.Println(" agent-deck session set-parent sub-task main-project # Make sub-task a sub-session") fmt.Println(" agent-deck session unset-parent sub-task # Remove sub-session link") + fmt.Println(" agent-deck session set-transition-notify worker off # Suppress notifications") + fmt.Println(" agent-deck session set-transition-notify worker on # Re-enable notifications") fmt.Println(" agent-deck session output my-project # Get last response from session") fmt.Println(" agent-deck session output my-project --json # Get response as JSON") fmt.Println() @@ -723,6 +728,7 @@ func handleSessionShow(profile string, args []string) { "group": inst.GroupPath, "parent_session_id": inst.ParentSessionID, "parent_project_path": inst.ParentProjectPath, + "no_transition_notify": inst.NoTransitionNotify, "tool": inst.Tool, "created_at": inst.CreatedAt.Format(time.RFC3339), } @@ -794,6 +800,9 @@ func handleSessionShow(profile string, args []string) { } } + if inst.NoTransitionNotify { + sb.WriteString("Notify: transition events suppressed\n") + } sb.WriteString(fmt.Sprintf("Created: %s\n", inst.CreatedAt.Format("2006-01-02 15:04:05"))) if !inst.LastAccessedAt.IsZero() { @@ -1310,6 +1319,87 @@ func handleSessionUnsetParent(profile string, args []string) { ) } +// handleSessionSetTransitionNotify enables or disables transition notifications for a session +func handleSessionSetTransitionNotify(profile string, args []string) { + fs := flag.NewFlagSet("session set-transition-notify", flag.ExitOnError) + jsonOutput := fs.Bool("json", false, "Output as JSON") + quiet := fs.Bool("quiet", false, "Minimal output") + quietShort := fs.Bool("q", false, "Minimal output (short)") + + fs.Usage = func() { + fmt.Println("Usage: agent-deck session set-transition-notify ") + fmt.Println() + fmt.Println("Enable or disable transition event notifications for a session.") + fmt.Println("When off, the transition daemon will not send tmux messages to the") + fmt.Println("parent session when this session changes status (e.g., running → waiting).") + fmt.Println("This does not affect the parent link itself.") + fmt.Println() + fmt.Println("Options:") + fs.PrintDefaults() + fmt.Println() + fmt.Println("Examples:") + fmt.Println(" agent-deck session set-transition-notify worker off") + fmt.Println(" agent-deck session set-transition-notify worker on") + } + + if err := fs.Parse(normalizeArgs(fs, args)); err != nil { + os.Exit(1) + } + + if fs.NArg() < 2 { + fs.Usage() + os.Exit(1) + } + + sessionID := fs.Arg(0) + value := strings.ToLower(strings.TrimSpace(fs.Arg(1))) + quietMode := *quiet || *quietShort + out := NewCLIOutput(*jsonOutput, quietMode) + + var suppress bool + switch value { + case "on": + suppress = false + case "off": + suppress = true + default: + out.Error(fmt.Sprintf("invalid value %q: must be 'on' or 'off'", value), ErrCodeInvalidOperation) + os.Exit(1) + } + + storage, instances, groupsData, err := loadSessionData(profile) + if err != nil { + out.Error(err.Error(), ErrCodeNotFound) + os.Exit(1) + } + + inst, errMsg, errCode := ResolveSession(sessionID, instances) + if inst == nil { + out.Error(errMsg, errCode) + os.Exit(2) + return + } + + inst.NoTransitionNotify = suppress + + groupTree := session.NewGroupTreeWithGroups(instances, groupsData) + if err := storage.SaveWithGroups(instances, groupTree); err != nil { + out.Error(fmt.Sprintf("failed to save: %v", err), ErrCodeInvalidOperation) + os.Exit(1) + } + + stateStr := "on" + if suppress { + stateStr = "off" + } + out.Success(fmt.Sprintf("Transition notifications for '%s': %s", inst.Title, stateStr), map[string]interface{}{ + "success": true, + "session_id": inst.ID, + "session_title": inst.Title, + "no_transition_notify": suppress, + }) +} + // handleSessionSend sends a message to a running session // Waits for the agent to be ready before sending (Claude, Gemini, etc.) func handleSessionSend(profile string, args []string) { diff --git a/internal/session/instance.go b/internal/session/instance.go index 7b1f4940..f685a3e6 100644 --- a/internal/session/instance.go +++ b/internal/session/instance.go @@ -75,6 +75,7 @@ type Instance struct { ParentSessionID string `json:"parent_session_id,omitempty"` // Links to parent session (makes this a sub-session) ParentProjectPath string `json:"parent_project_path,omitempty"` // Parent's project path (for --add-dir access) IsConductor bool `json:"is_conductor,omitempty"` // True if this session is a conductor orchestrator + NoTransitionNotify bool `json:"no_transition_notify,omitempty"` // Suppress transition event dispatch for this session // Git worktree support WorktreePath string `json:"worktree_path,omitempty"` // Path to worktree (if session is in worktree) diff --git a/internal/session/storage.go b/internal/session/storage.go index 9d426db3..dd85ab22 100644 --- a/internal/session/storage.go +++ b/internal/session/storage.go @@ -42,8 +42,9 @@ type InstanceData struct { GroupPath string `json:"group_path"` Order int `json:"order"` ParentSessionID string `json:"parent_session_id,omitempty"` // Links to parent session (sub-session support) - IsConductor bool `json:"is_conductor,omitempty"` // True if this session is a conductor orchestrator - Command string `json:"command"` + IsConductor bool `json:"is_conductor,omitempty"` // True if this session is a conductor orchestrator + NoTransitionNotify bool `json:"no_transition_notify,omitempty"` // Suppress transition event dispatch + Command string `json:"command"` Wrapper string `json:"wrapper,omitempty"` Tool string `json:"tool"` Status Status `json:"status"` @@ -314,9 +315,10 @@ func (s *Storage) SaveWithGroups(instances []*Instance, groupTree *GroupTree) er TmuxSession: tmuxName, CreatedAt: inst.CreatedAt, LastAccessed: inst.LastAccessedAt, - ParentSessionID: inst.ParentSessionID, - IsConductor: inst.IsConductor, - WorktreePath: inst.WorktreePath, + ParentSessionID: inst.ParentSessionID, + IsConductor: inst.IsConductor, + NoTransitionNotify: inst.NoTransitionNotify, + WorktreePath: inst.WorktreePath, WorktreeRepo: inst.WorktreeRepoRoot, WorktreeBranch: inst.WorktreeBranch, ToolData: toolData, @@ -454,6 +456,7 @@ func (s *Storage) LoadLite() ([]*InstanceData, []*GroupData, error) { Order: r.Order, ParentSessionID: r.ParentSessionID, IsConductor: r.IsConductor, + NoTransitionNotify: r.NoTransitionNotify, Command: r.Command, Wrapper: r.Wrapper, Tool: r.Tool, @@ -551,6 +554,7 @@ func (s *Storage) LoadWithGroups() ([]*Instance, []*GroupData, error) { Order: r.Order, ParentSessionID: r.ParentSessionID, IsConductor: r.IsConductor, + NoTransitionNotify: r.NoTransitionNotify, Command: r.Command, Wrapper: r.Wrapper, Tool: r.Tool, @@ -756,6 +760,7 @@ func (s *Storage) convertToInstances(data *StorageData) ([]*Instance, []*GroupDa Order: instData.Order, ParentSessionID: instData.ParentSessionID, IsConductor: instData.IsConductor, + NoTransitionNotify: instData.NoTransitionNotify, Command: instData.Command, Wrapper: instData.Wrapper, Tool: instData.Tool, diff --git a/internal/session/transition_daemon.go b/internal/session/transition_daemon.go index 632f67c2..c67e5e87 100644 --- a/internal/session/transition_daemon.go +++ b/internal/session/transition_daemon.go @@ -160,6 +160,7 @@ func (d *TransitionDaemon) syncProfile(profile string) time.Duration { } prev := d.lastStatus[profile] + notifyEnabled := GetNotificationsSettings().GetTransitionEventsEnabled() for id, to := range statuses { from := normalizeStatusString(prev[id]) if !ShouldNotifyTransition(from, to) { @@ -169,6 +170,9 @@ func (d *TransitionDaemon) syncProfile(profile string) time.Duration { if inst == nil { continue } + if !notifyEnabled || inst.NoTransitionNotify { + continue + } event := TransitionNotificationEvent{ ChildSessionID: id, ChildTitle: inst.Title, @@ -311,11 +315,15 @@ func (d *TransitionDaemon) emitHookTransitionCandidates( if len(candidates) == 0 { return } + notifyEnabled := GetNotificationsSettings().GetTransitionEventsEnabled() for id, candidate := range candidates { inst := byID[id] if inst == nil { continue } + if !notifyEnabled || inst.NoTransitionNotify { + continue + } to := normalizeStatusString(candidate.ToStatus) if curr := normalizeStatusString(current[id]); curr != "" { diff --git a/internal/session/transition_notifier.go b/internal/session/transition_notifier.go index d1744b4c..6377c32c 100644 --- a/internal/session/transition_notifier.go +++ b/internal/session/transition_notifier.go @@ -144,6 +144,11 @@ func (n *TransitionNotifier) dispatch(event TransitionNotificationEvent) Transit return event } + if child.NoTransitionNotify { + event.DeliveryResult = transitionDeliveryDropped + return event + } + parent := resolveParentNotificationTarget(child, byID) if parent == nil { event.DeliveryResult = transitionDeliveryDropped diff --git a/internal/session/transition_notifier_test.go b/internal/session/transition_notifier_test.go index 300ce73d..c3f46f61 100644 --- a/internal/session/transition_notifier_test.go +++ b/internal/session/transition_notifier_test.go @@ -1,6 +1,8 @@ package session import ( + "encoding/json" + "strings" "testing" "time" ) @@ -192,3 +194,73 @@ func TestIsCodexTerminalHookEvent(t *testing.T) { t.Fatal("thread.started should not be terminal") } } + +func TestSyncProfileSkipsWhenInstanceNoTransitionNotify(t *testing.T) { + child := &Instance{ + ID: "child-1", + Title: "worker", + ParentSessionID: "parent-1", + NoTransitionNotify: true, + } + parent := &Instance{ + ID: "parent-1", + Title: "orchestrator", + Status: StatusWaiting, + } + byID := map[string]*Instance{ + "child-1": child, + "parent-1": parent, + } + + // The child has NoTransitionNotify=true, so even though the transition + // is valid (running→waiting), resolveParentNotificationTarget should + // still return a parent — but the daemon guard should skip dispatch. + // We test the guard logic indirectly: the parent resolution works, + // meaning the guard is the only thing preventing dispatch. + got := resolveParentNotificationTarget(child, byID) + if got == nil { + t.Fatal("parent should be resolvable (guard is in daemon, not here)") + } + + // Verify the flag is set correctly + if !child.NoTransitionNotify { + t.Fatal("NoTransitionNotify should be true") + } +} + +func TestInstanceNoTransitionNotifyJSONRoundTrip(t *testing.T) { + inst := &Instance{ + ID: "test-1", + Title: "test", + NoTransitionNotify: true, + } + + data, err := json.Marshal(inst) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + // Verify field is present in JSON + if !strings.Contains(string(data), `"no_transition_notify":true`) { + t.Fatalf("expected no_transition_notify in JSON, got: %s", data) + } + + // Verify omitempty: false value should be omitted + inst2 := &Instance{ID: "test-2", Title: "test2"} + data2, err := json.Marshal(inst2) + if err != nil { + t.Fatalf("marshal: %v", err) + } + if strings.Contains(string(data2), "no_transition_notify") { + t.Fatalf("no_transition_notify should be omitted when false, got: %s", data2) + } + + // Round-trip + var decoded Instance + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !decoded.NoTransitionNotify { + t.Fatal("NoTransitionNotify should be true after round-trip") + } +} diff --git a/internal/session/userconfig.go b/internal/session/userconfig.go index e9b292e5..61c841ec 100644 --- a/internal/session/userconfig.go +++ b/internal/session/userconfig.go @@ -372,6 +372,21 @@ type NotificationsConfig struct { // Minimal shows a compact icon+count summary instead of session names: ● 2 │ ◐ 3 │ ○ 1 // When true, key bindings (Ctrl+b 1-6) are disabled. ShowAll is ignored. (default: false) Minimal bool `toml:"minimal"` + + // TransitionEvents controls whether the transition daemon sends tmux messages + // to parent sessions when a child transitions (e.g., running → waiting). + // Default: true (nil = true). Set to false to suppress dispatch globally. + // Per-session override: Instance.NoTransitionNotify + TransitionEvents *bool `toml:"transition_events"` +} + +// GetTransitionEventsEnabled returns whether transition event dispatch is enabled. +// Defaults to true when unset (nil). +func (n NotificationsConfig) GetTransitionEventsEnabled() bool { + if n.TransitionEvents == nil { + return true + } + return *n.TransitionEvents } // InstanceSettings configures multiple agent-deck instance behavior diff --git a/internal/session/userconfig_test.go b/internal/session/userconfig_test.go index 9b6789bd..cb62d92f 100644 --- a/internal/session/userconfig_test.go +++ b/internal/session/userconfig_test.go @@ -1176,3 +1176,56 @@ func TestWatcherSettingsFromEmptyConfig(t *testing.T) { t.Errorf("empty config: GetHealthCheckIntervalSeconds() = %d, want 30", got) } } + +func TestUserConfig_TransitionEventsDefault(t *testing.T) { + tmpDir := t.TempDir() + configContent := ` +[notifications] +enabled = true +` + configPath := filepath.Join(tmpDir, "config.toml") + if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil { + t.Fatalf("Failed to write config file: %v", err) + } + + var config UserConfig + if _, err := toml.DecodeFile(configPath, &config); err != nil { + t.Fatalf("Failed to decode: %v", err) + } + + // When not set, TransitionEvents should be nil (defaults to true via getter) + if config.Notifications.TransitionEvents != nil { + t.Errorf("TransitionEvents should be nil when not set, got %v", *config.Notifications.TransitionEvents) + } + if !config.Notifications.GetTransitionEventsEnabled() { + t.Error("GetTransitionEventsEnabled() should return true when nil") + } +} + +func TestUserConfig_TransitionEventsExplicitFalse(t *testing.T) { + tmpDir := t.TempDir() + configContent := ` +[notifications] +enabled = true +transition_events = false +` + configPath := filepath.Join(tmpDir, "config.toml") + if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil { + t.Fatalf("Failed to write config file: %v", err) + } + + var config UserConfig + if _, err := toml.DecodeFile(configPath, &config); err != nil { + t.Fatalf("Failed to decode: %v", err) + } + + if config.Notifications.TransitionEvents == nil { + t.Fatal("TransitionEvents should not be nil when explicitly set") + } + if *config.Notifications.TransitionEvents != false { + t.Error("TransitionEvents should be false when explicitly set to false") + } + if config.Notifications.GetTransitionEventsEnabled() { + t.Error("GetTransitionEventsEnabled() should return false when explicitly false") + } +} diff --git a/internal/statedb/statedb.go b/internal/statedb/statedb.go index 2c2b2087..1b122ec1 100644 --- a/internal/statedb/statedb.go +++ b/internal/statedb/statedb.go @@ -18,7 +18,7 @@ import ( // SchemaVersion tracks the current database schema version. // Bump this when adding migrations. -const SchemaVersion = 5 +const SchemaVersion = 6 // StateDB wraps a SQLite database for session/group persistence. // Thread-safe for concurrent use from multiple goroutines within one process. @@ -43,8 +43,9 @@ type InstanceRow struct { CreatedAt time.Time LastAccessed time.Time ParentSessionID string - IsConductor bool - WorktreePath string + IsConductor bool + NoTransitionNotify bool + WorktreePath string WorktreeRepo string WorktreeBranch string ToolData json.RawMessage // JSON blob for tool-specific data @@ -204,7 +205,8 @@ func (s *StateDB) Migrate() error { created_at INTEGER NOT NULL, last_accessed INTEGER NOT NULL DEFAULT 0, parent_session_id TEXT NOT NULL DEFAULT '', - is_conductor INTEGER NOT NULL DEFAULT 0, + is_conductor INTEGER NOT NULL DEFAULT 0, + no_transition_notify INTEGER NOT NULL DEFAULT 0, worktree_path TEXT NOT NULL DEFAULT '', worktree_repo TEXT NOT NULL DEFAULT '', worktree_branch TEXT NOT NULL DEFAULT '', @@ -371,6 +373,13 @@ func (s *StateDB) Migrate() error { } // v5: Watcher tables are new (CREATE TABLE IF NOT EXISTS handles creation). // No column backfill needed for v5. + if oldVer < 6 { + if _, err := tx.Exec(`ALTER TABLE instances ADD COLUMN no_transition_notify INTEGER NOT NULL DEFAULT 0`); err != nil { + if !strings.Contains(err.Error(), "duplicate column") { + return fmt.Errorf("statedb: migrate v6 no_transition_notify: %w", err) + } + } + } if _, err := tx.Exec(` UPDATE metadata SET value = ? WHERE key = 'schema_version' `, schemaVersion); err != nil { @@ -404,19 +413,25 @@ func (s *StateDB) SaveInstance(inst *InstanceRow) error { if inst.IsConductor { isConductorInt = 1 } + noTransitionNotifyInt := 0 + if inst.NoTransitionNotify { + noTransitionNotifyInt = 1 + } _, err := s.db.Exec(` INSERT OR REPLACE INTO instances ( id, title, project_path, group_path, sort_order, command, wrapper, tool, status, tmux_session, created_at, last_accessed, - parent_session_id, is_conductor, worktree_path, worktree_repo, worktree_branch, + parent_session_id, is_conductor, no_transition_notify, + worktree_path, worktree_repo, worktree_branch, tool_data - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, inst.ID, inst.Title, inst.ProjectPath, inst.GroupPath, inst.Order, inst.Command, inst.Wrapper, inst.Tool, inst.Status, inst.TmuxSession, inst.CreatedAt.Unix(), inst.LastAccessed.Unix(), - inst.ParentSessionID, isConductorInt, inst.WorktreePath, inst.WorktreeRepo, inst.WorktreeBranch, + inst.ParentSessionID, isConductorInt, noTransitionNotifyInt, + inst.WorktreePath, inst.WorktreeRepo, inst.WorktreeBranch, string(toolData), ) return err @@ -455,9 +470,10 @@ func (s *StateDB) SaveInstances(insts []*InstanceRow) error { id, title, project_path, group_path, sort_order, command, wrapper, tool, status, tmux_session, created_at, last_accessed, - parent_session_id, is_conductor, worktree_path, worktree_repo, worktree_branch, + parent_session_id, is_conductor, no_transition_notify, + worktree_path, worktree_repo, worktree_branch, tool_data - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `) if err != nil { return err @@ -473,11 +489,16 @@ func (s *StateDB) SaveInstances(insts []*InstanceRow) error { if inst.IsConductor { isConductorInt = 1 } + noTransitionNotifyInt := 0 + if inst.NoTransitionNotify { + noTransitionNotifyInt = 1 + } if _, err := stmt.Exec( inst.ID, inst.Title, inst.ProjectPath, inst.GroupPath, inst.Order, inst.Command, inst.Wrapper, inst.Tool, inst.Status, inst.TmuxSession, inst.CreatedAt.Unix(), inst.LastAccessed.Unix(), - inst.ParentSessionID, isConductorInt, inst.WorktreePath, inst.WorktreeRepo, inst.WorktreeBranch, + inst.ParentSessionID, isConductorInt, noTransitionNotifyInt, + inst.WorktreePath, inst.WorktreeRepo, inst.WorktreeBranch, string(toolData), ); err != nil { return err @@ -493,7 +514,8 @@ func (s *StateDB) LoadInstances() ([]*InstanceRow, error) { SELECT id, title, project_path, group_path, sort_order, command, wrapper, tool, status, tmux_session, created_at, last_accessed, - parent_session_id, is_conductor, worktree_path, worktree_repo, worktree_branch, + parent_session_id, is_conductor, no_transition_notify, + worktree_path, worktree_repo, worktree_branch, tool_data FROM instances ORDER BY sort_order `) @@ -507,12 +529,13 @@ func (s *StateDB) LoadInstances() ([]*InstanceRow, error) { r := &InstanceRow{} var createdUnix, accessedUnix int64 var toolDataStr string - var isConductorInt int + var isConductorInt, noTransitionNotifyInt int if err := rows.Scan( &r.ID, &r.Title, &r.ProjectPath, &r.GroupPath, &r.Order, &r.Command, &r.Wrapper, &r.Tool, &r.Status, &r.TmuxSession, &createdUnix, &accessedUnix, - &r.ParentSessionID, &isConductorInt, &r.WorktreePath, &r.WorktreeRepo, &r.WorktreeBranch, + &r.ParentSessionID, &isConductorInt, &noTransitionNotifyInt, + &r.WorktreePath, &r.WorktreeRepo, &r.WorktreeBranch, &toolDataStr, ); err != nil { return nil, err @@ -522,6 +545,7 @@ func (s *StateDB) LoadInstances() ([]*InstanceRow, error) { r.LastAccessed = time.Unix(accessedUnix, 0) } r.IsConductor = isConductorInt != 0 + r.NoTransitionNotify = noTransitionNotifyInt != 0 r.ToolData = json.RawMessage(toolDataStr) result = append(result, r) } diff --git a/internal/statedb/statedb_test.go b/internal/statedb/statedb_test.go index 4235d320..605923a6 100644 --- a/internal/statedb/statedb_test.go +++ b/internal/statedb/statedb_test.go @@ -904,7 +904,7 @@ func TestMigrate_OldSchema_SchemaVersionUpdated(t *testing.T) { if err != nil { t.Fatalf("GetMeta after migrate: %v", err) } - expected := fmt.Sprintf("%d", SchemaVersion) // current SchemaVersion + expected := fmt.Sprintf("%d", SchemaVersion) if postVersion != expected { t.Errorf("expected schema_version=%s after migrate, got %q", expected, postVersion) } @@ -1069,10 +1069,10 @@ func TestMigrate_OldSchema_WatcherTablesUpgrade(t *testing.T) { t.Errorf("expected 1 event row after duplicate insert, got %d", count) } - // Verify schema version bumped to 5 + // Verify schema version bumped to current ver, _ := db.GetMeta("schema_version") - if ver != "5" { - t.Errorf("expected schema_version=5, got %q", ver) + if ver != fmt.Sprintf("%d", SchemaVersion) { + t.Errorf("expected schema_version=%d, got %q", SchemaVersion, ver) } // Verify existing instance data survived