Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend/internal/adapters/agent/claudecode/claudecode.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ func New() *Plugin {
// accepted. See ports.ActivitySignaler.
func (p *Plugin) EmitsSubmitActivity() bool { return true }

// EmitsBlockedActivity signals that Claude Code fires both pre- and post-tool
// hooks, so Activity.State can flip to blocked mid-turn on a permission dialog
// and the guarded send loop can clear it once the tool completes. Only
// claude-code (and its hook-delegators) carry this trio; see
// ports.ActivitySignaler.
func (p *Plugin) EmitsBlockedActivity() bool { return true }

var _ adapters.Adapter = (*Plugin)(nil)
Expand Down
4 changes: 2 additions & 2 deletions backend/internal/lifecycle/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1156,9 +1156,9 @@ type blockOnNthGetStore struct {
func (s *blockOnNthGetStore) GetSession(ctx context.Context, id domain.SessionID) (domain.SessionRecord, bool, error) {
s.reads++
if s.reads == s.flipAt {
if rec, ok := s.fakeStore.sessions[s.id]; ok {
if rec, ok := s.sessions[s.id]; ok {
rec.Activity.State = domain.ActivityBlocked
s.fakeStore.sessions[s.id] = rec
s.sessions[s.id] = rec
}
}
return s.fakeStore.GetSession(ctx, id)
Expand Down
11 changes: 9 additions & 2 deletions backend/internal/session_manager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import (
"github.com/aoagents/agent-orchestrator/backend/internal/domain"
"github.com/aoagents/agent-orchestrator/backend/internal/ports"
aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process"
"github.com/aoagents/agent-orchestrator/backend/internal/skillassets"
"github.com/aoagents/agent-orchestrator/backend/internal/sessionguard"
"github.com/aoagents/agent-orchestrator/backend/internal/skillassets"
)

// Sentinel errors returned by the Session Manager; callers match them with
Expand Down Expand Up @@ -1461,7 +1461,14 @@ func (m *Manager) Send(ctx context.Context, id domain.SessionID, message string)
// satisfy both; every other harness opts out via EmitsBlockedActivity —
// see ports.ActivitySignaler.
rec, ok, err := m.store.GetSession(ctx, id)
if err != nil || !ok {
if err != nil {
// Confirmation is best-effort and never fails the send (the message
// was already delivered above); log so a store error is not swallowed
// silently.
m.logger.Warn("send: confirm skipped, session lookup failed", "sessionID", id, "error", err)
return nil
}
if !ok {
return nil
}
if m.harnessNudgeSafe(rec.Harness) {
Expand Down
4 changes: 2 additions & 2 deletions backend/internal/session_manager/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3606,9 +3606,9 @@ type blockAfterFirstReadStore struct {
func (s *blockAfterFirstReadStore) GetSession(ctx context.Context, id domain.SessionID) (domain.SessionRecord, bool, error) {
s.reads++
if s.reads >= 4 {
if rec, ok := s.fakeStore.sessions[s.id]; ok {
if rec, ok := s.sessions[s.id]; ok {
rec.Activity.State = domain.ActivityBlocked
s.fakeStore.sessions[s.id] = rec
s.sessions[s.id] = rec
}
}
return s.fakeStore.GetSession(ctx, id)
Expand Down
18 changes: 9 additions & 9 deletions backend/internal/sessionguard/guard.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,19 @@ type SessionReader interface {
type Outcome int

const (
// SuppressedUnknown: the pre-write session read failed, so the state is
// unknown and the guard failed closed. Deliberately the zero value — a
// forgotten assignment must never read as a successful send.
// SuppressedUnknown is returned when the pre-write session read failed, so
// the state is unknown and the guard failed closed. Deliberately the zero
// value — a forgotten assignment must never read as a successful send.
SuppressedUnknown Outcome = iota
// Sent: the message was written to the session's pane (a messenger failure
// surfaces as Sent plus a non-nil error: the write was attempted).
// Sent means the message was written to the session's pane (a messenger
// failure surfaces as Sent plus a non-nil error: the write was attempted).
Sent
// SuppressedNotFound: no session row exists for the id.
// SuppressedNotFound means no session row exists for the id.
SuppressedNotFound
// SuppressedTerminated: the session is terminated; its pane is gone or
// about to be reaped.
// SuppressedTerminated means the session is terminated; its pane is gone
// or about to be reaped.
SuppressedTerminated
// SuppressedAwaitingUser: the session awaits the human — blocked on a
// SuppressedAwaitingUser means the session awaits the human — blocked on a
// permission decision (Deliver and Nudge), or waiting at the prompt for
// the next instruction (Nudge only).
SuppressedAwaitingUser
Expand Down
Loading