Commit Graph

8745 Commits

Author SHA1 Message Date
GuoQing Zhang 72fd1e8f0f fix: unused pattern 2026-07-10 18:49:46 +08:00
GuoQing Zhang 8925d75cfe Merge branch 'feat/2.6.0' of github.com:dataelement/bisheng into feat/2.6.0 2026-07-10 18:32:04 +08:00
GuoQing Zhang 76a6c79b51 feat: support MySQL TLS configuration 2026-07-10 18:27:21 +08:00
GuoQing Zhang b1c83c91a9 fix(platform): keep tooltips above select menus 2026-07-10 18:06:18 +08:00
GuoQing Zhang 43edbce50c fix(platform): cache resource permission checks 2026-07-10 17:20:10 +08:00
GuoQing Zhang 8d9958c221 fix(platform): stabilize workflow metadata filters 2026-07-10 17:19:50 +08:00
GuoQing Zhang 93b6d24780 fix: initialize quick-start knowledge workflow nodes 2026-07-10 16:40:42 +08:00
GuoQing Zhang 3075089e10 fix: bind knowledge file ids from query 2026-07-10 15:32:09 +08:00
GuoQing Zhang c407ceb12d fix(approval): settle withdrawn channel subscriptions 2026-07-10 13:47:03 +08:00
LineWalker cefa784ea0 feat(client): calm neutral notice + manual retry for transient LLM errors
A model rate limit is the vendor's availability blip, not a BiSheng fault, so it
no longer renders as the red danger card. Transient/retryable errors (rate_limit /
network_timeout / service_unavailable) now show a calm neutral ServiceBusyNotice
(grey, role=status) with a one-tap Retry; terminal errors (quota exhausted / auth /
content filter / unknown) keep the red failure card -- red means act, grey means
just a hiccup.

- ServiceBusyNotice: shared neutral notice (optional title, detail disclosure,
  Retry button), styled after the existing task-terminated banner.
- Task mode: TaskErrorCard branches transient->notice and regains an onRetry prop,
  wired only on the /linsight ExecutionFlow via continueConversation (same SV).
  /c inline tasks get the calm visual but no button (re-send via the main input).
- Daily mode: thread the SSE error status_code through onError onto the message
  (ChatMessage.errorCode); AiMessageBubble renders the notice + Retry (reusing the
  already-wired regenerate) for transient codes {12046,429,503,10540,12045} and
  suppresses the copy/feedback toolbar on it.
- i18n: add com_error_retry (重试 / Retry / 再試行).

Pure frontend; backend classification from the prior commit already routes the
rate limit here. vite build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:13:31 +08:00
LineWalker 46074138ed fix(linsight): treat MaaS insufficient_quota (429) as retryable throttling
MaaS gateways (Aliyun Bailian / DashScope and other OpenAI-compatible
endpoints) reuse OpenAI's `insufficient_quota` code + "exceeded your
current quota" message for TPM/TPS throttling (429-Throttling.AllocationQuota),
which recovers on its own. The classifier treated it as billing exhaustion
(FAIL_FAST / quota_exhausted), so a transient rate limit surfaced as a scary
"quota used up / top up" failure instead of a friendly "service busy, retry".

Narrow _QUOTA_SIGNATURES to unambiguous money wording only (arrearage /
insufficient balance / 余额不足 / 欠费) and move the generic quota/额度/配额
family -- including insufficient_quota -- into _RATE_LIMIT_SIGNATURES. Quota is
still checked first, so a genuine arrears signal keeps winning over a co-occurring
throttle word. Net: insufficient_quota + 429 now auto-retries (middleware) and, if
exhausted, renders the friendly rate_limit copy in both task and daily chat mode;
genuine balance exhaustion still fails fast with the top-up guidance.

Update the classifier / resilience / workstation unit tests and add R4-R6 to the
mock-model E2E runner (retry-then-recover, exhausted->rate_limit, billing->fail-fast).
53 unit tests + 9 E2E checks green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:11:26 +08:00
GuoQing Zhang 0b4b99bfd7 feat:remove auto add CITATION_PROMPT_RULES 2026-07-09 20:05:39 +08:00
dolphin fce368f177 fix(platform): make page heights license-banner-aware; fix form file-type image var backfill
F037 license banner shifts all admin content down by its height, but page
scroll containers hardcode viewport-relative heights (calc(100vh - Npx)) that
ignore it, so their bottoms fell off-screen and could not be scrolled to when
the banner was shown.

