Code review finding (P2): when chat_stream successfully emits the first
chunk (broadcasted to UI) but then breaks during subsequent chunks, the
fallback path called gateway.chat() and broadcasted the FULL content
via _replay_stream — causing the UI to see partial streamed content
followed by duplicated full content.
Fix: if `total` already contains partial content from first_chunk,
return it directly without calling gateway.chat(). Partial content is
better than duplicated content. The non-streaming fallback only runs
when no chunks were broadcasted (total is empty).
Add test_stream_breaks_after_first_chunk_returns_partial covering this
edge case: first chunk yields "部分内容", second __anext__ raises
RuntimeError. Asserts result is the partial content, gateway.chat() is
never called, and broadcast_event is called exactly once (for the
first chunk only).
Commit 36b0296 changed expert speech generation from gateway.chat()
(non-streaming) to gateway.chat_stream() (streaming) for progressive
UI output. However, DashScope/DeepSeek via LiteLLM occasionally accept
the streaming request but never emit the first SSE chunk — the async
generator hangs indefinitely with no error and no timeout, freezing
the entire board.
Root cause: `async for chunk in gateway.chat_stream(...)` blocks
forever when the provider silently stalls. The moderator path
(gateway.chat) still works because it's synchronous — that's why
the moderator opening always succeeds but experts hang.
Fix: pull the first chunk via `__anext__()` wrapped in
`asyncio.wait_for(timeout=30s)`. If no chunk arrives within 30s,
close the stream and fall back to non-streaming gateway.chat() +
_replay_stream() (which splits the response into small chunks for
progressive UI rendering). This preserves the "逐个输出" UX while
guaranteeing the board never hangs on a stalled streaming provider.
Add 4 unit tests covering:
- Normal streaming works (existing path)
- First-chunk timeout → fallback to chat()
- Empty stream → fallback to chat()
- chat_stream raising → fallback to chat()
E2E verification: 5-expert board with DashScope completes in ~277s
(status=completed), all experts produce content, no hang.
Same branch as the earlier _handle_llm_gateway fix (commit d0fe661).
AgentPool.create_agent() injects only llm_gateway (v2 path) into
ConfigDrivenAgent and never sets the legacy _llm_client field. The
previous _handle_llm_generate implementation gated the entire LLM
call on `self._llm_client is None`, so every expert/board agent
created via the pool silently fell through to the
`llm_generate_no_client` placeholder — which is why the private
board moderator/experts reported "LLM 不可用" even when the gateway
and providers were healthy.
Mirror the same precedence used by `_handle_direct`: prefer
`_llm_gateway` (with agent_name/task_type for usage tracking), fall
back to `_llm_client` for backward compatibility, and only return
the no-client placeholder when both are unavailable.
Add a unit test that constructs an agent with `llm_gateway` only
(_llm_client is None) and asserts the gateway.chat path is taken.
Test / backend-test (pull_request) Has been cancelledDetails
Test / frontend-unit (pull_request) Has been cancelledDetails
Test / api-e2e (pull_request) Has been cancelledDetails
Test / frontend-e2e (pull_request) Has been cancelledDetails
P0 #1: Fix autonomy_paused event deadlock — split _check_autonomy_pause
into _detect_autonomy_pause (non-blocking) + _await_autonomy_resume
(blocking). Caller now yields the pause event BEFORE awaiting resume,
so the frontend receives it and the resume handler doesn't deadlock.
P1 #2+#6: Fix retrieve_prompt_reflection field mapping — read
output_summary/reflection/quality_score from MemoryItem.value dict
(matching EpisodicMemory.search shape) instead of metadata. Score
filtering uses stored quality_score, not search relevance score.
P1 #3: Add optional task_type filter to cleanup_expired so
prompt_reflection TTL cleanup doesn't delete all episodic records.
P1 #4: Disable parallel tool execution in autonomy mode — dangerous
tools must go through _check_autonomy_gate, which only runs in the
serial path.
P1 #5: Add _track_tool_result_for_autonomy to parallel result loop
so tool failures are counted toward the consecutive_failures threshold.
Tests: adapt test_autonomy_paused.py to new detect/await interface;
fix test_lead_reflection_retrieval.py mock shape (fields in value dict).
137 IQ-boost tests pass, ruff clean.
- ReflexionEngine.retrieve_prompt_reflection(): searches EpisodicMemory
for historical reflections on similar task_input, returns best version
by score (defaults min_score=0.5). Non-blocking: failure → None.
- TeamOrchestrator._decompose_task: prepends historical reflection hint
to Lead's planning prompt when reflexion_engine is wired and a
high-score reflection exists. Default prompt preserved on miss/failure.
- 12 unit tests covering retrieve path (7) + decompose integration (5).
Add explicit support for parallel execution of dependency-free subtasks:
- TeamPlan.get_independent_subtasks(): returns phases with depends_on==[]
(introspection entry — topological_sort already groups them in layer 0)
- TeamOrchestrator.MAX_INDEPENDENT_SUBTASKS=10: aligns with router.MAX_EXPERTS
- _rebalance_independent_subtasks(): when Lead over-decomposes (>10
independent subtasks), re-decompose once with merge hint. Fallbacks:
no gateway → keep original; LLM error → keep original; single-phase
fallback → keep original (don't collapse 11→1); still over → return
new (MAX_PHASES truncation handles it)
Tests: 15 new tests covering get_independent_subtasks, topological_sort
layer contract, SharedWorkspace path uniqueness, rebalance (5 paths),
and full execute() integration. Existing TeamOrchestrator tests pass.
Test / backend-test (pull_request) Has been cancelledDetails
Test / frontend-unit (pull_request) Has been cancelledDetails
Test / api-e2e (pull_request) Has been cancelledDetails
Test / frontend-e2e (pull_request) Has been cancelledDetails
Fixes 4 bugs blocking AE1 acceptance scenario (rollup returns 0):
1. Decimal not recognized as numeric: PostgreSQL JSONB returns numbers
as Decimal; _evaluate_rollup's isinstance(v, (int,float)) missed them.
Added _to_float() helper (also excludes bool, an int subclass).
Applied to _evaluate_lookup for JSON serialization safety.
2. add_relation_link clobbers record values: update_record_values does
full-replace, so setting the relation field wiped amount/etc.
Added merge_record_values (|| operator) and switched all relation
link mutations to use it (add + remove + reverse cleanup).
3. enqueue_recalc ON CONFLICT DO NOTHING swallowed re-enqueues: once a
task reached 'done', the unique constraint silently dropped new
requests for the same (record_id, field_id) — field showed stale
data forever. Changed to ON CONFLICT DO UPDATE: reset done/error
tasks to pending; leave pending/calculating as-is (no double-process).
4. No cross-table recalc on record update: update_record_values only
triggered recalc on the record's own table; rollup on the target
table never updated when source values changed. Added
_trigger_cross_table_recalc() — finds relation fields on the
record's table, triggers recalc on linked target records.
Tests:
- AE1 acceptance: 3 e2e tests (SUM on insert, update-triggered recalc,
AVG aggregation) — all pass against real PG.
- Bidirectional relation deletion: verifies _cleanup_reverse_link
removes source from target's reverse field.
- SSRF guard: 3 tests blocking loopback, private IP, non-HTTP scheme.
- Updated test_recalc_deduplication for new ON CONFLICT DO UPDATE
semantics (returns existing task, not None).
- Fixed test_crash_recovery to use stale_threshold=0 (was always broken
with default 600s threshold).
Test / backend-test (pull_request) Has been cancelledDetails
Test / frontend-unit (pull_request) Has been cancelledDetails
Test / api-e2e (pull_request) Has been cancelledDetails
Test / frontend-e2e (pull_request) Has been cancelledDetails
P1#1 config_driven: propagate trace_outcome into output_data so
lifecycle._is_failure_path() detects non-success outcomes
P1#2 portal: route through ConfigDrivenAgent.execute_stream (not
react_engine.execute_stream directly) so evolution hooks fire
and trace_outcome propagates; add pre-built messages support in
_build_llm_messages
P1#3 sandbox: make network_block reentrant via module-level reference
counter + threading.Lock - concurrent VERIFICATION phases no
longer permanently block all new connections
P1#4 chat: replace dead isinstance(_PlanExecEngine) check with
hasattr(_spec_review_handler) to wire the spec review gate
P1#5 plan_exec_engine: complete max_reflections threading chain
(PlanExecEngine + ReActStepExecutor constructors)
P1#6 plan_exec_engine: enforce phase budgets (max_steps from
phase_budgets, not hardcoded 5)
P1#7 plan_exec_engine: use current plan (not stale plan var) in
aggregation after replan
P1#8 plan_exec_engine: map failure to failed status (not success)
P1#9 app: add drain timeout for pending evolution tasks on shutdown
P1#10 portal: handle spec_review_reply in WS handler
P1#11 chat: persist spec_review_request/reply/timeout to conversation
store so reload can reconstruct gate state
Tests: 116 related tests pass; 26 pre-existing failures unchanged
(stash-verified). ruff lint clean.
Add a spec review gate to PlanExecEngine that pauses execution after the
first Spec is generated, awaiting the user's confirm/reject decision.
On approval execution continues; on rejection the engine replans (capped
at 2 replans); on 30-min timeout the Spec is parked (not failed) so the
user can resume later.
- spec_manager: add parked status + park()/resume() methods
- plan_exec_engine: add spec_review_handler param, wire gate into both
execute_stream and _execute_loop with replan cap, emit
spec_review_request/spec_review_reply events, handle timeout to park
- chat.py: whitelist new events, add spec_review_reply WS handler,
wire _spec_review_handler closure (30-min timeout), cleanup on disconnect
- portal.py: persist spec_review_id/decision/feedback for page reload
- tests: 20 unit tests covering happy path, rejection/replan, timeout,
cancellation, backward compat, handler errors, park/resume round-trips
Replaces the broken write_file placeholder (no real implementation, only
_FakeTool stubs in cli/benchmark.py) with a structured editor offering four
commands: create, str_replace, insert_at_line, view.
Security model (file-system analog of the 6-layer terminal security paradigm,
reject-by-default + prefix match):
1. Reject absolute paths (force relative interpretation vs workspace root).
2. Reject any .. path component (path traversal).
3. Path.resolve() follows symlinks, then relative_to(workspace_root)
rejects symlink escape and residual traversal.
Data-loss guard: create refuses to overwrite existing files. str_replace
requires a unique anchor (0 or >1 matches error). insert_at_line is 1-based
(0 = prepend, > EOF = append). All FS I/O wrapped in asyncio.to_thread.
Registers str_replace_editor in _DEFAULT_CORE_TOOLS (replacing write_file)
so its full description is always injected into the LLM prompt. Updates
test_tool_search.py which used write_file as a sample core tool.
Tests: 34 cases in test_str_replace_editor.py cover happy path, edge cases
(empty file, multi-match, insert at 0/beyond EOF, view range), error paths
(overwrite refusal, anchor not found, path traversal, absolute path, symlink
escape, unknown command, missing args), and integration contract (in
_DEFAULT_CORE_TOOLS, exported from agentkit.tools, schema enum, prompt
injection via _build_tool_use_prompt).
Verification: ruff check clean; targeted regression suite 412 passed
(the single failure in test_calendar_tool.py is a pre-existing date-sensitive
bug in an untouched file, today 2026-07-03 Friday makes the next-Wednesday
assertion fail).
The test was asserting port 8001 (old default) but config.py now loads .env.dev which sets AGENTKIT_SERVER_PORT=18001 per the project port standardization (18001/18002/15173/15174).
Test / backend-test (pull_request) Has been cancelledDetails
Test / frontend-unit (pull_request) Has been cancelledDetails
Test / api-e2e (pull_request) Has been cancelledDetails
Test / frontend-e2e (pull_request) Has been cancelledDetails
Replace emoji across codebase: YAML avatars -> first char, frontend banners -> Ant Design Vue components, CLI status -> OK/FAIL/WARN labels, terminal -> [WARN]/[OK]/[PENDING], Bitable DB default -> table, App.vue font cleanup, test fixtures -> first char letters. shell.avatar type upgraded to string | Component.
Test / backend-test (pull_request) Has been cancelledDetails
Test / frontend-unit (pull_request) Has been cancelledDetails
Test / api-e2e (pull_request) Has been cancelledDetails
Test / frontend-e2e (pull_request) Has been cancelledDetails
Addresses 4 actionable findings (1 P1 + 3 P2) from ce-code-review of
feat/ui-ue-enhancement (PR #13), now merged to main (8066e0b).
P1 — expert_step payload alignment (_phase_executor.py)
The thinking/tool_call/tool_result event payloads were missing the
fields the frontend WsServerMessage contract requires
(expert_name/expert_color/content/step). Frontend code consuming these
events silently degraded. Now all expert_step broadcasts carry the
full contract; tool_call/tool_result keep step_data for the raw payload.
P2 #1 — execute_stream CancellationToken registration (config_driven.py)
execute_stream() bypassed BaseAgent.execute() and never registered a
CancellationToken, so cancel_task() could not cooperatively cancel a
streaming task. Now registers the token and cleans it up in finally.
P2 #2 — team_synthesis orphan milestone cleanup (orchestrator.py)
If synthesis streaming was interrupted (cancel/exception), no terminal
team_synthesis event was emitted, leaving the frontend streaming
milestone spinning forever. Now an inner try/except emits a terminal
team_synthesis with status=cancelled|error before re-raising, so the
frontend can finalize the milestone. The success path also carries
the synthesis_id.
P2 #3 — synthesis_id dedup (orchestrator.py + types.ts + chatStream.ts)
Without an identifier, the frontend could not precisely match a
team_synthesis terminal event to its streaming milestone (especially
across retries/concurrent teams). The backend now injects a stable
synthesis_id (`{plan.id}:synthesis`) into both team_synthesis_chunk
and team_synthesis events; the frontend uses it for exact milestone
matching and treats error/cancelled status as terminal.
Test updates
- Updated test_thinking_events_forwarded_as_expert_step to assert the
new payload contract (expert_id/name/color/content/step).
- Added test_tool_call_events_forwarded_as_expert_step covering
tool_call/tool_result payload shape (content=tool_name摘要 +
step_data=原始 payload).
Verification
- ruff check: clean
- pytest tests/unit/experts/test_phase_executor_streaming.py: 14/14
- npm run typecheck: clean
- vitest: 126/127 (1 unrelated baseline failure in tauri-auth.test.ts)
Residuals doc: docs/residual-review-findings/feat-ui-ue-enhancement.md