mirror of
https://github.com/lobehub/lobehub.git
synced 2026-09-20 04:56:13 +08:00
fix/topic-switch-message-refresh
13854 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a9313e4041 |
🐛 fix: prevent send continuation from yanking user back to the sent topic
A send's continuation calls switchTopic(createdTopicId) after the persistence round-trip resolves. When the user navigated to another topic while that await was in flight, the unconditional switch yanked the UI (and the URL) back to the sent topic. Add an onlyIfActiveTopicIn guard to switchTopic: send flows pass every conversation bucket their conversation can currently live under -- the client-minted topic id before the server confirms it, the persisted id after the optimistic re-key. The switch is dropped when the user has already moved elsewhere. Accepting a set rather than a single id is what keeps the guard from dropping the send's own normal adoption of the topic it just created. The store's activeTopicId uses null for "no topic" but is typed string and can hold undefined from callers that never went through switchTopic, so normalize before comparing: an unset field still matches an explicit null (the blank new-conversation view). The guard runs before the epoch bump, so a skipped switch never invalidates a concurrent switch's pending revalidation. Applied at all four send-flow call sites: the minted-topic adoption (guarded against the view the send started from), and the client / hetero / gateway persistence continuations. |
||
|
|
cc2f8dccf9 |
💄 style: expand device settings detail pane (#19680)
* 💄 style(devices): expand device detail pane * 💄 style(devices): open device detail as a page-level right rail Round 1 feedback rejected both checks: the device list was left-hugging instead of centered, and the detail read as a small card beside the list rather than a real side panel — with no coverage of a device carrying many recent directories. The list lost its centering because the previous pass widened the settings content column to `none` for this tab so the detail card could sit beside it. Restore the shared 1024px reading column and make Devices a full-width tab that owns its own layout instead: NavHeader + centered SettingContainer + a page-level RightPanel. Opening the detail now only narrows the space the list centers in. DeviceDetailPanel splits into a fixed header and a scrolling body so a device with a long working-directory history scrolls inside the rail instead of stretching the page. In the workspace list card the host height stays auto, so the panel keeps growing with its content exactly as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
185d3c4858 |
🐛 fix(goal): lay manager plans out by round and surface agent-written deliverables (#19587)
* 🐛 fix(goal): lay manager plans out round by round The main Agent plan had no dependency field and every planned task was wired only to the problem node, so a multi-round Goal rendered as one flat row. Plans now accept dependsOn (existing task node IDs or earlier indexes in the same plan), written as depends_on edges after validating every reference; the terminal acceptance Task depends on the delivered leaf Tasks so it renders last. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(task): title a run handoff with its result, not a status The handoff title becomes the Goal finding title, and titles like 'Completed X' told the reader nothing about what was found. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(document): credit documents written by lh doc to the agent run Agents that write reports through lh doc create/edit never passed a tool runtime, so no document Work was registered and a Goal's deliverables stayed empty. The CLI now sends LOBEHUB_OPERATION_ID and the document router credits the write to the run's root operation when the operation is owned and running. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): create the Goal acceptance node and its links atomically The acceptance node and its decomposes/depends_on edges were written in separate transactions. An interruption between them left the node detached for good, because no later tick comes back to link an existing node. They now commit in one transaction, so a failure rolls the node back and the next tick retries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(goal): show the final acceptance as a module awaiting sign-off The terminal Goal acceptance row only carried a small status chip, so the owner could not tell a delivery awaiting sign-off from one that failed, and the final report was out of reach. The row now states the acceptance itself (awaiting your acceptance / did not pass / in progress / accepted), previews the latest acceptance report under it (also under a gate opened on that acceptance), and opens the full report in the side panel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): show the final acceptance report only once the goal ran through The report view appeared on a final acceptance that was still lost, retrying or parked on a gate, next to a ledger of failed attempts. It now renders only when the acceptance task is resolved and its latest round passed (awaiting sign-off) or was accepted; every other state keeps the ordinary row. Drops the gate-attached report card and the copy for states that no longer render. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): preview the acceptance task delivery when the round wrote no report A final acceptance judged by the verifier agent settles without a verify report, so a finished Goal showed only a placeholder. The preview now falls back to the document the acceptance task registered, then to its newest finding, and opens that in the side panel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 feat(goal): replace the task list with the final acceptance document Once the goal's final acceptance finished and passed, nothing is left to advance, so the whole current-task list gives way to the acceptance document: its title, a content preview and the sign-off action, with the full text in the side panel. Until then the list is unchanged. The preview now carries the full report text, the document body, or the delivery finding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): preview the acceptance report, not the task delivery The final acceptance card replacing the task list previewed the acceptance Task's own delivery when no round wrote a report. That is the product being accepted, not the judgment the owner signs off on. Read the report the round wrote, else each check's verdict and reasoning from the latest round, and open that round's verify report in the Portal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): show the goal's final document in place of the task list The finished Goal replaced its task list with the acceptance report, while the owner opens a finished Goal to read what it produced. Show the newest document the work registered as a deliverable instead (preferring the work over the acceptance task), preview its content, and open the document itself in the Portal. Goals that left no document keep the list. Drops the report helpers and their now-unused copy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f799ae4893 |
⚡️ perf(tools): project the whole command-output family, not just one member (#19695)
`lobe-local-system/runCommand` got a projector because its output is stored three times. It is not the only tool with that shape — `register.ts` points `codex/command_execution`, `claude-code/Bash`, `opencode/bash` and `pi/bash` at the SAME shared `RunCommand` card, which reads `stdout || output || content` for the body and `success` / `exitCode` for the collapsed row. One shape, so the existing projector already fits; it was simply never registered for them. This matters because the hetero cohort was getting nothing from the read-path projection. Measured on production over 24h, inside topics that contain an external agent's tool calls, the payload is essentially all `codex` (48 MB) and `claude-code` (4.5 MB) rows and zero `lobe-*` rows — none of which had a projector. `codex/command_execution` alone is 44 MB/day, and its state is `stdout` 13 MB plus a byte-identical `output` 13 MB, with the message body carrying a third copy. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c866610586 |
⚡️ perf: make step completion state snapshots opt-in (#19682)
* ⚡️ perf: remove unused final state from step completion events * ⚡️ perf: make step state snapshots opt-in per agent run * 🐛 fix: use a valid FinishReason in the finish executor test `'done'` is the resulting state status, not a FinishReason, so tsgo failed type-check on the new opt-in snapshot test. Use `'completed'` instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
49b635d80c |
🐛 fix(hetero): restore projected tool bodies before replaying a transcript (#19694)
A resume replay is the one place the message store feeds a model instead of a render. When a CLI session has been garbage-collected, the store's rows are rebuilt into a transcript that the external agent then resumes from — so a tool body the read path projected away is written to disk as "this tool returned nothing", and every later turn reads it back that way. Restore those bodies first. The fetch is bounded and only runs on the rebuild path, which is already doing far more expensive work; a failed fetch replays the trimmed body rather than losing the user's prompt. Narrow in practice — a hetero topic's tool rows are almost entirely `codex` / `claude-code` calls, which have no projector — but `lobe-local-system/runCommand` does appear inside hetero topics, and this failure is silent and persistent. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5fe21123b8 |
🐛 fix: restore composer only when cancelling an unaccepted send (#19674)
* 🐛 fix: restore composer only for unaccepted sends * 🐛 fix: preserve live draft edits during cancelled topic rollback |
||
|
|
afc7b01320 | 🐛 fix: recognize JSON device gateway dispatch errors (#19692) | ||
|
|
9b99536f01 |
⚡️ perf(agent): read a turn's invariant facts once in the send window (#19690)
* ⚡️ perf(agent): read a turn's invariant facts once in the send window Between the user pressing send and the operation existing, two questions were asked twice: a routed device answered the same system-info RPC over its WebSocket (tool discovery, then operation prep), and the user's settings row was read once for the market access token behind LobeHub skills and again for the memory and timezone settings. `createRunFacts` remembers each answer for the turn, keyed so a device's workspace and personal scopes stay apart and a share visitor's row stays their own. It remembers "there is none" too, so an unreachable device is not asked again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(agent): keep a failed settings read a failure in the turn's facts Swallowing the rejection let `execAgent` continue and read the absent row as "nothing is set", which its `memorySettings?.enabled !== false` default turns into memory enabled — the opposite of the conservative `false` a failed read used to leave behind. The reader propagates the rejection now, remembering it so the turn does not retry; the market token read, which is genuinely optional, catches it at its own call site. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
dde87d1534 | 🐛 fix: allow stopping and deleting terminal task runs (#19688) | ||
|
|
0cc24ebcf0 | ✨ feat(device): record CPU architecture on the devices table (#19510) | ||
|
|
066d22fd0c | ✨ feat(hetero): hand off rate-limited chats to another agent (#19080) | ||
|
|
a193769686 | ⚡️ perf: share one message read across step entry consumers (#19683) | ||
|
|
92367e93a6 | 🐛 fix: add optimistic feedback for V2 user question submissions (#19681) | ||
|
|
8f5a65efdb | ♻️ refactor(hetero): unify local and remote Codex account quota (#19678) | ||
|
|
83ad641c64 |
🔥 chore(ci): retire issue automation workflows now handled by github-bot (#19684)
* 🔥 chore(ci): retire issue-auto-comments workflow, now handled by github-bot * 🔥 chore(ci): retire issue automation workflows now handled by github-bot * 👷 ci: skip test/e2e runs for docs and automation-only changes |
||
|
|
f2dcabe604 |
✨ feat(desktop): native pull request tab in the Working Sidebar (#19506)
* ✨ feat(git): pull request detail + actions via gh Claude-Session: https://claude.ai/code/session_01Ro42BvDqhaAgJdrq34DR4Q * ✨ feat(device): pull request detail + action RPC legs Claude-Session: https://claude.ai/code/session_01Ro42BvDqhaAgJdrq34DR4Q * 🐛 fix(device): reattach useGitRemoteBranches JSDoc after PR detail hook insert Claude-Session: https://claude.ai/code/session_01Ro42BvDqhaAgJdrq34DR4Q * ✨ feat(pr-tab): merge dock resolver Claude-Session: https://claude.ai/code/session_01Ro42BvDqhaAgJdrq34DR4Q * 🐛 fix(pr-tab): unreachable bypass rules label, drop fabricated review count, redundant showPush check Claude-Session: https://claude.ai/code/session_01Ro42BvDqhaAgJdrq34DR4Q * ✨ feat(pr-tab): native pull request tab in Working Sidebar Claude-Session: https://claude.ai/code/session_01VkbqqBUWVZiY8pcbcDuhM3 * 🐛 fix(pr-tab): review round 1 — SplitButton, scoped dock errors, hook tests Claude-Session: https://claude.ai/code/session_01VkbqqBUWVZiY8pcbcDuhM3 * 🐛 fix(pr-tab): final review — fork-safe delete branch, action timeout, input guards Claude-Session: https://claude.ai/code/session_01VkbqqBUWVZiY8pcbcDuhM3 * ✅ test(device): add isCrossRepository to PR detail fixture Claude-Session: https://claude.ai/code/session_01VkbqqBUWVZiY8pcbcDuhM3 * 🐛 fix(pr-tab): cap the expanded checks list inside the merge dock Claude-Session: https://claude.ai/code/session_01VkbqqBUWVZiY8pcbcDuhM3 * 🐛 fix(pr-tab): render HTML in PR description and comments Claude-Session: https://claude.ai/code/session_01VkbqqBUWVZiY8pcbcDuhM3 * 🐛 fix(pr-tab): contain wide markdown and use ScrollArea for the pane and checks list Claude-Session: https://claude.ai/code/session_01VkbqqBUWVZiY8pcbcDuhM3 * 🐛 fix(pr-tab): lock ScrollArea viewports horizontally and cap the dock checks viewport Claude-Session: https://claude.ai/code/session_01CH7hPABkcAQBAL7dwHYATB * 🐛 fix(pr-tab): address P1 review findings * ♻️ refactor(pr-tab): GitHub-style merge dock, incremental merge context, base branch picker * ⚡️ perf(pr-tab): render core data first and separate local status * ✨ feat(pr-tab): expand failed CI checks in merge dock |
||
|
|
33244b9a92 |
🐛 fix: normalize agent message errors and structured IM replies (#19676)
* 🐛 fix: normalize agent message errors and structured IM replies * 💄 style: make chat error details opt-in * 💄 style: unify error details background and compact narrow layouts * ✅ test: assert normalized message errors in persistence integration |
||
|
|
e13ad41733 |
✨ feat(server): serve tool results as render-facing view models on the v2 read path (#19666)
* ✨ feat(server): add a tool view-model layer to the UI message read path Tool messages carry one blob that serves two audiences: the model replays the whole thing into the LLM context, while the screen picks a handful of fields out of it. Measured on production, the screen's share is tiny — a crawlSinglePage render reads title/url/description and `content.length`, and never the 37 kB page body sitting next to them. This adds the seam that lets the two be separated: a pure, dependency-free projector per `identifier/apiName`, applied over `UIChatMessage[]` on the way out of `MessageService.queryMessages`. The split is a layer boundary rather than a query flag. The UI read path goes through the service; the LLM context read calls `MessageModel.query` directly and so cannot reach a projector even by accident — no caller has a parameter to forget. The registry ships empty, so this commit changes no bytes on the wire. Projectors land per tool in follow-ups; a tool without one keeps today's behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(tools): project the four heaviest tool payloads to what the UI renders Registers projectors for the tools production measurements put at the top of the tool payload, and moves the surfaces that need the stored bytes onto an on-demand fetch. What each projector keeps, from an audit of the components that consume it: - crawlSinglePage / crawlMultiPages — the card reads title, url, description, crawler and `content.length`; the 37 kB page body it never touches becomes a 200-char preview, and the tool message body (a second copy, for the model) goes entirely. `length` is pinned before truncating so the character count stays honest. - runCommand — the output is stored three times (message body, `state.stdout`, legacy `state.output`) and the render reads one. The other two go. The surviving copy is NOT truncated; it renders inline in a scrollable block. - readDocument — the only surface is an inspector chip showing `state.title`. `state.content` and `state.xml`, two representations of the same document, go with the body. Nothing is lost, only moved. `message.getToolResultPayload` returns the stored content and plugin state for one message, and three surfaces call it when they are opened — never on mount, which would pull everything back down on load: - the crawl detail portal, when its local body is shorter than the pinned length - the raw/debug panel, which only mounts once toggled on - the fallback renderer, behind an explicit control, since an empty body there is ambiguous between "returned nothing" and "projected away" Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(server): only project tool payloads for the WebSocket v2 cohort The projected read path was applied to every caller of `MessageService.queryMessages`, on the assumption that the LLM context is assembled server-side and so can never see it. That holds on the server — the context read goes through `MessageModel.query` directly — but not in the browser: a client-mode run builds its context from `dbMessagesMap` (`executeClientAgent({ messages: displayMessages })`), which is filled by the very read being projected. Tool results would have gone missing from the model's context, silently. Gate on the `gatewayMux` lab opt-in, read server-side from `users.preference`. That cohort is exactly the one whose runs always execute server-side, so the browser there never needs the stored payload for anything but rendering. The preference is memoized per service instance — a gateway run queries once per step — and any failure to read it falls back to the whole payload. Turning the lab off does not invalidate already-cached projected lists. Left alone on purpose: the mux is on its way to being the only runtime, and the gate goes with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(server): project the conversation read, and name the load control Two defects a live run surfaced that the unit tests could not. `message.getMessages` builds its own `MessageModel` — it passes different query options than `MessageService.queryMessages` — so the conversation load never reached the projector at all. Measured against a seeded topic, the response was byte-identical to the unprojected one (193,619 bytes) while the WS snapshot path was already projecting. The projection step is now public on the service and the router applies it explicitly; the shared-topic branch stays unprojected, since an anonymous visitor has no authenticated way to fetch the stored payload back. The fallback renderer's on-demand control reused `debug.response`, so it rendered as a button labelled "Response" — a section heading, not an action, giving no hint that it loads what the read path left behind. It gets its own key. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 📝 docs(acceptance): record two worktree bootstrap traps L-S23: a hand-built node_modules symlink farm passes vitest and tsgo but cannot start the dev server, and a fresh install cannot be the fallback in a repo that commits no lockfile. L-S24: the dev port is allocated per run and `/` answers 302 to /signin, so a remembered port and a 200 predicate both read as "never came up". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(chat): load a projected tool result on expand, not on a button Expanding a tool row is already the user asking to see its result, so a second click to fetch what the read path left behind is a step that earns nothing. The fallback renderer now fetches as it mounts and shows a skeleton under the Response heading until the body lands. Mounting is the right trigger rather than a render-time effect: this component is the Accordion item's child and only mounts once the row opens. Verified in the app — expanding the assistant group fires no request, expanding one row fires exactly one. The control's dedicated string goes with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(tools): move the command output out of the conversation too Review asked why `runCommand` still shipped ~10 kB of state after projection. Because the projector kept `stdout`: the card renders the output inline, so it looked like the card needed it up front. It doesn't — the card lives inside the row's detail, which only mounts when the row is expanded, the same property that made load-on-expand safe for the fallback renderer. So the output goes with the other two copies, and what stays is the settled metadata the collapsed row reads: exit code, success, background flag, command id, stderr, produced files. Measured on the same seeded topic, that state went from 10,040 to 112 bytes and the whole response from 17,494 to 7,580 — 25.5x against the unprojected 193,619. Which surface has to fetch the payload back is now something the projector declares (`storedPayloadNeededBy`), carried to the client on the existing `payloadOmitted` field as `'detail' | 'render'`, rather than a second registry the projectors could drift from. `ToolRender` hydrates the `'render'` tools as it mounts; `'detail'` tools keep a self-sufficient card and only their detail portal fetches. Measured per step: loading the conversation, expanding the assistant group, and expanding the crawl row each fetch nothing; expanding the command row and the document row fetch exactly once each. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(server): stop pushing the whole AgentState on agent_runtime_init `agent_runtime_init` shipped the raw `initialState` to every non-visitor subscriber: the entire `AgentState`, which carries the LLM context in `messages` plus the tool-set maps that `stripStateForStream` already treats as the size problem on `finalState`. It reached the wire unstripped because `publishAgentRuntimeInit` pushes directly rather than through `publishStreamEvent`, and because the existing chokepoint only looks for a `finalState` key. Nothing reads it. The web handler logs the event and breaks, the CLI prints a fixed line, and the gateway transport does not handle the type at all. Shared- agent visitors have been receiving `{ status }` alone for exactly this reason; every run now gets the same shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(server): close three gaps Codex review found in the projected read path **Recovery RPC skipped the conversation guard.** `getToolResultPayload` returned a tool's stored content and state after only the model's ownership scoping, which in a workspace is workspace-wide. A member holding a message id — including one kept after access was revoked — could read output from a conversation they cannot open. It now runs `assertCanViewMessageTargets` first, the same way the list read guards with `assertCanUseTopicTargets`. **Share visitors could never recover a projected payload.** `queryUiMessages` runs under the CREATOR's identity, so the gate read the creator's lab preference and pushed the visitor a projected snapshot; the recovery RPC then runs as the VISITOR against ownership-scoped reads that cannot see a creator-owned row, so the crawl portal, the raw viewer and the fallback had no way to fill it back in. Visitor snapshots now keep their payloads whole. **A projected tool read as still running.** The completion test was `!!result.content`, which an emptied body fails, so while the assistant message was busy a finished tool was swapped back to its loading placeholder. The stored length now rides `ChatToolResult` and one shared `hasToolResultBody` answers the question for both the group's completion check and the status indicator — the acceptance round missed this because its fixture was a settled conversation. Codex's fourth finding no longer applies: it described the projector deleting `output` while keeping an empty `stdout`, and that branch is gone — both keys are dropped together now and the card hydrates on expand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✅ test(server): cover tool projection options in runtime snapshots * ♻️ refactor: slim ws2 step message transport --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9d78862fee |
🔀 chore(release): restore main-to-canary merge ancestry (#19673)
The previous main-to-canary sync copied the v2.2.17 changes into canary
as a single-parent commit (`189bd0abe5`). Consequently, `origin/main` is
not an ancestor of canary, and the next weekly release encounters
conflicts against already synchronized changes.
This PR records main (`787e3d20aa`) as a merge parent while preserving
the exact canary file tree (`7334cca4b9`). It restores ancestry without
reverting subsequent canary changes.
**Merge using “Create a merge commit”. Do not squash or rebase this PR:
either would discard the ancestry this PR restores.** The weekly release
will be based on canary after this PR is merged.
#### Audit
- Inspected the main-only release history and the prior sync commit.
- Compared all 75 paths changed between shared baseline `3bb887ca4c` and
main: 74 main file blobs are already reachable in canary history.
- The remaining `package.json` differs at the prior sync point only by
subsequent editor, UI, and pnpm upgrades; its release version is already
2.2.17.
- `CHANGELOG.md` and `changelog/v2.json` match main exactly.
#### Test
- [x] Tested locally
- [ ] Added/updated tests
- [x] No tests needed
Git checks confirm that the merge tree is identical to its canary
parent, main is its second parent, and main/v2.2.17/canary are all
ancestors. A simulated merge back into main is conflict-free and
preserves the candidate tree. No application tests were rerun because no
file content changes.
- Acceptance: not required; this is a Git ancestry repair with no
product behavior or file-content changes.
#### 🔗 Related Issue
Follow-up to the main-to-canary sync recorded in commit `189bd0abe5`
(its subject references PR 19435, which currently returns 404 from the
GitHub API).
|
||
|
|
d5bd0a5f77 |
🔀 chore(release): reconcile weekly candidate with main history
Main v2.2.17 was already synchronized into canary by |
||
|
|
7334cca4b9 |
♻️ refactor(mecha): assemble the run's skill pool in the shared core (#19667)
* ♻️ refactor(mecha): assemble the run's skill pool in the shared core `assembleSkillPool(sources, options)` owns the candidate pool `<available_skills>` is built from and `activateSkill` resolves against: precedence project > db > agent-skills > builtin on a name collision, the disabled drop, the share allowlist, and the device-only builtin gate. `isBuiltinSkillEnabled` moves with it; `src/helpers/skillFilters` re-exports it with the client's desktop default. The browser supplies two of the four sources (it scans no project directory and mounts no agent-document bundles) and, per the alignment review, stops narrowing the listing in manual skill mode: manual withholds the discovery tools, it does not hide what exists — the server semantics. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(mecha): keep unselected skills out of the pool in manual mode Manual activation mode promises "only user-selected tools and skills are available to AI", but withholding the two discovery tools does not deliver it: `activateSkill` ships with the always-on `lobe-skills` tool and resolves against the skill pool. The shared pool now collapses the selectable sources to the run's enabled plugin ids in manual mode, so an unselected skill is neither listed nor activatable. Project and device skills come from the working directory and can never be picked in the UI, so the mode does not speak about them and they stay. This also closes the same hole on the server, which never narrowed at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(mecha): keep the builtin skill gate out of the package barrel `src/helpers/skillFilters` is used by store slices and settings UI. Importing the gate from the package root pulled the whole agent-runtime graph in behind it, and a settings test that partially mocks `@lobechat/const` broke on a manifest it never meant to load. The gate moves to its own leaf module with a `@lobechat/mecha/builtinSkillGate` subpath export. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3af256caae |
🐛 fix(tts): remove obsolete TTS UI (#19651)
* 🐛 fix(settings): remove obsolete OpenAI TTS setting * ♻️ refactor(tts): retire message TTS UI |
||
|
|
8a8b999993 |
✨ feat(expertise): distil delivery standards from rejected acceptance checks, and restate them as they repeat (#19629)
* ✨ feat(expertise): distil delivery standards from the checks a reviewer rejected The reviewer's rejections are the only structured record of what they actually require, and nothing read them: expertise ingestion mined conversations only. This turns a settled acceptance round into expertise the same way a topic completion already does, reusing the whole lesson dedup / hit / snapshot path. Trigger: a new round attaching to the acceptance settles the previous one. The product's reject flow ends at a clipboard copy (`onCopyReview`), so no server-side event marks "I finished reviewing" — but the next round landing proves the previous round's judgement is final. Folding into a draft round is deliberately not a trigger: no new round opened, so nothing settled. - `persistDomainRun` now takes an explicit run descriptor instead of assuming a topic/agent, so both ingestion paths share it. - One observation writes one hit per rejection it cites, so `hitCount` counts violations rather than analysis passes. - The prompt is a separate chain: it must generalize ("this divider isn't needed" → "decorative dividers are not added"), cite its sources, and state when the standard does not apply. Restating one rejection verbatim yields a rule that never fires again, which is the failure this pipeline exists to avoid. - Domains resolve through the project the acceptance belongs to, falling back to the owner's always-on domains; when neither exists, a delivery-standards domain is created, owned by the user and merely mounted on the project so a sibling project can mount the same one later. Distillation runs out of band and never throws into the attach path: losing a lesson is cheaper than losing the reviewer's round. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(expertise): read the circled frames, and stop losing rounds to `{}` nulls Replayed one real round (7 rejections, all circled) against the pinned model and found two things unit tests cannot see, because a unit test hands the parser a constructed object while the model hands it whatever it feels like. 1. Every `['string', 'null']` field came back as `{}` under a strict json_schema, so `RejectionAnalysisSchema.parse` threw and the entire round was lost. The nullable fields are plain strings now, with "" as the "none" the service maps back to null. A test walks the schema and fails on any resurrected union. 2. The distillation was text-only, which is close to useless here: 616 of this owner's 876 rejections circle a region on a screenshot, and "this isn't aligned" / "this cyan is too light" cannot be understood from the words. One round's frames are now inlined as data URIs, circled ones first, and each circled region is rendered with its label and normalized position so the prompt can point at the frame it belongs to. Inlined rather than linked because a link makes the provider fetch from our storage, which is unreachable for a local or private bucket. Frames are capped and the overflow is named in the prompt, so a withheld frame is never read as proof that nothing was wrong there. Also makes `limits` mandatory prose rather than an optional null. On the first replay the model left it null on all 7 observations; a standard with no stated exemption gets applied everywhere and becomes noise. Verified on the replayed round: parse clean, all 7 limits filled with real exemptions, and re-feeding the same rejections with those 7 lessons present attaches 7/7 with 0 new lessons — the dedup path holds, so repeats do not fragment the library. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(expertise): keep a distilled standard to what the reviewer actually said Reviewing the first replay's output against the rejections it came from showed the distillation inventing the two things that matter most. `reasoning` was manufacturing design rationale. "This order is reversed" became "follows common messaging interaction conventions and does not match user visual habits" — plausible prose the reviewer never wrote, which a later compile step would then enforce as if they had. It now carries the reviewer's own words, or says outright that they gave no reason. `limits` was inventing exemptions. "Except on mobile or narrow containers", "except in large rich-text editors" — none of it stated by anyone. An invented exemption silently narrows a standard the reviewer stated without limit, so the field is now filled only from a boundary they drew or a contradicting rejection in the same round, and otherwise says so. Generalization also gets a test it can fail rather than the vague "strip the screen names": could a delivery on a completely unrelated screen violate this? A new `subject` field forces the answer to be written down — a model that cannot say what the concrete thing is an example of has not generalized at all. The prompt also caps the climb at what was actually objected to, so one rejected divider does not become a rule about every visual element. Replayed the same production round (7 rejections, all circled) on v2: reasons are the reviewer's own words on all 7, limits honestly empty on all 7, and the abstraction climbs where it should — "the annotation border on a screenshot" became "highlight and annotation lines drawn over content". Known cost, not yet addressed: climbing a level can drop the direction out of a standard. "Place the add-reaction control after the existing reactions" became "maintain the correct order", which is no longer something a delivery can be judged against. Two of seven regressed this way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(expertise): explain why a standard holds, and say who supplied the reason The previous pass cut `reasoning` down to the reviewer's own words to stop the distillation inventing rationale. That over-corrected: quoting the complaint back carries no information, duplicates the evidence stored on the hit, and strips out the one thing that lets a standard reach a screen nobody has built yet — the mechanism. "A pale line disappears against a white background, so the reader cannot tell which region was circled" is what makes the rule portable; "the reviewer said the cyan is too light" is not. So the reason is explanatory again, and the real hazard is handled where it belongs: `reasonSource` records whether the reviewer stated it or the distillation supplied it. The compile step downstream turns a lesson's reason into an enforced criterion, so a reason the model invented must never read as something the reviewer said. That field only works if it is decided by an observable rather than a judgement call. Asked "did the reviewer say why", the model marked 3 of 7 as `reviewer` on rejections that were pure instructions — "put it in one row, annotations left, actions right", "there's an extra line here" — which is exactly the direction of error the field exists to prevent. Asked instead "does their text state a consequence or a cause", all 7 came back correct on replay: only "the cyan is too light, I can't see it" kept `reviewer`, because it names the consequence. Inferred reasons are marked in the lesson body so the distinction survives into the UI, and the prompt now says to answer `inferred` when in doubt: under-claiming costs nothing, over-claiming puts words in the reviewer's mouth. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(expertise): judge a distilled reason by whether it settles a case nobody described "The mechanism is what lets a rule transfer to a new situation" is not just guidance — it is a test the model can run on its own sentence, so the prompt now states it that way: could someone facing a case this round never described settle it using your reason alone? That test is what separates a mechanism from the objection restated in nicer words. "Harms visual tidiness", "feels inelegant", "adds cognitive load" all fail it, because tidiness and elegance are the judgement under review — they settle nothing. The prompt names those phrasings and asks for the observable underneath: what does a person fail to see, misread, mis-click, or have to do twice. Replayed on the same production round, 5 of 7 reasons moved from verdict to mechanism: "loses the value of an inline screenshot" became "the reader cannot make out the content and must click to enlarge every time"; "misleads the user into thinking it is a network fault" gained the action that follows, "and retries blindly". Two did not, and the reason is worth recording rather than tuning away: a purely aesthetic standard has no functional mechanism underneath it. Pressed for one, the model can only produce a synonym of the verdict ("visual noise" for "untidy"). Those lessons want an honest "this is the owner's taste" more than they want an invented mechanism — a distinction worth making explicit once there is enough volume to see how common they are. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(expertise): let a standard admit it is taste rather than fake a mechanism Not every standard has a mechanism underneath it. Some of what a reviewer requires is simply what they prefer, and pressing the distillation for a reason anyway produces a synonym of the objection — "visual noise" for "untidy", "fragments related actions" for "I want it in one row". Those read as objective and would be enforced as if they were, which is worse than an admitted preference. `reasonKind` records which one a standard is, orthogonal to `reasonSource`: one says who supplied the reason, the other says whether the reason rests on something observable. The model's own failure is the detector — if the only reason it can produce is a synonym of the complaint, that is the signal to stop and answer "taste". A taste reason still has to be actionable: "the owner does not accept dividers that were not asked for; regions are separated by spacing and container edges alone" says what to do next without pretending to be physics. It is also load bearing downstream. A taste standard has no objective test, so the compile step must not turn it into a criterion that blocks a delivery on its own — noted where the field is declared, since that step lands later. Replayed on the same production round: 4 mechanism / 3 taste, and the three are exactly the ones that had been fabricating mechanisms. The four keep the reasons they had, one of them improving ("also leaks how the backend handles errors"). Both qualifiers are rendered into the lesson body, so the distinction survives into the UI rather than living only in the analysis. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(expertise): restate a standard once the reviewer has rejected it several times Ingestion writes a lesson from the first rejection that produces it and, on every rejection after that, only bumps its counters. So a standard that has fired eight times is still worded for the screen it was born on, and still carries no boundary — both of which are measurable on this owner's history. Replayed against 898 circled rejections: of 44 rejections in the same category as a distilled standard but from other acceptances, the standard reproduced the reviewer's actual objection on 4, while firing on 39% of deliveries they had accepted. A rule written from one case and bounded by nothing does exactly that. It is worth fixing rather than routing around, because the repeats are real: grouping those rejections by the standard behind them puts 62% of the spacing complaints and 40% of the unrequested-decoration ones into groups of 2+, largest 12. Placement reaches only 11%, which is why refusing has to be a first-class answer rather than a failure. The pass reads every instance at once, so the wording can climb to what they share instead of what one of them said, and it reads deliveries the reviewer ACCEPTED alongside them, which is the only input that can justify a limit. A boundary inferred from the instances is invented; one read off a delivery they shipped is theirs. On the divider group this is the difference between "输入框内部 工具栏不应添加多余的分隔线" and a standard that exempts danger-action separators in menus, table headers and sibling list rows, each citing the accepted delivery it was read from — verified against those frames rather than taken on trust. Two design points came out of running it on real rows instead of fixtures: `generalized: false` is an answer, not an error. Instances that share a category do not share a standard: "put the status next to the title" and "move topics above the profile" are both placement and are not one rule. The refusal is recorded as a revision anyway, or the same instances are re-read on every subsequent rejection. Taste survives consolidation. Given six spacing rejections the model answered `taste` and left the boundary unwritten rather than manufacturing a mechanism from six data points — which is the behaviour that keeps a preference from being enforced as if it were objective. Consolidation runs after the write and never inside it: a round of rejections that is safely recorded must not roll back because the rewrite of an unrelated standard failed. No schema change — `revision.kind = 'generalize'` and `prevTitle` were designed for this and had no writer until now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(expertise): make every generalized boundary name the delivery it rests on The consolidation prompt asked the model never to invent an exemption, but nothing checked. Its `limits` was one free-text sentence, and the deliveries it cited ("S2") were per-request labels that meant nothing once the request was gone — so "why is this allowed" had no answer, and an invented exemption looked exactly like a real one. `limits` is now a list, each entry citing the shipped deliveries it was read from. `resolveLimits` maps those labels back to check result ids and drops any entry that resolves to nothing: no refs, a rejected instance's ref, a label that was never listed. What survives is written into the lesson and, with its ids, into `revision.evidence` alongside the instances and the shipped set the pass was offered — refusals included, so "not one standard" can be checked against the same rows. Fixes a second way to get a boundary wrong, in the other direction: a rewrite used to replace the lesson's limits wholesale, so a boundary the reviewer had stated could be silently dropped, widening the standard past what they said. A carried-over limit is now marked as such and kept only if its wording is already in the standard; when nothing survives, the existing limits section stays. Replayed on the divider group from production: two exemptions, citing S2 (the profile "⋯" menu that isolates Delete) and S4 (the documents table), resolved to their check result ids; both frames checked by eye. Spacing wrote none, and the mixed placement group still refused. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(expertise): distil the final round, and stop two rounds racing without a queue Three fixes from review, all on paths the replays never exercised. **The last round of every acceptance was never learned from.** Distillation was scheduled only when a later round attached, on the reasoning that a new round is the first server-side proof the previous one is settled. But a terminal accept or reject settles a round just as surely, and it is the only thing that can settle the last one — no later round ever follows it. `stampDecision` now returns the round it stamped and both terminal paths hand it to distillation. Accepting still settles it: a reviewer can accept overall while individual checks were rejected along the way, and those rejections are the material. Triggering a round twice is harmless — the reflection key makes the second pass a no-op. **Without a queue, two rounds settling together raced.** The queue path buys serialization with `flowControl`; the local path had nothing, so both runs could see no bound domain and create duplicate defaults, or distil against the same stale lesson catalog and fork a lesson that should have attached — the persistence lock only serializes the writes, and would record both. Local runs now chain per reviewer, on the same key the queue uses. A failed distillation does not cancel the rounds queued behind it. **Consolidation may no longer promote taste to mechanism.** Replaying one group twice with an unchanged prompt returned `taste` once and `mechanism` once. The two are not symmetric: `mechanism` is the permissive value — it is what lets a standard compile into a criterion that blocks a delivery on its own. Ingestion decides this at the lesson's birth from the reviewer's own words, so a later pass promoting it means model noise can arm the gate with a mechanism nobody stated. Admitting taste needs no such proof, so downgrades still apply. Also corrects what `expertise_lessons.backtest` claims. Its doc described scoring a standard's precision against past decisions as the pre-compile gate; measuring that on 898 circled rejections showed the labels are too incomplete to gate on — a reviewer circles the worst thing in a delivery, so a standard correctly spotting a defect they did not circle reads as a false alarm. The column keeps its place and its emptiness, with the numbers written down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(expertise): keep the reviewer's own limits, and move the revision counter with a system pass Three more review findings, all in the consolidation pass. **The next user edit of a consolidated lesson would have failed.** A system pass numbered its revision from the log's maximum but left `expertise_lessons. currentRevision` behind, while `reviseLesson` numbers from `currentRevision + 1` against a unique `(lesson, revision)`. Two system passes, or one after a user edit, and the reviewer's next rewrite collides. The counter now moves inside the same transaction as the revision — including on a refusal, which records a revision too — and it is derived from the larger of the log and the counter, so a lesson whose counter has already drifted repairs itself. **A rewrite could silently drop a boundary the reviewer drew.** `limits` was the model's complete list, so carrying an existing limit depended on it echoing that limit back: omit one of two and the exemption disappears, widening a standard the reviewer had bounded. Existing limits are now kept verbatim by the service and never re-derived from the answer; the model returns only what it is ADDING. The one exception is ingestion's "边界未由评审者说明" placeholder, which only a reader can tell from a real boundary — so the model answers that as a flag, and the placeholder is dropped only when a validated exemption arrives to replace it. **Serialization was keyed too narrowly.** Domains and their bindings are workspace-wide, so two members reviewing at once race exactly as one member's two rounds do, and a per-user key lets each mint a different default domain and split the catalog. Both the local chain and the queue's `flowControl` now key on the catalog: the workspace when there is one, the user otherwise. Replayed against production rows again, since the limits contract changed: the divider group wrote two exemptions citing the profile "⋯" menu and the documents table, both resolving to real check result ids; a variant carrying a pre-existing reviewer limit kept that limit and appended to it. One of those replays came back degenerate — the model emitted collapsed text — and the pass contained it: the invented refs resolved to nothing, so no boundary was written and the reviewer's limit survived untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(expertise): sample only the accepted rounds the caller may read **A teammate's creator-only round could end up quoted in a shared standard.** The contrast set was every accepted check in the workspace, but a workspace shares its lesson catalog, not every round in it: a round is creator-only unless it is public. This pass puts a delivery's title, the reviewer's comment and their circled notes into a prompt, then persists the result as a limit the whole workspace reads — so the sample is now restricted the way a reader would be. The caller's own checks, plus other members' only where the round they belong to is public; a check with no round is the caller's own by the scope predicate. **One rejection could count as two.** A repeated label in the model's answer ("R1", "R1") wrote two hits for the same rejection and incremented `hitCount` twice. That count ranks the standards list and decides core-versus-niche, so the distortion is durable even though consolidation later deduplicates the ids. Deduplicated at the write itself rather than at one caller, since that is the only place hits are made and the topic path shares it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6f64c187b1 |
♻️ refactor(mecha): assemble the tool manifest pool in the shared core (#19616)
* ♻️ refactor(mecha): assemble the tool manifest pool in the shared core `assembleManifestPool(sources, options)` owns the pool rules both tools-engine constructors used to duplicate: a connector that produced a manifest replaces the same-named plugin, manifests executed outside the connector path get the connector's tool permissions patched in, context-aware builtins resolve for the run, every source drops entries the engine could not use, and excluded identifiers leave the pool from every source. The connector → manifest listing and the permission patch are shared too; the server's builder only adds the endpoint and credentials the runtime needs, the browser's only maps its store rows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ♻️ refactor(mecha): build the activator discovery pool in the shared core (#19617) The server pipeline no longer owns the discovery rules. `@lobechat/mecha` now decides which identifiers the tools engine considers (`resolveInvocationToolIds`), what the activator may discover mid-run (`resolveDiscoveryPool`: the enabled manifests, discoverable builtins, the connector families with their source, the sandbox and device reachability deletions, and the single ingest guard every source passes through) and which tools dispatch to the client (`resolveClientExecutors`). `toolDiscovery` shrinks by ~180 lines and only supplies the run's facts. Device reachability keeps coming from the execution plan's kind, so an `auto` run with several machines online still offers the picker. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * 🔊 fix(mecha): report the manifests the exclusion actually removed The diagnostic logged the size of the excluded set, which overstates the count whenever it names an identifier no source contributed — exactly the device builtins the walls already dropped. The assembler returns the real before/after delta instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
230b8037f1 |
🐛 fix(hetero): stop settling a run whose output ingest discarded as success (#19658)
* 🐛 fix(hetero): stop settling a run whose output ingest discarded as success A device/sandbox hetero run can finish with every one of its events thrown away and still be recorded as a clean turn: the user is left on the `...` assistant placeholder forever, with no error, no retry affordance, and the topic/task retired as answered. `heteroIngest` has two paths that discard a whole batch and answer 200 — the operation row is no longer `running`, and the topic's `runningOperation` marker no longer names this operation. The producer reads 200 as delivery, so `BatchIngester.drain()` succeeds, `exitedClean` is true, and `heteroFinish` receives `result: 'success'` for a run that persisted nothing. Observed on a real run: the CLI did all the work (16 steps, opened the PR it was asked for, $5.31 of tokens) while the server stored one `stream_start` and then silently dropped the rest. Three changes, so the loss is reported wherever it can be: - `heteroIngest` returns `{ accepted, reason }` and stamps the first refusal on the operation row. A refusal is permanent, so it is an ack, not a transport error the producer should retry. - `BatchIngester` fails the stream on a refusal instead of retrying, which surfaces at `drain()` and makes the run finish as an error carrying why. - `heteroFinish` reads the row marker back and downgrades a `success` receipt to a failure, covering producers that predate the ack contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(hetero): let terminal-operation callbacks reach the refusal path `authorizeOperationCallback` rejects an operation-token callback with CONFLICT as soon as its row is terminal, so `heteroIngest` never reached the refusal it was just taught to report: the ingester spent its whole retry budget on a permanent failure, no rejection marker was recorded, and `heteroFinish` was turned away by the same guard — leaving the turn on its unresolved placeholder. `hetero:ingest` and `hetero:finish` now opt into `allowTerminalOperation`. The allowance is per call site, never implied by the capability, so token renewal (which also asks for `hetero:ingest`), goal mutations and `model:invoke` stay strict. Every other check still runs, and neither callback gains any write power: the service refuses them on the same terminal status, which is precisely the answer they exist to deliver. It also aligns the narrow token with the user-session producers, which skip this guard entirely today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
91f36af7de |
🐛 fix(agent-runtime): dedupe archived tool results by content and page readKnowledge (#19665)
* 🐛 fix: dedupe archived tool results by content and page readKnowledge Archived tool results were keyed by `{topicId}_{toolCallId}`, so the same oversized knowledge-base read persisted a byte-identical document per topic, and the `.tool-results` root folder could be created twice under concurrency. `readKnowledge` also returned whole files, which pushed almost every call past the archive threshold and bloated the model context. - Archive under `./.tool-results/{sha256(content)}.txt`; reuse an existing identical archive and only add the topic binding. Fall back to `{hash}_{toolCallId}.txt` when the hash path holds different content. - Run mkdir/stat/write/verify/associate inside one transaction guarded by a per-agent `pg_advisory_xact_lock`, so concurrent archives cannot create duplicate root folders or duplicate rows. - `readKnowledge` returns a bounded window per file (default 400 lines, 10k chars) with `offset`/`limit` for paging; the prompt carries the line range and a continue hint, the card shows the range, and the manifest and system role tell the model to page instead of re-reading. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * 🐛 fix: enforce the readKnowledge char cap on oversized single lines A line longer than the per-call cap (minified JSON, generated text) was returned whole, which defeated the bound and could still push the tool result past the archive threshold. Cut such a line at the cap, keep advancing one line so paging cannot stall, and tell the model the line was cut and where to continue. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
1b4df00363 | 🐛 fix(tasks): restore compact filled task groups (#19654) | ||
|
|
48773ff4c4 |
💄 style(page-editor): keep the line comment marker inside the editor column (#19664)
Drop the thread count from the comments panel header, and move the hover marker into the column's end padding so it no longer spills past the scroll pane where the open panel covered it and forced horizontal scrolling. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
ebe586289d |
✨ feat(page-editor): align document comments beside their source text (#19659)
* ✨ feat(page-editor): align document comments beside their source text Adds a gutter that positions comment threads next to the highlighted text they annotate instead of stacking them in a flat list below the document, with per-line quick-comment affordance and anchored/ unanchored thread partitioning in the comment list API. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * 🌐 i18n: sync generated locale translations Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * 🐛 fix(page-editor): hand failed anchored drafts back and bound the gutter's eager load - A failed anchored submission stashed while a newer draft owned the shared slot was written under `anchored-failed:<clientId>` and never read again, so the toast was the last the reader saw of their text. The provider now moves the stash back into the slot whenever that slot frees (the newer draft sent or cancelled, or the next open) and republishes its anchor, remounting the gutter composer beside the original selection with the failed words and attachments, ready to retry. The inline composer, always mounted and already holding the slot's state, adopts the stash itself on mount instead. - Anchored threads were drained to the last page whenever the document mounted, panel open or not, and every result mounted twice (gutter and list). Fetch ahead only while the gutter shows them, up to ANCHORED_EAGER_PAGE_LIMIT pages; a picked highlight past that fetches its thread on demand through the existing deep-link pin, and the list below the body now pages both caches so the rest stays reachable. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * 🐛 fix(page-editor): forward paging keys from gutter controls and clear the marker over root whitespace - Keyboard focus usually enters the panel on a card's button or link, and the key forwarder skipped every key for any interactive target, so PageUp/PageDown from there did nothing: the control has no use for them and the browser default cannot scroll the sibling document pane. Decide ownership per key — a field or editable region keeps everything typed into it, a button or link keeps only Space (its activation). - Over the body's own whitespace between or after blocks, `blockAt` is null and the move handler returned early, leaving the marker for the last crossed line actionable for text no longer under the pointer; the host-level handler never sees those moves either, since the body contains the target. Clear it there unless the pointer is on the bridge to (or over) the current marker, the one path meant to survive. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * 🐛 fix(page-editor): report a missing deep-linked reply once when the gutter duplicates the thread An anchored thread shown in both the gutter and the flat list received the same focus in both copies, so a deep link to a missing or failed reply ran both copies' missing-focus effect: two toasts, and the focus token advanced twice. The flat copy keeps the focus for its highlight but no longer answers for a miss when the gutter shows the same thread. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * 🐛 fix(page-editor): partition thread pages client-side when the server ignores the scope The `anchored` filter on listThreads is new in this change. A production backend predating it strips the unknown key and answers the anchored and document scopes with the same mixed pages — which is exactly what the documented debug proxy (latest SPA against app.lobehub.com) produces — so the two caches overlapped and the list below the body rendered every such thread twice, with duplicate React keys. Filter each page to its scope's own subset on the client; the cursor still walks the server's order, so paging stays consistent. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
697f66a800 | ✨ feat: expose router affinity hooks (#19648) | ||
|
|
b0ef4b2ef1 |
⬆️ chore: upgrade actions/checkout to v7 (#19656)
⬆️ chore: upgrade actions/checkout to v7 across all workflows - Bump actions/checkout from v3/v4/v6 to v7 (latest release v7.0.1) in 36 files (61 references) - Includes workflows, docs example, keeping version references consistent - Note: v7 blocks checking out fork PRs for pull_request_target / workflow_run triggers by default (safer defaults); verified existing usages only checkout the base ref so no behavior change expected Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com> |
||
|
|
270a4a50cb |
✨ feat(fts-search): add pg_like provider for plain PostgreSQL (#19188)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
2a3f0bcff6 |
🗃️ chore(database): record where a distilled standard came from, and what it rests on (#19627)
* 🗃️ chore(database): record where a distilled standard came from, and what it rests on Four columns for turning a reviewer's rejections into delivery standards. Nothing reads them yet; the capture pipeline and the backtest are stacked on top. **`expertise_hits.source_check_result_id`** — the reverse lookup "which rejections taught this lesson". A hit could only reach an acceptance through `evidence_id`, and 260 of this owner's 876 rejections circled no evidence at all, so that path loses a third of the provenance. `on delete set null`: the link is provenance, not ownership, so deleting an acceptance must not delete the rule it taught — pinned by a test that deletes the check result and asserts the hit survives. **`expertise_lessons.reason_kind`** — whether the standard rests on a mechanism or on the owner's taste. Both are legitimate; only one can be enforced automatically, because a taste standard has no test a delivery could be measured against. This has to be a column rather than a note in the lesson body: the compile step reads it to refuse compiling taste into a criterion that blocks work on its own, and the body's wording follows the reviewer's own language, so matching on a phrase would silently miss for anyone not writing in Chinese. **`expertise_lessons.reason_source`** — whether the reviewer gave the reason or the distillation supplied it. An inferred reason is worth keeping, but it must never carry the weight of something the reviewer actually said. Both are decided when a lesson is born. A later rejection that supplies a real mechanism should arguably upgrade a `taste` lesson; the upgrade rule is not worked out, so that does not happen yet. **`expertise_lessons.backtest`** — how a standard scored against the reviewer's own past decisions before anything compiles it. The metric is precision on the units it fired on, deliberately not recall: every standard covers a narrow slice, so recall is near zero for all of them and separates nothing, while firing on approved deliveries is the failure that makes someone switch the feature off. Read against the base rate, not 1.0 — 876 of 1,956 judged units are rejections, so random firing already scores ~0.45. jsonb because the metric set is still moving. **`EXPERTISE_SUBJECT_TYPES` gains `standalone`** so a practice run can mirror the subject of the acceptance it came from (36 of 193 carry no in-product subject). `text({ enum })` emits no constraint, so it costs no migration. No indexes: the read paths are lesson → hits (already indexed) and the lesson row itself, and `expertise_hits` is the one table here with real scale risk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 📝 docs(database): write the expertise schema comments in English Comment-only change; the schema itself is untouched (verified by comparing the comment-stripped AST before and after). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🗃️ chore(database): keep which delivery each generalized boundary rests on A consolidation pass rewrites a standard from several rejections and may write exemptions into its `limits`. The only thing that makes such an exemption legitimate is a delivery the reviewer accepted while the standard's subject was plainly in view — and the rewrite text cannot carry that: the model cites deliveries by per-request labels ("S2") that mean nothing once the request is gone. Without a stored link, "why is this allowed" has no answer a week later. `expertise_lesson_revisions.evidence` (jsonb, nullable) records the rejected deliveries the pass read, the accepted ones it was offered, and for each boundary the accepted check result ids it was read from. null on a person's rewrite, whose authority is `feedback`. jsonb rather than a link table: a pass reads a handful of deliveries and is audited one revision at a time; the ids are provenance, so a deleted check simply stops resolving. Folded into the branch's unshipped 0165 migration by regenerating it, per the migration guide, so the PR still carries a single migration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8b7a674d22 |
✨ feat(cli): add lh doctor to diagnose the whole chain in one command (#19626)
* ✨ feat(cli): add `lh doctor` to diagnose the whole chain in one command Diagnosing a broken `lh` today means knowing which layer to suspect: `status` answers only "does the gateway accept this machine", `whoami` only "who am I". Everything else — a node too old for the CLI's own imports, a device gateway that belongs to another deployment, a workspace scope saved under a different account, a provider with no key — surfaces much later as an error that names the symptom rather than the cause. `lh doctor` walks the chain in order and stops reporting derived symptoms once it finds a root cause: runtime → endpoints → credentials → scope → run context → server → device → execution - Profiles: bare `doctor` runs the core layers (fast, cheap); `--profile connect | agent | hetero | selfhost | all` adds the deeper ones. - `--offline` skips everything that touches the network, `--deep` opts into the costly checks, so a diagnostic never spends money by default. - `--json` prints the same report object the terminal renderer consumes, one entry per check with `status`, `detail`, `evidence`, `fix` and — for skips — the upstream check or flag that excluded it. - Exit code contract: 0 unless something failed, `--strict` promotes warnings. - `--fix` applies only the repairs a check knows how to make (stale workspace scope, unparseable settings.json, loose permissions, leftover daemon state) and re-runs the check so the report shows the repaired state. - `--deep --agent <id|slug>` starts a real run and reports tokens and cost, proving the chain end to end instead of inspecting it. Also extracts the credential precedence (`LOBEHUB_JWT` > explicit flags > `LOBEHUB_CLI_API_KEY` > stored login) into `pickAuthSource`, shared by `resolveToken`, the tRPC client and doctor — it was spelled out twice and had already drifted on which env names it accepts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(cli): stop doctor from leaking proxy credentials or repairing healthy state Five findings from an independent review of the doctor series, all real: - `--fix` deleted the pid/status files of a daemon that was merely reconnecting, orphaning a live process `connect stop` could no longer find. Repairs now run only for the finding a check marks `evidence.repairable`, and each repair re-checks which state it was handed. The runner skips repairs entirely for findings without that marker, so a warning with nothing to undo no longer reports a failed repair. - Proxy env vars were copied into the report verbatim. `http://user:pass@host` is a normal way to configure one, and the report is meant to be pasted into an issue — URL credentials are now redacted. - A self-hosted server with the default device gateway failed `endpoints.resolution`, which skipped reachability, credentials, identity and every agent check behind it. An installation that only uses the HTTP API needs no device gateway, so this is a warning; the device profile's own handshake still fails concretely. - `--fix` dropped a valid saved workspace scope whenever `LOBEHUB_WORKSPACE_ID` overrode it for one run, silently moving the user to personal scope once the override went away. - `getAiProviderList` reports a provider's toggle, not whether a key is stored, so an enabled-but-keyless provider was reported as ready and the run then died with `InvalidProviderAPIKey` — the exact failure the check exists to pre-empt. Agent readiness now reads that one provider's key vault and names where the key comes from, saying "not verified" rather than passing when a restricted API key cannot see vaults at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 📝 docs(cli): record why the node check cannot catch a too-old runtime Closing that gap means changing how the CLI is launched, which is not this command's to change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(cli): keep doctor from crashing, over-claiming, or leaking identifiers Ten findings from a second independent review, all confirmed: - A socket failure reaching the gateway arrives as an EventEmitter `error` event, which `connect().catch()` cannot see. With no listener the whole report died with "Unhandled error" instead of reporting a failed handshake — verified by removing the new listener and reproducing the crash against a refused port. - Server, gateway and proxy URLs are redacted at the report boundary, not just in the proxy variable: a basic-auth URL in `LOBEHUB_SERVER` used to print its password in `--json`. Gateway error text also quoted the WebSocket query string, carrying hostname, device id and user id; the account email is now masked too. The raw values still flow to whatever connects with them. - "Enabled on the server" no longer claims a key exists. `deepseek` is enabled unconditionally in the server's own config, and `ENABLED_OPENAI` / `ENABLED_OLLAMA` default to true, so the report now says the server reports it enabled and that server-side keys are invisible from here. - A provider holding only a `baseURL` is a local runtime (Ollama, LM Studio), not a missing key: it warns instead of failing, so it no longer blocks the deep round trip through `dependsOn`. - The round trip only reads "no longer tracked" as success once it has seen the run live. The server also answers null when there is no state at all, which made the check incapable of failing. - A saved workspace scope is only repairable when the account is identified and differs. Under `LOBEHUB_CLI_API_KEY` there is no readable subject at all, and `--fix` was deleting a selection that was still valid for the stored login. - The settings repair no longer overwrites an existing `.bak`. - The update check bounds its own registry request: the runner's timeout would otherwise surface as a failure and a non-zero exit, for something this check only ever reports as a warning. - The command sets `process.exitCode` instead of calling `process.exit`, which does not wait for stdout to drain and can truncate a piped JSON report. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(cli): redact the server URL the workspace-scope check reports Sweeping every check's evidence for an injected credential — the way the acceptance round does — turned up one the previous pass missed: the scope check copies the resolved server URL verbatim, so a basic-auth self-hosted URL put its password into `--json`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(cli): make the URL redactor linear instead of backtracking CodeQL flagged js/polynomial-redos: matching the URL lazily and then requiring a `?` backtracks across the remainder of the string at every starting position, so an error message with many `ws://` runs takes quadratic time. Tokenise first and decide per token — same output, no backtracking, with a test that fails if the quadratic shape comes back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(cli): scrub the doctor report centrally and stop it lingering after exit Seven findings from a third independent review, all confirmed: - Redaction had leaked in a third place: Node's fetch error quotes the full request URL, and capability and daemon evidence copied gateway URLs as-is. Per-check redaction had now missed an instance in each of three rounds, so the runner scrubs every result — detail, fix, evidence, repair messages — before it enters the report: URL userinfo, WebSocket query strings and email addresses, by linear token scanning rather than a backtracking pattern. - With `process.exit` gone, the process waited for the event loop to empty, so a request abandoned by a check timeout, or the registry check's uncleared timer, kept it alive — up to the full `--timeout` after the report printed. The command now exits from a stdout write callback: after the output has drained, and no later. - The registry check clears its timer and gets a runner budget that always outlasts its own deadline; a sub-second `--timeout` no longer turns its warning into a failure. - A settings.json that cannot be read is no longer classified as corrupt and offered to `--fix`, which moved a possibly valid file aside unparsed. The repair also re-confirms the parse failure before moving anything. - A run that stops being tracked is inconclusive even after it was seen running: the server returns null for lost state too, so only `isCompleted` proves the round trip. - A live daemon that has not reported a connection state is a warning, not "connected as device unknown". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4366b09a92 | 🐛 fix: isolate agent share visitor files (#19649) | ||
|
|
1adb625d4c | 🐛 fix: refresh work galleries after document deletion (#19647) | ||
|
|
b3f55110f0 | ✨ feat: capture provider-native stream diagnostics (#19643) | ||
|
|
170161bb93 | 🐛 fix(task): keep artifact previews in run drawer (#19641) | ||
|
|
0ec5e3e404 |
✨ feat(agent-share): let share visitors attach files to a turn (#19569)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
1edd085789 | 🌐 i18n: add copy for invalid shared page format (#19628) v2.2.18-canary.18 | ||
|
|
101c110474 |
🐛 fix(desktop): stop crashing the main process when a user folder cannot be resolved (#19598)
* 🐛 fix(desktop): stop crashing the main process when a user folder cannot be resolved On Windows, app.getPath('pictures') (and music/videos/downloads) throws when the Known Folder is redirected or missing. The gateway system_info_request handler called it without a guard, so sending a message took down the whole app. Resolve those folders through a tolerant helper, catch handler failures, and drop unresolved paths before they reach the prompt template so it falls back to '(not reported)'. * 🐛 fix(desktop): always answer failed system-info requests and bound the gateway read A rejected getShellInfo() or an unguarded app.getPath call used to be logged without a correlated reply, and getDeviceSystemInfo() posted without a timeout, so the agent run stayed parked for the lifetime of the socket. Reply with success: false on failure and apply the shared device-query timeout so the run degrades to unavailable system info. * ✅ test(desktop): cover a throwing optional folder lookup in the gateway system-info handler |
||
|
|
5c12148b8e | 🐛 fix(auth): allow Android passkey origins (#19640) | ||
|
|
1e5fd6270a |
✨ feat(heterogeneous-agents): run pi over the RPC transport with cross-turn process reuse (#18346)
Co-authored-by: AmAzing129 <115673583+AmAzing129@users.noreply.github.com>v2.2.18-canary.17 |
||
|
|
52756f6904 |
✨ chore(eval): add run Harbor Skill (#19625)
* ✨ feat(eval): add local Harbor harness * ✨ feat(eval): support local and cloud Harbor targets * 🐛 fix(eval): expose npm lh binary to Harbor shells * 🐛 fix(eval): harden Harbor bootstrap and runner |
||
|
|
9c377eecf0 |
✨ feat(page-editor): anchor document comments to a text selection (#19602)
* ✨ feat(page-editor): anchor document comments to a text selection Let a reader create a comment from a selected run of the document body. The comment stores a text-quote anchor (quote + prefix/suffix + offsets) that re-locates itself against the current body on every load, and the editor paints an inline highlight and thread badge for each anchored root comment. Replies inherit the thread's anchor. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 🐛 fix(page-editor): paint every anchored thread and keep selection comments on locked pages Body highlights now come from a document-wide listAnchors endpoint instead of the paginated thread cards, so an anchored comment beyond the loaded pages is still discoverable from the body; picking such a run pins its thread the way a deep link does. The Comment selection action moves to a selection-only floating toolbar that stays registered while the editor is read-only, so a reader can still comment on a selection while another collaborator holds the edit lock. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 🐛 fix(page-editor): pin a body-picked thread without scrolling to the list A focus target now carries whether landing scrolls the card into view: a notification deep link does, a pick made in the document body does not, so pointing at a sentence never throws the reader down to the comments. Also drops the "badge" wording from the anchor contract — the body paints a highlight for each anchored thread, nothing more. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ⚡️ perf(page-editor): resolve comment anchors once per distinct quote and body text Anchor resolution moves into a pure resolveAnchors step that reuses every result while the flattened body text is unchanged and scans once per distinct anchor, so a formatting-only update no longer re-runs a full indexOf for each orphaned quote. Also stops marking the composer's quote row aria-disabled: only a jump target whose run is gone is disabled, not a row holding an operable Remove control. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
846740a38f |
💄 style(goal): drop the duplicated 8px block padding around goal sections (#19619)
Each section of the goal process-control band wrapped its content in an extra 8px block padding. The accordion header already carries 6px of its own, so every title ended up floating in ~14px of space above its content and ~14px below the previous one, which read as an oversized hover bar on a three-character label. Remove the wrapper: the header keeps its 6px (it is the click target), and the sections now sit 6px under their titles. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
585fa593a5 |
🔖 chore(cli): bump version to 0.0.55 (#19618)
Ships the hetero `/goal` conversation flow (#19588), the operation-token renewal for long hetero exec runs (#19549), the wrapped artifact share URL (#19536) and readable task slugs (#19503). Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1f311b14b2 |
♻️ refactor(mecha): decide the tool rules of a run in the shared core (#19604)
* ♻️ refactor(mecha): decide the tool rules of a run in the shared core Move the tool-mode derivation, the per-tool enable rules, the default tool selection and the device walls out of the two tools-engine constructors into `@lobechat/mecha` as `resolveToolRules(request)`. Both hosts now assemble facts (pinned plugins, execution target, device facts, model capabilities, knowledge / memory / search flags) and feed the shared result to `createEnableChecker`. Alignment agreed in review: - the mode is chat / custom / agent from the chat config alone on both hosts; the browser no longer demotes a run to chat mode when the model cannot call functions (the engine marks the tools incompatible instead) - the cloud sandbox tool is offered for the `auto` target on the browser too, and memory follows the host-resolved flag - the device picker and the device walls only apply behind a device gateway; the browser runtime supplies no device facts and keeps its platform filter The pure mode / plan predicates move to `@lobechat/types`; the device tool registry and the builtin-tool wall move to mecha, re-exported from their server homes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 🐛 fix(mecha): never offer the device picker without a gateway Gateway facts and device policy are separate inputs now: the picker and the Computer Use opt-in need a gateway device, while the policy / plan walls apply with or without one. A self-hosted server without a gateway whose persisted target is `device` no longer advertises a picker it cannot dispatch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
7bca055b6a |
✨ feat(resource): preview files and pages in an inline right detail panel (#19599)
* ✨ feat(resource): inline right detail panel for file clicks Single click on a file row now opens an in-context split-screen detail dock (metadata + live preview) instead of jumping into the fullscreen editor. Double click keeps the committed fullscreen gesture and the ?file= deep-link write. Folders and pages keep their existing behaviors. * 🐛 fix(resource): render the fetched file in the detail panel The panel read a nonexistent `fromQuery` field off the SWR response, so a file that was not already in the loaded list left the panel blank. Move the lookup into `useDetailPanelFile`, which reads SWR's `data`, and cover it with a regression test. Also import `DraggablePanel` from base-ui and stop passing `flexShrink` through to the DOM. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 feat(resource): collapse the detail panel properties by default Give the preview the whole dock and move the file properties behind an info toggle in the panel header, next to download and close, matching the fullscreen editor's header actions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(resource): preview pages in the detail panel and fit grid columns to its width Single click on a page row now opens the right detail panel with a read-only Markdown preview; double click keeps the fullscreen page editor. The library sidebar tree keeps opening the fullscreen editor, since it also renders where no explorer panel exists. The masonry grid derives its column count from its own content width (200px minimum card, 2 to 5 columns), so opening the panel drops it to two columns instead of squeezing four. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(resource): address review on the detail panel lookup and double click Resolve the panel item from the resource store as well as the legacy fileList. Explorer rows only live in resourceMap, so every first open of a visible row rendered a blank panel and fired a duplicate getKnowledgeItem request. Wait out the double-click window before expanding a closed panel. Expanding the panel narrows the explorer and reflows the masonry, which could move the clicked card before the second click and swallow the double click. Switching an already open panel stays instant, and a double click cancels the pending open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>v2.2.18-canary.16 |