feat(persistence): PostgreSQL assistant store, chat persist fixes, gateway scripts

- Add SQLAlchemy Core repos, pg adapter/compat, assistant_store factory, Alembic bootstrap and migration/cutover scripts.

- Harden chat_message writes (NUL scrub for PG), turn_uuid on attempt failure, WS turn_runner fallbacks and gateway executed_turn_uuid init.

- start_gateway: log paths, PS7 stderr handling via cmd, background stdout/stderr redirect; runtime assistant_runtime_log_dir export.

- Ops: clear_all_chat_sessions with PG-only --postgresql and env-gated wipe; clear_postgres_chat_sessions.ps1.

- Tests: SA repos, pg compat, persist fallback, smoke env isolation; CI and docs touch-ups.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-05-14 09:33:50 +08:00
parent 2b32d11f43
commit d14e9d3596
103 changed files with 7574 additions and 1641 deletions

View file

@ -31,10 +31,12 @@ from svc.files.file_attachments import (
from interfaces.ws.common import normalize_ws_attachments
from svc.files.session_export import export_session_json, export_session_markdown
from svc.persistence.sqlite_store import SqliteStore
from svc.persistence.assistant_store import get_assistant_store
from runtime.gateway import OclawGateway
from runtime.plan_agent_v2.switch import v2_feature_enabled
from runtime.types import StandardMessage, normalize_interaction_mode, normalize_requested_specialist
from runtime.chat.history_tool_result_compact import compact_tool_results_in_session_history
from runtime.chat.persist_terminal_fallback import persist_assistant_text_if_turn_missing
def _oclaw_config_path() -> Path:
@ -233,6 +235,23 @@ def _extract_manager_instruction_text(reasoning_content: str) -> str | None:
return instr or None
def _instruction_from_dispatch_assignment_block(text: str) -> str | None:
"""Extract manager instruction only from **comprehensive dispatch** reasoning blocks.
Without this guard, any assistant ``reasoning_content`` that contains ``instruction:\\n`` followed
by the user's literal question (common in thinking traces) would register as a duplicate
``instruction_text`` and :func:`_filter_internal_instruction_user_messages` would drop the real
``user_text`` row — after a failed assistant persist the API could return **zero** messages on refresh.
"""
if not text or "instruction:\n" not in text:
return None
s = str(text)
tl = s.lower()
if "任务分配" not in s and "task assignment" not in tl:
return None
return _extract_manager_instruction_text(s)
def _filter_internal_instruction_user_messages(msgs: list[Any]) -> list[Any]:
"""Hide legacy polluted user rows that equal manager dispatch instruction text."""
instruction_texts: set[str] = set()
@ -249,7 +268,7 @@ def _filter_internal_instruction_user_messages(msgs: list[Any]) -> list[Any]:
if rc:
candidates.append(rc)
for text in candidates:
instr = _extract_manager_instruction_text(text)
instr = _instruction_from_dispatch_assignment_block(text)
if instr:
instruction_texts.add(instr)
if not instruction_texts:
@ -362,6 +381,32 @@ def _resolve_chat_session(store: SqliteStore, ctx: dict[str, Any], session_id: s
return store.get_session_for_user(session_id=session_id, tenant_id=tenant_id, user_id=user_id)
def _resolve_chat_session_allow_claim_orphan(
store: SqliteStore, ctx: dict[str, Any], session_id: str
):
"""Like :func:`_resolve_chat_session`, but if ``chat_session`` exists with **no** ``ui_session_owner`` row
(common after SQLite→PG imports that only copied ``chat_session`` / ``chat_message``), attach the current
user once so ``get_session_for_user`` JOIN succeeds. Does **not** override an existing owner (wrong user).
"""
resolve = _resolve_chat_session
sess = resolve(store, ctx, session_id)
if sess is not None:
return sess
if _is_administrator_chat_viewer(ctx):
return None
sid = str(session_id or "").strip()
tenant_id = str(ctx.get("tenant_id") or "").strip()
user_id = str(ctx.get("user_id") or "").strip()
if not sid or not tenant_id or not user_id:
return None
if store.get_session(sid) is None:
return None
if store.get_ui_session_owner(session_id=sid) is not None:
return None
store.ensure_ui_session_owner(session_id=sid, tenant_id=tenant_id, user_id=user_id)
return resolve(store, ctx, session_id)
def _effective_user_text(*, text: str, attachments: list[dict[str, Any]] | None, store: SqliteStore) -> str:
"""Streamlit 等价:仅有附件时也要落库一条用户消息,否则模型侧无输入。"""
t = (text or "").strip()
@ -825,7 +870,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
offset: int = Query(default=0, ge=0),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
tenant_id = str(ctx.get("tenant_id") or "")
user_id = str(ctx.get("user_id") or "")
@ -855,7 +900,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
tenant_id = str(ctx.get("tenant_id") or "")
user_id = str(ctx.get("user_id") or "")
@ -881,18 +926,18 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
tenant_id = str(ctx.get("tenant_id") or "")
user_id = str(ctx.get("user_id") or "")
sess = _resolve_chat_session(store, ctx, session_id)
sess = _resolve_chat_session_allow_claim_orphan(store, ctx, session_id)
if not sess:
raise HTTPException(status_code=404, detail="session_not_found")
title = str(payload.get("title") or "").strip()[:_SESSION_TITLE_MAX_LEN] or (
"新会话" if _api_lang(store) == "zh" else "New Chat"
)
store.rename_session(session_id, title)
s = _resolve_chat_session(store, ctx, session_id)
s = _resolve_chat_session_allow_claim_orphan(store, ctx, session_id)
return {
"ok": True,
"session": {
@ -908,11 +953,11 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
session_id: str,
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
tenant_id = str(ctx.get("tenant_id") or "")
user_id = str(ctx.get("user_id") or "")
sess = _resolve_chat_session(store, ctx, session_id)
sess = _resolve_chat_session_allow_claim_orphan(store, ctx, session_id)
if not sess:
raise HTTPException(status_code=404, detail="session_not_found")
if _is_administrator_chat_viewer(ctx):
@ -941,11 +986,11 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
tenant_id = str(ctx.get("tenant_id") or "")
user_id = str(ctx.get("user_id") or "")
sess = _resolve_chat_session(store, ctx, session_id)
sess = _resolve_chat_session_allow_claim_orphan(store, ctx, session_id)
if not sess:
raise HTTPException(status_code=404, detail="session_not_found")
last_id = store.get_last_message_id(session_id)
@ -976,11 +1021,11 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
format: str = Query(default="md", description="md or json"),
authorization: str | None = Header(default=None),
) -> Response:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
tenant_id = str(ctx.get("tenant_id") or "")
user_id = str(ctx.get("user_id") or "")
sess = _resolve_chat_session(store, ctx, session_id)
sess = _resolve_chat_session_allow_claim_orphan(store, ctx, session_id)
if not sess:
raise HTTPException(status_code=404, detail="session_not_found")
fmt = (format or "md").strip().lower()
@ -1006,11 +1051,11 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
limit: int = Query(default=_CHAT_MSG_LIMIT, ge=1, le=20000),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
tenant_id = str(ctx.get("tenant_id") or "")
user_id = str(ctx.get("user_id") or "")
sess = _resolve_chat_session(store, ctx, session_id)
sess = _resolve_chat_session_allow_claim_orphan(store, ctx, session_id)
if not sess:
raise HTTPException(status_code=404, detail="session_not_found")
meta = store.get_session_messages_meta(session_id)
@ -1034,9 +1079,9 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
performance remains stable. It only touches `role=tool` chat_message rows.
"""
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
sess = _resolve_chat_session(store, ctx, session_id)
sess = _resolve_chat_session_allow_claim_orphan(store, ctx, session_id)
if not sess:
raise HTTPException(status_code=404, detail="session_not_found")
limit_messages = int(payload.get("limit_messages") or 5000)
@ -1072,9 +1117,9 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
message_id: int,
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
sess = _resolve_chat_session(store, ctx, session_id)
sess = _resolve_chat_session_allow_claim_orphan(store, ctx, session_id)
if not sess:
raise HTTPException(status_code=404, detail="session_not_found")
ok = store.delete_message(session_id=str(session_id), message_id=int(message_id))
@ -1089,12 +1134,12 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
limit: int = Query(default=20, ge=1, le=200),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_administrator_chat_viewer(ctx)
tenant_id = str(ctx.get("tenant_id") or "")
user_id = str(ctx.get("user_id") or "")
sess = _resolve_chat_session(store, ctx, session_id)
sess = _resolve_chat_session_allow_claim_orphan(store, ctx, session_id)
if not sess:
raise HTTPException(status_code=404, detail="session_not_found")
after_iso = str(after or "").strip()
@ -1138,12 +1183,12 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
max_chars: int = Query(default=80_000, ge=1_000, le=200_000),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_administrator_chat_viewer(ctx)
tenant_id = str(ctx.get("tenant_id") or "")
_ = str(ctx.get("user_id") or "")
sess = _resolve_chat_session(store, ctx, session_id)
sess = _resolve_chat_session_allow_claim_orphan(store, ctx, session_id)
if not sess:
raise HTTPException(status_code=404, detail="session_not_found")
wiki_root = _wiki_root_from_config()
@ -1177,11 +1222,11 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
session_id: str,
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
tenant_id = str(ctx.get("tenant_id") or "")
user_id = str(ctx.get("user_id") or "")
sess = _resolve_chat_session(store, ctx, session_id)
sess = _resolve_chat_session_allow_claim_orphan(store, ctx, session_id)
if not sess:
raise HTTPException(status_code=404, detail="session_not_found")
s_mm, s_em = _resolve_session_dialog_chat_settings(
@ -1212,11 +1257,11 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
tenant_id = str(ctx.get("tenant_id") or "")
user_id = str(ctx.get("user_id") or "")
sess = _resolve_chat_session(store, ctx, session_id)
sess = _resolve_chat_session_allow_claim_orphan(store, ctx, session_id)
if not sess:
raise HTTPException(status_code=404, detail="session_not_found")
memory_mode = _normalize_memory_mode(payload)
@ -1254,7 +1299,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
def api_chat_user_mode_get(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
tenant_id = str(ctx.get("tenant_id") or "")
user_id = str(ctx.get("user_id") or "")
@ -1274,7 +1319,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
tenant_id = str(ctx.get("tenant_id") or "")
user_id = str(ctx.get("user_id") or "")
@ -1311,7 +1356,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
offset: int = Query(default=0, ge=0),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_administrator_chat_viewer(ctx)
tenant_id = str(ctx.get("tenant_id") or "")
@ -1333,7 +1378,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
offset: int = Query(default=0, ge=0),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_administrator_chat_viewer(ctx)
tenant_id = str(ctx.get("tenant_id") or "")
@ -1353,7 +1398,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
limit: int = Query(default=200, ge=20, le=1000),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
lang = _api_lang(store)
labels = _dispatch_reason_labels_with_overrides(store)
@ -1392,7 +1437,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
def api_chat_get_ui_lang(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
_ = resolve_auth(store, authorization)
return {"ok": True, "lang": _api_lang(store)}
@ -1402,7 +1447,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
_ = resolve_auth(store, authorization)
lang = str(payload.get("lang") or "").strip().lower()
if lang not in ("zh", "en"):
@ -1414,7 +1459,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
def api_chat_get_dispatch_reason_labels(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_administrator_chat_viewer(ctx)
raw = str(store.get_setting(_DISPATCH_REASON_LABELS_SETTING_KEY) or "").strip()
@ -1439,7 +1484,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
body = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_administrator_chat_viewer(ctx)
overrides = body.get("overrides")
@ -1465,7 +1510,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
def api_chat_get_specialist_flags(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_administrator_chat_viewer(ctx)
flags = _specialist_flags_with_overrides(store)
@ -1483,7 +1528,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
body = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_administrator_chat_viewer(ctx)
raw_flags = body.get("flags")
@ -1507,7 +1552,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
channel: str,
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_administrator_chat_viewer(ctx)
ch = _normalize_channel_dispatch_channel(channel)
@ -1533,7 +1578,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
body = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_administrator_chat_viewer(ctx)
ch = _normalize_channel_dispatch_channel(channel)
@ -1553,7 +1598,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
def api_chat_get_attachment_limits(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_administrator_chat_viewer(ctx)
limits = _tabular_limits_from_oclaw_config()
@ -1565,7 +1610,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
body = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_administrator_chat_viewer(ctx)
raw = body.get("limits")
@ -1639,7 +1684,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
def api_chat_profile_get(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
tenant_id = str(ctx.get("tenant_id") or "").strip()
user_id = str(ctx.get("user_id") or "").strip()
@ -1671,7 +1716,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
tenant_id = str(ctx.get("tenant_id") or "").strip()
user_id = str(ctx.get("user_id") or "").strip()
@ -1702,7 +1747,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
authorization: str | None = Header(default=None),
file: UploadFile = File(...),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
tenant_id = str(ctx.get("tenant_id") or "").strip()
user_id = str(ctx.get("user_id") or "").strip()
@ -1735,7 +1780,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
def api_chat_profile_avatar_delete(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
tenant_id = str(ctx.get("tenant_id") or "").strip()
user_id = str(ctx.get("user_id") or "").strip()
@ -1749,9 +1794,9 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
session_id: str,
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
sess = _resolve_chat_session(store, ctx, session_id)
sess = _resolve_chat_session_allow_claim_orphan(store, ctx, session_id)
if not sess:
raise HTTPException(status_code=404, detail="session_not_found")
sid = str(session_id)
@ -1766,7 +1811,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
attachment_id: str,
authorization: str | None = Header(default=None),
) -> Response:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
tenant_id = str(ctx.get("tenant_id") or "").strip()
user_id = str(ctx.get("user_id") or "").strip()
@ -1828,7 +1873,7 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
limit_messages: int = Query(default=50_000, ge=1, le=500_000),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_administrator_chat_viewer(ctx)
tenant_id = str(ctx.get("tenant_id") or "").strip()
@ -1854,12 +1899,12 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
tenant_id = str(ctx.get("tenant_id") or "")
user_id = str(ctx.get("user_id") or "")
role = str(ctx.get("role") or "member")
sess = _resolve_chat_session(store, ctx, session_id)
sess = _resolve_chat_session_allow_claim_orphan(store, ctx, session_id)
if not sess:
raise HTTPException(status_code=404, detail="session_not_found")
text_raw = str(payload.get("text") or "").strip()
@ -1971,6 +2016,13 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
specialist_executor_factory=specialist_factory,
)
reply = gw_result.reply_text
persist_assistant_text_if_turn_missing(
store=store,
session_id=str(session_id),
turn_uuid=str(getattr(gw_result, "turn_uuid", "") or ""),
final_text=str(reply or ""),
log_prefix="admin_chat_http_text_fallback_persisted",
)
except GenerationInterrupted:
msg = "已中断回答。" if lang == "zh" else "Response stopped."
store.add_message(session_id=session_id, role="assistant", content=msg)
@ -1992,12 +2044,12 @@ def include_chat_routes(router: APIRouter, *, resolve_auth: Callable[[SqliteStor
) -> StreamingResponse:
"""Server-Sent Events: token deltas + progress + tool_ui, then done (assistant persisted by run_turn)."""
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
tenant_id = str(ctx.get("tenant_id") or "")
user_id = str(ctx.get("user_id") or "")
role = str(ctx.get("role") or "member")
sess = _resolve_chat_session(store, ctx, session_id)
sess = _resolve_chat_session_allow_claim_orphan(store, ctx, session_id)
if not sess:
raise HTTPException(status_code=404, detail="session_not_found")
text_raw = str(payload.get("text") or "").strip()

View file

@ -21,6 +21,7 @@ from runtime.agents.specialists import (
)
from svc.config.paths import db_path
from runtime.orchestration.evaluation import eval_summary
from svc.persistence.assistant_store import get_assistant_store
from svc.persistence.sqlite_store import (
LLM_BUILTIN_OLLAMA_PROFILE_ID,
SqliteStore,
@ -169,7 +170,7 @@ def include_model_mgmt_routes(
def api_models_state(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
uid = str(ctx.get("user_id") or "").strip()
@ -224,7 +225,7 @@ def include_model_mgmt_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
# 与能进入控制台一致:切换「当前选用」不写密钥,仅需读权限即可。
_require_permission(ctx, "admin:read")
@ -242,7 +243,7 @@ def include_model_mgmt_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_models_mutate(ctx)
profiles = store.list_llm_profiles(visible_only=True, **_models_list_kwargs(ctx))
@ -268,7 +269,7 @@ def include_model_mgmt_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_models_mutate(ctx)
profiles = store.list_llm_profiles(visible_only=True, **_models_list_kwargs(ctx))
@ -285,7 +286,7 @@ def include_model_mgmt_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_models_mutate(ctx)
profiles = store.list_llm_profiles(visible_only=True, **_models_list_kwargs(ctx))
@ -311,7 +312,7 @@ def include_model_mgmt_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
visible = _truthy(payload.get("chat_model_selector_visible"), default=True)
@ -324,7 +325,7 @@ def include_model_mgmt_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_models_mutate(ctx)
name = str(payload.get("name") or "").strip() or "新配置"
@ -354,7 +355,7 @@ def include_model_mgmt_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_models_mutate(ctx)
pid = str(profile_id or "").strip()
@ -401,7 +402,7 @@ def include_model_mgmt_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_models_mutate(ctx)
pid = str(profile_id or "").strip()
@ -428,7 +429,7 @@ def include_model_mgmt_routes(
profile_id: str,
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_models_mutate(ctx)
pid = str(profile_id or "").strip()
@ -446,7 +447,7 @@ def include_model_mgmt_routes(
def api_models_members(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_grant_manager(ctx)
tid = str(ctx.get("tenant_id") or "").strip()
@ -481,7 +482,7 @@ def include_model_mgmt_routes(
profile_id: str = Query(...),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_grant_manager(ctx)
tid = str(ctx.get("tenant_id") or "").strip()
@ -497,7 +498,7 @@ def include_model_mgmt_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_grant_manager(ctx)
tid = str(ctx.get("tenant_id") or "").strip()
@ -524,7 +525,7 @@ def include_model_mgmt_routes(
profile_id: str = Query(...),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_grant_manager(ctx)
tid = str(ctx.get("tenant_id") or "").strip()
@ -539,7 +540,7 @@ def include_model_mgmt_routes(
profile_id: str = Query(..., description="llm_profile id"),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_grant_manager(ctx)
tid = str(ctx.get("tenant_id") or "").strip()
@ -555,7 +556,7 @@ def include_model_mgmt_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_grant_manager(ctx)
tid = str(ctx.get("tenant_id") or "").strip()
@ -589,7 +590,7 @@ def include_model_mgmt_routes(
user_id: str = Query(...),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_grant_manager(ctx)
tid = str(ctx.get("tenant_id") or "").strip()
@ -606,7 +607,7 @@ def include_model_mgmt_routes(
limit_logs: int = Query(default=100, ge=1, le=500),
limit_summary: int = Query(default=500, ge=1, le=5000),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
summary = eval_summary(store, limit=limit_summary)
@ -630,7 +631,7 @@ def include_model_mgmt_routes(
format: str = Query(default="csv", description="csv or json"),
limit: int = Query(default=100_000, ge=1, le=200_000),
) -> Response:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
fmt = str(format or "csv").strip().lower()

View file

@ -34,6 +34,7 @@ from runtime.orchestration.vector_store import read_vector_memory_runtime
from svc.config.paths import PROJECT_ROOT, db_path
from svc.config.passwords import load_expected_password
from svc.persistence.sqlite_store import SqliteStore
from svc.persistence.assistant_store import get_assistant_store
from runtime.agents.specialists import discover_specialist_ids, parse_agent_profile_bindings
from runtime.tools.mcp.installer import (
_safe_server_id,
@ -442,7 +443,7 @@ def build_admin_router() -> APIRouter:
def api_internal_tools_reload(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
@ -467,7 +468,7 @@ def build_admin_router() -> APIRouter:
def api_tools_exposure_trace_setting_get(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
raw = str(store.get_setting("AIA_TRACE_TOOL_EXPOSURE_PLAN") or "").strip().lower()
@ -479,7 +480,7 @@ def build_admin_router() -> APIRouter:
payload: dict[str, Any] | None = Body(default=None),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
payload = payload or {}
@ -501,7 +502,7 @@ def build_admin_router() -> APIRouter:
role: str | None = Query(default=None),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
from runtime.tools.exposure_plan import build_internal_tool_specs
@ -539,7 +540,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
"""One-shot diagnostic summary for role-based tool exposure."""
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
@ -604,7 +605,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
"""Preview the final tools injected to LLM for a role (internal + MCP + wire policy)."""
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
from runtime.tools.exposure_plan import build_llm_tools_plan
@ -726,7 +727,7 @@ def build_admin_router() -> APIRouter:
@router.get("/admin/api/channels")
def api_channels(authorization: str | None = Header(default=None)) -> dict[str, Any]:
ctx = _resolve_auth(SqliteStore(db_path()), authorization)
ctx = _resolve_auth(get_assistant_store(), authorization)
_require_permission(ctx, "admin:read")
reg = build_channel_registry()
items = [{"name": k, "type": reg[k].__class__.__name__} for k in sorted(reg.keys())]
@ -734,7 +735,7 @@ def build_admin_router() -> APIRouter:
@router.get("/admin/api/stack/status")
def api_stack_status(authorization: str | None = Header(default=None)) -> dict[str, Any]:
ctx = _resolve_auth(SqliteStore(db_path()), authorization)
ctx = _resolve_auth(get_assistant_store(), authorization)
_require_permission(ctx, "admin:read")
items = []
for s in status_services():
@ -786,7 +787,7 @@ def build_admin_router() -> APIRouter:
@router.post("/admin/api/stack/up")
def api_stack_up(channel: str = "wecom", authorization: str | None = Header(default=None)) -> dict[str, Any]:
ctx = _resolve_auth(SqliteStore(db_path()), authorization)
ctx = _resolve_auth(get_assistant_store(), authorization)
_require_permission(ctx, "admin:runtime:write")
# Use same defaults as CLI; this starts detached processes and writes runtime state.
import argparse
@ -807,7 +808,7 @@ def build_admin_router() -> APIRouter:
@router.post("/admin/api/stack/down")
def api_stack_down(authorization: str | None = Header(default=None)) -> dict[str, Any]:
ctx = _resolve_auth(SqliteStore(db_path()), authorization)
ctx = _resolve_auth(get_assistant_store(), authorization)
_require_permission(ctx, "admin:runtime:write")
import argparse
@ -884,14 +885,14 @@ def build_admin_router() -> APIRouter:
@router.get("/admin/api/runtime/anomalies")
def api_runtime_anomalies(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
return _collect_runtime_anomalies(store)
@router.post("/admin/api/runtime/cleanup")
def api_runtime_cleanup(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:runtime:write")
killed: list[dict[str, Any]] = []
@ -928,7 +929,7 @@ def build_admin_router() -> APIRouter:
@router.get("/admin/api/runtime/prewarm/status")
def api_runtime_prewarm_status(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
return runtime_prewarm_status(store=store)
@ -938,7 +939,7 @@ def build_admin_router() -> APIRouter:
payload: dict[str, Any] | None = Body(default=None),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:runtime:write")
body = payload or {}
@ -950,7 +951,7 @@ def build_admin_router() -> APIRouter:
def _run() -> None:
try:
_ = run_runtime_prewarm(reason=reason, store=SqliteStore(db_path()))
_ = run_runtime_prewarm(reason=reason, store=get_assistant_store())
except Exception:
pass
@ -963,14 +964,14 @@ def build_admin_router() -> APIRouter:
role: str | None = Query(default=None),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
return runtime_prewarm_prompts_snapshot(store=store, role=str(role or "").strip().lower() or None)
@router.get("/admin/api/runtime/scan-artifacts")
def api_runtime_scan_artifacts(authorization: str | None = Header(default=None)) -> dict[str, Any]:
ctx = _resolve_auth(SqliteStore(db_path()), authorization)
ctx = _resolve_auth(get_assistant_store(), authorization)
_require_permission(ctx, "admin:read")
root = (PROJECT_ROOT / "runtime" / "data" / "scan").resolve()
allowed_prefixes = ("history_entries_", "state_scan_")
@ -995,7 +996,7 @@ def build_admin_router() -> APIRouter:
@router.post("/admin/api/runtime/scan-artifacts/cleanup")
def api_runtime_scan_artifacts_cleanup(authorization: str | None = Header(default=None)) -> dict[str, Any]:
ctx = _resolve_auth(SqliteStore(db_path()), authorization)
ctx = _resolve_auth(get_assistant_store(), authorization)
_require_permission(ctx, "admin:runtime:write")
root = (PROJECT_ROOT / "runtime" / "data" / "scan").resolve()
allowed_prefixes = ("history_entries_", "state_scan_")
@ -1017,7 +1018,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
body = payload or {}
ctx = _resolve_auth(SqliteStore(db_path()), authorization)
ctx = _resolve_auth(get_assistant_store(), authorization)
_require_permission(ctx, "admin:runtime:write")
root = (PROJECT_ROOT / "runtime" / "data" / "scan").resolve()
try:
@ -1068,7 +1069,7 @@ def build_admin_router() -> APIRouter:
scope: str | None = Query(default=None),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:read")
rows = store.list_tenants(limit=500)
@ -1083,7 +1084,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
name = str(payload.get("name") or "").strip() or "Team"
@ -1096,7 +1097,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
tenant_id = str(payload.get("tenant_id") or "").strip()
@ -1128,7 +1129,7 @@ def build_admin_router() -> APIRouter:
user_id: str | None = Query(default=None),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:user:read")
_require_tenant_scope(ctx, tenant_id)
@ -1152,7 +1153,7 @@ def build_admin_router() -> APIRouter:
include_inactive: int = Query(default=1),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:user:read")
_require_tenant_scope(ctx, tenant_id)
@ -1172,7 +1173,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:user:write")
tenant_id = str(payload.get("tenant_id") or "").strip()
@ -1234,7 +1235,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:user:write")
tenant_id = str(payload.get("tenant_id") or "").strip()
@ -1269,7 +1270,7 @@ def build_admin_router() -> APIRouter:
limit: int = Query(default=1000),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:user:read")
_require_tenant_scope(ctx, tenant_id)
@ -1288,7 +1289,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:user:write")
tenant_id = str(payload.get("tenant_id") or "").strip() or str(ctx.get("tenant_id") or "").strip()
@ -1327,7 +1328,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:user:write")
tenant_id = str(payload.get("tenant_id") or "").strip()
@ -1366,7 +1367,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:user:delete")
tenant_id = str(payload.get("tenant_id") or "").strip()
@ -1435,7 +1436,7 @@ def build_admin_router() -> APIRouter:
user_id: str = Query(default=""),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
rmode = _workspace_path_policy_read_mode(ctx)
if rmode is None:
@ -1468,7 +1469,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
wmode = _workspace_path_policy_write_mode(ctx)
if wmode is None:
@ -1518,7 +1519,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:user:delete")
tenant_id = str(payload.get("tenant_id") or "").strip()
@ -1557,7 +1558,7 @@ def build_admin_router() -> APIRouter:
@router.get("/admin/api/bind-codes")
def api_bind_codes(tenant_id: str, authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:user:read")
_require_tenant_scope(ctx, tenant_id)
@ -1570,7 +1571,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:user:write")
tenant_id = str(payload.get("tenant_id") or "").strip()
@ -1588,7 +1589,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:user:write")
tenant_id = str(payload.get("tenant_id") or "").strip()
@ -1608,7 +1609,7 @@ def build_admin_router() -> APIRouter:
@router.get("/admin/api/wecom/config")
def api_wecom_config(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
return {
@ -1625,7 +1626,7 @@ def build_admin_router() -> APIRouter:
@router.get("/admin/api/wecom/health")
def api_wecom_health(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
svc = next((s for s in status_services() if str(s.name) == "channel:wecom"), None)
@ -1672,7 +1673,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
store.set_setting("wecom_mode", "bot_api")
@ -1695,7 +1696,7 @@ def build_admin_router() -> APIRouter:
@router.post("/admin/api/wecom/unbind")
def api_wecom_unbind(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
with store._connect() as conn:
@ -1722,7 +1723,7 @@ def build_admin_router() -> APIRouter:
session_id: str | None = Query(default=None),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
rows = store.list_agent_audit_logs(limit=200, session_id=session_id)
@ -1734,7 +1735,7 @@ def build_admin_router() -> APIRouter:
limit: int = Query(default=80),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
rows = store.list_session_tool_health(session_id=session_id, limit=limit)
@ -1746,7 +1747,7 @@ def build_admin_router() -> APIRouter:
session_id: str | None = Query(default=None),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
if not session_id:
@ -1763,7 +1764,7 @@ def build_admin_router() -> APIRouter:
limit: int = Query(default=80),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
tenant_id = str(ctx.get("tenant_id") or "")
@ -1794,7 +1795,7 @@ def build_admin_router() -> APIRouter:
include_attempts: int = Query(default=1),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
tenant_id = str(ctx.get("tenant_id") or "")
@ -1840,7 +1841,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
"""Return a best-effort replay bundle for a single turn (trace_id)."""
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
sid = str(session_id or "").strip()
@ -1940,7 +1941,7 @@ def build_admin_router() -> APIRouter:
@router.get("/admin/api/plugins")
def api_plugins(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:user:write")
rows = store.list_tool_plugins()
@ -1948,7 +1949,7 @@ def build_admin_router() -> APIRouter:
@router.get("/admin/api/tool-policy")
def api_tool_policy(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
# Oclaw takeover: legacy tool-policy switches are disconnected (kept in DB for later).
@ -2047,7 +2048,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
# Oclaw takeover: legacy tool-policy switches are disconnected (kept in DB for later).
@ -2240,7 +2241,7 @@ def build_admin_router() -> APIRouter:
@router.get("/admin/api/mcp/servers")
def api_mcp_servers(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
rows = McpRegistry(store).list_servers(enabled_only=False)
@ -2253,7 +2254,7 @@ def build_admin_router() -> APIRouter:
@router.get("/admin/api/mcp/export")
def api_mcp_export(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
return {
@ -2267,7 +2268,7 @@ def build_admin_router() -> APIRouter:
limit: int = Query(default=20),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
return {"ok": True, "items": store.list_mcp_install_failure_summary(limit=limit)}
@ -2278,7 +2279,7 @@ def build_admin_router() -> APIRouter:
limit: int = Query(default=200),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
return {
@ -2292,7 +2293,7 @@ def build_admin_router() -> APIRouter:
role: str | None = Query(default=None),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
from svc.llm.tool_wire_policy import build_tool_wire_snapshot
@ -2307,7 +2308,7 @@ def build_admin_router() -> APIRouter:
from svc.llm.tool_wire_policy import SETTINGS_KEY_ADMIN_CONFIG, load_merged_admin_config
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
raw = store.get_setting(SETTINGS_KEY_ADMIN_CONFIG)
@ -2359,7 +2360,7 @@ def build_admin_router() -> APIRouter:
from svc.llm.tool_wire_policy import SETTINGS_KEY_ROLE_MODE_BY_ROLE
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
role = str(payload.get("role") or "").strip().lower()
@ -2398,7 +2399,7 @@ def build_admin_router() -> APIRouter:
) -> dict[str, Any]:
from svc.llm.tool_wire_policy import SETTINGS_KEY_PENALTY_STATE, SETTINGS_KEY_PENALTY_STATE_BY_ROLE
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
r = str(role or "").strip().lower()
@ -2440,7 +2441,7 @@ def build_admin_router() -> APIRouter:
)
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
role = str(payload.get("role") or "").strip().lower()
@ -2511,7 +2512,7 @@ def build_admin_router() -> APIRouter:
)
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
try:
@ -2570,7 +2571,7 @@ def build_admin_router() -> APIRouter:
per_source_limit: int = Query(default=6),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
query = str(q or "").strip()
@ -2585,7 +2586,7 @@ def build_admin_router() -> APIRouter:
refresh: int = Query(default=0),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
items = trending_mcp_market(force_refresh=bool(int(refresh or 0)), per_source_limit=per_source_limit)
@ -2593,14 +2594,14 @@ def build_admin_router() -> APIRouter:
@router.get("/admin/api/mcp/dependencies")
def api_mcp_dependencies(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
return {"ok": True, "items": detect_local_dependencies()}
@router.get("/admin/api/mcp/binding")
def api_mcp_binding(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
available = _ordered_mcp_roles()
@ -2627,7 +2628,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
available = _ordered_mcp_roles()
@ -2661,7 +2662,7 @@ def build_admin_router() -> APIRouter:
@router.get("/admin/api/mcp/specialists")
def api_mcp_specialists(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
available = _ordered_mcp_roles()
@ -2679,7 +2680,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
available = _ordered_mcp_roles()
@ -2705,7 +2706,7 @@ def build_admin_router() -> APIRouter:
@router.get("/admin/api/experts")
def api_experts_list(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
items = [_serialize_expert_row(x) for x in list_experts()]
@ -2717,7 +2718,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
expert_id = normalize_expert_id(payload.get("id"))
@ -2755,7 +2756,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
files_raw = payload.get("files") if isinstance(payload.get("files"), dict) else {}
@ -2788,7 +2789,7 @@ def build_admin_router() -> APIRouter:
@router.delete("/admin/api/experts/{expert_id}")
def api_experts_delete(expert_id: str, authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
eid = normalize_expert_id(expert_id)
@ -2814,7 +2815,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
source_type = str(payload.get("source_type") or "").strip().lower()
@ -2882,7 +2883,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
server_id = str(payload.get("server_id") or "").strip()
@ -2952,7 +2953,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
server_id = str(payload.get("server_id") or "").strip()
@ -3073,7 +3074,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
server_id = str(payload.get("server_id") or "").strip()
@ -3133,7 +3134,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
server_id = str(payload.get("server_id") or "").strip()
@ -3163,7 +3164,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
source_type = str(payload.get("source_type") or "").strip().lower()
@ -3192,7 +3193,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
server_id = str(payload.get("server_id") or "").strip()
@ -3217,7 +3218,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
apply_gateway_mcp_env_to_os()
@ -3268,7 +3269,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
apply_gateway_mcp_env_to_os()
@ -3327,7 +3328,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
apply_gateway_mcp_env_to_os()
@ -3347,7 +3348,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
enabled_only = bool(payload.get("enabled_only", True))
@ -3372,7 +3373,7 @@ def build_admin_router() -> APIRouter:
run the same health + tools/list + replace flow as ``check-all`` (only those servers).
"""
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
apply_gateway_mcp_env_to_os()
@ -3426,7 +3427,7 @@ def build_admin_router() -> APIRouter:
def api_mcp_e2e_check(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
apply_gateway_mcp_env_to_os()
@ -3520,7 +3521,7 @@ def build_admin_router() -> APIRouter:
limit: int = Query(default=100),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
if tenant_id:
@ -3535,7 +3536,7 @@ def build_admin_router() -> APIRouter:
limit: int = Query(default=300),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
if tenant_id:
@ -3567,7 +3568,7 @@ def build_admin_router() -> APIRouter:
offset: int = Query(default=0),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
if tenant_id:
@ -3582,7 +3583,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:memory:write")
memory_id = str(payload.get("memory_id") or "").strip()
@ -3597,7 +3598,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:memory:write")
tenant_id = str(payload.get("tenant_id") or "").strip() or None
@ -3635,7 +3636,7 @@ def build_admin_router() -> APIRouter:
@router.get("/admin/api/memory/config")
def api_memory_config(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
runtime = read_vector_memory_runtime(store)
@ -3666,7 +3667,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:memory:write")
store.set_setting("MEMORY_VECTOR_ENABLED", "1" if str(payload.get("enabled") or "").lower() in ("1", "true", "yes", "on") else "0")
@ -3704,7 +3705,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:memory:write")
try:
@ -3717,7 +3718,7 @@ def build_admin_router() -> APIRouter:
@router.post("/admin/api/secrets/migrate")
def api_secrets_migrate(authorization: str | None = Header(default=None)) -> dict[str, Any]:
"""Migrate legacy b64 secrets to fernet (requires AIA_ASSISTANT_MASTER_KEY)."""
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:tenant:write")
try:
@ -3738,7 +3739,7 @@ def build_admin_router() -> APIRouter:
@router.get("/admin/api/secrets/status")
def api_secrets_status(authorization: str | None = Header(default=None)) -> dict[str, Any]:
"""Expose legacy secret stats for admin UI warnings."""
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:read")
stats = store.legacy_secret_stats()
@ -3749,14 +3750,14 @@ def build_admin_router() -> APIRouter:
@router.post("/admin/api/auth/bootstrap")
def api_auth_bootstrap() -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
_ensure_admin_bootstrap(store)
return {"ok": True}
@router.post("/admin/api/auth/login")
def api_auth_login(payload: dict[str, Any] | None = Body(default=None)) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
_ensure_admin_bootstrap(store)
tenant_id = str(payload.get("tenant_id") or "").strip()
if not tenant_id:
@ -3820,13 +3821,13 @@ def build_admin_router() -> APIRouter:
@router.get("/admin/api/auth/me")
def api_auth_me(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
return {"ok": True, "session": ctx}
@router.post("/admin/api/auth/logout")
def api_auth_logout(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
token = _extract_bearer(authorization)
if token:
store.revoke_auth_session(session_token_hash=_sha256_hex(token))
@ -3838,7 +3839,7 @@ def build_admin_router() -> APIRouter:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_ops_ai_auth(store, authorization)
_require_permission(ctx, "admin:read")
@ -4045,7 +4046,7 @@ def build_admin_router() -> APIRouter:
offset: int = Query(default=0),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_ops_ai_auth(store, authorization)
_require_permission(ctx, "admin:read")
lim = max(1, min(int(limit), 200))
@ -4068,7 +4069,7 @@ def build_admin_router() -> APIRouter:
@router.get("/admin/api/ops-ai/health")
def api_ops_ai_health(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_ops_ai_auth(store, authorization)
_require_permission(ctx, "admin:read")
return {"ok": True, "service": "oclaw", "component": "ops-ai", "status": "ok", "caller": str(ctx.get("username") or "")}
@ -4082,7 +4083,7 @@ def build_admin_router() -> APIRouter:
status: str | None = Query(default=None),
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = _resolve_auth(store, authorization)
_require_permission(ctx, "admin:user:write")
a = str(action or "").strip()

View file

@ -36,6 +36,7 @@ from runtime.skills_market import get_market_adapter, normalize_skill_market_pro
from runtime.tools.skills_runtime.subprocess_exec import run_skill_runtime_entry
from svc.config.paths import db_path
from svc.persistence.sqlite_store import SqliteStore
from svc.persistence.assistant_store import get_assistant_store
_SKILL_MARKET_PROVIDER_KEY = "AIA_SKILL_MARKET_PROVIDER"
@ -85,7 +86,7 @@ def include_skill_routes(
@sk.get("")
def api_skills_list(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
items = list_skills_with_status(store=store)
@ -93,7 +94,7 @@ def include_skill_routes(
@sk.get("/mode")
def api_skills_mode_get(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
raw_prompt = str(store.get_setting("AIA_SKILLS_PROMPT_IN_SYSTEM") or "").strip().lower()
@ -114,7 +115,7 @@ def include_skill_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
if "prompt_in_system" in payload:
@ -153,7 +154,7 @@ def include_skill_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
source_dir = str(payload.get("source_dir") or "").strip()
@ -187,7 +188,7 @@ def include_skill_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
archive_url = str(payload.get("archive_url") or "").strip()
@ -221,7 +222,7 @@ def include_skill_routes(
limit: int | None = None,
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
query = str(q or "").strip()
@ -236,7 +237,7 @@ def include_skill_routes(
slug: str | None = None,
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
s = str(slug or "").strip()
@ -252,7 +253,7 @@ def include_skill_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
s = str(payload.get("slug") or "").strip()
@ -309,7 +310,7 @@ def include_skill_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
name = str(payload.get("name") or "").strip()
@ -346,7 +347,7 @@ def include_skill_routes(
@sk.get("/binding")
def api_skills_binding_get(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_tenant_write(ctx)
roles, mapping, _valid = _normalized_skill_binding(store)
@ -368,7 +369,7 @@ def include_skill_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_tenant_write(ctx)
if "enabled" in payload:
@ -403,7 +404,7 @@ def include_skill_routes(
@sk.get("/effective")
def api_skills_effective(authorization: str | None = Header(default=None)) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
roles, mapping, _valid = _normalized_skill_binding(store)
@ -494,7 +495,7 @@ def include_skill_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
name = str(payload.get("name") or "").strip()
@ -538,7 +539,7 @@ def include_skill_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
name = str(payload.get("name") or "").strip()
@ -554,7 +555,7 @@ def include_skill_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
name = str(payload.get("name") or "").strip()
@ -570,7 +571,7 @@ def include_skill_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
name = str(payload.get("name") or "").strip()
@ -604,7 +605,7 @@ def include_skill_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
auto_name = str(payload.get("name") or "")
@ -642,7 +643,7 @@ def include_skill_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
source = str(payload.get("source") or "").strip().lower()
@ -706,7 +707,7 @@ def include_skill_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
name = str(payload.get("name") or "").strip()
@ -736,7 +737,7 @@ def include_skill_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
_payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
items = list_skills_with_status(store=store)
@ -798,7 +799,7 @@ def include_skill_routes(
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
payload = payload or {}
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
name = str(payload.get("name") or "").strip()
@ -850,7 +851,7 @@ def include_skill_routes(
include_execution: bool = False,
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
store = SqliteStore(db_path())
store = get_assistant_store()
ctx = resolve_auth(store, authorization)
_require_admin(ctx)
manifests = discover_workspace_skill_manifests()

View file

@ -688,6 +688,28 @@ function _buildRenderRows(msgs) {
return rows;
}
/** True when persisted history does not show a completed assistant reply for the latest turn (e.g. ends with user only). */
function _needsWsTextFallbackFromRenderRows(renderRows) {
const rows = Array.isArray(renderRows) ? renderRows : [];
if (!rows.length) return true;
const last = rows[rows.length - 1];
const lr = String((last && last.role) || "").toLowerCase();
if (lr === "user") return true;
if (lr !== "assistant") return true;
const items = last && Array.isArray(last._items) ? last._items : [];
for (const it of items) {
const k = String((it && it.kind) || "").toLowerCase();
if (k === "assistant_text" && String((it && it.text) || "").trim()) return false;
if (k === "reasoning" && String((it && it.text) || "").trim()) return false;
if (k === "tool_result") {
if (String((it && it.text) || "").trim()) return false;
if (Array.isArray(it.attachments) && it.attachments.length) return false;
}
}
if (String((last && last.content) || "").trim()) return false;
return true;
}
function t(key, vars) {
let s = (I18N[currentLang] && I18N[currentLang][key]) || (I18N.en && I18N.en[key]) || key;
if (vars && typeof s === "string") {
@ -3739,25 +3761,35 @@ async function renderChatUi() {
},
};
const loadMessagesForActive = async () => {
const loadMessagesForActive = async (opts = {}) => {
loadMessagesForActive._rid = (loadMessagesForActive._rid || 0) + 1;
const rid = loadMessagesForActive._rid;
shouldFollowMessages = true;
if (!activeId) {
loadMessagesForActive._needsWsTextFallback = true;
messagesEl.innerHTML = "";
statusBar.textContent = sessions.length ? "" : t("chat.noSessions");
if (!sessions.length) messagesEl.appendChild(el("div", { class: "muted", text: t("chat.empty") }));
return;
return 0;
}
statusBar.textContent = t("chat.loading");
loadMessagesForActive._needsWsTextFallback = true;
try {
const resp = await apiGet(
`/admin/api/chat/sessions/${encodeURIComponent(activeId)}/messages?limit=${CHAT_MESSAGES_FETCH_LIMIT}`,
);
if (rid !== loadMessagesForActive._rid) return;
if (rid !== loadMessagesForActive._rid) return 0;
const msgs = Array.isArray(resp.messages) ? resp.messages : [];
const renderRows = _buildRenderRows(msgs);
loadMessagesForActive._needsWsTextFallback = _needsWsTextFallbackFromRenderRows(renderRows);
const total = intOr(resp.message_count, msgs.length);
// End-of-turn hydrate: if the server returns nothing renderable yet (PG commit lag, transient API
// glitch) but the caller still has a live stream worth keeping, do not wipe messagesEl — that
// caused "stream flashes then entire dialog is empty" when reasoning/tool-output mode reloads.
if (!renderRows.length && opts.keepDomIfNoHistoryRows) {
statusBar.textContent = "";
return 0;
}
messagesEl.innerHTML = "";
if (total > msgs.length) {
messagesEl.appendChild(
@ -3776,10 +3808,13 @@ async function renderChatUi() {
}
statusBar.textContent = "";
scrollMessagesToBottom(true);
return renderRows.length;
} catch (e) {
// Keep existing message list on reload failure (e.g. toggle reload races),
// so users don't perceive "messages disappeared".
loadMessagesForActive._needsWsTextFallback = true;
statusBar.textContent = `${t("chat.error")}: ${String(e)}`;
return -1;
}
};
@ -4847,25 +4882,39 @@ ${autoLimit ? `<div style="margin-top:8px;"><span class="muted">auto-added claus
renderStreamComposite();
_markStreamTerminal("end", t("chat.status.end"));
ok = true;
} else if (adminChatShowToolOutput) {
// WS final message may only contain the last assistant_text snapshot and
// omit persisted reasoning rows. Hydrate from history to avoid
// end-of-turn "reasoning disappears until refresh".
try {
if (streamRow && streamRow.parentNode) streamRow.remove();
} catch (_) {}
await loadMessagesForActive();
} else if (adminChatShowToolOutput || sawStreamToolRefAttachments) {
// Hydrate from history for reasoning / tool panels / ref attachments. If the DB has not
// yet persisted the assistant reply (or only has the user row), n>0 used to skip
// appendFinalAssistant and removed the stream bubble — leaving an empty pane like the
// user screenshot. Use _needsWsTextFallback when WS still holds usable text.
const hadStream =
hasRealStreamText ||
(Array.isArray(chatStreamSegments) && chatStreamSegments.length > 0) ||
!!String(chatStream || "").trim();
const fbLine = chatStream || extractWsAssistantText(payload.message || {});
const fbTrim = String(fbLine || "").trim();
const fbOk = !!fbTrim && !_isSilentReplyStream(fbTrim);
const n = await loadMessagesForActive({ keepDomIfNoHistoryRows: hadStream });
const needWsFallback =
hadStream && fbOk && (n < 0 || !!loadMessagesForActive._needsWsTextFallback);
if (n >= 0) {
try {
if (streamRow && streamRow.parentNode) streamRow.remove();
} catch (_) {}
}
if (needWsFallback) {
if (n < 0) {
try {
if (streamRow && streamRow.parentNode) streamRow.remove();
} catch (_) {}
}
ok = await appendFinalAssistant(payload.message, fbLine);
} else if (n > 0) {
ok = true;
} else {
ok = n === 0;
}
scrollMessagesToBottom(true);
ok = true;
} else if (sawStreamToolRefAttachments) {
// Streaming UI cannot render image_ref/relay_pointer; recover from persisted history so images appear
// without requiring a manual refresh.
try {
if (streamRow && streamRow.parentNode) streamRow.remove();
} catch (_) {}
await loadMessagesForActive();
scrollMessagesToBottom(true);
ok = true;
} else {
ok = await appendFinalAssistant(payload.message, chatStream || extractWsAssistantText(payload.message || {}));
}