Files
kandev/apps/backend/pkg/websocket/message.go
T
João Salavisa 2e108b040f refactor: session resumption cleanup and AgentInstance → AgentExecution rename (#23)
* feat: session resumption with ACP session ID support

- Add useSessionResumption hook for automatic session recovery on page reload
- Fix ACPSessionID not being set on AgentInstance in Launch() and createInstance()
- Add task.session.status and task.session.resume WebSocket actions
- Fix shell terminal retry logic for resumed sessions
- Fix taskSessionState reading from Zustand store instead of local state
- Remove dead code: unused stream handling functions in lifecycle/manager.go
- Fix test mock interfaces (CleanupStaleInstanceByTaskID, NewManager signature)
- Add enhanced logging for session resumption debugging
- Simplify page-client.tsx state merging logic

* refactor: remove unused AgentController (dead code)

- Delete agent_controller.go - entirely unused duplicate of Manager methods
- Remove agentController field from Manager struct
- Remove AgentController initialization in NewManager

The AgentController duplicated StartAgentProcess, initializeACPSession, and
PromptAgent from Manager but was never called. All agent control operations
go through Manager's own methods.

* refactor: remove unused MemoryRepository (dead code)

- Delete memory.go - entire MemoryRepository implementation unused
  (replaced by SQLite repository)
- Delete memory_test.go - tests for removed MemoryRepository

Found via 'deadcode' tool analysis.

* refactor: consolidate agentctl packages into server/client structure

Reorganize the confusing dual agentctl packages:
- internal/agentctl/* → internal/agentctl/server/
- internal/agent/agentctl/* → internal/agentctl/client/

Structure is now:
  internal/agentctl/
  ├── client/      - HTTP client for talking to agentctl servers
  │   ├── client.go
  │   ├── standalone.go
  │   ├── acp.go
  │   └── launcher/
  ├── server/      - agentctl server implementation (runs in containers)
  │   ├── api/
  │   ├── adapter/
  │   ├── acp/
  │   ├── config/
  │   ├── instance/
  │   ├── process/
  │   └── shell/
  └── types/       - Shared types used by both client and server

Also consolidated shared types (GitStatusUpdate, FileInfo, etc.) from
process/types.go into the shared types/ package.

* refactor: rename AgentInstance to AgentExecution for clarity

Rename AgentInstance -> AgentExecution across the entire codebase to better
reflect what it represents: an agent actively working on a task, not just
a generic 'instance'.

Changes:
- AgentInstance struct -> AgentExecution
- InstanceStore -> ExecutionStore (new file: execution_store.go)
- RecoveredInstance -> RecoveredExecution
- All method names: GetInstance* -> GetExecution*, ListInstances -> ListExecutions, etc.
- All parameters: instanceID -> executionID
- All local variables: instance -> execution
- DTOs and JSON tags: agent_instance_id -> agent_execution_id
- Database columns: agent_instance_id -> agent_execution_id

Also consolidates duplicate prompt logic:
- Merge sendInitialPrompt and SendPrompt into single SendPrompt method
- Add validateStatus parameter to differentiate initial vs follow-up prompts

Also extracts ACP session management to session.go:
- Move initializeACPSession logic to SessionManager.InitializeAndPrompt
- Move PromptAgent logic to SessionManager.SendPrompt
- Manager now delegates to SessionManager for all prompt operations

Also removes duplicate event publishing:
- Remove 9 duplicate m.publish*() methods from manager.go
- Use existing EventPublisher methods via m.eventPublisher.Publish*()

Also extracts types to dedicated file:
- Move AgentExecution, LaunchRequest, PromptResult, etc. to types.go

Net reduction: ~600 lines removed from manager.go through deduplication
and better separation of concerns.

* fix(backend): Docker runtime now creates instance via ControlServer

The Docker runtime was incorrectly creating an agentctl.Client pointing
directly to port 9999 (the ControlServer port), then trying to call
instance-specific APIs like /api/v1/agent/configure which only exist
on per-instance servers.

Fixed by making Docker runtime follow the same flow as standalone mode:
- LaunchContainer now uses StandaloneCtl to create an instance via the
  control API, then creates Client pointing to the instance port
- RecoverInstances queries the ControlServer to get the instance port
- Added waitForHealth helper to wait for agentctl to be ready

Also improved HTTP error handling in agentctl client:
- All client methods now check HTTP status codes before JSON decoding
- Better error messages include status code and response body

* fix: restore session resumption imports and variables

* fix: add sessionId prop back to TaskLayout
2026-01-16 22:14:19 +00:00

131 lines
3.4 KiB
Go

// Package websocket provides WebSocket message types and protocol definitions.
package websocket
import (
"encoding/json"
"time"
)
// MessageType represents the type of WebSocket message
type MessageType string
const (
MessageTypeRequest MessageType = "request"
MessageTypeResponse MessageType = "response"
MessageTypeNotification MessageType = "notification"
MessageTypeError MessageType = "error"
)
// Message is the base envelope for all WebSocket messages
type Message struct {
ID string `json:"id,omitempty"`
Type MessageType `json:"type"`
Action string `json:"action"`
Payload json.RawMessage `json:"payload"`
Timestamp time.Time `json:"timestamp"`
}
// Request represents a client request message
type Request struct {
ID string `json:"id"`
Type MessageType `json:"type"`
Action string `json:"action"`
Payload json.RawMessage `json:"payload"`
}
// Response represents a server response message
type Response struct {
ID string `json:"id"`
Type MessageType `json:"type"`
Action string `json:"action"`
Payload json.RawMessage `json:"payload,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
// Notification represents a server push notification
type Notification struct {
Type MessageType `json:"type"`
Action string `json:"action"`
Payload json.RawMessage `json:"payload"`
Timestamp time.Time `json:"timestamp"`
}
// ErrorPayload represents an error response payload
type ErrorPayload struct {
Code string `json:"code"`
Message string `json:"message"`
Details map[string]interface{} `json:"details,omitempty"`
}
// NewRequest creates a new request message
func NewRequest(id, action string, payload interface{}) (*Message, error) {
data, err := json.Marshal(payload)
if err != nil {
return nil, err
}
return &Message{
ID: id,
Type: MessageTypeRequest,
Action: action,
Payload: data,
Timestamp: time.Now().UTC(),
}, nil
}
// NewResponse creates a new response message
func NewResponse(id, action string, payload interface{}) (*Message, error) {
data, err := json.Marshal(payload)
if err != nil {
return nil, err
}
return &Message{
ID: id,
Type: MessageTypeResponse,
Action: action,
Payload: data,
Timestamp: time.Now().UTC(),
}, nil
}
// NewNotification creates a new notification message
func NewNotification(action string, payload interface{}) (*Message, error) {
data, err := json.Marshal(payload)
if err != nil {
return nil, err
}
return &Message{
Type: MessageTypeNotification,
Action: action,
Payload: data,
Timestamp: time.Now().UTC(),
}, nil
}
// NewError creates a new error response message
func NewError(id, action, code, message string, details map[string]interface{}) (*Message, error) {
payload := ErrorPayload{
Code: code,
Message: message,
Details: details,
}
data, err := json.Marshal(payload)
if err != nil {
return nil, err
}
return &Message{
ID: id,
Type: MessageTypeError,
Action: action,
Payload: data,
Timestamp: time.Now().UTC(),
}, nil
}
// ParsePayload parses the payload into the given struct
func (m *Message) ParsePayload(v interface{}) error {
if m.Payload == nil {
return nil
}
return json.Unmarshal(m.Payload, v)
}