Skip to content

fix(agent-org): harden recovery, task orchestration, and run finality#373

Open
ShiboSheng wants to merge 4 commits into
developfrom
fix/issue-272-agent-org-recovery-invariants
Open

fix(agent-org): harden recovery, task orchestration, and run finality#373
ShiboSheng wants to merge 4 commits into
developfrom
fix/issue-272-agent-org-recovery-invariants

Conversation

@ShiboSheng

@ShiboSheng ShiboSheng commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR turns Agent Org recovery from a collection of best-effort wake heuristics into a durable, task-driven orchestration system.

It started as a fix for the watchdog and eligibility gaps tracked in (#272), including:

  • tasks silently stalling after recovery budgets are exhausted;
  • unread inbox rows failing to wake the correct member;
  • failed-member recovery waking peers that cannot actually work;
  • unbounded coordinator repair loops;
  • repeated task/member scans;
  • stale task ownership, restart recovery, and fixture drift.

Live Agent Org runs then exposed adjacent design gaps: duplicate wakes, stale coordinator intervention, incomplete task graphs, missing dependencies, workers modifying the wrong tasks, ownerless auto-claim ambiguity, Planner approval deadlocks, output not reaching downstream tasks, premature run completion, and UI panels disagreeing about task state.

The final result establishes one coherent model for:

  • Run lifecycle
  • Member sessions
  • Task ownership and authority
  • Dynamic task dependencies
  • Durable task outputs
  • Inbox delivery
  • Idempotent Wake dispatch
  • Planner approval
  • Failure and restart recovery
  • Atomic Run finality
  • UI state projection

Fixes #272.

Scope

Compared with the current develop target, this PR changes approximately:

  • 132 files
  • +13,904 / -3,290 lines
  • Rust Agent Org runtime, stores, scheduler and lifecycle
  • Tauri command and type wiring
  • Group Chat, task/Kanban and Agent Org settings UI
  • Rust unit/integration tests
  • Rendered UI E2E coverage
  • Architecture and frontend audit reports

A substantial part of the added code is state-matrix, concurrency, transaction, frontend, and E2E coverage.


Issue #272 coverage

Issue item Final behavior
E1 — budget-exhausted eligible members Detects when no eligible member has a viable automatic recovery path and escalates a typed repair reason to the Coordinator instead of waiting forever.
E2 — unread inbox suppresses recovery Evaluates unread delivery per member. Alice having unread mail no longer suppresses Bob. Inbox rows remain the durable truth; Wake is only the doorbell.
E3 — one active member masks peer stalls The conservative run-level active-member guard remains explicit and tested. A full member-level watchdog is intentionally deferred.
E4 — failed-member peer wakes waste tokens Failed owned work becomes pending and ownerless while preserving task metadata and eligibility. It is returned to the Coordinator for explicit reassignment; workers no longer wake and auto-claim it.
E5 — coordinator repair loops Adds persistent, fingerprinted 1/5/15-minute backoff for coordinator notices and member recovery attempts.
E6 — maintenance debt Prunes terminal-run budgets, uses UTC deadlines, handles corrupt timestamps, filters running runs in SQL, uses skipped missed ticks, removes dead claim/stale-release code, and uses EXISTS for unread checks.
E7 — repeated O(T²·M) scanning Removes the worker auto-claim paths that required repeated task × member scans and computes dependency/availability state once per snapshot.
Issue comment fixture drift Repairs the four inbox_drain fixtures so ownerless tasks contain valid claimant eligibility. The unrelated bundled-skill baseline remains outside this PR.

The fixture failures and their relationship to the eligibility whitelist were confirmed in the [#272 issue discussion](#272 (comment)).


Design and implementation overview

Chapter 1 — Separate sources of truth

The baseline sometimes inferred one subsystem’s state from another—for example, treating an idle session as evidence that a run was finished, or treating an unread inbox row as proof that a wake had been scheduled.

This PR keeps each state dimension independent:

State Meaning Durable source
AgentOrgRun Whether the team project may continue agent_org_runs
AgentSession What a member process is doing now agent_sessions
AgentOrgTask Work status, owner, dependencies and eligibility agent_org_tasks
AgentInbox Messages that still need to be consumed agent_inbox
Wake intent Whether a turn was queued, coalesced, rejected or completed session scheduler / turn intents
PlanApproval Whether a Planner revision is pending, approved or needs changes agent_org_plan_approvals
Recovery budget Whether and when recovery may retry agent_org_recovery_attempts

Important consequences:

  • Idle does not mean the Run is complete.
  • Running does not mean every member is healthy.
  • Pending does not mean every eligible worker may claim a task.
  • Unread does not mean a Wake was accepted.
  • all Tasks being completed does not mean the Coordinator has already delivered a final answer.

The UI is a projection of these durable states, not another source of truth.


Chapter 2 — Coordinator, workers and the Rust control plane

The Coordinator and workers remain LLM sessions, but recovery and consistency are enforced by deterministic Rust code.

flowchart TD
    U["User goal"] --> C["Coordinator LLM"]
    C --> G["Create a dynamic task graph"]
    G --> DB["SQLite durable state"]
    DB --> W["Wake assigned member"]
    W --> M["Worker LLM executes"]
    M --> DB

    DB --> A["Recovery Analyzer<br/>Rust rules, no model call"]
    A --> E["Recovery Executor<br/>Rust side effects"]
    E --> W
    E --> C

    DB --> F["Run Finality Reconciler<br/>Rust transaction"]
    F --> END["Run terminal state"]
Loading

Responsibilities are now explicit:

  • The Coordinator reasons about decomposition, ownership and repair.
  • Workers execute tasks they actually own.
  • The Recovery Analyzer reads a snapshot and proposes actions without side effects.
  • The Recovery Executor rechecks the Run before dispatching those actions.
  • The Finality Reconciler atomically decides whether a Run can end.

Analyzer and Executor are not additional agents and consume no model tokens.


Chapter 3 — Pure recovery analysis and persistent budgets

The previous watchdog mixed inspection, budget mutation, notification and Wake dispatch.

Recovery is now split into:

  1. Read Run, Session, Task, Inbox, Approval, Intervention and budget state.
  2. Classify members explicitly as active, wakeable, in backoff, exhausted, paused, pending materialization, missing or archived.
  3. Produce a side-effect-free RecoveryPlan.
  4. Recheck the Run before every action.
  5. Dispatch Wake, notify the Coordinator, clean budgets, or reconcile finality.

A plan may contain multiple actions in one tick. One unread member no longer suppresses an unrelated repair or terminal check.

Recovery attempts are persisted with:

  • Run ID
  • action kind
  • member or Coordinator target
  • stable reason fingerprint
  • accepted attempt count
  • UTC next-allowed time

Only an accepted Wake consumes an attempt. Coalesced, rejected, paused and no-work outcomes do not.


Chapter 4 — Wake becomes an idempotent doorbell

Every Agent Org Wake uses a deterministic key:

agent-org-wake:{run_id}:{member_id}

Concurrent requests for the same member coalesce into one real turn.

Wake outcomes distinguish:

  • Enqueued
  • Coalesced
  • DeferredPaused
  • DeferredIntervention
  • NoWork
  • RunTerminal
  • SessionUnavailable
  • Failed

Session status is no longer written to Running before enqueue:

  • rejected enqueue leaves the Session unchanged;
  • coalesced enqueue leaves the Session unchanged;
  • only the scheduler starting the real turn writes Running;
  • finalization writes the resulting Idle or Failed state.

Before a queued Wake reaches the provider, it rechecks the Run:

  • Running continues;
  • Paused preserves Inbox rows and performs no model call;
  • terminal or missing Runs cannot be revived;
  • database errors fail closed.

If Inbox drain produces no real input, the turn returns WakeNoop instead of sending an empty model nudge.

Turn intents also receive explicit terminal states such as coalesced, rejected, stale and failed, preventing orphaned queued intents from blocking finality.


Chapter 5 — Coordinator intervention no longer causes Wake storms

A stale Coordinator intervention was the upstream cause of hundreds of repeated Coordinator Wake intents during live testing.

The final rule is:

  • ordinary messages to the Coordinator or Group Chat are organization instructions;
  • they do not establish a user-intervention lease;
  • only direct user takeover of a worker session establishes intervention;
  • legacy Coordinator intervention records are cleared when read.

While a worker is under direct user intervention:

  • background Wake is quietly deferred;
  • Inbox rows remain unread;
  • no provider call is made;
  • no blue running-state flash loop is produced;
  • returning the worker to organization control resumes durable delivery.

Intervention identity is based on persisted member_id, not UI labels or guessed agent types.


Chapter 6 — Atomic dynamic task graphs

Coordinator task creation is no longer limited to a sequence of independent task_create calls.

task_graph_create validates and writes a complete graph atomically:

  • Task IDs
  • owners
  • eligibility
  • dispatch policy
  • execution mode
  • dependencies
  • unknown references
  • self-dependencies
  • multi-node cycles
  • roster membership

Either the entire graph is committed or no task is created.

The backend does not hard-code:

Planner → Implementer → Reviewer → Tester

The Coordinator may create:

  • fully serial workflows;
  • explicitly parallel work;
  • one upstream feeding several downstream tasks;
  • multiple upstream tasks joining into one final task;
  • workflows without Planner, Reviewer or Tester.

The backend guarantees graph correctness, not a fixed organizational recipe.


Chapter 7 — Explicit dispatch and dependency confirmation

Each task explicitly declares a dispatch policy:

  • immediate
  • after_dependencies

after_dependencies must include valid dependency IDs and cannot form a cycle.

To prevent the Coordinator’s prose from saying “after A and B” while structured data only lists A, task creation includes a local dependency-confirmation gate:

  1. Read the current open task graph once.
  2. Compute direct and indirect dependency coverage.
  3. Identify open tasks omitted from the proposed dependency closure.
  4. Return one structured confirmation result.
  5. Let the Coordinator either add the missing dependencies or explicitly confirm parallelism.

Explicit parallelism uses:

allow_parallel_with_unlisted_open_tasks=true

This analysis is deterministic local Rust code and does not consume tokens.

The check is repeated inside the writer lock and SQLite immediate transaction so a concurrent new task cannot slip between validation and insertion.


Chapter 8 — Task authority and the meaning of Ownerless

Task authority is separate from message routing.

Hierarchy Mode controls who may communicate with whom. It does not automatically grant permission to mutate another member’s task.

Final task authority rules:

  • the Coordinator can manage the whole Run;
  • a hierarchical manager may manage permitted direct-report work;
  • workers may start, update and complete their own tasks;
  • workers cannot modify peer tasks;
  • workers cannot reassign their own work to peers;
  • workers cannot complete another member’s task;
  • ownerless tasks cannot be self-claimed.

owner = NULL now means:

This task currently has no responsible member and is waiting for explicit Coordinator reassignment.

It no longer means an open worker claim pool.

eligible_member_ids is a candidate constraint for Coordinator assignment, not ownership and not permission to self-claim.

Ownerless auto-claim was removed from:

  • Watchdog
  • Inbox drain
  • Resume
  • dependency unlock
  • task side effects
  • execution-mode prepeek
  • failure recovery
  • ordinary messages

A normal message cannot bypass task authority. Work-bearing messages to a worker must reference a ready, unresolved task already owned by that worker.


Chapter 9 — Durable TaskOutput and downstream delivery

A completed task now records a durable output rather than only a status bit.

Task output can include:

  • summary
  • content
  • artifact IDs
  • producing member
  • production timestamp

When an upstream task completes:

  1. Its output is stored.
  2. A typed TaskCompleted event is emitted.
  3. Newly ready downstream tasks are identified from the transaction outcome.
  4. Their assigned members receive TaskAssigned with bounded dependency outputs.
  5. Downstream workers receive the real upstream result in their production turn input.

Tasks that have downstream consumers cannot complete without an output summary.

This prevents “completed” tasks whose reviewers receive no artifact or context.

Task state is monotonic: a completed task cannot be silently reopened. Changed requirements use follow-up tasks so Run finality remains explainable.

Task mutations return transaction-local before/after outcomes, ensuring assignment, completion and dependency side effects fire once rather than from stale pre-transaction reads.


Chapter 10 — Planner execution and durable approval

Execution mode travels with the task:

  • execution_mode=plan
  • execution_mode=build

The root Coordinator is no longer pushed into generic Plan Mode during an active Agent Org Run. Planning is performed by a member that owns a real in-progress Plan task.

create_plan requires:

  • an active Agent Org member;
  • an owned in-progress Plan task;
  • non-empty plan content;
  • a Run that still permits mutation.

Agent Org configuration now supports three approval policies:

  • Coordinator
  • User
  • Automatic

Plan approval is durable and revisioned. It records:

  • Run
  • source Planner and Session
  • source task
  • revision
  • content
  • policy
  • status
  • feedback
  • timestamps

For user approval, Group Chat renders an approval card where the user can:

  • approve;
  • edit and approve;
  • request changes;
  • provide feedback;
  • see loading and inline error states.

Requesting changes atomically updates approval state and writes Planner feedback to Inbox.

Approving atomically:

  • marks the revision approved;
  • saves the plan as TaskOutput;
  • completes the Plan task;
  • unlocks real downstream dependencies;
  • emits the required typed events.

Waiting for plan approval is intentionally quiet:

  • the Run remains Running;
  • the UI shows AwaitingPlanApproval;
  • Planner is not woken every minute;
  • no model call or recovery budget is consumed.

Chapter 11 — Worker lifecycle and failure disposition

Before a Build worker turn stops, Rust checks whether the worker still owns an in-progress Build task.

If the worker produced a result but forgot to complete the task:

  • it receives one bounded correction opportunity in the same turn;
  • it is instructed not to redo the work;
  • it may complete the task with output or report that it cannot;
  • an atomic guard prevents concurrent stop paths from creating a correction loop.

Plan tasks are excluded because they may legitimately remain in progress while awaiting approval.

MemberIdle and TaskCompleted now have different meanings:

  • MemberIdle says a member turn ended;
  • TaskCompleted says a specific task produced a durable completion.

If work remains unfinished, MemberIdle carries the relevant task IDs so the Coordinator knows exactly what needs repair.

On explicit member failure:

  • affected in-progress tasks become pending and ownerless;
  • metadata, eligibility and dependency state are preserved;
  • task history is written in the same transaction;
  • the failed member is not immediately self-woken;
  • the Coordinator receives the task IDs for explicit reassignment;
  • later retry behavior is controlled by the persistent recovery budget.

A merely old updated_at timestamp no longer steals ownership from a still-Running member. Staleness produces a Coordinator notice; only explicit failure, cancellation, timeout, shutdown or restart recovery may change ownership.


Chapter 12 — Pause, resume and restart recovery

Paused Runs are quiet:

  • queued Wake execution no-ops;
  • Inbox rows remain unread;
  • tasks and approvals remain durable;
  • providers are not called;
  • budgets are not consumed.

Resume rebuilds progress from persisted state and durable resume/inbox events, not the old in-memory queue. Task existence alone does not generate an empty worker turn.

Application restart recovery now classifies abandoned turn intents:

  • optimistic/queued → stale
  • running → failed
  • abandoned live sessions → terminal recovery state

It then reconciles:

  • sessions
  • failed task disposition
  • intervention state
  • unread Inbox rows
  • plan approvals
  • recovery attempts
  • Run finality

Running Runs are selected directly in SQL:

WHERE status = 'running'

They are no longer hidden behind a “load 500 arbitrary runs, then filter in Rust” limit.


Chapter 13 — Atomic Run finality and completion certificate

Run completion is no longer inferred from “the task cards look done.”

The completion certificate checks:

  • the task board is not empty;
  • every task is completed;
  • the Coordinator is no longer actively executing;
  • workers are no longer actively executing;
  • no unread Inbox rows remain;
  • no plan approval is pending;
  • no user intervention remains active;
  • no optimistic, queued or running turn intent remains;
  • the Coordinator has completed a final turn after the latest task update.

Task mutation and Run reconciliation share the same writer lock and SQLite immediate transaction.

This removes the race:

Reconciler sees all tasks complete
→ another turn creates a new task
→ Run becomes Completed
→ open task remains under a terminal Run

Only two serializable outcomes are possible:

  1. Reconciliation wins; the Run becomes terminal and later task mutation is rejected.
  2. Task mutation wins; reconciliation sees open work and cannot complete the Run.

Chapter 14 — CLI capability boundary and UI projection

CLI Agent Org members are explicitly disabled until they have production parity.

Current CLI members do not have the full combination of:

  • production Inbox drain;
  • Agent Org task tools;
  • correct resume bridge;
  • Agent Org MCP/tool surface;
  • production E2E coverage.

This PR therefore:

  • rejects new or updated Agent Orgs containing CLI members;
  • performs the same preflight before creating a Run or root Session;
  • reports unsupported member IDs and CLI transport types;
  • hides CLI agents from Agent Org-specific member pickers;
  • preserves the ability to deserialize, open and delete historical definitions;
  • leaves normal standalone CLI sessions unchanged.

The frontend also gains:

  • explicit Run phases such as coordinating, dispatching, members working, awaiting approval, finalizing and terminal states;
  • consistent active-member calculation;
  • a Group Chat plan-approval card;
  • unified Task outcome parsing;
  • more consistent Team Tasks and Kanban state;
  • structured, recoverable tool guidance instead of treating every expected constraint as a red system failure;
  • synchronized English and Chinese copy.

The right-side live monitor remains a runtime projection rather than a new persistent historical dashboard. Group Chat Team Tasks remain the durable task view.


Chapter 15 — Performance, tests and audit coverage

Performance and data-boundary changes include:

  • running-run filtering in SQL;
  • SELECT EXISTS for unread checks;
  • removal of repeated auto-claim task × member scans;
  • one dependency-closure computation per graph snapshot;
  • UTC recovery deadlines;
  • explicit handling of corrupt timestamps;
  • Unicode-aware output limits;
  • atomic approval + Inbox delivery;
  • transaction-local task mutation outcomes;
  • MissedTickBehavior::Skip.

Automated coverage includes:

  • recovery member-state matrices;
  • unread Inbox recovery;
  • recovery budgets and reason fingerprints;
  • concurrent Wake coalescing;
  • queue full/closed behavior;
  • Pause and terminal execution gates;
  • task authority and ownerless rejection;
  • dynamic dependency graphs and cycle detection;
  • dependency confirmation and explicit parallelism;
  • durable TaskOutput delivery;
  • Planner approval, edits and changes requested;
  • transaction rollback paths;
  • worker stop correction;
  • restart intent recovery;
  • Run finality versus concurrent task creation;
  • CLI negative validation;
  • Group Chat, approval, recovery and Kanban UI paths.

Final targeted verification after merging the latest develop:

Check Result
Agent Org Rust tests 178 / 178 passed
External-import conflict-resolution tests 23 / 23 passed
Agent Org frontend tests 25 / 25 passed
cargo check -p org2 passed
Merge commit scoped Cargo Clippy passed
lint-staged / ESLint / formatting hooks passed
circular dependency hook 0 reported

Earlier full-suite verification on the feature diff produced:

  • Agent Core: 2,984 / 2,986 passed
  • Frontend: 4,071 / 4,076 passed

Those remaining failures were in unchanged repository baseline areas and were kept outside this PR.

After merging origin/develop@09894ca7d, full pnpm typecheck also reports an existing target-branch error at:

ContextInfoButton.tsx:468
Type 'string | undefined' is not assignable to type 'string'

That file is identical to the current target develop and is outside this PR’s feature diff.

Architecture and frontend audits are included under:

  • docs/architecture-audit-2026-07-12/
  • docs/architecture-audit-2026-07-13/
  • docs/architecture-audit-2026-07-14/
  • docs/frontend-ui-audit-2026-07-12/
  • docs/frontend-ui-audit-2026-07-13/
  • docs/frontend-ui-audit-2026-07-14/

Chapter 16 — End-to-end behavior after this PR

An Agent Org Run now follows this model:

flowchart TD
    U["User submits a goal"] --> C["Coordinator designs a dynamic Task Graph"]
    C --> G["Atomically create Tasks, Owners and Dependencies"]
    G --> R["Dispatch only ready root Tasks"]
    R --> M["Assigned Worker executes"]
    M --> O["Persist TaskOutput and emit TaskCompleted"]
    O --> N["Unlock only real downstream dependencies"]
    N --> M

    M --> P{"Plan Task?"}
    P -->|"Yes"| A["Wait for Coordinator, User or Automatic approval"]
    A --> N

    O --> Q["Coordinator reads the completion certificate"]
    Q -->|"Blockers remain"| WAIT["Wait for a real Inbox or Task event"]
    Q -->|"Everything is complete"| F["Coordinator gives the final user response"]
    F --> END["Complete the Run inside a transaction"]

    WAIT --> WD["Watchdog inspects durable state"]
    WD -->|"Real recovery action exists"| C
Loading

The final design can be summarized in eight rules:

  1. The Coordinator owns organization-level assignment.
  2. Workers only operate on tasks they are authorized to manage.
  3. Ownerless tasks are never automatically claimed.
  4. Dependencies determine when work may begin.
  5. TaskOutput carries results across Sessions.
  6. Inbox is the durable message; Wake is only the idempotent doorbell.
  7. Watchdog is a deterministic Rust safety system, not another model.
  8. A Run completes only after Tasks, Sessions, Inbox, intents, approvals and the Coordinator’s final response are all reconciled.

Compatibility and migration

  • New recovery and approval tables use idempotent initialization.
  • No historical backfill is required.
  • Historical malformed data remains readable and is escalated for repair.
  • Historical CLI Agent Org definitions remain readable and removable.
  • CLI support is restricted only inside Agent Org; standalone CLI sessions are unchanged.
  • Task eligibility remains in validated metadata JSON for now.
  • No destructive schema normalization is included.

Mandarin Chinese Tech Doc of this PR

issue272_agentorg_changes.html


Agent Org Run Testing

tested on:
issue #305

screenshot:

image image

Fixes #272 by making Agent Org recovery event-driven, wake dispatch idempotent, and run/task finality transactional. Enforce coordinator-owned assignment, add durable planner approval flows, improve lifecycle recovery and UI state projection, and cover the production paths with Rust, frontend, and E2E tests.

Pre-commit hook ran. Total eslint: 0, total circular: 0
…t-org-recovery-invariants

# Conflicts:
#	src-tauri/crates/agent-core/src/specialization/external_import/tests.rs

Pre-commit hook ran. Total eslint: 0, total circular: 0
@ShiboSheng
ShiboSheng requested a review from Neonforge98 July 14, 2026 15:08
…ent-org-recovery-invariants

Pre-commit hook ran. Total eslint: 0, total circular: 0
…ent-org-recovery-invariants

# Conflicts:
#	src-tauri/crates/agent-core/src/core/session/turn/processor/inbox_drain/mod.rs
#	src-tauri/src/agent_sessions/event_pipeline/extractors/tests/extractors_tests.rs

@Harry19081 Harry19081 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High severity (confirmed in code)

  1. reconcile_run_finality is a permanent write-lock heartbeat.
    The old reconcile_if_terminal was read-only with an early exit when the root session wasn't terminal. The new function (agent_org_runs/store.rs:217-455 on the PR branch) unconditionally takes the process-global sessions writer mutex + BEGIN IMMEDIATE, then runs: a recursive CTE with ROW_NUMBER() window over all descendant agent_sessions, a code_sessions read, a task aggregate, an unread-inbox EXISTS, a second recursive CTE over session_turn_intents, and an unconditional UPDATE agent_member_interventions — even when the run is obviously still active. It's called from:

the run-view command polled every 2.5s per open org panel (up to 3 concurrent pollers exist: ChatView, WorkStation Communication, AppSwitcher);
lifecycle.rs:476 after every member/coordinator turn end;
the watchdog, per tick per running run;
task mutation commands.
Every open org chat panel now serializes an exclusive write transaction against all session persistence roughly every 2.5 seconds for the run's entire life. The same poll also calls active_for_member(coordinator), which the PR turned into a write-on-read (agent_member_interventions.rs:205-210), so it's two writer-lock acquisitions per poll — and the coordinator-clear UPDATE is executed twice.

  1. task_list — a pure read tool — takes the global writer lock too.
    task_list_get.rs:109 → completion_snapshot (agent_org_runs/store.rs:656+) wraps the same recursive-CTE + full-board pipeline in with_sessions_writer + IMMEDIATE transaction. The PR's new prompt text explicitly instructs the coordinator to call task_list after every TaskCompleted and before finishing, and members call it at turn start — so this read blocks all app-wide DB writes many times per run. (One agent claim I refuted: the code_sessions query is indexed by idx_code_sessions_parent_org_member; the cost is the lock scope, not a table scan.)

  2. agent_inbox is insert-only and gets fully rescanned.
    There is zero DELETE FROM agent_inbox in the tree, and the PR makes rows far bigger (TaskAssigned now embeds up to 50KB of inline dependency outputs; plan-approval requests ~20KB) and more numerous (TaskCompleted + MemberIdle rows per task, watchdog redelivery). Two consumers scan the whole table per run:

the watchdog (agent_org_watchdog.rs:556-562) loads and JSON-decodes every row once per 60s tick per quiescent running run, just to collect historical TaskAssigned ids;
the run-view command ships the entire inbox (read + unread, full payload JSON) over the Tauri bridge every 2.5s poll, along with full pending-plan markdown.
CPU, RAM, and IPC per tick/poll grow monotonically with run age. A SQL-side filter for the watchdog and a LIMIT/read-cutoff for the view would make both O(small).

  1. N+1 full task-board loads with fat metadata.
    Completed task outputs (up to 20KB each) are now stored inside metadata_json, and the board is repeatedly reloaded and re-parsed: update_inner loads the full board 3× per update, two byte-identical (agent_org_tasks/store.rs:666, 710, 761); a completing task_update cascades into ~6+U full-board loads (its own list, dispatch_task_completed, dispatch_ready_assigned_tasks_unblocked_by, plus one more per unblocked task inside enqueue_task_assigned_to); the turn-end/stop-gate/prompt paths each list the board again per turn. A 100-task board with ~10KB average outputs means megabytes of redundant JSON parsing per turn, much of it inside the write transaction. Related: task tool responses serialize each task's output twice (tasks.rs:559-560 — the extracted output field plus the raw metadata still containing it), doubling the bytes fed into the coordinator's LLM context on every task_list.

Medium severity
5. Un-closable-run treadmill. The watchdog's terminal_candidate gate is weaker than reconcile_run_finality's (which additionally requires no pending turn intents, root quiescence, and coordinator_observed_latest_tasks). A run passing the first but failing the second — e.g., a task updated after the coordinator's last terminal turn with nothing left to wake it — never closes, and re-runs the full reconcile write-transaction every tick forever plus every 2.5s while its panel is open. This is a liveness bug as much as a CPU one.

  1. Idle-member rewakes bypass the recovery budget. agent_org_watchdog.rs:194-202 returns true unconditionally for Idle members with unread mail, and no budget row is recorded for Idle wakes (inbox_wake.rs:292-302), so the 1/5/15-minute backoff can never engage. If a drain persistently fails to mark rows read (member-id resolution failure, drain-guard commit failure), that's a real LLM turn dispatched every 60 seconds indefinitely — unbounded API cost with no escalation path.

  2. Unbounded durable growth elsewhere. agent_org_plan_approvals stores full plan_content per revision with no length cap and no delete path (a planner iterating 30 revisions of an 80KB plan permanently adds ~2.4MB, duplicated again into task metadata and inbox rows); agent_org_task_events and session_turn_intents are also insert-only. None of these is scanned on a hot path today, so it's disk/cold-query growth rather than CPU.

  3. Blocking work in wrong contexts. approve() does std::fs::write inside the writer lock + IMMEDIATE transaction (agent_org_plan_approvals.rs:359); the new org tools call synchronous SQLite directly on async executor threads with no spawn_blocking (unlike sibling tools that do); and the watchdog opens a fresh SQLite connection per query — roughly R×(10 + 2M + 3T) connection opens per tick (~350/min for 3 runs × 8 members × 30 tasks). Churn, not blow-up, but it compounds items 1–3.

Frontend (mild)
resolveOrgTaskOperationOutcome (orgTaskOutcome.ts:56-86) unconditionally JSON.parses the tool result before its early-return for events that already carry an outcome, and OrgTaskAdapter calls it in the render body unmemoized — O(events × task-list size) parses per chat re-render pass, sustained because the run-view poll delivers a fresh object every 2.5s. Cheap fix: hoist the early return, add useMemo.
Full pending-plan markdown rides the 2.5s poll (bounded by the memoized Markdown renderer, but the IPC payload itself repeats).
Otherwise clean: no new timers/intervals/listeners, effect deps correct, two mount-time RPCs actually removed.
Verified fine (so you don't re-check)
The pre-PR unbounded REWAKE_BUDGETS HashMap is genuinely fixed (moved to a SQLite table pruned every tick); the watchdog uses MissedTickBehavior::Skip in spawn_blocking with no overlap or duplicate spawns; wake coalescing works (stable agent-org-wake:{run}:{member} key held until turn completion, pinned by a 20-concurrent-wakes test); the claimed O(T²·M) scan removal is real; inbox payload size caps are enforced at insert; the drain hot path lost 2–4 queries per turn vs develop.

Two side findings worth passing to a correctness review: task_graph_create events never reach extract_org_task (the dispatcher gate in extractors.rs:84-97 doesn't include it, so its chat block likely renders without task data), and the watchdog discards inner scan errors while the first failing run aborts recovery for all later runs that tick.

@Harry19081

Copy link
Copy Markdown
Member

All five dimension reviews are complete and cross-verified. Here is the consolidated report.

TL;DR
Beyond the RAM/CPU issues already reported, the reviews found two high-severity correctness bugs (a single failed worker turn can permanently kill a whole run; the PR's own new task_graph_create tool renders broken or destructively on every non-polled UI surface), a class of permanently-stuck runs the watchdog silently ignores, a silent-failure watchdog design where one poisoned run disables recovery for all others, and a Rust-level E2E suite that is likely broken against a real binary — including two scenarios still pinning behavior this PR removed. Security fundamentals are solid (no injection, no path traversal, authority properly enforced); the security findings are resource-exhaustion caps only. i18n and wire parity are clean.

Critical — should block merge

  1. One failed or user-cancelled worker turn can instantly Abandon an entire run (correctness — hand-verified)
    The Abandoned gate in reconcile_run_finality checks only incomplete_tasks > 0 && root terminal && all workers terminal — none of the Completed-branch guards (unread inbox, pending intents, interventions). Coordinator root sessions settle at Completed (a terminal status) after every successful turn, and a failed member turn persists Failed before the member finalize hook runs. So in a one-worker org (or any moment when every materialized worker session is terminal — e.g. a provider outage, or the user pressing Stop): the finalize hook requeues the task, inserts a coordinator recovery notice, then synchronously reconciles → run flips to Abandoned, a terminal state with no resume path (mark_resumed only handles paused). The just-inserted notice and queued coordinator wake are dead on arrival. The entire 1/5/15-minute recovery budget system the PR builds never gets a chance. Notably, agent_org_pause_run avoids this only by accident of ordering.

  2. task_graph_create is broken on every non-polled UI surface — and wipes the replay Kanban (architecture + frontend, repro-tested)
    The extractor dispatch gate (extractors.rs:83-101) matches only TASK_CREATE|TASK_UPDATE|TASK_LIST|TASK_GET; the misc_extractor arm for TASK_GRAPH_CREATE is unreachable dead code. Consequences, verified with an actual vitest repro the reviewer wrote: the chat transcript shows a coordinator's 32-task graph as opaque raw JSON; the Communication timeline renders an empty "updated to-dos" bubble; and the Kanban replay path treats the event as an empty snapshot and clears every previously accumulated task. The durable board shows the tasks — the exact "panels disagree about task state" failure class this PR set out to fix, introduced for the PR's own new tool. No extractor test covers the routing.

  3. The Rust E2E suite's org-task scenarios are likely broken against a real binary (tests)
    The PR added a #[cfg(not(test))] run-row existence gate to task creation, but no e2e-test-crate scenario creates an agent_org_runs row — seeding fails outside cfg(test), likely taking down ~13 scenarios. One scenario still POSTs a debug route this PR deleted (permanent 404). Two scenarios still assert removed semantics: released_task_can_be_claimed_by_idle_peer pins the autonomous-claim behavior the PR eliminated, and accepted_shutdown_releases_owned_open_tasks asserts owner→null where HEAD now escalates to the coordinator. Someone should run this suite against a real build before merging; if it passes today, that itself is evidence the gate isn't wired the way it looks.

High — silent-failure and stuck-state class
4. Watchdog errors vanish and one poisoned run starves the rest. The tick wrapper catches only the JoinError — the inner Result is discarded with no log — and inside the loop, the first failing run aborts recovery for every run behind it (ordered updated_at DESC, so a frequently-touched corrupt run permanently shadows the others). A single corrupt task row is a realistic trigger, because row_to_task hard-fails and list operations propagate the first row error — one bad metadata_json poisons every task list for that run, contradicting the PR's "malformed data is escalated for repair" claim.

  1. Permanently stuck Running runs with an empty recovery plan. The watchdog's terminal_candidate gate is weaker than reconcile's, and when reconcile declines, the fall-through plan is empty — no wake, no notice, no log, retried every 60s forever. Concrete unrecoverable states (nothing un-sticks them except manual user action or restart): an unread inbox row addressed to an archived/CLI/never-materialized member; the final task's TaskCompleted insert failing (it's outside the completion transaction, warn-and-return) so coordinator_observed_latest_tasks stays false with no unread row to wake anyone; a corrupt timestamp, which timestamp_at_or_after silently converts to "never observed" — opposite polarity to the watchdog's own fail-open corrupt-timestamp policy; and an empty task board, which can satisfy neither the Completed (task_count > 0) nor Abandoned (incomplete > 0) branch. None of these states has a test (the recovery executor recover_stalled_run has zero tests at any level).

  2. Pending plan approvals are cancelled on every app restart (hand-verified). Startup pauses all running runs; the watchdog's first tick fires immediately and cancels pending approvals for any non-running run. A user's approval card silently disappears on restart (or any pause >60s), nothing recreates it, and recovery is a 15-minute stale-owner notice followed by a full replan turn.

Medium
Security (resource exhaustion only): create_plan.content has no size cap before hitting disk and SQLite (the 20KB cap only bounds the inbox excerpt), and Plain.text, approval feedback, task description/metadata_json are uncapped. Side bug: a >4,000-char description makes TaskAssigned dispatch silently fail validation — the owner is never notified. Everything else passed: no SQL injection (uniformly parameterized), no path traversal (plan paths are server-derived; slugs sanitized), authority and run-scoping enforced everywhere, no XSS (react-markdown without rehype-raw), prompt content XML-escaped and attributed on the live path.
At-least-once duplication windows: DrainGuard commit isn't atomic with turn success — a crash between transcript persist and mark-read replays rows into duplicate transcript user messages; ShutdownResponse side effects can duplicate MemberTerminated rows; and the plan-mode override staged in-memory at drain time is lost on crash — plus, statically traced: a plan-revision wake turn runs in Build mode (the staged Plan override lands one turn late and can then leak into an unrelated later user turn).
blocks edges never gate readiness — only blocked_by does. The schema and cycle validator both accept blocks, making it look load-bearing, but a task "blocked" only via another task's blocks list dispatches immediately.
A worker can DELETE an ownerless task it's merely eligible for (admin rights via eligibility ⊆ allowed), erasing the coordinator's queued work — falsifying the strict reading of "workers can no longer modify the wrong tasks".
Approval respond error after commit: if post-commit notification fails, the UI gets an error for an approval that actually succeeded, and the retry surfaces raw agent_org_plan_approval_stale_revision untranslated in the card.
Legacy outcome fallback disagrees with Rust: the TS fallback classifies an args-only terminal task_update as "succeeded" where the Rust extractor fails closed — the replay Kanban can materialize a task from an update that never persisted. Task deletion also renders three different ways across surfaces (Cancelled column vs row removal vs "Update task detail" with untranslated "deleted").
Architecture debt worth fixing while it's fresh: the run-finality predicate exists in four divergent copies; the blockers-resolved predicate was deleted and re-inlined 5×; a byte-identical quiescence predicate is defined twice; a cfg(test) fork sits inside a production invariant gate; the test-env schema init is missing the watchdog table (silently refused wakes in tests); budget probes swallow DB errors into opposite silent defaults; the in-flight intent status set is hardcoded as SQL literals in three places across two crates; and the task_create tool-schema doc still promises autonomous claiming to the model — a model-facing contradiction of the PR's own design.
Wire nits: TS executionMode on the run-view task is never populated by Rust (always undefined); task tool responses ship each output twice (output + raw metadata).
What held up well
The approval FSM's compare-and-set semantics are solid (double-approve, approve-after-supersede, stale-revision all correctly rejected, with genuine forced-failure rollback tests). Wake coalescing is real and pinned by a genuine 20-task contention test. The two-thread finality-vs-create serializability test is a model race test. The repaired inbox fixtures encode honest invariants rather than test-forcing. The deleted settings E2E spec's coverage moved rather than vanished. i18n en/zh parity is 100% on new keys, and Rust↔TS enum/wire parity passed field-by-field except the one executionMode ghost.

Per the architecture-audit skill's requirements: layers 2–5 and 7–10 were covered; layer 1 (compile/clippy gates) and dynamic wire measurements were skipped — worth running cargo clippy and tsc --noEmit in CI before merge (the frontend reviewer did verify tsc --noEmit and the changed vitest suites pass).

Top asks for the author, in order: fix the Abandoned gate to respect unread/intents (finding 1); add TASK_GRAPH_CREATE to the extractor dispatch gate and a routing test (2); log and isolate per-run watchdog failures (4); give declined-reconcile terminal candidates an escalation path instead of an empty plan (5); scope approval cancellation to terminal runs only, not paused (6); and get the e2e-test crate running against a real binary (3).

The five full reviewer reports (with evidence snippets and checked-and-fine lists) are preserved in this session if you want any single finding expanded, and the PR branch worktree is still at scratchpad/pr373. If you'd like, I can also draft these as GitHub review comments on the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(agent org): improving agent org efficiency and fixing edge cases

2 participants