Repository navigation
fix: network optimizations - #263
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
💤 Files with no reviewable changes (1)
WalkthroughThis PR extends network messaging with mentions, delivery metadata, subscriptions, channel fanout policy, task designation fan-out, and thread-to-task promotion. It also adds the related storage, daemon, CLI, web UI, config, and schema wiring. ChangesNetwork coordination and task fan-out platform
Estimated code review effort: 5 (Critical) | ~180 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant NetworkConversationsAPI
participant TaskManager
participant GlobalDB
participant NetworkTaskStatusObserver
Client->>NetworkConversationsAPI: POST promote-task
NetworkConversationsAPI->>TaskManager: create task from thread message
TaskManager->>GlobalDB: persist task
NetworkConversationsAPI->>GlobalDB: PutNetworkTaskThreadOrigin
NetworkConversationsAPI-->>Client: task + origin
TaskManager->>NetworkTaskStatusObserver: task event
NetworkTaskStatusObserver->>GlobalDB: ListNetworkTaskThreadOrigins
NetworkTaskStatusObserver->>GlobalDB: PutTaskDesignationRollup
NetworkTaskStatusObserver->>NetworkConversationsAPI: send thread status / rollup update
sequenceDiagram
participant DeliveryCoordinator
participant SubscriptionStore
participant GuidanceStore
participant Prompter
DeliveryCoordinator->>SubscriptionStore: resolve delivery mode
alt digest mode
DeliveryCoordinator->>DeliveryCoordinator: collect digest batch
DeliveryCoordinator->>Prompter: render digest prompt
else full mode
DeliveryCoordinator->>Prompter: render single prompt
end
DeliveryCoordinator->>GuidanceStore: markGuidanceDelivered
DeliveryCoordinator->>DeliveryCoordinator: allocate prompt cost
Possibly related PRs
Suggested labels: enhancement, area/network, area/tasks, area/storage, area/cli, area/web Suggested reviewers: compozy-core-maintainers 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
React Doctor found no issues. 🎉 Reviewed by React Doctor for commit |
There was a problem hiding this comment.
Actionable comments posted: 14
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
internal/daemon/composed_assembler_test.go (1)
495-526: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winWrap test body in
t.Run("Should ...").This test doesn't use the
t.Run("Should ...")subtest pattern required for Go tests in this repo.As per path instructions: "MUST use t.Run("Should...") pattern for ALL test cases".
♻️ Proposed refactor
func TestComposedAssemblerAssembleStartupLoadsNetworkResponseRegisterSection(t *testing.T) { t.Parallel() - - resolver := NewHarnessContextResolver(HarnessRuntimeSignals{}) - assembler := NewComposedAssembler( - WithSectionSelector(NewSectionSelector(resolver, nil)), - WithPromptSectionDescriptors(defaultStartupPromptSectionDescriptors(nil, nil, nil)...), - ) - - got := assembleStartupPrompt( - t, - assembler, - session.StartupPromptContext{ - SessionType: session.SessionTypeUser, - Channel: "builders", - }, - testPromptAgent("Base prompt."), - t.TempDir(), - ) - - networkSkill, err := skillbundled.LoadResource(bundledAghSkillName, bundledNetworkReference) - if err != nil { - t.Fatalf("LoadResource(%q, %q) error = %v", bundledAghSkillName, bundledNetworkReference, err) - } - if !strings.Contains(got, "# AGH Network Response Register") || - !strings.Contains(got, "Threads decide and discuss; actionable work is promoted to tasks") { - t.Fatalf("AssembleStartup() = %q, want compact network response register", got) - } - if strings.Contains(got, strings.TrimSpace(networkSkill)) { - t.Fatalf("AssembleStartup() loaded full network reference instead of compact register: %q", got) - } + t.Run("Should render compact network response register instead of full skill", func(t *testing.T) { + t.Parallel() + resolver := NewHarnessContextResolver(HarnessRuntimeSignals{}) + assembler := NewComposedAssembler( + WithSectionSelector(NewSectionSelector(resolver, nil)), + WithPromptSectionDescriptors(defaultStartupPromptSectionDescriptors(nil, nil, nil)...), + ) + + got := assembleStartupPrompt( + t, + assembler, + session.StartupPromptContext{ + SessionType: session.SessionTypeUser, + Channel: "builders", + }, + testPromptAgent("Base prompt."), + t.TempDir(), + ) + + networkSkill, err := skillbundled.LoadResource(bundledAghSkillName, bundledNetworkReference) + if err != nil { + t.Fatalf("LoadResource(%q, %q) error = %v", bundledAghSkillName, bundledNetworkReference, err) + } + if !strings.Contains(got, "# AGH Network Response Register") || + !strings.Contains(got, "Threads decide and discuss; actionable work is promoted to tasks") { + t.Fatalf("AssembleStartup() = %q, want compact network response register", got) + } + if strings.Contains(got, strings.TrimSpace(networkSkill)) { + t.Fatalf("AssembleStartup() loaded full network reference instead of compact register: %q", got) + } + }) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/daemon/composed_assembler_test.go` around lines 495 - 526, Wrap the body of TestComposedAssemblerAssembleStartupLoadsNetworkResponseRegisterSection in a t.Run("Should ...") subtest to match the repo’s required Go test pattern. Keep the existing assertions and setup inside the new subtest, and preserve the outer t.Parallel() behavior where appropriate by using the test name as the single subtest for the assembled startup prompt check.Source: Path instructions
internal/api/core/network_details.go (1)
1625-1649: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
cloneNetworkMessageEntrydrops theMentionsfield.The clone was updated to copy
SizeBytes(line 1646) but theMentionsfield added elsewhere in this PR is missing from the returned struct. This function backssummarizeNetworkMessageHistory'sconversation/presence-episode entries (lines 1442, 1455), so any message rendered through those paths will silently lose its mentions even thoughmessage.Mentionsis correctly used for participant tallying (which reads the original, uncloned entry).🐛 Proposed fix
func cloneNetworkMessageEntry(entry store.NetworkMessageEntry) store.NetworkMessageEntry { return store.NetworkMessageEntry{ MessageID: strings.TrimSpace(entry.MessageID), WorkspaceID: strings.TrimSpace(entry.WorkspaceID), SessionID: strings.TrimSpace(entry.SessionID), Channel: strings.TrimSpace(entry.Channel), Surface: strings.TrimSpace(entry.Surface), ThreadID: strings.TrimSpace(entry.ThreadID), DirectID: strings.TrimSpace(entry.DirectID), Direction: strings.TrimSpace(entry.Direction), PeerFrom: strings.TrimSpace(entry.PeerFrom), PeerTo: strings.TrimSpace(entry.PeerTo), + Mentions: cloneTrimmedStrings(entry.Mentions), Kind: strings.TrimSpace(entry.Kind), WorkID: strings.TrimSpace(entry.WorkID), ReplyTo: strings.TrimSpace(entry.ReplyTo), TraceID: strings.TrimSpace(entry.TraceID), CausationID: strings.TrimSpace(entry.CausationID), Intent: strings.TrimSpace(entry.Intent), Text: entry.Text, PreviewText: strings.TrimSpace(entry.PreviewText), Body: cloneRawMessage(entry.Body), SizeBytes: entry.SizeBytes, Timestamp: entry.Timestamp.UTC(), } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/api/core/network_details.go` around lines 1625 - 1649, cloneNetworkMessageEntry is missing the new Mentions field, so cloned network messages lose mention data even though the original entry has it. Update the returned store.NetworkMessageEntry in cloneNetworkMessageEntry to copy Mentions alongside the existing fields, preserving the value used by summarizeNetworkMessageHistory for conversation and presence-episode entries. Keep the clone behavior consistent with the rest of the struct by copying the field directly from entry.internal/store/types.go (1)
827-841: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRequire a coordinator peer for coordinator fanout.
Line 834 accepts
fanout_policy="coordinator"even whenCoordinatorPeerIDis blank, creating a channel policy that has no coordinator to activate.Proposed fix
- if err := ValidateNetworkFanoutPolicy(e.FanoutPolicy); err != nil { + policy := NormalizeNetworkFanoutPolicy(e.FanoutPolicy) + if err := ValidateNetworkFanoutPolicy(policy); err != nil { return err } - if strings.TrimSpace(e.CoordinatorPeerID) != "" { - if err := validateNetworkPeerID(e.CoordinatorPeerID, "network channel coordinator_peer_id"); err != nil { + coordinatorPeerID := strings.TrimSpace(e.CoordinatorPeerID) + if policy == NetworkFanoutPolicyCoordinator && coordinatorPeerID == "" { + return fmt.Errorf("store: network channel coordinator_peer_id is required for coordinator fanout") + } + if coordinatorPeerID != "" { + if err := validateNetworkPeerID(coordinatorPeerID, "network channel coordinator_peer_id"); err != nil { return err } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/store/types.go` around lines 827 - 841, `NetworkChannelEntry.Validate` currently allows `fanout_policy="coordinator"` without any `CoordinatorPeerID`, which leaves coordinator fanout without an active coordinator. Update the validation in `NetworkChannelEntry.Validate` to require a non-blank `CoordinatorPeerID` whenever `ValidateNetworkFanoutPolicy(e.FanoutPolicy)` resolves to coordinator; keep the existing `validateNetworkPeerID` check for non-empty IDs and return a clear validation error when the coordinator peer is missing.internal/store/globaldb/global_db.go (1)
1489-1509: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftDon’t change historical migration replay.
These changes alter migration 25/26 rebuild behavior by adding new columns/schema through old helpers. Since migrations 43–47 already append the new schema work, keep historical migration helpers byte-for-byte compatible with their original behavior and let only tail migrations add
fanout_policy,coordinator_peer_id, andmentions_json. As per coding guidelines,internal/store/**/*.go: “SQLite migration registries are append-only. Never insert, reorder, rename, renumber, or change an existing migration identity after it may have been applied.”Also applies to: 1566-1627
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/store/globaldb/global_db.go` around lines 1489 - 1509, The historical migration helper is being changed to include newer schema fields, which breaks replay compatibility for already-applied migrations. Revert the changes in the migration 25/26 rebuild path in globaldb helpers so they remain byte-for-byte consistent with their original behavior, and keep the new columns like fanout_policy, coordinator_peer_id, and mentions_json confined to the later tail migration code paths. Use the existing migration registry helpers in internal/store/globaldb/global_db.go and the later migration blocks as the only place where the new schema should be appended.Source: Coding guidelines
internal/network/delivery.go (1)
579-610: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winMove guidance persistence outside
c.mu.Line 594 performs store I/O while the delivery coordinator mutex is held, so a slow SQLite write can block unrelated queue operations, waiters, and delivery progress.
Proposed fix
- c.mu.Lock() - defer c.mu.Unlock() + c.mu.Lock() queue := c.queues[target] if queue == nil || queue.token != item.SessionToken { + c.mu.Unlock() return } state := c.guidance[target] state.replyDelivered = true state.loaded = true if lifecycleWorkID(item.Envelope) != "" { state.protocolDelivered = true } c.guidance[target] = state - if c.guidanceStore != nil { - if err := c.guidanceStore.PutNetworkDeliveryGuidanceState( + guidanceStore := c.guidanceStore + persisted := store.NetworkDeliveryGuidanceState{ + SessionID: target, + ReplyGuidanceDelivered: state.replyDelivered, + ProtocolGuidanceDelivered: state.protocolDelivered, + } + c.mu.Unlock() + + if guidanceStore != nil { + if err := guidanceStore.PutNetworkDeliveryGuidanceState( c.lifecycleCtx, - store.NetworkDeliveryGuidanceState{ - SessionID: target, - ReplyGuidanceDelivered: state.replyDelivered, - ProtocolGuidanceDelivered: state.protocolDelivered, - }, + persisted, ); err != nil && c.logger != nil {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/network/delivery.go` around lines 579 - 610, The guidance state persistence in delivery coordination is happening while c.mu is held, which can block unrelated queue and waiter activity during a slow store write. Refactor the logic in the delivery path around c.guidance, c.guidanceStore.PutNetworkDeliveryGuidanceState, and the c.logger.Warn handling so the mutex only protects in-memory state updates, then perform the persistence after unlocking using a copied snapshot of the guidance state and target/session values.
🟡 Minor comments (9)
web/src/routes/_app/settings/network.tsx-383-397 (1)
383-397: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAdd inline validation for Digest flush interval
web/src/routes/_app/settings/network.tsx:383-397This is the only new delivery field that skips the validation/error pattern used by the sibling inputs. It accepts any string until save, so invalid durations surface only as a generic save failure instead of inline feedback.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/routes/_app/settings/network.tsx` around lines 383 - 397, The Digest flush interval input in the network settings form is missing the same inline validation/error handling used by the sibling delivery fields. Update the Input and its surrounding state logic in the settings page component to validate the value as the user types, surface the field-specific error inline, and keep the existing save flow unchanged; use the same pattern already implemented for the other delivery interval inputs in this form.Source: Coding guidelines
internal/tools/builtin/tasks.go-539-551 (1)
539-551: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winReject empty fan-out designation payloads at the tool schema boundary.
designations: []and{"brief": ""}currently pass this schema, so tool callers can make avoidable mutating requests that only fail deeper in the task service.Proposed schema tightening
"designations":{ "type":"array", + "minItems":1, "items":{ "type":"object", "required":["brief"], "properties":{ - "brief":{"type":"string"}, + "brief":{"type":"string","minLength":1}, "metadata":{}, "idempotency_key":{"type":"string"} },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/tools/builtin/tasks.go` around lines 539 - 551, The task tool schema for designations is too permissive because the `designations` array in the schema built in `internal/tools/builtin/tasks.go` allows empty fan-out payloads and empty `brief` values. Tighten the `designations` item schema used by the task tool so callers cannot submit `designations: []` or a designation with an empty `brief`, and keep the change localized to the schema definition in the task tool setup around the `designations` property.web/src/systems/network/components/shell/channel-header.tsx-206-220 (1)
206-220: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winUnhandled rejection propagates from
updateChannel.mutateAsync.
mutateAsyncrejects on failure; thisonSubmitdoesn't catch it, so the rejection bubbles intoChannelPolicyDialog'shandleSubmit. See the companion comment inchannel-policy-dialog.tsxfor the root-cause fix.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/systems/network/components/shell/channel-header.tsx` around lines 206 - 220, The ChannelPolicyDialog onSubmit handler is letting the rejection from updateChannel.mutateAsync escape, which bubbles into the dialog submit flow. Update the onSubmit callback in channel-header.tsx to handle the async failure explicitly by catching the mutation error (or otherwise swallowing/forwarding it safely) around updateChannel.mutateAsync, so ChannelPolicyDialog’s handleSubmit does not receive an unhandled rejection.web/src/systems/network/components/shell/channel-policy-dialog.tsx-111-120 (1)
111-120: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winWrap
onSubmitin try/catch to avoid an unhandled rejection on failure.
onSubmitultimately callsmutateAsync, which rejects on error. SincehandleSubmitisn't awaited by React's formonSubmit, a failed submission becomes an unhandled promise rejection, andonOpenChange(false)is skipped by the throw rather than by an explicit decision.🛡️ Proposed fix
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => { event.preventDefault(); const payload: NetworkChannelUpdateRequest = { purpose: purpose.trim(), fanout_policy: policy, coordinator_peer_id: coordinatorPeerId.trim(), }; - await onSubmit(payload); - onOpenChange(false); + try { + await onSubmit(payload); + onOpenChange(false); + } catch { + // Failure is surfaced via toast in the mutation's onError handler; + // keep the dialog open so the user can retry. + } };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/systems/network/components/shell/channel-policy-dialog.tsx` around lines 111 - 120, Wrap the ChannelPolicyDialog submit flow in a try/catch so a failed async submit does not become an unhandled rejection. Update handleSubmit to call onSubmit(payload) inside try/catch, and only call onOpenChange(false) after a successful submit; in the catch, handle or surface the error explicitly and avoid closing the dialog on failure. Use the handleSubmit and onSubmit symbols in channel-policy-dialog.tsx to locate the change.web/src/systems/network/components/thread-overlay/thread-subscription-control.tsx-56-70 (1)
56-70: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winError state not surfaced — dropdown falls back to "Default" on fetch failure.
When
subscriptionsquery fails,currentbecomesnulland the label shows "Default" even if a subscription exists, while the trigger stays interactive. A user could then unintentionally reset/overwrite an existing subscription during a transient fetch error.🛠️ Suggested fix
- const isBusy = upsert.isPending || remove.isPending || subscriptions.isFetching; + const isBusy = upsert.isPending || remove.isPending || subscriptions.isFetching; + const hasError = subscriptions.isError; const label = modeLabel(current?.mode);and disable the trigger / show an indicator when
hasErroris true instead of silently defaulting to "Default".As per path instructions, "Handle all states — loading, error, and empty (never assume
dataexists)".Also applies to: 93-114
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/systems/network/components/thread-overlay/thread-subscription-control.tsx` around lines 56 - 70, The thread subscription dropdown is treating a failed `useQuery(networkSubscriptionsOptions(...))` the same as an empty result, so `current` becomes null and the control silently falls back to “Default”. Update `thread-subscription-control.tsx` in the `subscriptions`/`current`/`label` flow to explicitly detect the query error state from `useQuery`, surface an error indicator or message, and disable the trigger while `hasError` is true so the user cannot overwrite an existing subscription during a transient failure. Also handle loading and empty states separately without assuming `subscriptions.data` exists, and apply the same state handling to the related trigger/overlay logic around the referenced subscription controls.Source: Path instructions
internal/api/spec/spec.go-1630-1649 (1)
1630-1649: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winAdd the
limitquery parameter tolistNetworkSubscriptions.
parsePositiveIntQueryreadslimit, so this endpoint’s spec is missing a supported pagination/filter parameter and generated clients won’t expose it.Suggested fix
Parameters: []ParameterSpec{ pathParam("workspace_id", "Workspace id"), pathParam("channel", "Network channel"), queryParam("peer_id", "Filter subscriptions by peer id", false), queryParam("thread_id", "Filter subscriptions by thread id", false), + intQueryParam("limit", "Maximum number of subscriptions to return"), },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/api/spec/spec.go` around lines 1630 - 1649, The listNetworkSubscriptions endpoint spec is missing the supported limit query parameter, so add it alongside peer_id and thread_id in the Parameters list for the network subscriptions operation. Use the existing queryParam helper in spec.go so generated clients expose the same pagination/filter input that parsePositiveIntQuery already accepts, keeping the spec aligned with the handler and contract.internal/api/core/network_conversations.go-891-899 (1)
891-899: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winOmit empty coordination-cost payloads.
CoordinationCostis optional, but this converter always returns a non-nil pointer; zero-cost threads serialize ascoordination_cost: {}instead of omitting the field.Proposed fix
func NetworkCoordinationCostPayloadFromStore( thread store.NetworkThreadSummary, ) *contract.NetworkCoordinationCostPayload { + if thread.DeliveredCount == 0 && + thread.PromptSizeBytes == 0 && + thread.EstimatedPromptTokens == 0 { + return nil + } return &contract.NetworkCoordinationCostPayload{ DeliveredCount: thread.DeliveredCount, PromptSizeBytes: thread.PromptSizeBytes, EstimatedPromptTokens: thread.EstimatedPromptTokens, } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/api/core/network_conversations.go` around lines 891 - 899, NetworkCoordinationCostPayloadFromStore always returns a non-nil payload, which causes empty coordination_cost values to be serialized instead of omitted. Update NetworkCoordinationCostPayloadFromStore to detect an all-zero NetworkThreadSummary cost and return nil in that case, and only build a contract.NetworkCoordinationCostPayload when at least one of DeliveredCount, PromptSizeBytes, or EstimatedPromptTokens is set.internal/api/core/network_details.go-130-133 (1)
130-133: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAlign fanout validation between create and update.
createNetworkChannelFanoutFieldsroutesstore.ValidateNetworkFanoutPolicyfailures throughStatusForNetworkError, but that error is not mapped there, so invalid fanout policy can return 500 on create while update returns 400.NetworkChannelEntry.Validate()only checks thatCoordinatorPeerIDis a valid peer ID; add the policy-specific coordinator requirement/forbidden rule in one place if that coupling is intended.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/api/core/network_details.go` around lines 130 - 133, The fanout validation path in create is inconsistent with update because `createNetworkChannelFanoutFields` and `NetworkChannelEntry.Validate` do not enforce the same policy-specific rules. Update the fanout handling around `createNetworkChannelFanoutFields` so invalid `store.ValidateNetworkFanoutPolicy` input is mapped to the same client-facing error behavior as update, and add the coordinator requirement/forbidden check in `NetworkChannelEntry.Validate` if that policy coupling belongs there. Keep the logic aligned with the existing `createNetworkChannelFanoutFields`, `NetworkChannelEntry.Validate`, and `StatusForNetworkError` flow so create and update reject the same invalid fanout requests.internal/store/globaldb/global_db_network_conversation_repository_test.go-243-337 (1)
243-337: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAdd the required test-placement metadata.
Before this new case, name the invariant, owning layer, and canonical suite so the test’s placement remains intentional. As per path instructions,
**/*_test.go: “Test placement is mandatory before test creation. Before adding, moving, or expanding any test, name the invariant, owning layer, and canonical suite.”Proposed addition
+ // Invariant: delivered prompt-cost updates aggregate by thread peer. + // Owning layer: globaldb network conversation repository. + // Canonical suite: TestGlobalDBWriteConversationMessageThreadSummaries. t.Run("Should aggregate delivered prompt cost by thread peer", func(t *testing.T) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/store/globaldb/global_db_network_conversation_repository_test.go` around lines 243 - 337, The new test case in globalDB network conversation repository needs the required test-placement metadata before the t.Run block. Add the invariant, owning layer, and canonical suite near the existing test around ListNetworkThreadPeerTokenStats and GetThread so the placement is intentional and traceable. Use the identifiers openTestGlobalDB, UpdateNetworkThreadPeerTokenStats, ListNetworkThreadPeerTokenStats, and GetThread to anchor the metadata to this test scope.Source: Path instructions
🧹 Nitpick comments (11)
internal/config/config.go (1)
459-472: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider upper bounds for the new byte-size fields.
MaxPayloadvalidates both a lower and upper bound (<= 0and> 1<<31-1), butResponseGuidanceMaxBytesandDeliveryStructuredBodyMaxBytesonly check positivity. Not blocking, but for consistency with the existing pattern you may want to cap these too.Also applies to: 1742-1762
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/config/config.go` around lines 459 - 472, Add upper-bound validation for the new byte-size settings in NetworkConfig alongside the existing MaxPayload checks. Update the validation logic that handles ResponseGuidanceMaxBytes and DeliveryStructuredBodyMaxBytes so they also reject values above the same practical maximum used for other size fields, matching the pattern in the config validation code and any related defaults/tests around those symbols.web/src/hooks/routes/use-task-detail-orchestration-tab.ts (1)
117-133: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winToast message doesn't pluralize correctly for a single run.
${result.runs.length} designated runs queued.always uses plural "runs", producing "1 designated runs queued." when only one designation is fanned out.✏️ Proposed fix
- toast.success(`${result.runs.length} designated runs queued.`); + const count = result.runs.length; + toast.success(`${count} designated run${count === 1 ? "" : "s"} queued.`);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/hooks/routes/use-task-detail-orchestration-tab.ts` around lines 117 - 133, The success toast in handleFanOutRuns always uses the plural “runs,” so update the message to handle singular vs plural based on result.runs.length. Keep the logic in use-task-detail-orchestration-tab’s handleFanOutRuns callback, and build the toast text from the count so it says “1 designated run queued.” for one item and “N designated runs queued.” otherwise.internal/api/core/tasks.go (1)
2119-2134: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReusing tool-invoke-status constants for an unrelated rollup schema.
toolInvokeStatusCompleted/toolInvokeStatusCanceled(defined ininternal/api/core/tools.gofor tool invocation status) are used as JSON map keys for the task designation rollup. It works today because the string values happen to coincide with"completed"/"canceled", but it couples two unrelated concerns and will silently drift if either constant's value ever changes for its original purpose. Define dedicated constants (or use literal strings, consistent with"failed","running", etc. in the same map) for the rollup schema.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/api/core/tasks.go` around lines 2119 - 2134, The task designation rollup in the JSON map is reusing tool-invoke status constants for unrelated keys, which tightly couples two separate schemas. Update the map construction in the rollup logic to use dedicated rollup key constants or plain literal strings for the completed and canceled fields, matching the other schema keys already present in this payload. Keep the change localized around the JSON encoding block so it no longer depends on toolInvokeStatusCompleted or toolInvokeStatusCanceled from tools.go.internal/observe/tasks.go (1)
1249-1268: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate designation-population logic across
observe/tasks.goandtask/live.go.
taskInboxRunSummaryhere andrunSummaryFromRunininternal/task/live.goboth build theRunSummaryand then separately overwriteDesignationGroupID/Designationviataskpkg.DesignationFromRun. Consider adding a small helper ontaskpkg.RunSummary(or a package-level function) that both call sites can share, so the derivation logic lives in one place.♻️ Proposed refactor (sketch)
+// in internal/task/designation.go +func ApplyDesignation(summary *RunSummary, run Run) { + if designation, ok := DesignationFromRun(run); ok { + summary.DesignationGroupID = designation.GroupID + summary.Designation = designation.Summary() + } +}Then both
taskInboxRunSummaryandrunSummaryFromRuncalltaskpkg.ApplyDesignation(summary, run)instead of repeating the conditional inline.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/observe/tasks.go` around lines 1249 - 1268, The designation-population logic is duplicated between taskInboxRunSummary and runSummaryFromRun, so extract the conditional taskpkg.DesignationFromRun handling into a shared helper on taskpkg.RunSummary or a package-level function. Update both call sites to build the base RunSummary and then invoke the shared helper (for example, ApplyDesignation) to set DesignationGroupID and Designation consistently in one place.internal/config/task_orchestration.go (1)
90-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the
5fan-out cap into a shared constant.The designated-run ceiling (
5) appears as the default, the validation upper bound here, and again as a hardcoded fallback ininternal/api/core/tasks.goandinternal/daemon/native_tools.go(per the provided downstream context). Centralizing it avoids drift if the cap ever changes.♻️ Proposed refactor
+const DefaultDesignatedRunMax = 5 + func DefaultTaskConfig() TaskConfig { return TaskConfig{ Orchestration: TaskOrchestrationConfig{ ... - DesignatedRunMax: 5, + DesignatedRunMax: DefaultDesignatedRunMax, ... func (c TaskOrchestrationConfig) Validate(path string) error { ... - if c.DesignatedRunMax <= 0 || c.DesignatedRunMax > 5 { + if c.DesignatedRunMax <= 0 || c.DesignatedRunMax > DefaultDesignatedRunMax { return fmt.Errorf("%s.designated_run_max must be between 1 and 5: %d", path, c.DesignatedRunMax) }Also applies to: 173-175
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/config/task_orchestration.go` at line 90, The designated-run fan-out cap is hardcoded as 5 in multiple places, so extract it into a shared constant and use it for the default, validation upper bound, and fallback paths. Update the configuration logic in task orchestration (including the `DesignatedRunMax` default/validation) and replace the duplicated fallback values in `internal/api/core/tasks.go` and `internal/daemon/native_tools.go` with the shared symbol so the cap stays consistent.internal/daemon/task_role_runtime.go (1)
590-618: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueExtra blank line when a designation is present.
taskRoleDesignationOverlayreturns a string with a trailing"\n", and the enclosing template also places%son its own template line ending in\n, so a present designation renders with a blank line after it in the prompt (Coordination channel:block). Not functionally wrong, but a minor prompt-formatting inconsistency.✏️ Optional cleanup
- return "Designated fan-out assignment: " + strings.Join(parts, ", ") + ".\n" + return "Designated fan-out assignment: " + strings.Join(parts, ", ") + "."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/daemon/task_role_runtime.go` around lines 590 - 618, The prompt formatting in taskRoleDesignationOverlay is adding an extra blank line when a designation exists because the helper returns a trailing newline and the surrounding fmt.Sprintf template already places the designation on its own line. Update taskRoleDesignationOverlay (and, if needed, the fmt.Sprintf block that uses designation) so the designation text is emitted without a double line break while preserving the rest of the taskRoleRuntime prompt formatting.internal/api/core/network_test.go (1)
1424-1509: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNew subtests skip
t.Parallel().Both new subtests ("Should update network channel policy", "Should upsert and list network subscriptions...") omit
t.Parallel()inside thet.Runclosure.As per coding guidelines,
**/*_test.go: "Uset.Run("Should ...")subtests in Go tests, witht.Parallelby default (opt-out witht.Setenv)".♻️ Suggested fix
t.Run("Should update network channel policy", func(t *testing.T) { + t.Parallel() updateResp := performRequest(t.Run("Should upsert and list network subscriptions with default channel metadata", func(t *testing.T) { + t.Parallel() upsertResp := performRequest(🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/api/core/network_test.go` around lines 1424 - 1509, The new network API subtests are missing the default parallelization used in Go tests. Add t.Parallel() at the start of each t.Run closure for the new cases in network_test.go, specifically the subtests around updating channel policy and upserting/listing subscriptions, unless they must opt out because they use t.Setenv or other shared state.Source: Coding guidelines
web/src/systems/network/components/shell/__tests__/channel-policy-dialog.test.tsx (1)
39-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider covering fanout-policy/coordinator selection changes.
Only the purpose-edit path is exercised; the new fanout policy and coordinator-peer selectors (including reset to "No coordinator") aren't tested.
As per coding guidelines,
web/**/*.{test,spec}.{js,ts,jsx,tsx}: "Write unit tests with high code coverage for critical paths".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/systems/network/components/shell/__tests__/channel-policy-dialog.test.tsx` around lines 39 - 68, The ChannelPolicyDialog test only covers editing the purpose field and misses the new fanout-policy and coordinator-peer selector behavior. Add coverage in ChannelPolicyDialog for changing fanout_policy and coordinator_peer_id, asserting onSubmit receives the updated values, and include a case that resets the coordinator selection back to “No coordinator” to verify the cleared state is submitted correctly. Use the existing render flow and userEvent interactions in this test alongside the current onSubmit and onOpenChange assertions.Source: Coding guidelines
internal/network/delivery_test.go (1)
271-323: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNew test doesn't use
t.Run("Should ...")subtests.
TestFormatNetworkMessageCapsStructuredBodyis a brand-new test but is written flat (no subtests), unlike the other new test in this file (TestDeliveryCoordinatorCoalescesDigestDeliveries) which properly wraps scenarios int.Run("Should ..."). As per path instructions, Go tests must uset.Run("Should...")subtests.♻️ Suggested wrap
func TestFormatNetworkMessageCapsStructuredBody(t *testing.T) { t.Parallel() - - envelope := Envelope{ - ... - } - ... + + t.Run("Should cap oversized structured trace body and mark truncation", func(t *testing.T) { + t.Parallel() + + envelope := Envelope{ + ... + } + ... + }) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/network/delivery_test.go` around lines 271 - 323, `TestFormatNetworkMessageCapsStructuredBody` should be wrapped in a `t.Run("Should ...")` subtest to match the test style used by `TestDeliveryCoordinatorCoalescesDigestDeliveries` and the repo’s Go test conventions. Keep the existing assertions and setup inside a single `t.Run` block, using a descriptive “Should…” name so the scenario is explicit and consistent with nearby tests.Source: Coding guidelines
internal/api/core/network_details.go (1)
792-794: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated participant-recording loop across three functions.
applyNetworkChannelMessages,summarizeNetworkMessageHistory, andsummarizeHistoricalParticipantCounteach independently repeat the same "record PeerFrom, PeerTo, then each Mentions entry" pattern. Consider extracting a shared helper (e.g.,recordMessageParticipants(recorder func(string), message store.NetworkMessageEntry)) to keep the three call sites in sync as the set of participant sources evolves.Also applies to: 1429-1431, 1466-1468
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/api/core/network_details.go` around lines 792 - 794, The participant-recording logic is duplicated in applyNetworkChannelMessages, summarizeNetworkMessageHistory, and summarizeHistoricalParticipantCount, so extract a shared helper (for example, a recordMessageParticipants-style function) that records PeerFrom, PeerTo, and each Mentions entry in one place. Update those three call sites to use the helper and keep the behavior identical so any future participant-source changes only need to be made once.web/src/systems/network/components/composer/composer.tsx (1)
77-97: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider a semantic role for the mentions container.
The
<div aria-label="Mention recipients">has no ARIA role, so some screen readers may not announce the label. Addingrole="group"(orrole="list"withrole="listitem"on eachPill) would make it reliably announced.♿ Optional fix
{state.mentions.length > 0 ? ( <div aria-label="Mention recipients" + role="group" className="flex min-h-5 flex-wrap items-center gap-1.5" data-testid={`network-composer-mentions-${testIdSuffix}`} >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/systems/network/components/composer/composer.tsx` around lines 77 - 97, The mentions container in composer.tsx uses aria-label without a semantic role, so screen readers may not announce it reliably. Update the container in the mentions render block to use a proper role such as group, or switch to list/listitem semantics if that better fits the Pills. Keep the existing aria-label and ensure the role is applied where state.mentions is rendered so the mention recipients section is announced consistently.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: dada6689-d3e8-4cae-b598-b229eb1ff259
⛔ Files ignored due to path filters (28)
internal/tools/builtin/testdata/native-tool-catalog.jsonis excluded by!**/*.jsonopenapi/agh.jsonis excluded by!**/*.jsonpackages/site/content/protocol/delivery.mdxis excluded by!**/*.mdxpackages/site/content/protocol/envelope.mdxis excluded by!**/*.mdxpackages/site/content/runtime/cli-reference/network/channels/meta.jsonis excluded by!**/*.jsonpackages/site/content/runtime/cli-reference/network/channels/update.mdxis excluded by!**/*.mdxpackages/site/content/runtime/cli-reference/network/digest-mode.mdxis excluded by!**/*.mdxpackages/site/content/runtime/cli-reference/network/meta.jsonis excluded by!**/*.jsonpackages/site/content/runtime/cli-reference/network/mute.mdxis excluded by!**/*.mdxpackages/site/content/runtime/cli-reference/network/send.mdxis excluded by!**/*.mdxpackages/site/content/runtime/cli-reference/network/subscribe.mdxis excluded by!**/*.mdxpackages/site/content/runtime/cli-reference/network/subscriptions.mdxis excluded by!**/*.mdxpackages/site/content/runtime/cli-reference/network/threads/meta.jsonis excluded by!**/*.jsonpackages/site/content/runtime/cli-reference/network/threads/promote.mdxis excluded by!**/*.mdxpackages/site/content/runtime/cli-reference/network/unmute.mdxis excluded by!**/*.mdxpackages/site/content/runtime/cli-reference/task/fan-out.mdxis excluded by!**/*.mdxpackages/site/content/runtime/cli-reference/task/meta.jsonis excluded by!**/*.jsonpackages/site/content/runtime/cli-reference/task/promote.mdxis excluded by!**/*.mdxpackages/site/content/runtime/core/network/channels-and-peers.mdxis excluded by!**/*.mdxpackages/site/content/runtime/core/network/delivery-and-safety.mdxis excluded by!**/*.mdxpackages/site/content/runtime/core/network/task-ingress.mdxis excluded by!**/*.mdxpackages/site/content/runtime/core/network/threads.mdxis excluded by!**/*.mdxpackages/site/content/runtime/guides/coordinate-agents-over-network.mdxis excluded by!**/*.mdxsdk/typescript/src/generated/contracts.tsis excluded by!**/generated/**skills/agh/references/native-tools.mdis excluded by!**/*.mdskills/agh/references/network.mdis excluded by!**/*.mdskills/agh/references/tasks-and-orchestration.mdis excluded by!**/*.mdweb/src/generated/agh-openapi.d.tsis excluded by!**/generated/**
📒 Files selected for processing (145)
internal/acp/types.gointernal/api/contract/contract.gointernal/api/contract/responses.gointernal/api/contract/settings.gointernal/api/contract/tasks.gointernal/api/core/agent_channels.gointernal/api/core/agent_channels_internal_test.gointernal/api/core/conversions.gointernal/api/core/interfaces.gointernal/api/core/memory.gointernal/api/core/network.gointernal/api/core/network_conversations.gointernal/api/core/network_details.gointernal/api/core/network_test.gointernal/api/core/prompt_stream.gointernal/api/core/settings.gointernal/api/core/settings_internal_test.gointernal/api/core/settings_test.gointernal/api/core/tasks.gointernal/api/core/tasks_test.gointernal/api/core/test_helpers_test.gointernal/api/core/tools.gointernal/api/httpapi/handlers_test.gointernal/api/httpapi/routes.gointernal/api/spec/spec.gointernal/api/testutil/network_stub.gointernal/api/udsapi/handlers_test.gointernal/api/udsapi/routes.gointernal/cli/agent_kernel.gointernal/cli/client.gointernal/cli/helpers_test.gointernal/cli/network.gointernal/cli/session.gointernal/cli/task.gointernal/config/config.gointernal/config/merge.gointernal/config/task_orchestration.gointernal/config/tool_surface.gointernal/daemon/boot.gointernal/daemon/composed_assembler_test.gointernal/daemon/daemon.gointernal/daemon/daemon_test.gointernal/daemon/harness_context.gointernal/daemon/native_create_tools.gointernal/daemon/native_tools.gointernal/daemon/network_response_register_prompt.gointernal/daemon/network_task_status_observer.gointernal/daemon/network_task_status_observer_test.gointernal/daemon/prompt_input_composite.gointernal/daemon/prompt_sections.gointernal/daemon/task_event_bridge_notifier.gointernal/daemon/task_role_runtime.gointernal/daemon/task_role_runtime_test.gointernal/daemon/task_runtime.gointernal/daemon/truncate_utf8.gointernal/network/audit.gointernal/network/delivery.gointernal/network/delivery_test.gointernal/network/envelope.gointernal/network/hooks_test.gointernal/network/manager.gointernal/network/manager_test.gointernal/network/router.gointernal/network/router_test.gointernal/network/transport_test.gointernal/network/validate.gointernal/observe/tasks.gointernal/situation/task_context.gointernal/store/globaldb/global_db.gointernal/store/globaldb/global_db_network_channels.gointernal/store/globaldb/global_db_network_channels_test.gointernal/store/globaldb/global_db_network_conversation_repository_test.gointernal/store/globaldb/global_db_network_conversations.gointernal/store/globaldb/global_db_network_messages.gointernal/store/globaldb/global_db_network_messages_test.gointernal/store/globaldb/global_db_network_preferences.gointernal/store/globaldb/global_db_network_token_stats.gointernal/store/globaldb/global_db_task.gointernal/store/globaldb/global_db_task_aux.gointernal/store/globaldb/global_db_task_claim.gointernal/store/globaldb/global_db_task_force.gointernal/store/globaldb/global_db_task_review.gointernal/store/globaldb/global_db_task_test.gointernal/store/globaldb/global_db_test.gointernal/store/globaldb/migrate_task_run_status.gointernal/store/network_conversation_types_test.gointernal/store/store.gointernal/store/types.gointernal/task/designation.gointernal/task/interfaces.gointernal/task/interfaces_integration_test.gointernal/task/live.gointernal/task/manager.gointernal/task/manager_test.gointernal/task/types.gointernal/tools/builtin/builtin_test.gointernal/tools/builtin/descriptors.gointernal/tools/builtin/extensions.gointernal/tools/builtin/network.gointernal/tools/builtin/tasks.gointernal/tools/builtin/toolsets.gointernal/tools/builtin_ids.goweb/src/hooks/routes/__tests__/use-settings-network-page.test.tsxweb/src/hooks/routes/use-task-detail-orchestration-tab.tsweb/src/routes/_app/settings/__tests__/-network.test.tsxweb/src/routes/_app/settings/network.tsxweb/src/routes/_app/tasks.$id.tsxweb/src/systems/network/adapters/network-api.tsweb/src/systems/network/components/composer/__tests__/channel-thread-composer.test.tsxweb/src/systems/network/components/composer/__tests__/composer.test.tsxweb/src/systems/network/components/composer/channel-thread-composer.tsxweb/src/systems/network/components/composer/composer.tsxweb/src/systems/network/components/composer/detail-composer.tsxweb/src/systems/network/components/composer/use-composer-state.tsweb/src/systems/network/components/shell/__tests__/channel-policy-dialog.test.tsxweb/src/systems/network/components/shell/channel-header.tsxweb/src/systems/network/components/shell/channel-policy-dialog.tsxweb/src/systems/network/components/thread-overlay/__tests__/thread-overlay-header.test.tsxweb/src/systems/network/components/thread-overlay/__tests__/thread-subscription-control.test.tsxweb/src/systems/network/components/thread-overlay/thread-overlay-header.tsxweb/src/systems/network/components/thread-overlay/thread-overlay.tsxweb/src/systems/network/components/thread-overlay/thread-subscription-control.tsxweb/src/systems/network/components/thread-overlay/use-thread-overlay-view.tsweb/src/systems/network/hooks/use-network-actions.tsweb/src/systems/network/index.tsweb/src/systems/network/lib/query-keys.tsweb/src/systems/network/lib/query-options.tsweb/src/systems/network/mocks/__tests__/network-mocks.test.tsweb/src/systems/network/mocks/fixtures.tsweb/src/systems/network/mocks/handlers.tsweb/src/systems/network/types.tsweb/src/systems/settings/mocks/fixtures.tsweb/src/systems/tasks/adapters/tasks-api.tsweb/src/systems/tasks/components/__tests__/tasks-detail-orchestration-panel.test.tsxweb/src/systems/tasks/components/__tests__/tasks-detail-runs-panel.test.tsxweb/src/systems/tasks/components/__tests__/tasks-fan-out-runs-card.test.tsxweb/src/systems/tasks/components/stories/task-overview-components.stories.tsxweb/src/systems/tasks/components/tasks-detail-orchestration-panel.tsxweb/src/systems/tasks/components/tasks-detail-runs-panel.tsxweb/src/systems/tasks/components/tasks-fan-out-runs-card.tsxweb/src/systems/tasks/hooks/use-task-actions.tsweb/src/systems/tasks/hooks/use-tasks-fan-out-runs-card.tsweb/src/systems/tasks/index.tsweb/src/systems/tasks/mocks/handlers.tsweb/src/systems/tasks/types.ts
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
internal/task/designation_test.go (1)
8-50: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winMissing test case for partial designation (group set, metadata unparsable).
Per
DesignationFromRun's documented behavior,ok=trueis returned wheneverDesignationGroupIDis non-empty — even whenMetadatafails to parse as JSON, in which caseIndex/Brieffall back to zero values. This meansApplyRunDesignationSummarywould silently overwrite a summary's existingDesignationwith an empty one in that scenario. Add a test case exercising a run with a non-emptyDesignationGroupIDbut missing/invalidMetadatato lock in the intended fallback behavior.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/task/designation_test.go` around lines 8 - 50, Add a test in TestApplyRunDesignationSummary that covers a run with a non-empty DesignationGroupID but missing or invalid Metadata, since DesignationFromRun returns ok=true in that case and ApplyRunDesignationSummary should preserve the group while falling back to zero-value designation fields. Use the existing ApplyRunDesignationSummary and RunSummary symbols to verify the summary gets the run’s DesignationGroupID and an empty/default RunDesignationSummary rather than remaining stale or failing.internal/store/globaldb/global_db_test.go (1)
460-460: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMinor: extract magic number
25into a named constant.Unlike the sibling prefix test (line 409) which derives its cutoff from
expectedGlobalMigrationPrefix(), this test hardcodes25. A named constant (e.g.,preNetworkActivationMigrationCount) would make the intent clearer and reduce the risk of silent drift if someone edits nearby test code without understanding what25represents.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/store/globaldb/global_db_test.go` at line 460, The test in global_db_test.go uses a hardcoded migration slice cutoff of 25, which should be replaced with a named constant to make the intent explicit and avoid silent drift. Update the RunMigrations call site in the global DB test to use a descriptive constant such as preNetworkActivationMigrationCount, and define it near the sibling migration-prefix test logic so the cutoff is easy to understand and maintain.internal/network/router.go (1)
1237-1240: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winLog the swallowed routing-resolution errors.
Both fallback branches (thread-participant lookup failure and fanout-policy lookup failure) discard
errentirely before defaulting to a degraded routing mode. This is the right fail-open behavior (fixes the earlier message-drop issue), but a persistent DB failure here will silently and permanently downgrade delivery mode (e.g.,coordinator/all_memberssilently becomescapability_match) with no diagnostic trail. Consider aslog.Warncall before defaulting so operators can detect the underlying resolver failure.Also applies to: 1285-1289
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/network/router.go` around lines 1237 - 1240, The fallback in router resolution is swallowing resolver errors, so add a warning log before defaulting to degraded delivery. In router.go, update the thread-participant path in the routing logic around threadParticipantSet, and the fanout-policy fallback path in the same router flow, to emit a slog.Warn with the caught err and enough context (such as envelope/target identifiers) before calling the empty/default delivery selector. Keep the fail-open behavior, but ensure both fallback branches preserve a diagnostic trail when the lookup fails.internal/daemon/network_task_status_observer.go (1)
42-80: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider functional options instead of growing positional parameters.
newNetworkTaskStatusObservernow takes 6 positional args after this change (addingqueueSize,timeout).Routerin this same PR (internal/network/router.go) already uses the functional-options pattern for equivalent constructor knobs. Adopting the same pattern here (newNetworkTaskStatusObserver(networkRuntime, tasks, opts ...Option)) would improve extensibility and match repo convention.Based on learnings and coding guidelines, "Use functional options for constructors —
NewManager(opts ...Option)."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/daemon/network_task_status_observer.go` around lines 42 - 80, newNetworkTaskStatusObserver is becoming harder to extend because it now relies on many positional parameters; switch it to a functional-options constructor to match the repo’s constructor pattern used by Router and NewManager. Update the observer constructor to accept opts ...Option, move queueSize and timeout into options with sensible defaults, and keep the existing nil/default handling inside newNetworkTaskStatusObserver so call sites stay readable and future knobs can be added without changing the signature.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/daemon/native_create_tools_test.go`:
- Around line 198-260: The callback closures in TestNativeNetworkChannelUpdate
are using the parent t.Fatalf/t.Fatal, which can break parallel subtest
execution and cause Goexit-style panics. Update the StubNetworkStore callbacks
(GetNetworkChannelFn, PatchNetworkChannelFn, WriteNetworkChannelFn) to avoid
calling Fatal/Fatalf on the outer test; instead, capture the unexpected
condition and assert it from the subtest’s own t, or return an error that the
subtest can check.
In `@internal/store/globaldb/global_db_network_channels.go`:
- Around line 103-161: PatchNetworkChannel currently reads the channel,
validates the merged snapshot, and then writes it with a separate ExecContext
call, which allows concurrent updates to violate the coordinator/fanout
invariant. Update GlobalDB.PatchNetworkChannel to perform the GetNetworkChannel,
patch.Apply, Validate, and UPDATE within one transaction (or add a DB-level
constraint/trigger) so coordinator_peer_id cannot be left empty when
fanout_policy is Coordinator. Use the existing PatchNetworkChannel,
GetNetworkChannel, and patch.Apply flow as the main place to fix the race.
---
Nitpick comments:
In `@internal/daemon/network_task_status_observer.go`:
- Around line 42-80: newNetworkTaskStatusObserver is becoming harder to extend
because it now relies on many positional parameters; switch it to a
functional-options constructor to match the repo’s constructor pattern used by
Router and NewManager. Update the observer constructor to accept opts ...Option,
move queueSize and timeout into options with sensible defaults, and keep the
existing nil/default handling inside newNetworkTaskStatusObserver so call sites
stay readable and future knobs can be added without changing the signature.
In `@internal/network/router.go`:
- Around line 1237-1240: The fallback in router resolution is swallowing
resolver errors, so add a warning log before defaulting to degraded delivery. In
router.go, update the thread-participant path in the routing logic around
threadParticipantSet, and the fanout-policy fallback path in the same router
flow, to emit a slog.Warn with the caught err and enough context (such as
envelope/target identifiers) before calling the empty/default delivery selector.
Keep the fail-open behavior, but ensure both fallback branches preserve a
diagnostic trail when the lookup fails.
In `@internal/store/globaldb/global_db_test.go`:
- Line 460: The test in global_db_test.go uses a hardcoded migration slice
cutoff of 25, which should be replaced with a named constant to make the intent
explicit and avoid silent drift. Update the RunMigrations call site in the
global DB test to use a descriptive constant such as
preNetworkActivationMigrationCount, and define it near the sibling
migration-prefix test logic so the cutoff is easy to understand and maintain.
In `@internal/task/designation_test.go`:
- Around line 8-50: Add a test in TestApplyRunDesignationSummary that covers a
run with a non-empty DesignationGroupID but missing or invalid Metadata, since
DesignationFromRun returns ok=true in that case and ApplyRunDesignationSummary
should preserve the group while falling back to zero-value designation fields.
Use the existing ApplyRunDesignationSummary and RunSummary symbols to verify the
summary gets the run’s DesignationGroupID and an empty/default
RunDesignationSummary rather than remaining stale or failing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: d16614eb-7661-4d6e-be26-6c148b653368
⛔ Files ignored due to path filters (7)
internal/tools/builtin/testdata/native-tool-catalog.jsonis excluded by!**/*.jsonopenapi/agh.jsonis excluded by!**/*.jsonpackages/site/content/runtime/core/configuration/config-toml.mdxis excluded by!**/*.mdxskills/agh/references/native-tools.mdis excluded by!**/*.mdskills/agh/references/network.mdis excluded by!**/*.mdskills/agh/references/tasks-and-orchestration.mdis excluded by!**/*.mdweb/src/generated/agh-openapi.d.tsis excluded by!**/generated/**
📒 Files selected for processing (58)
internal/api/core/agent_channels_internal_test.gointernal/api/core/network_conversations.gointernal/api/core/network_details.gointernal/api/core/network_internal_test.gointernal/api/core/network_test.gointernal/api/core/tasks.gointernal/api/core/tasks_test.gointernal/api/spec/spec.gointernal/api/spec/spec_test.gointernal/api/testutil/network_stub.gointernal/config/config.gointernal/config/config_test.gointernal/config/merge.gointernal/config/task_orchestration.gointernal/config/task_orchestration_test.gointernal/config/tool_surface.gointernal/daemon/composed_assembler_test.gointernal/daemon/daemon_test.gointernal/daemon/native_create_tools.gointernal/daemon/native_create_tools_test.gointernal/daemon/native_tools.gointernal/daemon/native_tools_test.gointernal/daemon/network_task_status_observer.gointernal/daemon/network_task_status_observer_test.gointernal/daemon/task_event_bridge_notifier.gointernal/daemon/task_role_runtime.gointernal/daemon/task_role_runtime_test.gointernal/network/delivery.gointernal/network/delivery_test.gointernal/network/manager.gointernal/network/manager_test.gointernal/network/router.gointernal/network/router_test.gointernal/observe/tasks.gointernal/store/globaldb/global_db.gointernal/store/globaldb/global_db_network_channels.gointernal/store/globaldb/global_db_network_channels_test.gointernal/store/globaldb/global_db_network_messages_test.gointernal/store/globaldb/global_db_task_test.gointernal/store/globaldb/global_db_test.gointernal/store/network_conversation_types_test.gointernal/store/store.gointernal/store/types.gointernal/task/designation.gointernal/task/designation_test.gointernal/task/live.gointernal/task/manager.gointernal/tools/builtin/tasks.gointernal/tools/native_test.gointernal/tools/schema.goweb/src/hooks/routes/__tests__/use-task-detail-orchestration-tab.test.tsxweb/src/hooks/routes/use-task-detail-orchestration-tab.tsweb/src/systems/network/components/composer/__tests__/composer.test.tsxweb/src/systems/network/components/composer/composer.tsxweb/src/systems/network/components/shell/__tests__/channel-policy-dialog.test.tsxweb/src/systems/network/components/shell/channel-policy-dialog.tsxweb/src/systems/network/components/thread-overlay/__tests__/thread-subscription-control.test.tsxweb/src/systems/network/components/thread-overlay/thread-subscription-control.tsx
🚧 Files skipped from review as they are similar to previous changes (29)
- internal/store/globaldb/global_db_network_messages_test.go
- internal/daemon/task_role_runtime_test.go
- internal/config/merge.go
- internal/config/tool_surface.go
- internal/store/globaldb/global_db_task_test.go
- internal/daemon/daemon_test.go
- internal/daemon/native_create_tools.go
- internal/store/network_conversation_types_test.go
- internal/task/manager.go
- internal/daemon/task_event_bridge_notifier.go
- internal/config/task_orchestration.go
- internal/daemon/task_role_runtime.go
- internal/daemon/composed_assembler_test.go
- internal/network/router_test.go
- internal/api/spec/spec.go
- internal/api/core/tasks.go
- internal/tools/builtin/tasks.go
- internal/api/core/agent_channels_internal_test.go
- internal/config/config.go
- internal/api/core/tasks_test.go
- internal/network/manager.go
- internal/api/core/network_test.go
- internal/store/types.go
- internal/api/core/network_details.go
- internal/network/delivery_test.go
- internal/daemon/native_tools.go
- internal/api/testutil/network_stub.go
- internal/api/core/network_conversations.go
- internal/network/delivery.go
There was a problem hiding this comment.
🧹 Nitpick comments (1)
internal/store/globaldb/global_db_network_channels.go (1)
238-244: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRedundant branching in error handling — both paths are identical.
Both the
errors.Is(err, sql.ErrNoRows)branch and the fallback return the exact same value. Simplify.♻️ Simplify redundant branch
entry, err := scanNetworkChannel(row) if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return store.NetworkChannelEntry{}, err - } - return store.NetworkChannelEntry{}, err + return store.NetworkChannelEntry{}, err } return entry, nil🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/store/globaldb/global_db_network_channels.go` around lines 238 - 244, The error handling in the scanNetworkChannel path has a redundant sql.ErrNoRows branch because both branches return the same store.NetworkChannelEntry{} and err. Simplify the logic in the function that processes scanNetworkChannel results by removing the errors.Is(err, sql.ErrNoRows) special case and just returning the empty entry plus the original error from the single err check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@internal/store/globaldb/global_db_network_channels.go`:
- Around line 238-244: The error handling in the scanNetworkChannel path has a
redundant sql.ErrNoRows branch because both branches return the same
store.NetworkChannelEntry{} and err. Simplify the logic in the function that
processes scanNetworkChannel results by removing the errors.Is(err,
sql.ErrNoRows) special case and just returning the empty entry plus the original
error from the single err check.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4f8c2721-6e68-4faf-aada-8dbceaaef677
📒 Files selected for processing (11)
internal/daemon/native_create_tools_test.gointernal/daemon/network_task_status_observer.gointernal/daemon/network_task_status_observer_test.gointernal/daemon/task_event_bridge_notifier.gointernal/network/manager.gointernal/network/router.gointernal/network/router_test.gointernal/store/globaldb/global_db_network_channels.gointernal/store/globaldb/global_db_network_channels_test.gointernal/store/globaldb/global_db_test.gointernal/task/designation_test.go
🚧 Files skipped from review as they are similar to previous changes (8)
- internal/daemon/native_create_tools_test.go
- internal/store/globaldb/global_db_network_channels_test.go
- internal/network/router_test.go
- internal/store/globaldb/global_db_test.go
- internal/daemon/task_event_bridge_notifier.go
- internal/daemon/network_task_status_observer_test.go
- internal/network/manager.go
- internal/network/router.go
Curated release highlights for v0.0.9 covering task blocking/recovery and needs-attention triage (#269), network delivery policies, subscriptions and thread-to-task promotion (#263), and clearer native tool errors and availability diagnostics (#266). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The release-only Full Integration Suite failed on main because integration-tagged tests never ran in the feature PRs' regular CI and drifted from #263's intentional network changes (each confirmed by passing unit tests in composed_assembler_test and router_test): - globaldb: pass &Summary to assertTaskSummaryMatchesTask; ListTasks returns []Summary by value. Fixes an integration-only build failure. - harness prompt: startup/resume prompts embed the compact network response register, not the full bundled network.md; network-turn augmenters include network_response_register. - prompt composite / harness: register the network_response_register descriptor (mirrors boot.go) so the resolver can enable it on network turns without tripping the "enabled but not registered" guard. - daemon + cli delivery: default channel fanout is capability_match, which digests non-activating broadcasts, so mention the recipient to exercise the full inbound-delivery path. Production code unchanged. Verified under -race: globaldb 512, daemon network/harness/prompt 147, cli network 32. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
## Release v0.0.9 This PR prepares the release of version v0.0.9. ### Changelog ## 0.0.9 - 2026-07-04 ### ♻️ Refactoring - Tasks orchestration (#269) ### 🐛 Bug Fixes - Network optimizations (#263) - Native tools (#266) - Align release-gated network integration tests with #263 behavior ### 📚 Documentation - Update skills - New skill - Add feature stories - Add v0.0.9 release notes ### 📦 Build System - Update go deps - Gitignore ### Release Notes #### Features ##### Network delivery policies, subscriptions, and thread-to-task promotion AGH network channels gain durable delivery control. Each channel now carries a fan-out policy — deliver to all members, route to a designated coordinator peer, or match by capability (the default) — managed with `agh network channels create` and `agh network channels update`. Peers and tasks can subscribe to channels: inspect subscriptions with `agh network subscriptions` and manage per-task subscriptions with `agh task subscribe <task-id>`. Executable thread messages can be promoted into durable workspace tasks straight from the CLI with `agh network promote`, or by an agent through the `agh__task_promote_from_thread` native tool, and designated sibling task runs can be fanned out as part of a workflow. Network and task views now surface task links, peer and coordination cost, and delivered size and token metrics, while mentions are normalized (trimmed, empties removed) and size and token limits are enforced as non-negative. ##### Task blocking, recovery, and needs-attention triage Tasks are now first-class to block, triage, and recover. A task can be explicitly blocked and unblocked with `agh task block <id>` and `agh task unblock <id>`, and every block is kept as durable history you can inspect with `agh task blocks <id>`. A new `needs_attention` status surfaces tasks that stalled and need a human or coordinator to step in — task details now carry the blocked reason and the wake creator so it is clear why a task is waiting and who nudged it. Clear the state with `agh task recover <id>`, recover a specific stalled run with `agh task run recover <run-id>`, or let an agent do it through the `agh__task_recover` native tool and the HTTP/UDS API. Task completion now returns the IDs of any tasks it created, and stronger validation rejects invalid created-task references before they are persisted. Claim tokens are redacted across task outputs and hook payloads, and the new `needs_attention_after` setting controls how long a task may wait before it is flagged for attention. #### Fixes ##### Clearer native tool errors and availability diagnostics Native and hosted tool calls now fail legibly. Hosted MCP tool calls return richer, structured JSON error details — the tool ID, an error code, and any denial reasons — instead of opaque failures, and advertised tools carry improved metadata and descriptions. When runtime diagnostic data cannot be retrieved, native tool lookups report a clear "unavailable" diagnostic rather than a silent miss. Hosted and session MCP availability handling is safer overall, with clearer informational and warning logs and graceful fallbacks when a feature is disabled. Documentation and the runtime envelope now consistently direct agents to the canonical `agh__*` tool IDs and the harness-returned tool references. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated a bundled web assets dependency to a newer version. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Summary by CodeRabbit