- LicenseBanner publishes its rendered height as the CSS var --license-banner-h
  on :root (0px when hidden, so all subtractions are no-ops without the banner)
- sweep every calc(100vh - Npx) page/scroll height to subtract the var
- MainLayout sidebar nav max-height subtracts the var too
- SystemPage 组织同步/角色管理: root lacked a fill+scroll container (or used a
  viewport calc miscalibrated for the nested TabsList) -> switch to h-full +
  internal overflow-y-auto so they fill their flex parent, banner-agnostic
- InputFormItem: in edit mode, switching the upload file type to an image-capable
  type no longer backfilled the image variable name (the new-item auto-fill effect
  is skipped when editing); backfill image_file / file_path on file-type change
2026-07-09 19:01:31 +08:00
open-master 4b52b32b03 merge remote feat/2.6.0 2026-07-07 16:58:58 +08:00
open-master 13edffc8ce fix(knowledge): show friendly ASR parse failure messages for knowledge-base model
Point users to System Model Settings → Knowledge Base Model → ASR when media parsing fails due to missing or invalid ASR configuration, including platform upload preview and client knowledge space tooltips.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 16:58:18 +08:00
GuoQing Zhang 2c8d553a97 Merge branch 'feat/2.6.0-beta4' into feat/2.6.0
# Conflicts:
#	src/backend/bisheng/knowledge/api/endpoints/knowledge_space.py
#	src/backend/bisheng/knowledge/domain/services/knowledge_space_service.py
#	src/backend/bisheng/workflow/common/knowledge.py
2026-07-07 12:08:00 +08:00
GuoQing Zhang ede6926391 feat(knowledge): replace folder file_num with has_failed_files; show folder update time
The space children/search endpoints no longer return the per-folder file_num
(an expensive recursive count). Folders now expose a lightweight has_failed_files
boolean (computed from the same status-grouped query, true when a child is
FAILED/VIOLATION) which drives the "retry" affordance precisely.

Client: folder rows drop the "x个项目" count and simply show the folder's most
recent update time; folder retry gating (single + batch) now uses has_failed_files
instead of successFileNum < fileNum. The actual retry stays fully backend-driven.
2026-07-07 11:33:03 +08:00
LineWalker 7364239893 fix(linsight): whole-conversation share returns all task turns to non-owner
session-version-list unconditionally narrowed the version list to
meta_data.versionId after the shared_to_session grant. A whole-conversation
(workbench_chat) share carries no versionId, so the filter collapsed the
list to empty and non-owner viewers saw "任务详情加载失败" while the owner
(user_id match, bypass branch) always worked.

