Files
LZH-YS1998 12817a4e60 fix(office-ui): native progress parity, ui_state lock hardening, approval-card idempotency
Native agent progress panel (company mode):
- ws_handler: filter runtime bookkeeping noise (turn/status/member_inbox_updated),
  keep tool_completed as tool_call, thinking summary previews content,
  preserve raw thinking_delta fragments (no strip; skip whitespace-only)
- frontend progressLog: summarize thinking by content preview; merge thinking
  by detail only so the 'Thinking' label never splices into text
- AgentProgressBlock: add bottom "Show more (N earlier steps)" toggle

ui_state.db "database is locked" hardening:
- ws_handler: isolate engine progress/kanban/runtime-event callbacks so UI
  persistence failures never crash work items
- chat_store: busy_timeout, _retry_locked backoff, idempotent insert_message
  (INSERT OR REPLACE), create_channel read-before-write to stop poll writes
- server: flock single-instance guard for `opc ui` per OPC home

Approval card duplicate-click bug:
- EscalationPanel: disable buttons on click with Submitting state and 30s
  reconnect fallback
- ws_handler: stale-escalation branch checks real card status (new
  chat_store.get_checkpoint_message); already-resolved cards get an accurate
  "already handled (decision: X)" reply without being re-marked stale;
  dedup identical helper messages within 120s to stop reply spam

Company mode prompt:
- add soft guidance that the runtime monitors state and re-activates roles,
  so leaders need not poll work items after delegation/review

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 16:18:01 +08:00

87 lines
3.2 KiB
Python

"""Factory for shared Office services."""
from __future__ import annotations
from types import TracebackType
from typing import Any, Awaitable, Callable
import aiosqlite
from opc.core.config import OPCConfig, get_opc_home
from opc.engine import OPCEngine
from opc.plugins.office_ui.agent_store import AgentStore
from opc.plugins.office_ui.chat_store import ChatStore
from opc.plugins.office_ui.event_adapter import EventAdapter
from . import OfficeServices
from .context import ModeState, OfficeServiceContext
class OfficeServiceFactory:
"""Async context manager that owns engine and UI-state persistence."""
def __init__(
self,
*,
config: OPCConfig | None = None,
project_id: str | None = None,
on_progress: Callable[..., Awaitable[None]] | None = None,
on_runtime_event: Callable[[Any], Awaitable[None]] | None = None,
on_escalation: Callable[..., Awaitable[str | None]] | None = None,
) -> None:
self.config = config
self.project_id = project_id
self.on_progress = on_progress
self.on_runtime_event = on_runtime_event
self.on_escalation = on_escalation
self.db: aiosqlite.Connection | None = None
self.engine: OPCEngine | None = None
self.services: OfficeServices | None = None
async def __aenter__(self) -> OfficeServices:
if self.config is None:
config_dir = get_opc_home() / "config"
self.config = OPCConfig.load(config_dir) if config_dir.exists() else OPCConfig()
self.engine = OPCEngine(
config=self.config,
project_id=self.project_id,
on_progress=self.on_progress,
on_runtime_event=self.on_runtime_event,
on_escalation=self.on_escalation,
)
self.db = await aiosqlite.connect(str(self.engine.opc_home / "ui_state.db"))
# Wait for a concurrent writer (e.g. a running office-UI server)
# instead of failing after sqlite's 5s default with 'database is locked'.
await self.db.execute("PRAGMA busy_timeout=30000")
agent_store = AgentStore(self.db)
await agent_store.initialize()
chat_store = ChatStore(self.db)
await chat_store.initialize()
event_adapter = EventAdapter()
await self.engine.initialize()
mode_state = ModeState(
exec_mode=await agent_store.get_server_state("exec_mode", "task"),
company_profile=await agent_store.get_server_state("company_profile", "corporate"),
task_preferred_agent=await agent_store.get_server_state("task_preferred_agent", "native"),
)
context = OfficeServiceContext(
engine=self.engine,
agent_store=agent_store,
chat_store=chat_store,
event_adapter=event_adapter,
mode_state=mode_state,
)
self.services = OfficeServices(context)
return self.services
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> None:
if self.engine is not None:
await self.engine.shutdown()
if self.db is not None:
await self.db.close()