mirror of
https://github.com/lobehub/lobehub.git
synced 2026-09-20 04:56:13 +08:00
fix/refer-topic-message-rendering
1542 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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) | ||
|
|
b3f55110f0 | ✨ feat: capture provider-native stream diagnostics (#19643) | ||
|
|
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> |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
394ba3a54f |
✨ feat(goal): create and supervise a goal from a hetero agent conversation with /goal (#19588)
* ✨ feat(goal): let /goal create and supervise a goal from a hetero agent conversation - /goal in a Claude Code / Codex / Kimi conversation injects instructions to run `lh goal create --conversation` and plan in the same run; no goal tool. - The server creates the goal from the running operation: the conversation agent supervises it, the goal is bound to the topic, and that run is adopted as the first planning turn (turn token returned to the CLI). - Device and gateway runs authenticate with an operation token carrying the new goal:manage capability; local desktop runs use the login with their topic. - lh hetero exec echoes LOBEHUB_OPERATION_ID / LOBEHUB_TOPIC_ID into the agent. - Management spend counts only planning turns, not the rest of the chat. - Conversation shows linked goals above the composer; a new Goal portal view shows the whole goal, and goal cards open it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): keep Claude Code from taking over /goal and plan from the create output - Strip the /goal prefix from the prompt sent to hetero CLIs. Claude Code has its own /goal command; sent verbatim it pinned a session goal and a stop hook that would not let the run end, while the LobeHub goal waited for that run to end before dispatching work. The instructions stay in the system context. - Tell the agent to plan from the graph printed by lh goal create instead of running lh goal show, which the run's operation token cannot call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): show a goal the running conversation creates without a reload The linked-goal tray read the topic's goals once on mount and only polled once a goal existed, so a /goal run that created one mid-run left the tray empty until the page reloaded. Poll while the conversation is generating. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): scope goal:manage to /goal runs and keep adopted-turn spend Codex review on #19588: - Every hetero run got goal:manage, so an ordinary run could create goals through the CLI; grant it only when the prompt is a /goal request (renewal copies the minted claims). - A later manager turn replaced the receipt's adopted/operationId, dropping the conversation run's cost from goal usage and the budget check; carry adoptedOperationId across turns and count it even after a topic handoff. - The linked-goal tray polled the goal list on every generation; poll for new goals only while a /goal request is generating. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8fa80700de |
🐛 fix(desktop): confirm hetero cancellation when the app lost its task record (#19603)
After a desktop restart both in-memory registries are empty, so cancelHeteroTask answered "No task found" without exited: true and the server kept the task unstoppable forever. Look the operation up in the OS process table instead: confirm the exit when no hetero exec wrapper is alive, terminate an orphaned wrapper group and confirm only after it exits, and stay unconfirmed when the lookup fails. Windows keeps today's answer. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c5ec20abab |
✨ feat(docker): run self-hosted Compose chats in Gateway Mode by default (#19583)
* ✨ feat(docker): run self-hosted Compose chats in Gateway Mode by default - Add the lobehub-gateway service to docker-compose and wire the lobe service to it; AGENT_GATEWAY_INTERNAL_URL keeps server pushes on the Compose network while browsers use AGENT_GATEWAY_URL. - S3_INTERNAL_ENDPOINT routes the server's own S3 requests to rustfs:9000 while presigned URLs keep the browser-facing S3_ENDPOINT, which local mode sets to localhost and the container cannot reach. - setup.sh generates a per-deployment JWKS_KEY and gateway token and writes the gateway URL for each deploy mode; the .env template drops the public JWKS key. - Warn loudly at container startup when an upgraded stack lacks the gateway settings or still uses the public example JWKS_KEY, and document the upgrade. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🔒 fix(docker): pass only the public JWK to the gateway container The gateway only needs the public half of JWKS_KEY to verify browser sessions, but it received the full private key set, so anyone able to read the gateway environment could sign tokens LobeHub trusts. setup.sh now also writes JWKS_PUBLIC_KEY (the same key without d/p/q/dp/dq/qi), Compose passes only that to the gateway, and the startup check warns when it is missing, still contains private fields, or does not match JWKS_KEY. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c2e0428bee |
💄 feat(goal): show who each task is assigned to on the goal page (#19591)
* 💄 feat(goal): show who each task is assigned to on the goal page The current-task list and the exploration graph named a task but never who was doing it, so a goal routed to a separate executor read the same as one the supervising agent runs itself. The graph read now carries each dispatched task node's assignee from its Task row, and both surfaces render the agent's avatar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 feat(goal): open the assignee's profile card and pin it top-right on graph cards Review feedback on the assignee avatar: a bare name tooltip says little, so hovering the avatar now opens the agent profile card groups already use. On exploration graph cards the avatar moves to the top-right corner and the verifier shield takes the bottom-left of the metric strip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 feat(agent-profile): label the agent runtime and hide the model for external agents Review feedback on the goal assignee card: it never said whether the agent is Claude Code or a built-in agent, and showed a LobeHub model for a Claude Code agent, which is not what runs it. The profile card now tags the name with the runtime (or "Built-in agent"), and an external agent shows the runtime it runs on instead of a model or a model switcher. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 feat(agent-profile): drop the runtime tag beside the agent name Review feedback: the card needs neither a "Built-in agent" tag nor the runtime tag next to the name. The footer line naming an external agent's runtime in place of the model stays. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): narrow the assignee read and refresh goal graphs after reassignment Codex review on #19591: - The graph poll read full task rows (instruction, editor data, config) for every task node just to take the assignee; read only id + assigneeAgentId. - Reassigning a task refreshed only task caches, so a paused or finished goal, which does not poll, kept showing the old executor; revalidate goal graphs when the assignee changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e966c4f948 |
🐛 fix(device): 修复设备文件操作误报不在批准的工作区内的多个场景 (#16962)
* 🐛 fix(device): register workspaces, allow symlinks, include worktree paths - include worktree (git.activeWorktree) in approved workspace roots - register device skill roots in an in-memory cache so previews pass the guard without polluting the UI's working-directory list - allow symlinked directories in desktop and CLI file preview check - detect Windows UNC paths correctly in the path containment check - normalize filesystem path traversal (../) in the CLI fallback check * ♻️ refactor(device): use pathe instead of manual win32/posix branch * 🐛 fix(device): restore missing pathe relative import |
||
|
|
f8cba5f95e |
⏪️ revert(chat): remove topic execution isolation (#19596)
* ⏪️ revert(chat): remove topic execution isolation * 🐛 fix(agent): honor workspace device overrides on resume |
||
|
|
119c09ab39 |
♻️ refactor(mecha): decide the model parameters of an LLM call in the shared core (#19586)
* ♻️ refactor(mecha): decide the model parameters of an LLM call in the shared core Move the model-parameter rules out of the server's per-attempt hints into `@lobechat/mecha` as `resolveModelParams(request, providers)`: which extend params the model consumes (user row → provider card → canonical card), where the reasoning effort comes from (topic pin, only for the pinned model and, in a group topic, the answering agent → model-instance config → sub-agent override), when assistant reasoning must be replayed (Kimi, DeepSeek thinking, Meta, card support, provider fallback), the capability defaults the context may assume, the history window, and the stream flag. The server adapter now only supplies lookups (`createServerModelParamsProviders`) and the run's frozen snapshots. `stream` falls back to the agent's `chatConfig.enableStreaming` when the operation did not pin one; the DeepSeek model-id predicates move to a dependency-free module so the shared package can import them without the model bank. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 🐛 fix(server): trace the streaming mode the request really uses The chat span recorded `ctx.stream ?? true` while the payload now honours the agent's chat config, so a newly non-streaming request was labelled as streaming in telemetry. The built context rides along on the LLM trace input and both the payload and the span read the same resolved value. Also point the add-model-provider integration map at the shared resolver as the policy owner. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
43e71aa778 |
✨ feat(agent-runtime): hand a server run's turn back when messages are queued behind it (#19585)
* ✨ feat(agent-runtime): hand a server run's turn back when messages are queued behind it A queued follow-up only interrupted a run at the next step boundary in the browser runtime; a Gateway run never learned about the composer's queue and made the follow-up wait for every remaining step. The client now mirrors whether messages are queued behind its running Gateway run into a per-operation flag (owner-checked, with a share-visitor mirror). The server reads it before each step and hands it to the agent's step context, so the existing queued-message interrupt ends the turn early. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(chat): skip the queued-messages request when a Gateway run starts with an empty queue Every Gateway run sent a pending:false flag request on start, even though a run starts unflagged and nothing was queued behind it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(agent-runtime): keep a queued follow-up's steer mark on the Gateway path In Gateway mode the server persists the user message and its snapshot replaces the client's optimistic row, but only the request trigger was forwarded. A follow-up drained from the input queue lost metadata.steer and rendered as a new turn instead of a continuation of the interrupted one. Forward steer through execAgentTask (and the share-visitor mirror) and persist it on the user message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(agent-runtime): order queued-flag writes and treat each step's read as authoritative Addresses review feedback on the queued-messages flag: - Queue-then-delete fired two independent requests; a delayed pending:true could land after pending:false and end the run with an empty queue. Writes for one operation are now sent in order, keeping only the latest wanted value while a request is in flight. - The step boundary only ever added hasQueuedMessages, so a context arriving with true would keep ending the turn after the queue emptied. The read now sets or removes the field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1f4d6a7116 |
♻️ refactor(chat): gather the browser's per-step context through the shared rules (#19577)
* ♻️ refactor(chat): gather the browser's per-step context through the shared rules The browser's `contextEngineering` no longer decides which facts a turn needs. It assembles a `ContextFactRequest` from the stores and hands the lookups to `createBrowserContextFactProviders`; `gatherContextFacts` in `@lobechat/mecha` applies the same rules as the server runtime. The chat service stops prefetching agent documents and building the agent builder context itself — both now come out of the shared rules. Browser behaviour aligned with the server: - inside a workspace the creds tool lists the workspace's organization creds (new `market.creds.listForContext`, scoped on the verified workspace header); personal creds otherwise - Composio services and connector status count only ACTIVE connections - the group builder's shared prompt reads the group content, and its official tools list comes from the shared catalog helper - custom (MCP) plugins are listed in the agent-management context - agent documents follow the edited agent while the builder is active, resolved cache-first through the store - `contextEngineering` accepts a `shareVisitor`, so a visitor run gets the same gates as on the server Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 🐛 fix(mecha): keep uninstalled builtins out of the builder's installed tools `listOfficialTools` marked every registered builtin as installed, so the browser lost the tool store's uninstalled filter and the builder could pin identifiers the user had removed. The uninstalled list is now a fact the host provides (`listUninstalledBuiltinIds`): the browser reads the tool store, the server reads the user's tool settings for the run's scope, and both builder contexts list those tools as `installed="false"`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 🐛 fix(mecha): seed uninstalled builtins on the server and keep visitor topic ownership in the browser - The server provider returned nothing for a never-configured scope, so a new account or a workspace without its own slot saw every builtin as installed. It now falls back to `defaultUninstalledBuiltinTools`, the same seed the browser's tool store applies. - The browser's `findTopic` dropped the ownership fields, so a share visitor's `<refer_topic>` could never pass the shared visibility rule. A visitor's store only holds topics the server already scoped to that visitor and agent, so the provider attributes cached topics to the visitor and routes their message reads through the share endpoint. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
237392c468 |
♻️ refactor(mecha): decide which context facts a step needs in the shared core (#19576)
* ♻️ refactor(mecha): decide which context facts a step needs in the shared core Move the "what to gather" rules for the per-step context (topic references, agent documents, onboarding, workspace links, plan/todo, credentials, Composio services, agent/group builder, agent management, official tools, sandbox and identity variables) into `@lobechat/mecha` as `gatherContextFacts(request, providers)`. Hosts now only answer typed lookups (`ContextFactProviders`); the server adopts it and its twelve providers collapse into one fetcher factory. Rule alignment with the browser, as agreed in review: - plan block is injected on the server when a plan document exists; history still wins for the TODO state - credentials are only fetched with `lobe-creds` and use the workspace's organization creds inside a workspace - Composio services are gated on the creds tool and the deployment feature, disabled ids come from the run's tri-state `disabledPluginIds` - onboarding follows the server rule, including the already-injected check - topic references skip when the context is already injected - agent-builder documents target the edited agent on both hosts - the agent-management context (available agents / providers / plugins) is now actually injected on the server; the previous computation was dead - official tools include LobeHub skill connectors on the server - share-visitor gates live in the shared rules, keyed on the principal Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 🔒 fix(mecha): keep the owner's agents, providers and plugins away from a share visitor Auto skill mode still enumerated the creator's other agents into <available_agents> for a visitor run. The agent-management facts are owner-scoped, so they are skipped whenever the request carries a share visitor; @-mentions still flow through. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 🐛 fix(server): offer installed custom MCP connectors in the agent-management context The run's tool source map only classifies skill, Composio and client tools, so filtering it for 'mcp' never yielded a custom connector and createAgent / updateAgent could not offer one. Read the user's installed custom plugins from the plugins table instead — pinned or not — which is what the browser lists from its tool store. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
73a2f2ec65 |
🐛 fix(goal): make the goal agent the supervisor and fix exploration graph layout (#19578)
* 🐛 fix(goal): make the goal agent the supervisor and fix exploration graph layout - goal.agentId is now the supervising agent: it owns the agent's goal list, the goal page and every main-Agent planning turn. The separate config.manager.agentId is removed. - New config.taskAgentId routes coordinator-created tasks to a dedicated executor (falls back to goal.agentId). Add setTaskAgent / `lh goal set-task-agent` and `lh goal create --task-agent`; setAgent no longer moves tasks held by a dedicated executor. - A supervision handoff opens the next management topic in the new agent's history; turns from the previous agent can no longer submit plans. - Task result portal and supervision panel get a "..." menu to copy the task link / task ID / topic ID. - Exploration graph places each node under its parents and relayouts on the cards' measured heights, so findings no longer overlap tall task cards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 fix(goal): move the task result menu next to the task name and open the supervision topic - The task result portal's "..." menu now sits right after the task name, like the goal and supervision headers, instead of beside the close button. - The supervision panel's "..." menu can open the manager topic in the supervising agent's own chat, next to Copy Topic ID. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): harden executor routing and supervisor handoff against races - Fall back to the goal agent when the dedicated executor was deleted; taskAgentId is JSON, so deleting the agent does not null it and createTask rejected the stale id on every tick. - Write config.taskAgentId with an atomic JSONB patch (setTaskAgent and restart) instead of replacing the whole config, so a concurrent budget or acceptance edit is not discarded. - Refuse a planning claim when the locked goal row names a different goal agent than the graph snapshot, so a handoff landing mid-claim does not dispatch the previous supervisor or spend the new one's turn. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): keep goal supervision topics out of the chat sidebar The main-Agent planning topic and the recovery supervisor topic were created without a trigger, so they showed up in the agent's chat sidebar and Recent like ordinary conversations. Tag both with a dedicated goal_supervision trigger and exclude it wherever system topics are filtered (sidebar fetch, topic selectors, Recent). The goal page still opens them by id. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c3a7642295 |
✨ feat(oauth-apps): support web applications with redirect URIs (#19504)
* ✨ feat(oauth-apps): support web applications with redirect URIs OAuth apps created from the settings page were device-flow only: no redirect URIs, no response types, and no client secret. That covers CLIs but leaves no way to register a server-side app that signs users in with a LobeHub account through the authorization code flow. Adds a `web` application type alongside the existing device type. A web app gets `authorization_code` + `refresh_token` grants, a `code` response type, the redirect URIs its owner registers, and a confidential client secret issued once at creation with a rotate action afterwards. The secret is encrypted at rest with `KeyVaultsGateKeeper`, mirroring how API keys are stored, and decrypted in the OIDC adapter because the token endpoint compares it in plaintext. A secret that fails to decrypt is dropped rather than passed through, so the request fails as `invalid_client` instead of making ciphertext the credential. Redirect URIs are matched exactly: no wildcards, no fragments, no embedded credentials, and https everywhere except loopback hosts. The same rules run in the form and again in the tRPC input, since only the server-side pass is binding. No migration: `oidc_clients` already carries every column this needs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(oauth-apps): keep the adapter's secret round-trip symmetric Two follow-ups from review. `Client.upsert` still wrote the secret in plaintext while `find` now expects ciphertext, so anything persisting a client through the adapter would read back a credential it could not authenticate with. Encrypt on the way in too, and leave the stored secret untouched when an update payload omits it. The conflict branch also referenced a camelCase key that never existed on the payload, so it silently wrote nothing. An `update` carrying an empty redirect URI list passed validation and stripped a web app of every callback, contradicting the create-time requirement. Reject a list that normalizes to empty wherever one is supplied. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 feat(oauth-apps): address acceptance review on create, detail and consent flows Application type is now picked from two icon cards that explain each kind, instead of a bare segmented toggle. Redirect URIs move out of the create dialog and are configured on the app page afterwards; the type stays at creation because it decides whether a client secret is issued and cannot change once the app exists. A freshly created web app shows that it has no redirect URIs yet and cannot sign anyone in until one is added. The detail page separates reading from editing. Basic info and redirect URIs each render read-only with an Edit action, and open a form with Save and Cancel only on request. Redirect URIs are edited as a list of rows rather than a newline-separated textarea. Authorization errors the provider cannot redirect back with, such as an unregistered redirect URI, now land on a dedicated page with a plain explanation per error code instead of a raw JSON dump. Only `error` and `error_description` are forwarded. The consent and sign-in confirmation cards center their title and subtitle so the text lines up with the centered application logo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ♿ fix(oauth-apps): label redirect URI row removal and color the list error The icon-only remove button on each redirect URI row had no accessible name, so assistive tech announced it as an unlabeled button. The "at least one redirect URI" message rendered in body color: antd's ErrorList only picks up error styling inside a Form.Item with an error status, and the list-level message sits outside one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d1a33225f7 |
⚙️ chore(goal): raise the default per-Task attempt budget from 3 to 8 (#19575)
Three attempts ran out on long-horizon Tasks whose work was already done: two were spent on infrastructure (an expired operation token, an abandoned run) and the third on a review asking for one more piece of evidence, so a finished Task stopped the whole goal on a person. The default now applies consistently everywhere a goal gets its budget: - `resolveTaskAttemptBudget` server default (goals without an explicit budget) - `DEFAULT_GOAL_MAX_ROUNDS`, which the create-goal modal prefills and submits - the createGoal tool manifest and the /goal system prompt, which told the model to default `maxIterations` to 3 and therefore pinned tool-created goals An explicit `recovery.maxAttemptsPerTask` is unchanged, and the 2–10 range stays. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fbd47003da |
♻️ refactor(server): split the per-step context gathering into fact providers (#19574)
* ♻️ refactor(server): split the per-step context gathering into fact providers `serverCallLlmContextBuilder` grew to 851 lines of sequential lookups against six database models, the market service and Composio, interleaved with the engine parameter assembly. Each lookup is one fact for the context snapshot and none depends on another, so they are now independent providers under `modules/Mecha/ContextEngineering/providers/` — topic references, agent documents, onboarding context, LobeHub skill placeholders, user info, workspace context, sandbox placeholders, plan TODO, creds list, Composio services, agent builder and group agent builder contexts — behind one `gatherServerContextFacts` that runs them concurrently and returns them grouped the way the snapshot consumes them (`agentDocuments`, `step`, `variables`). The builder is left with orchestration: hints, gather, assemble, run the engine under its span. Every provider keeps its share-visitor gate and best-effort error handling verbatim; the two dynamic imports become static. The agent / group builders share `listOfficialTools`. Concurrency is the one behavior change: a step now pays for the slowest lookup instead of the sum, which is the remaining server-side cost on the send path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ⚡️ perf(server): cap the context fact fan-out at eight concurrent lookups Twelve providers share one Postgres pool and two of them reach external services; an unbounded `Promise.all` let a single step grab every connection at once. `runBounded` drives the thunks through `p-map` with a concurrency of 8 while keeping each result position's type, and the gatherer test now asserts the cap directly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
08704326a3 |
🐛 fix(expertise): skip history topics when the provider account refuses (#19539)
* 🐛 fix(expertise): skip history topics when the provider account refuses A user's own provider account failing (out of credits, revoked key) threw out of the expertise-history topic step, so Upstash logged it as a 500 and retried every topic of the backfill against an account that cannot recover. Finish those runs as a skip; transient failures still throw and retry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ♻️ refactor(expertise): handle provider account refusals in the ingestion service The skip belongs next to the other ingestion outcomes, not in the Upstash workflow, so both backfill runtimes share it. The local fallback loop now also stops at the first refusal instead of hitting the same dead account per topic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0fff23f8b5 |
⚡️ perf(agent): fetch LobeHub skill tools concurrently with a per-request timeout (#19533)
* ⚡️ perf(agent): fetch LobeHub skill tools concurrently with a per-request timeout Tool discovery runs on the execAgent send path, before the operation exists, so the user watches the "sending" spinner for all of it. LobeHub skill manifests were fetched one connection after another, and only listConnections had a timeout: a user with several connected skills paid the sum of every live MCP probe, and one hanging provider held the whole send. - getLobehubSkillManifests fetches every connection's tools with Promise.all and keeps manifests in connection order. - The live probe and the static fallback each get their own LOBEHUB_SKILL_DISCOVERY_TIMEOUT_MS budget, so a hung live probe still leaves room for the static list. Callers that pass no timeout (the tools router) behave as before. - Discovery's IO stages (model metadata, LobeHub skills, Composio, online devices, device system info) now emit spans, the only breakdown of the wait between "message saved" and "operation started". - loadModels is a static import like its other server callers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ♻️ refactor(market): bound skill tool discovery fan-out with p-map Promise.all let the number of connected skills decide how many tool-list requests hit Market at once. p-map keeps connection order and caps the fan-out at LOBEHUB_SKILL_DISCOVERY_CONCURRENCY (5). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(agent): mark degraded tool discovery stages in their spans Codex review: two discovery stages absorb their failures, so their spans stayed healthy and the new telemetry could not tell a Market timeout from a user with no connected skills. - traceDiscoveryStage moves to pipeline/discoveryTracing and hands the span to the stage; markDegradedStage records a failure count and sets ERROR when the stage swallowed failures. - model_metadata counts rejected Promise.allSettled lookups. - getLobehubSkillManifests takes an optional onError, called for each failed provider (with its providerId) and for a failed connection lookup; the lobehub_skills span records the manifest and failure counts from it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a452cb6b71 |
🐛 fix(verify): send evidence frames inline and stop review errors from spending Task attempts (#19565)
* 🐛 fix(verify): send evidence frames inline and stop review errors from spending Task attempts The Acceptance review handed screenshots to the provider as links the provider had to download. When it could not reach the storage behind them (a bucket on localhost or a private network), every review of that check failed with "Error while downloading file", and the Goal Task paused as "Verification could not run". The coordinator recovers that string by starting another builder attempt, so the same delivery was redone until the attempt budget ran out without ever being judged. - Read frames from our own storage and send them as data URIs (link fallback above 5 MB or when the bytes cannot be read). Shared by the review predictor and the inline LLM judge. - Retry a Goal review that could not run once in place, which absorbs a dropped connection. - If it still cannot run, pause with a new ACCEPTANCE_REVIEW_ERRORED_ERROR contract that has no recovery branch, so it goes to a person or the main Agent instead of another paid attempt the builder cannot fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(verify): retry only the check whose review could not run The settle path retried an errored Goal review by rerunning the whole review. Every other check was re-asked and its opinion upserted over the first one, so in a review where one check rejected and another errored, a nondeterministic second pass could turn the rejection into an acceptance and let the delivery complete. Retry the errored check alone, inside the review, and keep every other verdict from the first pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
687d50f5c3 | 🐛 fix: show safe traceable errors in shared topics (#19560) | ||
|
|
8e32440965 |
🐛 fix(workspace): default documents to edit and pin self in assignee picker (#19555)
🐛 fix(workspace): default workspace documents to edit and surface self in the assignee picker Workspace documents graded every member down to `view` by default, so a page published into the workspace read as read-only for everyone but its creator even though the server's write path never enforced that level. Privacy for a written page is already carried by `visibility: 'private'`, so publishing one is the creator choosing to collaborate. Also lifts the current user out of the workspace member group in the task assignee picker, so assigning a task to yourself no longer means scanning the directory. Claude-Session: https://claude.ai/code/session_019XxbYCrsrChYN2MbLSoMfc Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
7222bf648e |
✨ feat(gateway): send op meta on init and issue user-level gateway tokens (#19556)
* ✨ feat(gateway): send op meta on init and issue user-level gateway tokens Groundwork for the per-user multiplexed gateway socket: the init call now carries the op's topic/agent/parent linkage so the hub can build a user-level feed, and clients can mint a user-scoped JWT without a running topic. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 🔒 fix(api-key): block restricted keys from minting the gateway user JWT `aiAgent.issueGatewayUserToken` inherits the `agent:read` namespace scope, but the cli-sandbox JWT it returns passes `oidcAuth` as ordinary non-API-key auth — the same reason `refreshGatewayToken` is on the blocked list. Add it there and cover both in the scope-guard tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
1488baa3fb |
🐛 fix(agent): settle deferred step work between inline steps (#19541)
* 🐛 fix(agent): settle deferred step work between inline steps Work that a step defers with `after()` normally waits for the HTTP response. Budget settlement is part of that work. Each LLM call reserves a budget hold in `beforeChat`, and releases it in the cost tracker's `onFinal`, inside `after()`. While every step was its own request, the hold was released as soon as that step responded. The inline step loop sends a single response once the whole loop finishes. So the hold from every step in an invocation stayed reserved until then, and available budget is `maxBudget - spend - reserved`. This shrinks the room each subsequent reservation sees. It also amplifies an existing reservation race that reports "Budget exceeded" while plenty of credit is left. After the loop rolled out, one heavy account got five such rejections within an hour with tens of millions of credits available. `runWithScheduledWorkScope` now wraps the loop. Inside it, `after()` starts work immediately and tracks it, and `flushScheduledWork` waits for that work at each step boundary, before the next step reserves again. The boundary wait is capped at 10s so one stuck telemetry call cannot stall an operation. Work still running when the loop ends goes back to the host, as before. Outside the scope, `after()` behaves exactly as it did. The fix relies on the async context reaching the budget callback. It does: agent LLM steps call the model runtime in-process, and the stream whose `flush()` fires `onFinal` is created inside the step. Node binds the context where a stream is created, and a regression test pins that behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(agent): hand the step to the queue when deferred work does not settle Codex review. The step-boundary flush gave up after 10s and the loop started the next step anyway. The captured work does not tell telemetry apart from the settlement that releases the previous step's budget hold, so a slow settlement let the next step reserve on top of the unreleased hold, bringing back the false "over budget" rejection this wait exists to prevent. `flushScheduledWork` now reports whether it settled. On timeout the loop stops inlining and hands the pending step to the queue, which is how it ran before steps were inlined: the unfinished work keeps running and the queue round-trip gives it time to land before the next reservation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c0c0ac0b27 |
🐛 fix(cli): renew the operation token during long hetero exec runs (#19549)
* 🐛 fix(cli): renew the operation token during long hetero exec runs `lh hetero exec` received its operation token once, as LOBEHUB_JWT, and the tRPC client froze it into the link when the client was built. Past the token's four hours every ingest was rejected and the run's output never reached the server. - Renew the token an hour before it expires through aiAgent.refreshHeteroOperationToken, retry transient failures every minute, and stop on UNAUTHORIZED / FORBIDDEN / CONFLICT. Desktop session tokens are left to their own refresh flow. - Read LOBEHUB_JWT per request in the CLI tRPC clients, so the renewed token reaches the ingest sink that holds its client for the whole run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(cli): stop renewing against a server without the renewal endpoint Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(cli): keep renewing the operation token until the finish receipt is sent Renewal was stopped before the final drain. The drain and the heteroFinish receipt both authenticate with the operation token and can sit in retries, so a token expiring in that window rejected the remaining events and the completion. Stop renewal only after the last server call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4565c140d6 |
🐛 fix(hetero): let a long-running hetero run renew its operation token (#19548)
The server signs the hetero-operation token for four hours and offered no way to get another. A Goal Task running longer than that got 401 "exp claim timestamp check failed" on every heteroIngest — heartbeats included — so the lease stopped renewing and the operation was reclaimed as abandoned while the agent kept working. aiAgent.refreshHeteroOperationToken re-mints a token with the same claims. It accepts only an operation token, and runs the same re-authorization every operation callback already does (operation still running, principal still active and permitted), so renewal never outlives revocation. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4c94aee97b |
✨ feat(acceptance): pin the list rail on wide screens and add shift/cmd multi-select (#19559)
* 🐛 fix(desktop): ignore stale dist bundles when orchestrating dev startup Claude-Session: https://claude.ai/code/session_01E7e9mKLWsnGPgvgB42oM3V * ✨ feat(acceptance): pin the list rail on wide screens and add shift/cmd multi-select Claude-Session: https://claude.ai/code/session_01E7e9mKLWsnGPgvgB42oM3V |
||
|
|
021bd4b4b3 |
🐛 fix(editor): rehost pasted document images (#19527)
* 🐛 fix(editor): rehost pasted document images * 🐛 fix(server): cap remote image fetch size * 🐛 fix(editor): handle rehosted image URLs * ✅ test: type happy-dom URL helper |
||
|
|
41ce61b35d | ⚡️ perf(search): skip ineligible message indexing with versioned capture (#19537) | ||
|
|
4367a885eb | 🐛 fix: clarify artifact delivery and preserve tool refusals (#19409) | ||
|
|
1918aaf433 |
✨ feat(acceptance): opt-in purge of evidence files when deleting (#19529)
* ✨ feat(acceptance): purge evidence files and rounds when an acceptance or run is deleted Deleting an acceptance (single, batch, or from the task detail) or a verify run now removes its rounds, reports and the evidence files only they referenced, freeing storage quota. A shared delete confirm previews the rounds, files and bytes the purge will free. Claude-Session: https://claude.ai/code/session_01LK4wVB74664gvoXthzwf6N * ✨ feat(acceptance): make full purge an explicit opt-in when deleting Deleting an acceptance detaches its rounds again by default, so shared round report links keep working. The delete confirm gains a checkbox that opts into purging every round, report and evidence file, previewing what the purge frees before the destructive path is taken. Claude-Session: https://claude.ai/code/session_01LK4wVB74664gvoXthzwf6N * 💄 style(acceptance): show the purge warning in place of the description when opted in Claude-Session: https://claude.ai/code/session_01LK4wVB74664gvoXthzwf6N * 🐛 fix(acceptance): scope purge to exclusive evidence files and honor storage policy - purge only files with source=acceptance that no run outside the purge set references - pass REMOVE_GLOBAL_FILE to deleteMany and skip storage deletes when it is off - include hashless evidence urls in the storage delete list - purgePreview takes ids[] and computes one joint preview excluding hashes still referenced elsewhere - task-detail delete confirm carries the consequence copy as a secondary line - row / batch onDelete let failures propagate so the confirm dialog stays open - removeBatch purges with concurrency 4 via mapWithConcurrency - storage delete is best-effort: a failure is logged and the purge still completes Claude-Session: https://claude.ai/code/session_01LK4wVB74664gvoXthzwf6N * 🐛 fix(s3): chunk DeleteObjects requests to 1000 keys Claude-Session: https://claude.ai/code/session_01LK4wVB74664gvoXthzwf6N |
||
|
|
4a964c3547 | 🐛 fix(desktop): show renderer OTA version as app.rN instead of app_rN in the update notice (#19540) | ||
|
|
fc346bc4cb |
🐛 fix(cli): print wrapped artifact share URL after publish (#19536)
* 🐛 fix(cli): print wrapped artifact share URL after publish CLI artifact publish was emitting the raw hosting publicUrl. Match the App by returning the product share link /share/artifact/<deploymentId>. * ♻️ refactor: drop src/utils/artifactShare re-export Callers import artifactShareUrl from @lobechat/utils/artifactShare. |
||
|
|
c33818b0d0 |
✨ feat(desktop): open Quick Composer as a composer, capture screenshots on demand (#19523)
Quick Composer used to drop straight into full-screen capture mode. It now opens as a plain composer: no scrim, no crosshair, prompt focused, and a text-only prompt can be sent. A camera button in the panel enters the existing capture flow; Esc backs out of an empty capture pass. The Screen Recording permission check and window enumeration move from startSession to a new screenCapture.beginCapture IPC, so the composer works without the permission. When access is missing the overlay closes before the prompt, since it sits above the dialog at screen-saver level. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
515f3b8461 |
🐛 fix(task-slug): keep combining marks and fall back to the instruction (#19519)
* 🐛 fix(task-slug): keep combining marks and fall back to the instruction `taskTitleSlug` only allowed `\p{L}` and `\p{N}`, so scripts built on combining marks were shredded one character at a time — `कार्य पूरा करें` became `क-र-य-प-र-कर`, `สวัสดี` became `สว-สด`. CJK needs no marks, which is why every existing case read as correct. Add `\p{M}`, and truncate by grapheme cluster so a cut can neither halve a surrogate pair nor strand a mark without its base letter. The Recent feed links a task by `COALESCE(name, instruction)` while `useCanonicalTaskSlug` read `name` alone, so a task with only an instruction had its slug stripped the moment the detail landed, and the copied link came out bare. Add a shared `taskSlugTitle` helper mirroring that COALESCE (`??`, so a name cleared to `''` still collapses the URL) and read it through store selectors everywhere a task link is built or repaired. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KN4NTg5b1DsUgHYtsqt5zg * 🔒 fix(task-slug): keep prompt bodies out of task URLs and bound the slug A task's instruction is a prompt body, not a title, so it must not reach a path segment — from there it lands in browser history, the analytics page view fired on every history change, access logs, and anything pasted from the clipboard. That was already true on canary for the Recent feed, which links a task by the display title (`COALESCE(name, instruction)`); the canonicaliser happened to strip it on arrival. Rather than making that fallback stick everywhere, drop it from URLs entirely: `slugTitle` carries `tasks.name` alone through the Recent payload, and the canonicaliser, the copied link and the Portal header all read `name`. A nameless task simply has no slug, and every surface now agrees on that instead of rewriting each other. Grapheme counting is also not a size bound. A single cluster accepts an unlimited run of combining marks, so `a` plus 10k accents stayed one grapheme and produced a ~20KB segment, past what browsers and proxies accept in a request line — and titles arrive from imported and generated content, not only from typing. Keep whole clusters, but stop at a code-point ceiling as well. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KN4NTg5b1DsUgHYtsqt5zg * 🌐 chore: update i18n Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KN4NTg5b1DsUgHYtsqt5zg * 🐛 fix(recents): move a task's slug source with an inline rename Splitting the link's slug source out of the display title left the two able to drift: renaming a task from Home updates `title` optimistically and on commit, but `slugTitle` kept the server's old name, so the row went on linking to the previous title's slug until the next sync. That is the same link-vs-canonicaliser disagreement this feature set out to remove, reintroduced one field over. A rename sets `tasks.name`, which is exactly what `slugTitle` carries, so both the committed reducer path and the optimistic selector overlay now move it with the title. Topics and documents have no slug source and keep theirs null. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KN4NTg5b1DsUgHYtsqt5zg --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
caefd70f7f |
♻️ refactor(server): resolve the run agent config through the shared mecha rules (#19525)
* ♻️ refactor(server): resolve the run agent config through the shared mecha rules `resolveRunAgentConfig` re-implemented the agent config resolution rules that `@lobechat/mecha` already owns for the client. It now gathers the snapshot from the server's sources (agent row, workspace member overrides, author-or-admin access, reply language), runs `resolveAgentConfig`, and layers only the per-call intents a run knows about: the member device override, callSubAgent chatConfig patches, the IM `/mode` override, persistence-attribution ids and the `instructions` append. Two rule gaps closed on the shared side so both hosts agree: - `AgentConfigResolverContext.modelOverride` carries a per-call model / provider pin (task and sub-agent spawns, explicit request overrides) into `resolveAgentModelConfig`; the runtime template renders the resolved model by default. - Builtin system-role precedence is decided by the builtin, not the host: `RuntimeContext.storedSystemRole` hands the row's own prompt to the runtime, and the inbox (a user-editable default assistant) honors it, while feature builtins keep their runtime prompt. Server-visible changes: regular agents now receive the reply-language instruction the client already appended; builtin runtime chatConfig merges apply; sub-agent runs keep lobe-agent and rely on its manifest resolver to hide `callSubAgent`, as the client does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 🐛 fix(server): apply the member device override before the builtin runtime policy Codex review: the device preference was layered after `resolveAgentConfig` had merged the builtin runtime's `agencyConfig`, so a saved `local` / `device` override replaced an onboarding runtime's `executionTarget: 'none'` and routed an internal agent to a device it disables. The override now lands on the row before the shared rules run, restoring the previous ordering where the runtime policy is merged last. Regression test covers both the builtin and the regular-agent case. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 🐛 fix(server): read the reply language for the share visitor on shared-agent runs Codex review: `AiAgentService` is constructed as the share owner, so the locale appended to a regular agent's system role was the owner's, instructing visitor conversations to reply in the creator's language. `resolveRunAgentConfig` now takes `shareVisitorUserId` and reads the reply language for that user on share-gated runs, matching how the timezone and user info are already resolved for the visitor. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |