refactor(agent): consolidate Agent model - Phase 1 complete#1894
refactor(agent): consolidate Agent model - Phase 1 complete#1894
Conversation
* feat(agent): steering * fix loop * fix lint * fix lint
- Replace duplicate types (ToolResult/Session/Message) with real project types - Implement ephemeralSessionStore satisfying session.SessionStore interface - Connect runTurn to real AgentLoop via runAgentLoop + AgentInstance - Fix subturn_test.go to match updated signatures and types Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
…and safe compression Separate context_window from max_tokens — they serve different purposes (input capacity vs output generation limit). The previous conflation caused premature summarization or missed compression triggers. Changes: - Add context_window field to AgentDefaults config (default: 4x max_tokens) - Extract boundary-safe truncation helpers (isSafeBoundary, findSafeBoundary) into context_budget.go — pure functions with no AgentLoop dependency - forceCompression: align split to safe boundary so tool-call sequences (assistant+ToolCalls → tool results) are never torn apart - summarizeSession: use findSafeBoundary instead of hardcoded keep-last-4 - estimateTokens: count ToolCalls arguments and ToolCallID metadata, not just Content — fixes systematic undercounting in tool-heavy sessions - Add proactive context budget check before LLM call in runAgentLoop, preventing 400 context-length errors instead of reacting to them - Add estimateToolDefsTokens for tool definition token cost Closes #556, closes #665 Ref #1439
Session history (GetHistory) contains only user/assistant/tool messages. The system prompt is built dynamically by BuildMessages and is never stored in session. The previous code incorrectly treated history[0] as a system prompt, skipping the first user message and appending a compression note to it. Fix: operate on the full history slice, and record the compression note in the session summary (which BuildMessages already injects into the system prompt) rather than modifying any history message.
estimateMessageTokens now counts ReasoningContent (extended thinking / chain-of-thought) which can be substantial and is persisted in session history. Media items get a fixed per-item overhead (256 tokens) since actual cost depends on provider-specific image tokenization.
Add context_window to config.example.json, the web configuration page (form model, input field, save handler), and i18n strings (en/zh). The field is optional — leaving it empty falls back to the 4x max_tokens heuristic.
Add tests that reflect actual session data shape: history starts with user messages (no system prompt), includes chained tool-call sequences, reasoning content, and media items. Exercises the proactive budget check path with BuildMessages-style assembled messages.
Fixes prealloc lint warning by using make() with capacity hint.
Introduce parseTurnBoundaries() which identifies each Turn start index in the session history. A Turn is a complete "user input → LLM iterations → final response" cycle (as defined in the agent refactor design #1316). findSafeBoundary now uses Turn boundaries instead of raw role-scanning, making the intent explicit: "find the nearest Turn boundary." forceCompression drops the oldest half of Turns (not arbitrary messages), which is simpler and more intuitive. The Turn-based approach naturally prevents splitting tool-call sequences since each Turn is atomic.
Two estimation bugs fixed: 1. Media tokens were added to the chars accumulator before the chars*2/5 conversion, resulting in 256*2/5=102 tokens per item instead of 256. Fix: add media tokens directly to the final token count, bypassing the character-based heuristic. 2. estimateMessageTokens counted both tc.Name and tc.Function.Name for tool calls, but providers only send one (OpenAI-compat uses function.name, Anthropic uses tc.Name). Fix: count tc.Function.Name when Function is present, fall back to tc.Name only otherwise. Also fix i18n hint text: "auto-detect" was misleading — the backend uses a 4x max_tokens heuristic, not actual model detection.
When the entire history is a single Turn (one user message followed by tool calls and responses, no subsequent user message), the only Turn boundary is at index 0. Previously the fallback returned targetIndex, which could land on a tool or assistant message — splitting the Turn. Return 0 instead, so callers (forceCompression, summarizeSession) see mid <= 0 and skip compression rather than cutting inside the Turn.
Session history only stores user/assistant/tool messages — the system prompt is built dynamically by BuildMessages. Remove the incorrect system message from TestAgentLoop_ContextExhaustionRetry test data to match the real data model that forceCompression operates on.
Document the semantic boundaries of context management as called for in the agent-refactor README (suggested document split, item 5): - context window region definitions and history budget formula - ContextWindow vs MaxTokens distinction - session history contents (no system prompt stored) - Turn as the atomic compression unit (#1316) - three compression paths and their ordering - token estimation approach and its limitations - interface boundaries between budget functions and BuildMessages Also documents known gaps: summarization trigger not using the full budget formula, heuristic-only token estimation, and reactive retry not preserving media references. Ref #1439
- Add subTurnResults sync.Map to AgentLoop for per-session channel tracking - Add register/unregister/dequeue methods in steering.go - Poll SubTurn results in runLLMIteration at loop start and after each tool, injecting results as [SubTurn Result] messages into parent conversation - Initialize root turnState in runAgentLoop, propagate via context (withTurnState/turnStateFromContext), call rootTS.Finish() on completion - Wire Spawn Tool to spawnSubTurn via SetSpawner in registerSharedTools, recovering parentTS from context for proper turn hierarchy - Refactor subagent.go to use SetSpawner pattern - Add TestSubTurnResultChannelRegistration and TestDequeuePendingSubTurnResults
- Add maxConcurrentSubTurns constant (5) and concurrencySem channel to turnState - Acquire/release semaphore in spawnSubTurn to limit concurrent child turns per parent - Add activeTurnStates sync.Map to AgentLoop for tracking root turn states by session - Implement HardAbort(sessionKey) method to trigger cascading cancellation via turnState.Finish() - Register/unregister root turnState in runAgentLoop for hard abort lookup - Add TestSubTurnConcurrencySemaphore to verify semaphore capacity enforcement - Add TestHardAbortCascading to verify context cancellation propagates to child turns
- Add initialHistoryLength field to turnState to snapshot session state at turn start - Save initial history length in runAgentLoop when creating root turnState - Implement session rollback in HardAbort via SetHistory, truncating to initial length - Add TestHardAbortSessionRollback to verify history rollback after abort - Import providers package in subturn_test.go for Message type This ensures that when a user triggers hard abort, all messages added during the aborted turn are discarded, restoring the session to its pre-turn state.
…bTurn - Fix turnState hierarchy corruption when SubTurns recursively call runAgentLoop by checking context for existing turnState before creating new root - Fix deadlock risk in deliverSubTurnResult by separating lock and channel operations - Fix session rollback race in HardAbort by calling Finish() before rollback - Fix resource leak by closing pendingResults channel in Finish() with panic recovery - Add thread-safety documentation for childTurnIDs and isFinished fields - Move globalTurnCounter to AgentLoop.subTurnCounter to prevent ID conflicts - Improve semaphore acquisition to ensure release even on early validation failures - Document design choice: ephemeral sessions start empty for complete isolation - Add 5 new tests: hierarchy, deadlock, order, channel close, and semaphore
Critical fixes (5): - Fix turnState hierarchy corruption in nested SubTurns by checking context before creating new root turnState in runAgentLoop - Fix deadlock risk in deliverSubTurnResult by separating lock and channel ops - Fix session rollback race in HardAbort by calling Finish() before rollback - Fix resource leak by closing pendingResults channel in Finish() with recovery - Add thread-safety docs for childTurnIDs and isFinished fields Medium priority fixes (5): - Move globalTurnCounter to AgentLoop.subTurnCounter to prevent ID conflicts - Improve semaphore acquisition to ensure release even on early validation failures - Document design choice: ephemeral sessions start empty for complete isolation - Add final poll before Finish() to capture late-arriving SubTurn results - Remove duplicate channel registration in spawnSubTurn to fix timing issues Testing: - Add 6 new tests covering hierarchy, deadlock, ordering, channel lifecycle, final poll, and semaphore behavior - All 12 SubTurn tests passing with race detector This resolves 10 critical and medium issues (5 race conditions, 2 resource leaks, 3 timing issues) identified in code review, bringing SubTurn to production-ready state.
- Fix synchronous SubTurn calls placing results in pendingResults channel, causing double delivery. Now only async calls (Async=true) use the channel. - Move deliverSubTurnResult into defer to ensure result delivery even when runTurn panics. Add TestSpawnSubTurn_PanicRecovery to verify. - Fix ContextWindow incorrectly set to MaxTokens; now inherits from parentAgent.ContextWindow. - Add TestSpawnSubTurn_ResultDeliverySync to verify sync behavior.
When the entire session history is a single Turn (e.g. one user message followed by a massive tool response), findSafeBoundary returns 0 and forceCompression previously did nothing — leaving the agent stuck in a context-exceeded retry loop. Now falls back to keeping only the most recent user message when no safe Turn boundary exists. This breaks Turn atomicity as a last resort but guarantees the agent can recover. Also updates docs/agent-refactor/context.md to document this behavior. Ref #1490
Major improvements to SubTurn implementation: **Fixes:** - Channel close race condition (sync.Once) - Semaphore blocking timeout (30s) - Redundant context wrapping - Memory accumulation (auto-truncate at 50 msgs) - Channel draining on Finish() - Missing depth limit logging - Model validation **Enhancements:** - Comprehensive documentation (150+ lines) - 11 new tests covering edge cases - Improved error messages All tests pass. Production-ready. Related: #1316
…go file Created /pkg/agent/turn_state.go (246 lines) containing: - turnStateKeyType and context key management - turnState struct definition - TurnInfo struct and GetActiveTurn() method - newTurnState(), Finish(), and drainPendingResults() methods - ephemeralSessionStore implementation - All context helper functions (withTurnState, TurnStateFromContext, etc.) Updated /pkg/agent/subturn.go (428 lines) by: - Removing the moved turnState struct and methods - Removing unused imports (sync, session) - Keeping SubTurn spawning logic, config, events, and result delivery All tests pass and the code compiles successfully.
- Fix context cancellation check order in concurrency timeout - Add structured logging for panic recovery - Replace println with proper logger for channel full warning - Simplify tool registry initialization logic - Remove unused ErrConcurrencyLimitExceeded error
Includes JSONL session persistence (#1170), spawn_status tool, Azure provider, credential encryption, and various fixes. SubTurn features preserved and integrated with new spawn_status functionality.
finishes early - identified need for Critical+heartbeat+timeout mechanism.
…cycle Problem: When parent turn finishes early, all child SubTurns receive "context canceled" error,because child context was derived from parent context. Solution: Implement a lifecycle management system that distinguishes between: - Graceful finish (Finish(false)): signals parentEnded, children continue - Hard abort (Finish(true)): immediately cancels all children Changes: - turn_state.go: - Add parentEnded atomic.Bool to signal parent completion - Add parentTurnState reference for IsParentEnded() checks - Modify Finish(isHardAbort bool) to distinguish abort types - subturn.go: - Add Critical bool to SubTurnConfig (Critical SubTurns continue after parent ends) - Add Timeout time.Duration for SubTurn self-protection - Use independent context (context.Background()) instead of derived context - SubTurns check IsParentEnded() to decide whether to continue or exit - loop.go: - Call Finish(false) for normal completion (graceful) - Add IsParentEnded() check in LLM iteration loop - steering.go: - HardAbort calls Finish(true) to immediately cancel children Behavior: - Normal finish: parentEnded=true, children continue, orphan results delivered - Hard abort: all children cancelled immediately via context - Critical SubTurns: continue running after parent finishes gracefully - Non-Critical SubTurns: can exit gracefully when IsParentEnded() returns true
refactor(agent): context boundary detection, proactive budget check, and safe compression
Problem: During subturn context limit or truncation recoveries, the recovery loops repeatedly called `runAgentLoop` with the same or modified `UserMessage`. Because `runAgentLoop` unconditionally adds the `UserMessage` to the session history, this resulted in: 1. Duplicate User Messages polluting the history upon `context_length_exceeded` retries. 2. The possibility of injecting empty User Messages if `opts.UserMessage` was artificially blanked out to work around the duplication. 3. Messy or duplicate entries during `finish_reason="truncated"` recovery injections. Solution: - Introduce `SkipAddUserMessage` boolean to `processOptions` to explicitly control whether the agent loop should write the user prompt to history. - Add an explicit `opts.UserMessage != ""` check in `runAgentLoop` to prevent polluting history with empty message content. - In `subturn.go`'s recovery loop, set `SkipAddUserMessage: contextRetryCount > 0` to skip writing the user message on context
fix(agent) scope steering
Feat/hook manager
…bus exit - spawnSubTurn: set result=nil on panic instead of constructing a non-nil ToolResult - HardAbort: roll back session history to initialHistoryLength after Finish() - drainBusToSteering: switch to non-blocking reads after first message so function returns promptly when the inbound channel is empty - remove obsolete documentation files
feat(agent): subturn
Two gateway tests were flaky due to race conditions: - TestGatewayStatusReturnsRestartingDuringRestartGap - TestGatewayRestartReturnsErrorStatusWhenReplacementFailsToStart The handleGatewayStatus function calls getGatewayHealth which can override the test's expected status. By mocking gatewayHealthGet to return an error, the tests now reliably verify the expected status values. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Consolidates Phase 1 of the Agent refactor into main, introducing a structured Agent definition format, an event-driven hook system (in-process + stdio JSON-RPC), and expanded observability/interruptibility primitives (steering, active turn tracking, SubTurn).
Changes:
- Add hooks configuration + auto-mounting (builtins + external process hooks) with defaults and tests.
- Introduce turn state tracking / steering improvements and new
/subagentscommand for runtime observability. - Update Agent definition loading to support
AGENT.mdYAML frontmatter with legacy fallback, plus new context budget utilities/docs.
Reviewed changes
Copilot reviewed 43 out of 64 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/config/config_test.go | Adds default + load tests for new hooks config surface. |
| pkg/config/config.go | Introduces HooksConfig, SubTurn defaults, steering mode, and context window settings. |
| pkg/commands/runtime.go | Extends runtime interface to expose active turn info to commands. |
| pkg/commands/cmd_subagents.go | Adds /subagents command for viewing active subagent activity. |
| pkg/commands/builtin.go | Registers the new subagents built-in command. |
| pkg/agent/turn.go | Adds active turn tracking + SubTurn-related state helpers, finishing/abort behavior. |
| pkg/agent/steering.go | Implements scoped steering queues, Continue(), and hard abort behavior. |
| pkg/agent/loop_test.go | Updates context exhaustion test assumptions re: session history composition. |
| pkg/agent/instance.go | Separates ContextWindow from MaxTokens with a default heuristic. |
| pkg/agent/hooks_test.go | Adds tests for in-process hook observation and interception. |
| pkg/agent/hook_process_test.go | Adds integration tests for stdio JSON-RPC process hooks. |
| pkg/agent/hook_process.go | Implements stdio JSON-RPC process hook client and mounting. |
| pkg/agent/hook_mount_test.go | Adds tests for config-driven auto-mounting of builtin/process hooks. |
| pkg/agent/hook_mount.go | Adds hook registry + config-driven auto-mount + env/observe validation helpers. |
| pkg/agent/events.go | Defines structured event kinds + payload types for observability. |
| pkg/agent/eventbus.go | Adds lightweight fan-out event bus with dropped-event metrics. |
| pkg/agent/definition_test.go | Adds tests for AGENT.md frontmatter parsing and legacy fallback behavior. |
| pkg/agent/definition.go | Implements AGENT.md YAML frontmatter parsing + tracked paths for cache invalidation. |
| pkg/agent/context_cache_test.go | Updates cache tests to use AGENT.md instead of IDENTITY.md. |
| pkg/agent/context_budget.go | Adds context budget estimation + safe turn-boundary compression helpers. |
| pkg/agent/context.go | Updates bootstrap loading + cache invalidation to follow new agent definition model. |
| docs/subturn.md | Documents SubTurn semantics, lifecycle, events, and operational constraints. |
| docs/steering.md | Documents scoped steering queues, Continue(), and interrupt behavior. |
| docs/design/steering-spec.md | Design spec for steering queue, bus drain, and loop integration. |
| docs/design/hook-system-design.zh.md | Chinese design doc for hook system layering and IPC strategy. |
| docs/agent-refactor/context.md | Documents context budgeting, compression paths, and token estimation model. |
| config/config.example.json | Adds context_window and hooks example configuration. |
| cmd/picoclaw/internal/onboard/helpers_test.go | Updates onboarding tests for structured agent bootstrap files. |
| README.zh.md | Updates README content but currently contains unresolved merge conflict markers. |
| README.vi.md | Updates README content but currently contains unresolved merge conflict markers. |
| README.pt-br.md | Updates README content but currently contains unresolved merge conflict markers. |
| README.fr.md | Updates README content but currently contains unresolved merge conflict markers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ts.mu.RLock() | ||
| children := append([]string(nil), ts.childTurnIDs...) | ||
| ts.mu.RUnlock() | ||
| for _, childID := range children { | ||
| if val, ok := ts.al.activeTurnStates.Load(childID); ok { | ||
| val.(*turnState).Finish(true) | ||
| } | ||
| } |
There was a problem hiding this comment.
activeTurnStates is keyed by sessionKey in registerActiveTurn, but hard-abort cascading here looks up children by childID (which appears to be a turn ID). This makes cascading abort unreliable (children won't be found, or the wrong entry could be finished). Align the keying strategy (e.g., store turnStates by turnID for all turns, or keep a separate map for turnID → turnState, or ensure childTurnIDs stores the same key used in activeTurnStates).
| ts.mu.RLock() | |
| children := append([]string(nil), ts.childTurnIDs...) | |
| ts.mu.RUnlock() | |
| for _, childID := range children { | |
| if val, ok := ts.al.activeTurnStates.Load(childID); ok { | |
| val.(*turnState).Finish(true) | |
| } | |
| } | |
| // We don't rely on the key type of activeTurnStates (which may be session keys); | |
| // instead, we find children by their parentTurnState pointer. | |
| ts.al.activeTurnStates.Range(func(_ any, value any) bool { | |
| childTS, ok := value.(*turnState) | |
| if !ok { | |
| return true | |
| } | |
| if childTS.parentTurnState == ts { | |
| childTS.Finish(true) | |
| } | |
| return true | |
| }) |
| // | ||
| // If no steering messages are pending, it returns an empty string. | ||
| func (al *AgentLoop) Continue(ctx context.Context, sessionKey, channel, chatID string) (string, error) { | ||
| if active := al.GetActiveTurn(); active != nil { |
There was a problem hiding this comment.
Continue() currently refuses to run if any active turn exists (since GetActiveTurn() returns the first active turn found). In the new architecture that supports multiple concurrent turns, this can incorrectly block Continue() for an idle session if another session is still active. Consider checking activity for the requested session scope (e.g., GetActiveTurnBySession(sessionKey) or getActiveTurnState(sessionKey)) instead of a global 'any active turn' check.
| if active := al.GetActiveTurn(); active != nil { | |
| if active := al.GetActiveTurnBySession(sessionKey); active != nil { |
| // TurnStateFromContext retrieves turnState from context (exported for tools) | ||
| func TurnStateFromContext(ctx context.Context) *turnState { | ||
| return turnStateFromContext(ctx) | ||
| } |
There was a problem hiding this comment.
This exported API returns an unexported concrete type (*turnState). That makes the API awkward for external packages and tightly couples them to internal implementation details. Prefer returning an exported interface (e.g., TurnState with only the safe methods) or an exported struct snapshot type, and keep turnState unexported.
| ListAgentIDs func() []string | ||
| ListDefinitions func() []Definition | ||
| GetEnabledChannels func() []string | ||
| GetActiveTurn func() any // Returning any to avoid circular dependency with agent package |
There was a problem hiding this comment.
Returning any here forces command handlers to use type assertions and makes the runtime contract ambiguous. A maintainable alternative is to define a small shared struct/interface in pkg/commands (or a tiny pkg/runtime/pkg/observer package) that both commands and agent can depend on without circular imports (e.g., type ActiveTurnView struct { ... } or type ActiveTurnProvider interface { ActiveTurnTree() (string, bool) }).
| done := make(chan error, 1) | ||
| go func() { | ||
| _, writeErr := ph.stdin.Write(body) | ||
| done <- writeErr | ||
| }() | ||
|
|
||
| select { | ||
| case err := <-done: | ||
| if err != nil { | ||
| return fmt.Errorf("write process hook %q message: %w", ph.name, err) | ||
| } | ||
| return nil | ||
| case <-ctx.Done(): | ||
| return ctx.Err() | ||
| } |
There was a problem hiding this comment.
Spawning a goroutine per outbound message can become costly under high event/intercept rates, and if ctx is canceled while stdin.Write blocks, the goroutine may linger until the write unblocks. Consider a single dedicated writer goroutine with a buffered channel (or reuse the existing writeMu and perform a direct write with bounded timeouts at higher call sites), so cancellation doesn't create unbounded goroutines.
| done := make(chan error, 1) | |
| go func() { | |
| _, writeErr := ph.stdin.Write(body) | |
| done <- writeErr | |
| }() | |
| select { | |
| case err := <-done: | |
| if err != nil { | |
| return fmt.Errorf("write process hook %q message: %w", ph.name, err) | |
| } | |
| return nil | |
| case <-ctx.Done(): | |
| return ctx.Err() | |
| } | |
| if _, err := ph.stdin.Write(body); err != nil { | |
| return fmt.Errorf("write process hook %q message: %w", ph.name, err) | |
| } | |
| return nil |
README.pt-br.md
Outdated
| | `picoclaw migrate` | Migrar dados de versões anteriores | | ||
| | `picoclaw auth login` | Autenticar com provedores | | ||
| | `picoclaw model` | Ver ou trocar o modelo padrão | | ||
| ======= |
There was a problem hiding this comment.
The file contains unresolved Git merge conflict markers (<<<<<<<, =======, >>>>>>>). Resolve the conflicts and remove all markers.
| ======= |
README.pt-br.md
Outdated
| | `picoclaw status` | Mostrar status | | ||
| | `picoclaw cron list` | Listar todas as tarefas agendadas | | ||
| | `picoclaw cron add ...` | Adicionar uma tarefa agendada | | ||
| >>>>>>> refactor/agent |
There was a problem hiding this comment.
The file contains unresolved Git merge conflict markers (<<<<<<<, =======, >>>>>>>). Resolve the conflicts and remove all markers.
| >>>>>>> refactor/agent |
README.fr.md
Outdated
|
|
||
| **Lisez `https://clawdchat.ai/skill.md` et suivez les instructions pour rejoindre [ClawdChat.ai](https://clawdchat.ai)** | ||
|
|
||
| <<<<<<< HEAD |
There was a problem hiding this comment.
The file contains unresolved Git merge conflict markers (<<<<<<<, =======, >>>>>>>). Resolve conflicts and remove all markers prior to merge.
| <<<<<<< HEAD |
README.fr.md
Outdated
| | `picoclaw migrate` | Migrer les données des anciennes versions | | ||
| | `picoclaw auth login` | S'authentifier auprès des fournisseurs | | ||
| | `picoclaw model` | Voir ou changer le modèle par défaut | | ||
| ======= |
There was a problem hiding this comment.
The file contains unresolved Git merge conflict markers (<<<<<<<, =======, >>>>>>>). Resolve conflicts and remove all markers prior to merge.
| ======= |
README.fr.md
Outdated
| | `picoclaw status` | Afficher le statut | | ||
| | `picoclaw cron list` | Lister toutes les tâches planifiées | | ||
| | `picoclaw cron add ...` | Ajouter une tâche planifiée | | ||
| >>>>>>> refactor/agent |
There was a problem hiding this comment.
The file contains unresolved Git merge conflict markers (<<<<<<<, =======, >>>>>>>). Resolve conflicts and remove all markers prior to merge.
| >>>>>>> refactor/agent |
Use main branch versions which have complete content.
The model command was missing from the README CLI Reference table.
- Add `picoclaw model` command to English README - Add `picoclaw model` command to Indonesian README All other translations already had the command.
Summary
This PR consolidates the Agent refactor from the
refactor/agentbranch intomain. It represents the completion of Phase 1 of the Agent refactor tracked in #1216.Core Implementations
pkg/agent/definition.goAGENT.mdformat with YAML frontmatter, backward compatible withAGENTS.mdpkg/agent/loop.go,pkg/agent/turn.gopkg/agent/eventbus.go,pkg/agent/events.gopkg/agent/context.go,pkg/agent/context_budget.gopkg/agent/hooks.go,pkg/agent/hook_*.gopkg/agent/steering.gopkg/agent/subturn.goWorkspace Changes
AGENTS.md→AGENT.md(new format with YAML frontmatter)IDENTITY.mdremoved (consolidated intoAGENT.md)SOUL.md,USER.mdupdated for new formatDocumentation
docs/agent-refactor/README.mddocs/agent-refactor/context.mddocs/hooks/README.md,docs/hooks/README.zh.mddocs/steering.mddocs/subturn.mdMerged PRs
Additional Changes
mainintorefactor/agentto include recent updates:pkg/channels/weixin/)TestGatewayStatusReturnsRestartingDuringRestartGap,TestGatewayRestartReturnsErrorStatusWhenReplacementFailsToStart)Tracking
Resolves #1316
Refs #1216
Remaining Work (Phase 2)
The following tasks have code implemented but need specification documentation:
Test plan
🤖 Generated with Claude Code