In 2.6 task mode the 2.0 integer-"version" re-run concept is dead (version is
now a datetime sort key, one session_version per turn), so a whole-conversation
share must return every turn's version. Narrow only when the share explicitly
pins a versionId (single-version linsight_session share); also guard
meta_data=None against a 500.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 22:41:09 +08:00
GuoQing Zhang 0b8fd3a536 docs(alembic): tighten migration rule to DDL-only, no data migration
Sharpen the earlier 'schema vs script' guidance into a hard rule: alembic
revisions may contain only DDL; the sole permitted data effect is a
server_default fill on an added column. Any read-then-write on existing rows
(SELECT->UPDATE/INSERT, INSERT...SELECT, dedup/backfill/cleanup/reconcile) is a
separate operational procedure, never in a revision — including cleanup needed
before a constraint (run as an ops prerequisite; the DDL fails loudly if data
wasn't prepared).

Removes the prior 'not part of a schema change' loophole. Notes existing
revisions (f001, f011 dedup, f035, ...) as grandfathered. Motivated by the
100k-user case where inline SELECT-then-UPDATE / per-row Python loops stall the
(now fail-fast) startup migration.

Updates both src/backend/AGENTS.md and the alembic AGENTS.md (Sec 5 + checklist).
2026-07-06 22:09:09 +08:00
GuoQing Zhang eb761410a2 Merge branch 'feat/2.6.0-beta4' of github.com:dataelement/bisheng into feat/2.6.0-beta4 2026-07-06 21:10:27 +08:00
GuoQing Zhang c976c90099 fix(chat): keep pasted screenshots unique and previewable in the input box
Clipboard screenshots always arrive as an image File named "image.png"
(or with an empty name), so pasting a second one collided on InputFiles'
name-based dedup and was dropped with a "duplicate file" toast. Pinned
image chips also only showed an icon + name, with no way to view the
image content.

- useFileDropAndPaste: give generically-named pasted images a unique
  name (image-<uuid>.ext); real filenames are preserved.
- InputFiles: generate a local object-URL preview for image files and
  thread it through chatFiles; revoke on remove / clear / unmount.
- AttachmentBar: image chips render a thumbnail and open a full-size
  preview on click; the remove button stops propagation.
- useAiChat: strip the client-only previewUrl before it reaches message
  state or the SSE payload.
2026-07-06 21:09:37 +08:00
Kinyoo 2f3cb371fc refactor(client): deep-thinking timeline — outer collapsible wrapper + rail connectors
DeepThinkingGroup regains an outer collapsible wrapper (正在/已深度思考(N 秒) header with a live ticker; auto-collapses on the streaming→done edge). ThinkingContent becomes the inner 思考内容 block (no timer; default-expand follows store.showThinking) and both it and ToolCallDisplay render a vertical rail connector (showConnector) so the timeline stays continuous when collapsed; web tool result auto-expands after finish.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 18:24:39 +08:00
dolphin 2e3281602c fix(knowledge): guard folder-chat session/history lists against non-array 500 envelopes
The response interceptor returns (does not throw) on a status_code===500
envelope, so http.get can hand back an error object as .data. getFolderSessions
used ?? [] which only rescues null/undefined, letting the object reach
ConversationHistory's sessions.map() and crash with 'e.map is not a function'
when switching knowledge spaces mid-load.

Coerce getFolderSessions/getFolderChatHistory/getFileChatHistory to arrays and
add an Array.isArray guard at the ConversationHistory render site.
2026-07-06 17:42:30 +08:00
open-master ccc8b9b397 feat(knowledge): add configurable ASR model in knowledge system settings
Expose optional speech-to-text model on the knowledge-base model tab and
resolve media transcription from knowledge LLM config instead of workbench ASR.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 17:42:05 +08:00
GuoQing Zhang abef9275dc Merge branch 'feat/2.6.0-beta4' of github.com:dataelement/bisheng into feat/2.6.0-beta4 2026-07-06 16:56:18 +08:00
GuoQing Zhang 96de66314c fix(worker): eliminate cross-loop async-DB errors in Celery workflow worker
Root cause: the workflow worker drives process-global async singletons (async
MySQL engine pool, OpenFGA httpx, aioredis) from multiple event loops —
run_async_task (Loop A), run_async_safe (Loop B), and transient loops from
asyncio.run / LangChain's per-batch asyncio.Runner. Pooled connections are
loop-bound, so cross-loop reuse raised 'Future attached to a different loop' /
'Event loop is closed', rolling back the DB session and failing workflow nodes.

- async_utils: run_async_safe now reuses a worker-registered bridge loop
  (set_preferred_bridge_loop) so run_async_task and run_async_safe share ONE
  loop; add run_on_bridge_loop() to hop foreign/throwaway-loop async work onto
  that bridge loop, propagating contextvars (tenant_id).
- worker/_asyncio_utils + main: register/unregister the persistent loop.
- workflow/common/knowledge: asyncio.run(_fetch) -> run_async_safe.
- workflow/callback/llm_usage_callback: hop on_llm_end/on_llm_error DB writes
  onto the bridge loop (LangChain runs async callbacks on a throwaway
  asyncio.Runner loop under sync invoke, poisoning the shared async pool).

No-op for FastAPI / non-worker processes (no bridge loop registered -> inline
await). Tests: test/common/test_async_utils.py (bridge unit tests),
test/workflow/test_llm_usage_callback_bridge.py (callback integration).
2026-07-06 16:49:36 +08:00
GuoQing Zhang ca72ae8bb1 fix(message): make inbox read-status writes idempotent under concurrency
batch_mark_as_read / mark_as_read used check-then-insert with no protection
against a concurrent transaction inserting the same (message_id, user_id) in
the SELECT->INSERT window, tripping ix_inbox_message_read_msg_user. batch used
one add_all + one commit, so a single conflict rolled back the whole batch —
every message stayed unread and the API 500'd (prod trace gkpj / DM -6602).

- batch_mark_as_read: on IntegrityError, rollback, re-read what now exists and
  insert only the still-missing ids (dialect-agnostic, same pattern as
  ChannelInfoSourceRepositoryImpl.batch_add); dedupe input ids too.
- mark_as_read: on conflict, rollback and return the row that won the race;
  re-raise only when it is not a duplicate-key race.
- handle_approval: guard the post-action auto-read (narrow IntegrityError
  catch) so a read-record race never surfaces the finished approval as a 500.

Adds deterministic concurrency regression tests that inject a competing insert
into the exact race window.
2026-07-06 16:10:52 +08:00
Kinyoo 78c0d9e0ab feat(knowledge): remember & restore the last-viewed folder chat across dock collapse
useFolderChat memoizes the active chat id per (space, folder) so a remount restores it instead of defaulting to the newest session; the collapsed-dock expand button now re-reveals the previous conversation rather than starting a new one (new chat stays a separate action).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 15:50:47 +08:00
open-master 4bbbcad77a fix(client): show upload file types tip in knowledge space add menu
Use Platform-aligned summary text with visible gray-on-white tooltip instead of the long format list that rendered invisible.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 15:15:50 +08:00
open-master 36dd86e34f fix(knowledge): align knowledge space add menu with design spec
Replace split upload button with a single dropdown listing folder creation,
file upload (with format tooltip), folder upload, and web link in order.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 14:18:36 +08:00
open-master 7c39a9b0c7 fix(knowledge): normalize media extension case for preview transcript
Lowercase file extensions in tmp preview object name resolution so
uppercase uploads like .MP3 upload transcript markdown to MinIO and
step-3 RichPreview can load recognized text.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 13:42:57 +08:00
dolphin 2a84fb0660 feat: initialize project configuration and state files for coaligne integration 2026-07-06 12:15:30 +08:00
open-master c575536d98 feat(knowledge): align media upload preview step and file size limits
Upload media transcript to MinIO for step-3 comparison with RichPreview
on the left and segments on the right. Set document limit to 50MB and
media limit to 1024MB across backend defaults and Platform upload UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 11:48:43 +08:00
Kinyoo eb56844a26 style(linsight): themed active state for input pills; no double exit-x on touch
KnowledgeSpaceSelect book icon rendered as a CSS mask so it turns brand-600 when active (was a baked #999 img); ToolsSelect/KnowledgeSpaceSelect active color documented as brand-600 to match checked rows; TaskModeToggle suppresses the hover binoculars→x swap on touch layout (≤1023px) where a persistent exit-x already shows, avoiding a double x.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 11:40:46 +08:00
Kinyoo 64aafa20e4 fix(client): round --primary HSL so hsl(var(--primary)) hits --brand-500 exactly
Integer HSL (222 100% 54% blue / 154 67% 29% green) rounded ~3/255 off the brand-500 rgb; use fractional HSL (221.51 100% 54.22% / 156 68% 29%) so bg-primary surfaces (e.g. picker checkboxes) match the brand exactly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 11:40:46 +08:00
open-master b78b39b50b fix(knowledge): reserve audio player height so speed menu is not clipped
Give native audio controls vertical room like video preview and drop
overflow-hidden on the player/transcript split so upward popups render fully.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 10:53:53 +08:00
Kinyoo d60efa5308 fix(linsight): clarify Enter-to-advance (both modes, IME-safe) + collapsible intent row
ClarifyCard custom answer: Enter confirms and advances for single AND multi-select (matches the 下一题 ↵ hint), guarded against IME composition Enter. IntentRow reworked to the DeepStepGroup layout — a self-toggling, collapsed-by-default node that lines up with the 已深度思考 node.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:40:28 +08:00
Kinyoo da0aad2373 refactor(client): migrate Prompts confirmations to the unified ConfirmDialog (UI unification infra)
Replace OGDialog/OGDialogTemplate delete/confirm dialogs in Prompts (admin settings, version delete, dash group item) with the app-wide useConfirm; touch up the ConfirmDialog gallery section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:40:28 +08:00
open-master f55de329f7 fix(knowledge): fix media preview overflow clipping in detail pages
Separate the media player from scroll containers so native audio playback
speed menus are not clipped, and align Platform video transcript tabs with
Client scroll layout. Reduce default video height in split preview panes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 10:30:19 +08:00
GuoQing Zhang e5543750a8 Merge branch 'feat/2.6.0-beta4' of github.com:dataelement/bisheng into feat/2.6.0-beta4 2026-07-05 00:58:54 +08:00
GuoQing Zhang 647e7e59b5 fix: org sync error 2026-07-05 00:58:49 +08:00
LineWalker b9a33dab9a fix(permission): knowledge_space creator is a permanent, non-removable owner
承接上一提交(owner/creator 解耦)。知识空间的所有权还有第三套真相来源——
SpaceChannelMember role=CREATOR 行,「我创建的」列表 + 文件读写删都认它,独立于
OpenFGA owner tuple 且转移/撤销所有者时从不清除。因此对知识空间,创建者是不可
剥夺的永久所有者,仅改 OpenFGA owner tuple 无法真正撤销其访问。

承认现状:知识空间创建者永久所有者、不可移除(对齐 channel 创建者的 is_creator
处理);其他资源类型(workflow/助手/工具等)保留上一提交的 owner/creator 无主兜底
解耦(它们无此 membership 旁路)。按资源类型分叉:
- ResourcePermissionItem 加 is_creator 字段(前端据此锁定行,零改动)
- _add_creator_owner_entry: knowledge_space 创建者始终显示为 owner+is_creator;
  其他类型保持无主兜底
- authorize 守卫: knowledge_space 拒绝移除/降级创建者 owner(非创建者 owner 仍可
  移除,创建者兜底不会无主);其他类型走无主守卫
- check fallback: knowledge_space 创建者永久 owner(豁免收窄)

6 个知识空间专属单测 + 更新既有断言;前端零改动(is_creator 逻辑现成)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 22:50:58 +08:00
LineWalker 51c34a8946 fix(permission): decouple owner role from creator so owner revoke takes effect
知识空间存在两名所有者时,移除创建者(admin)的所有者权限弹「撤销成功」却不
生效:创建者的 owner 身份有两个来源(OpenFGA tuple + DB Knowledge.user_id),
撤销只删 tuple,而 check L6 fallback 与列表 _add_creator_owner_entry 都从 DB
user_id 无条件回填 owner,令其复活。

改为「无主兜底」解耦模型:owner 纯由 tuple 管理(可授/撤/转),creator 仅在资源
会完全无主时兜底(INV-2)。
- check fallback: creator→owner 仅当无其他 owner tuple 时成立(新增
  _resource_has_active_owner);FGA 宕机路径保持宽松兜底不变
- 列表合成: has_creator_owner → has_any_owner,有别的 owner 不再合成创建者
- 写守卫泛化: _can_remove_self_owner_relation → _can_remove_owner_relations,
  拦截「移除后无 owner」并返回 PermissionLastOwnerError(19007),同请求 owner
  grant 算存活者(支持一步转移)

前端零改动(assertSuccess 遇 19007 走已有 error_last_owner 分支)。
新增 6 个单测 + 更新既有守卫测试错误码。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 22:05:52 +08:00
LineWalker 7669c1a3cf docs(agents): dedupe layered AGENTS.md, add runtime topology + backend pitfalls
- root: fix client react-query v5->v4 (@tanstack ^4.28), add 5-line runtime
  topology block to §1, release-contract.md path in SDD step 0, accurate §7
  pitfall pointers, docs-index pointers and instruction-file loading map in §8
- frontend platform/client: remove the 7 hard rules duplicated from root §4
  (single source of truth; kills the <600 vs <=600 drift), keep only
  app-specific rules; promote any-minimize + named-exports to root §4
- backend: new Known Pitfalls section (tenant auto-filter is SELECT-only —
  bulk UPDATE/DELETE and raw SQL need hand-written tenant conditions; ruff
  PostToolUse hook drops not-yet-used imports; Celery Beat x multi-tenant
  context; DB config 100s Redis TTL) + Quick Map accuracy fixes
- docs/README.md: drop dead links (qa/, archive/, gateway-deploy dir), add
  PRD/ and observability/ rows

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 19:07:21 +08:00
LineWalker 063dd79049 fix(linsight): strip internal output/ · scratch/ paths from the run summary
The system-prompt guardrail (don't echo workspace paths in the final answer) is
probabilistic — the model still mirrors tool results like `/output/report.md` into
its summary, leaking the internal workspace zone to end users. Add a deterministic
net: stripWorkspacePaths() removes the output/ · scratch/ folder prefix (keeping the
filename) from the answer before it renders, so the path never reaches the user
regardless of model adherence. Targeted regex leaves prose and non-file mentions
alone. Applied in ResultSection, so all three carriers (ExecutionFlow /
TaskTurnPanel / ConversationRound) are covered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 19:03:36 +08:00
LineWalker 5684a8358f fix(linsight): de-dupe task-result panel, stop re-announcing files in the answer
The fixed report-link row and the model's final answer both announced the
deliverable (and the answer leaked the internal output/ path), and a single-file
run listed the same file in both the link row and the output-files card. Give
each block one job:
- prompt step 4 (收尾): summarize the deliverable's content/conclusion only, no
  filenames / output/ paths / "已完成" status (the UI owns completion + files).
- ResultSection: render the output-files card only for multi-file runs; a single
  deliverable is already surfaced by the report-link row above.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 18:39:53 +08:00
LineWalker d7fa4c8ce7 fix(linsight): strip reasoning-model <think> markers in task mode
qwen3.5-35b-a3b leaves <think>/</think> boundary markers in its output and
BiSheng has no generic stripper, so they leaked into two surfaces:
- task title generation failed -> session stuck on "New Chat" (empty content
  raised, or a dirty long string overflowed message_session.name VARCHAR(255))
- the thinking narration rendered bare <think> / <think tags (stream-split)

Add common/utils/think_tags.py (strip_think_block / strip_reasoning_tags) and
apply it at the two linsight sources:
- _extract_thinking (stream_event_mapper) drops the markers, keeps the
  reasoning, and emits nothing for a marker-only chunk
- task_title_generate cleans + clamps the title and falls back to a slice of
  the user's question instead of "New Chat"

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 18:24:21 +08:00
LineWalker 9aaa2fa962 feat(linsight): per-call diagnostic log for task-mode model calls
Task-mode LLM calls are not persisted to llm_call_log (that F017 audit is
workflow-only, and lacks finish_reason / tool args anyway), so the write_file
truncation loop was hard to diagnose from the DB. Emit one greppable line per
model call in the resilience middleware (BS_LINSIGHT_LLM_CALL): finish_reason,
token usage, and tool-call ARG KEYS only — never values, so a huge write_file
`content` is never dumped. The loop fingerprint is unmistakable:
finish_reason=length + write_file(file_path) (content key absent) + truncated=True.

Best-effort: the diagnostic never raises and never affects the model call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 18:09:04 +08:00
LineWalker 7050b7d165 fix(linsight): bound write_file truncation loop with tool-loop breaker + salvage
Root cause: a large write_file `content` arg is truncated (finish_reason=length)
before its bytes are emitted, so parse_partial_json drops the key and every retry
re-raises "content: Field required", spinning to recursion_limit=200 (~19min). Not
a model-can't-count-2-args problem — the huge tool argument gets cut off/parsed away.

- L2: detect finish_reason=length + a truncated tool call in the resilience
  middleware, nudge to write in smaller parts, bounded retry (truncation_retry_limit).
- L3: new LinsightToolLoopBreakerMiddleware — soft corrective hint then hard stop
  (aafter_model raises LinsightToolLoopError) on consecutive same-tool failures,
  with guards against aborting a model that recovered / switched tools.
- L4: classify GraphRecursionError / LinsightToolLoopError as TASK_ABORTED.
- Salvage: _handle_task_partial renders the intermediate analysis + retrieved
  knowledge as a normal (COMPLETED) result instead of a raw recursion error;
  shared across the fresh/resume/continue drivers via _stash_partial_abort.
- Config (LinsightConf): tool_failure_soft_limit=3 / tool_failure_hard_limit=8 /
  truncation_retry_limit=2.

72 unit tests + 1 integration test (aafter_model raise propagates through a real
create_agent().ainvoke(), beating recursion_limit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 17:32:12 +08:00
LineWalker 1870ae7db1 feat(linsight): header the question block as # 用户问题 in the first user message
Every other block in the seed message is headed (# 当前时间 / # 可用文件 /
# 可用知识库 / # 前情回顾); the question alone was bare and bled into the time
block above it. Add a # 用户问题 header (both the fresh _build_agent_input and the
continue path) so the question is clearly delimited.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 17:32:12 +08:00