mirror of
https://github.com/lobehub/lobehub.git
synced 2026-09-20 04:56:13 +08:00
feat/quick-note
1493 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
055901b1be | 🐛 fix(quick-note): preserve pinned document history revisions | ||
|
|
527d4f31d9 | ✨ feat(quick-note): gate access with user rollout flags | ||
|
|
51f87c5236 | 🐛 fix(quick-note): harden persistence and CI | ||
|
|
41e46f056a | ✨ feat(quick-note): add configurable agentic analysis | ||
|
|
9e325d3ed2 | ✨ feat(quick-note): add agentic processing pipeline | ||
|
|
791974d831 |
✨ feat(desktop): collect renderer garbage while idle and from the DevDock memory panel (#19497)
Oilpan keeps freed blink_gc pages committed after large DOM churn, so the renderer stays at ~3G private while only a few hundred MB is live. Run HeapProfiler.collectGarbage from the main process when no window has focus for 30s and a renderer exceeds 1.5G resident (5 min cooldown), and expose the same call as a GC button in the DevDock memory breakdown. Claude-Session: https://claude.ai/code/session_01Y6cR5U6xxfWWwJHzeeV5Sa |
||
|
|
2f96875229 | 🐛 fix(desktop): apply renderer OTA reliably (#19509) | ||
|
|
93fc45ff77 |
♻️ refactor(agent-runtime): carve principal, plan and host slots out of state.metadata (#19513)
Third and last step of the state restructure (#19498 world/binding, #19505 origin). The remaining creation-time facts in `state.metadata` fall into three business groups, each now a typed slot on `AgentState`: - `principal` — under whose authority the run acts and what it may do: `actor.{shareVisitor, bot, deviceScope}`, `policy.deviceAccess`, `audit.{clientIp, userAgent}`. Decided at creation and re-presented, never re-derived, at each dispatch boundary. - `plan` — how the run executes: `execution` (the resolved `ExecutionPlan`), `skills`, `stream`, `eval`, `workingDirectory`. The duplicated `metadata.modelRuntimeConfig` is dropped in favour of the existing top-level `modelRuntimeConfig`. - `host` — what the host needs to deliver and retry the run: `hooks` (serialized webhook configs) and `queue.{retries, retryDelay}`. Carried by the runtime without interpretation. `ExecutionPlan` / `ExecutionPlanUnroutedReason` move to `@lobechat/types` so the runtime can type the slot; `src/helpers/executionTarget` re-exports them. `normalizeAgentState` lifts the legacy keys on load and folds a legacy `metadata.modelRuntimeConfig` into the top-level slot when it is absent. `HookDispatcher.dispatch` accepts the wire-shape `SerializedAgentHook[]` the slot carries and narrows to `SerializedHook` itself; `isAgentShareRun` takes the state; `resolveRunActiveDeviceId` / `resolveRunProjectSkills` read `plan` and `principal`; the dynamic intervention resolvers receive the plan's working directory alongside the ledger. `metadata` is now the run ledger only: what the runtime and host write while the operation executes. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
35098de372 |
✨ feat: add readable task URL slugs and workspace seat copy (#19503)
* ✨ feat: add readable task URL slugs and workspace seat copy Task detail URLs gain a cosmetic `/task/:taskId/:slug` segment. The slug is built from the task title and never participates in resolution — `taskId` stays the only key — so stale or hand-edited slugs still resolve, and `useCanonicalTaskSlug` rewrites the URL to the current title. Also bundles the workspace seat locale strings for pending-invite seat accounting, and the knowledge base library visibility confirmation copy. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WtR156jiuRTA4aa5eBZUL * 🐛 fix(document): scope the library mirror update to the caller's own file `setVisibility` mirrors a page's visibility onto its paired `files` row, but scoped the update by file id and workspace alone. `fileId` is a plain foreign key and a document can point at a file somebody else uploaded — parsing another member's shared file yields a caller-owned document that keeps the original `fileId` — so taking such a derived page private also hid the uploader's original from the whole workspace. Require `files.userId` to match the caller, the same guard the `documents` update in this transaction already applies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WtR156jiuRTA4aa5eBZUL * 🐛 fix(task): collapse the task URL when the title is cleared `useCanonicalTaskSlug` gated on the title itself, so a resolved task whose name is empty looked identical to one whose detail had not loaded yet. Clearing a task title persists `name: ''` (the input saves the raw value), which left the URL pinned to the slug of the title that had just been deleted. Read loaded-ness separately from the title so an empty name is honoured and the URL collapses back to `/task/:taskId`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WtR156jiuRTA4aa5eBZUL * 🐛 fix(task): stop the My tasks board from hiding automated tasks The "My tasks" board copied `automated: false` from the boards around it, but its list view sends no automation filter at all. A scheduled or heartbeat task assigned to the caller therefore disappeared the moment they switched that same collection from list to board — the one place the two views must agree. Extracts the scope-to-query mapping into `buildKanbanGroupQuery` so the rule is stated once and covered by unit tests instead of living in a nested ternary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WtR156jiuRTA4aa5eBZUL --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
518059aed1 |
♻️ refactor(agent-runtime): carve the typed origin slot out of state.metadata (#19505)
Second slot of the state restructure started in #19498. Every identity, trigger and lineage fact about a run — which conversation node it hangs on, who asked for it, whether it is a child and of whom — lived as loose keys in the untyped `state.metadata` bag, spread in from `appContext` at creation and read back by string key across the runtime package, the server executors, completion, abandonment and intervention paths. `AgentState.origin` (`AgentRunOrigin`) now carries them, frozen at operation creation and mirroring the durable operation row: - conversation node: agentId / topicId / threadId / groupId / sessionId / documentId / scope / taskId / userId / workspaceId - trigger: trigger / sourceMessageId / defaultTaskAssigneeAgentId / signal (was `agentSignal`) / continuation (was `agentInterventionContinuation`) - run tree: `lineage.{isSubAgent, orchestrationRole, parentOperationId, progressAnchor}` (was `subAgentProgress`) `normalizeAgentState` is generalised to a key → slot-path table and lifts the legacy keys on state load, treating `null` legacy values as absent so the slots only ever hold `undefined`. `RuntimeExecutorContext`, `resolveRunActiveDeviceId` and the lifecycle helpers read `state.origin`; `CompletionLifecycle` threads the origin alongside the remaining metadata; `parseAgentSignalMarker` validates a raw marker so the completion payload extractor can read `origin.signal` while operation rows keep `metadata.agentSignal`. Audit facts (`clientIp`, `userAgent`) and the runtime-written intervention preparation stay in `metadata` for the principal / ledger slots. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
3df8460895 |
✨ feat(goal): let the system planner lead and hand problems to the main Agent (#19477)
* ✨ feat(goal): let the system planner lead and hand problems to the main Agent Configuring a main Agent used to replace the coordinator's own planning. `tick` asked it first and returned early, so on a Goal that had both, the exploration planner never got a turn — and `goal.create` rejected the combination outright ("Manager mode owns initial and subsequent planning"). The two are layers, not rivals, and the split is visible in their vocabularies. Exploration says `expand | revise | verify`: it can move within the graph but cannot express "this goal is mis-framed", "the judge was wrong" or "stop and ask". A main Agent says `tasks | verify | retry | escalate`, which is the supervisory half. Meanwhile the main Agent was only ever consulted about failures that passed the transport whitelist, so a Goal could park on its owner for hours with a main Agent configured and idle. Invert the order: - The system planner owns the ordinary path. With exploration configured, `advance` only settles a turn already in flight — a turn already paid for has to land — and starts no new one. A Goal whose only planner is the main Agent is still led by it. - `gateOrTakeOver` replaces the three `openFailureDecision` call sites. Every stop-on-a-person funnels through there, and the coordinator only reaches it once its own policy is out of moves: the failure matched no recovery branch, the attempt budget ran out, or the reason could not be classified. That is exactly the class the deterministic lane cannot express, so it is handed over. The ladder is supervision, then the main Agent, then the Gate. - An invited turn skips the "is there unfinished work" and "is this a recognised transport failure" narrowings. Those ask whether an *uninvited* turn should interrupt running work; the caller has already answered a harder question. Turn limits, budgets and the compare-and-swap claim still hold. - An invited turn with no turn budget left declines instead of pausing the Goal, so the Gate keeps carrying the real question instead of "the main Agent is out of turns". - The create-time exclusivity check is gone: exploration, supervision and seed tasks may now accompany a main Agent. Ordering resolves what exclusivity did. Prompt cohort goes to v4: a takeover turn is told which problem it inherited and that the Goal stops on a person without it, so `escalate` means "a human really is required" rather than being the only reachable answer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): keep supervision first and turn a takeover escalation into its Gate Two P1s from Codex review, both caused by the combinations this PR newly allows. **Supervision was losing its place in the ladder.** With `manager` + `supervision` and no `exploration`, `mayStartTurn` is true, so an uninvited turn claimed a recognised transport failure at the top of `tick` and returned early — `decideNextMove` never reached `failure_decision`, and `reviewFailure` never ran. That reverses the documented order, spends a planning turn on a failure the supervisor recovers by itself, and skips the supervisor's stricter actor-authored-status check. An uninvited turn now stands down from failures entirely when supervision is enabled; whatever the supervisor declines arrives at `gateOrTakeOver`, which invites the Agent properly. **An escalation had nothing to escalate to.** The takeover contract says `escalate` puts the Gate back one turn later, but the escalate branch only paused the Goal and recorded a status event — no Decision, no answerable question, and every later tick sat on `goal_paused`. An escalation from a takeover turn no longer touches the Goal's status, so the next tick reaches the same handover point; there `takeOver` declines a problem its own escalation already answered and the Gate opens, carrying the Agent's reason in the question. An ordinary planning turn's escalation still pauses, because nothing is holding a Gate behind it. `managerState.problem` records what a turn was invited for. It is what separates a takeover turn from ordinary planning, and what keeps the same problem from being handed over twice. Both regressions are pinned: reverting either fix turns its test red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): let a takeover commit the moves it was offered, keyed per task Codex review round 2. Both findings are the takeover contract promising more than the code accepts. **Three of the four advertised actions could not commit.** `submit` refuses `tasks` and `verify` whenever any task node is unfinished — which a takeover's inherited task always is — and `retry` is refused for anything outside the transport whitelist. So the prompt offered four moves and only `escalate` could ever land. The unfinished-work guard exists to stop an *uninvited* turn from double planning the frontier; a takeover's inherited work is stuck by definition, which is why it was handed over, so a corrective task is the answer rather than the thing to forbid. The guard is now waived for an invited turn, and the prompt stops advertising a retry for failures that will be refused. **The already-answered key was too coarse.** "Task attempt budget was exhausted" is the same sentence for every task that reaches it, so keying on the reason alone let a second task skip its own takeover and inherit the first task's diagnosis in its gate — resolving the first gate does not clear `managerState`. The key is now task and reason together, through a shared `problemKey` so the writer and the reader cannot drift. While there: any committed plan now counts as an answer, not just `escalate`. If the Agent's move did not unstick the Goal, handing the same problem back buys the same move; the Gate is the honest next step and the Agent's reasoning rides on it. Both regressions are pinned: reverting either fix turns its test red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): retire the work a takeover plan replaces Codex review round 3, and it is right that the previous fix only moved the wall. Accepting `tasks` and `verify` on an invited turn changed validation and nothing else: the inherited task node stayed nonterminal, so the next tick's frontier reached that older blocked node before the freshly created corrective node and routed straight back to `failure_decision`, while terminal verification could not start at all while it was unfinished. The advertised actions committed and the Goal still fell through to the human Gate. A `tasks` or `verify` plan from a takeover turn now retires the node it inherited. That is the same move the Gate offers a person under "Retire task", scoped to the single node this turn was invited about, attributed to the main Agent, and only reachable on an invited turn. It abandons a direction rather than re-spending on it, so it stays clear of the open question about who may pay for another attempt. `managerState.problemTaskId` carries the blocked task alongside the dedupe key, so the node is found without parsing a composite string whose reason half is free text. Pinned: the test ticks past the submission and asserts the replaced node is retired and the Goal does not land on a Gate. Reverting the retire turns it red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): hand a failed terminal acceptance to the main Agent too Codex review round 4, and it is the same species as the previous three: a guard written for uninvited turns sitting above the invited branch. `startTurn` returns null whenever a terminal acceptance task exists on the graph. That rule means "verification exists, stop planning more work", which is right for an uninvited turn and exactly wrong for a takeover invited BECAUSE the terminal acceptance is the thing that failed. So a Goal with a main Agent configured still reached a bare human Gate on the one failure where a second opinion matters most. The guard now lives inside the uninvited branch with the others. The retirement added in the previous commit deliberately does NOT apply to the acceptance node. `decideWithoutFrontier` finds that task by TITLE regardless of status, so retiring it parks the Goal on `no_progress` with neither a Gate nor a verdict — and failing the Goal is the Gate's own `retire` answer, coupled to that option rather than to the node's status. A takeover on a failed acceptance can therefore diagnose an incorrect judge and put that reasoning on the Gate, or plan corrective work; making corrective work supersede a failed acceptance by itself needs the acceptance lifecycle to allow superseding, and that is a separate change. Both limits are stated in the README rather than left for the next reviewer. Pinned: one test asserts the takeover starts for a failed acceptance with no Gate opened, another asserts the acceptance node is never retired. Reverting either half turns its test red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): refuse takeover plans that would strand the graph Codex review round 5. Two of its three findings are defects in what the previous commits shipped; the third is a real gap that this PR does not close, and the README now says so instead of leaving it for the next reviewer. **Retiring a node something depends on strands the dependent.** A prerequisite counts as met only when it is `resolved`, so a retired predecessor blocks its dependents forever and the Goal lands on `no_frontier`. Rewiring them onto the replacement would be the answer, but the graph model has no edge removal, so the old `depends_on` edge would keep pointing at the retired node. Retirement is refused when the stuck node has dependents, and that block goes to the Gate. **A failed Goal acceptance cannot be superseded, so stop accepting plans that try.** The acceptance task is matched by TITLE regardless of status: a corrective task returns to the same failed node, and `verify` sets `readyForAcceptance` without producing a fresh run. Both submissions used to be accepted and then strand. `tasks` and `verify` are now refused for an acceptance takeover, and the prompt names that limit together with the dependents one, so the offer and the answer are the same sentence and a turn is not spent discovering the difference. **Not closed: exploration's own stop conditions.** The experiment limit, a spent revision allowance and a planner error all pause the Goal directly rather than passing through `gateOrTakeOver`, and `eligible()` then rejects the paused Goal — so the planner whose "out of moves" most deserves a second opinion never reaches the main Agent. Closing it requires an invited turn to revive a Goal the system paused, which changes who may restart one; that belongs with the open question about who may pay for another attempt, not in this PR. Both fixes are pinned: reverting either turns its test red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
73a0528546 |
♻️ refactor(agent-runtime): carve typed world/binding slots out of state.metadata (#19498)
* ♻️ refactor(agent-runtime): carve typed world/binding slots out of state.metadata Every reader of the prompt-side run context (agent config, group roster, project instructions, connector note, user memory/timezone, search decision, channel facts, eval context) and of the device binding (active device id/platform/system info) used to reach into the untyped `state.metadata` bag, so a value that was not spelled out in the creation-time field list silently disappeared. `AgentState` now carries two typed slots derived from what those fields mean: - `world` — what the model is told about the run's world. Frozen at operation creation; the context engine only reads it. - `binding` — late-bound execution facts. The one business slot the host may rewrite at a step boundary (`computeDeviceContext`). `normalizeAgentState` lifts the legacy metadata keys into the slots at the persistence boundary (Redis + in-memory state load) so blobs written before this change keep working, without carrying the data twice. `RuntimeExecutorContext` drops its per-field copies; readers go through `state.world` / `state.binding`, and `resolveRunActiveDeviceId` takes the state instead of the metadata bag. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ♻️ refactor(agent-runtime): read the trace header model from world.agent The trace recorder still reached into `metadata.agentConfig` through an `as any` cast, which the key grep missed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ♻️ refactor(agent-runtime): declare the search decision shape instead of importing model-bank The runtime package should not take a dependency on the model catalog for one five-field interface. `SearchDecisionSnapshot` is declared next to the world slot and is structurally identical to the `model-bank` resolver output, so the server keeps passing its value unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
7712888372 |
⚡️ perf(agent): run consecutive agent steps in one invocation (#19470)
* ⚡️ perf(agent): run consecutive steps in one invocation Every step boundary cost a full queue round-trip: ~2.9s at p50 across production traces, with nothing under 2s. That is 13.6% of all agent-run wall time, plus another 8.4% for the same hop before step 0, and it rises past a quarter of wall time on multi-step operations. The QStash worker now keeps stepping inside one invocation for as long as the operation has a next step ready and its time budget allows. Past the budget the pending step goes back to the queue and a fresh invocation picks it up, so the tail behaves exactly as it does today. - `executeStep` takes `inlineContinuation` and hands the next step back as a `continuation` instead of publishing it. Every terminal outcome and every park (waiting_for_human, pending approval, async-tool wait) leaves it absent, so the loop stops on its own without special-casing them. - One lock owner spans the whole loop. The operation lock is now re-entrant for the owner that already holds it; owner tokens carry a random UUID, so a redelivery from the queue still loses the race. The lock is no longer dropped and re-claimed at each boundary. - Only the first step carries the delivery's one-shot payload. Inlined steps must not replay a human input or an approval that already ran. - The existing `stepCount > stepIndex` stale-delivery guard already covers a redelivery arriving after the loop advanced past its step. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🔧 chore(agent): set the inline step deadline to 450s Leaves ~150s of headroom under the route's 600s maxDuration, which covers the p99 LLM step (~125s), so a step that starts just under the deadline still finishes inside the invocation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(agent): park the inline step envelope so a dead loop can recover Codex review caught a failure mode the loop introduced. Today a worker that dies mid-step recovers: state never advanced, so QStash redelivers the same step and it runs again. With the loop, a worker that dies during step N+2 leaves state at N+2 while the only queued message is still the original step N. The `stepCount > stepIndex` stale-delivery guard ACKs that redelivery, and the operation is stranded with nothing queued. The loop deliberately does not publish the next step, so the envelope it would have published is parked in Redis instead, written before the step runs. A redelivery that lands behind a parked envelope resumes from the envelope rather than from its own (older) index, which is the same recovery the queue used to provide. - `agent_runtime_inline_resume:{operationId}` holds the envelope under the existing 2h operation TTL, and is dropped with the other operation keys. - Cleared when a step ends without a continuation, and when the deadline hands the pending step back to the queue — in both cases something else owns the next step and a lingering pointer could re-run it. - A resumed run carries none of the delivery's one-shot payload: it stands in for a later iteration of the dead loop, not for the original message. - Read failures degrade to null; without an envelope the handler just runs the step it was given. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(agent): fall back to the queue when the inline envelope can't be parked `saveInlineResume` swallowed its Redis error, so a failed park still inlined the next step — with no envelope and no queue message behind it, which is exactly the stranding the envelope exists to prevent. It now reports failure and inlining is conditional on the park succeeding. When Redis is unavailable the step takes the ordinary queue round-trip: one step boundary slower, but the recovery path stays intact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(agent): gate the inline step loop behind a runtime feature flag Makes the loop a rollout switch rather than a deploy-time behaviour change, so it can be turned on for a slice of traffic and turned off again without shipping anything. Uses the existing RuntimeConfig path rather than a new mechanism: the flag `inline_agent_steps` joins `FeatureFlagsSchema`, resolves through `getServerFeatureFlagsStateFromRuntimeConfig` (Redis-backed, ~5s in-process cache), and accepts either a boolean or a user-ID allowlist like `dev_dock` and `agent_share` do. Develop Center already writes that snapshot. Default is off, so merging and deploying this changes nothing until the flag is published. - The gate fails closed: any error resolving it falls back to one step per delivery. - Looking up a parked envelope is deliberately NOT gated. Operations that were mid-loop when the flag flipped off have an envelope and nothing queued behind them, so ignoring it would strand exactly the runs a rollback is meant to protect. - Envelope clearing moves from `executeStep` to the handler, which is the only place that knows the invocation is done with it — including the flag-off resume path, which `executeStep` no longer sees as inline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(agent): keep the retry count and surface envelope read failures Two more findings from Codex review, both about the inline loop's recovery path silently turning a retryable delivery into a dead end. Preserve `externalRetryCount` when resuming from a parked envelope. A step that parked for approval but failed to persist its Review is only replayed while the count is nonzero (`shouldReplayPendingInterventionLifecycle`). The resume path dropped it along with the one-shot payload, so the stale guard ACKed the step and the approval never became available. The count describes the delivery, not the step's payload, so it belongs on both paths. Surface a failed envelope read instead of reporting "no envelope". The two have opposite consequences: absent means run the delivered step, unreadable may mean an operation is mid-loop with nothing queued behind it. Collapsing them ACKed that delivery as stale and stranded the run. The read now propagates and the queue retries. An unparseable envelope stays non-fatal — retrying cannot fix it, so it is reported and treated as absent rather than looping into the DLQ. Clearing stays best-effort: a stale envelope only resumes a step ahead of the delivered index, and the next park overwrites it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(agent): only redirect plain step deliveries to a parked envelope Self-review finding. The envelope lookup keyed purely on `parked.stepIndex > stepIndex`, regardless of what the delivery carried. That silently rewrites deliveries that exist for their own reason — approvals, async-tool resumes, barrier probes, member-timeout watchdogs — into ordinary step runs with their payload dropped. The group-member timeout is the sharp edge: `scheduleGroupMemberTimeout` publishes it on the member operation at `stepIndex: 0`, so ANY envelope is "ahead" of it. A member that is mid-loop always has one, so the watchdog would be swallowed and the parked supervisor would wait forever. It compounds: the hijacked delivery then loses the lock race against the loop that is still running, and on its way out clears that loop's envelope — removing the recovery pointer from a live operation. The lookup is now skipped entirely unless the delivery is a plain "run the next step" message, which also saves a Redis read on every watchdog and intervention delivery. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(agent): clear the inline envelope only while holding the lock Codex review. Clearing was an unconditional DEL on the operation's envelope key, which lets a worker delete a pointer it does not own. A redelivery can read envelope k, stall while the active worker finishes k and parks k+1, then lose the lock race and be told its step is stale. On the way out it cleared the key — deleting the live worker's newer envelope. If that worker then died during k+1, its own redelivery would hit the stale guard with no pointer to resume from, stranding the operation. Ownership of the operation lock is the right predicate: whoever holds it is the worker whose envelope this is. Clearing is now a two-key Lua compare against the lock's owner token, mirroring the existing release script, so a worker that lost the race no-ops instead of deleting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
abdd7ff74f |
♻️ refactor(context-engine): let the engine assemble the system message (#19485)
* ♻️ refactor(context-engine): let the engine assemble the system message The server composed the final system prompt by string-appending onto `agentConfig.systemRole` from three files across the pipeline: the resolver, `operationPrep` (project instructions), and `toolDiscovery` (borrowed-connector attribution). That is why the resolved config had to stay mutable all the way through, and why each append site carried a comment vouching for the aliasing — "appending here still reaches the LLM because `createOperation` reads `agentConfig` below". Correctness depended on nobody cloning the object, which the `do not clone` warning enforced by convention rather than by type. The mechanism for doing this properly already existed and was being bypassed. `BaseSystemRoleProvider` is purpose-built — subclasses return content, the base finds or creates the system message and joins with `\n\n` — and the engine has a declared "Phase 2: System Message Assembly" where a dozen injectors already use it. So the two late contributions become injectors. `ProjectInstructionsInjector` takes the files rather than a rendered block, because wrapping them is presentation and belongs to whoever owns the system message. `ConnectorOwnershipInjector` takes a rendered note, because naming the authorizers needs a user lookup the engine cannot perform — the same split `EvalContextSystemInjector` makes with `envPrompt`. Both run immediately after `SystemRoleInjector`, which is exactly where their text used to land. Placing them later would silently reorder the prompt behind every other Phase 2 provider; keeping the position keeps it byte-identical. The data reaches the engine on `AgentConfigWithId`, which already carries run-scoped fields stamped by one stage for another (`subAgentChatConfigOverride`). Same transport as before, structured payload instead of a concatenated string — no new plumbing through the state blob, and no size change, since the same text already travelled inside `systemRole`. Not included: the persona-level appends inside the resolver (builtin runtime role, page/task agent, per-call instructions). Those compose what the agent IS, which the resolve phase owns; these two are run context, which the engine owns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ♻️ refactor(agent): carry system-message context on the operation, not the config Follow-up to the previous commit, which moved the two late system-prompt contributions into context-engine injectors but carried their payloads on `AgentConfigWithId`. That was the same category error in a better shape: it removed the string concatenation and kept smuggling run context through the agent's configuration. Project instructions come from the workspace and the connector note from tool discovery — neither is part of what the agent IS. They now travel the way every other piece of run context does. There is an established family of them on the operation metadata — `agentShareVisitor`, `botContext`, `botPlatformContext`, `discordContext`, `userTimezone`, `evalContext` — each mapped onto `RuntimeExecutorContext` and consumed downstream, and `evalContext` in particular already feeds an injector. `expertise` is the exact precedent and this follows it end to end: `prepareOperation` and `discoverTools` return the values instead of mutating the config, `startOperation` puts them on the operation, `createOperation` persists them into `state.metadata`, `AgentRuntimeService` maps them onto the executor context, and the context builder reads them from there. Persisting matters rather than being incidental: steps can be claimed by different workers, so anything the engine needs has to survive on the operation. `createOperation` builds `state.metadata` from an explicit field list, so a value not named there is dropped without a word — the same shape of trap as the zod schema that silently stripped `createdAt`. The two fields added to `AgentConfigWithId` in the previous commit are removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(agent): forward the system-message context, and restore its order Two from review, and the first one meant the whole change was dead on the server. `serverMessagesEngine` destructures a fixed parameter list, so the two new fields never reached `MessagesEngine` and both injectors always saw `undefined`. The old concatenation was already gone, so every server run would have lost its project instructions and connector attribution outright — `AGENTS.md` silently absent from the prompt. Neither the injector unit tests nor the type checker caught it: the tests exercised the injectors directly, and the call site passes an intermediate object, which disables excess-property checking. So the regression test lives at the wrapper-to-engine seam, not on the injectors. Verified it fails without the forwarding. The injector order was also wrong. `discoverTools` runs before `prepareOperation`, so the old appends produced persona → connector note → project instructions; the injectors had them the other way round, against an adjacent comment claiming the prompt was byte-identical. Connector attribution now precedes the project instructions, with a test pinning it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✅ test(agent): cover every link the system-message context travels The forwarding bug shipped with green injector tests because they exercised the injectors directly. The wrapper test added with the fix only covers the last link; a value dropped anywhere earlier is just as silent and just as total. Every failure this feature has had — the wrapper's fixed parameter list, and `createOperation` assembling `state.metadata` from an explicit field list — was a name missing from a list rather than broken logic. Nothing about that shape surfaces in behaviour: the injectors keep working, they simply never receive anything. So each link now has an assertion of its own, and each was verified to fail with its forwarding removed: createOperation params → state.metadata (AgentRuntimeService.test) executor context → engine input (serverCallLlmContextBuilder.test) engine input → system message (serverMessagesEngine.test) One link is still covered by reading rather than by a test: `state.metadata` → executor context. And a true end-to-end run through `execAgent` is disproportionately expensive here, because the project instructions originate in the device workspace-scan path — reaching them needs an activeDeviceId, a device row and a scan result, none of which is the thing under test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✅ test(agent): pin the stage order the system message depends on Drives the real `execAgent` pipeline and asserts that tool discovery runs before operation prep. That order is the reason connector attribution precedes the project instructions in the assembled system message, and until now it rested on a comment citing two line numbers — the same kind of claim that was wrong the first time this ordering was written down. Carrying a real payload end to end through this harness turned out to cost more than it returns. The project instructions originate in the device workspace-scan path, so reaching them needs an active device, a device row, a fresh cached scan, and the agent-skill lookup that happens to share the same `try` — each mock revealing the next, and none of it the thing under test. The payload is therefore covered link by link instead, which also matches the failure shape: every regression it has had was a name missing from an explicit field list, not broken logic. Worth recording separately: project instructions and the project skill lookup share one `try`, so a skill-model failure silently drops the instructions too. Pre-existing, unrelated to this change, and not fixed here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ef6e3b1fe5 |
✨ feat(desktop): add built-in completion sound choices (#19480)
* ✨ feat(desktop): add built-in completion sound choices Ship three more synthesized chimes next to the marimba default (glass bell, soft tone, xylophone) and let the notification settings pick one from a select; importing a sound now lives inside that select and picking a built-in drops the imported pack. The generator script renders every built-in from one instrument × motif table; the default output stays byte-identical. Dev builds no longer compute the renderer main hash unless OTA is forced, since the lockfile pin check only matters for packaged builds. Claude-Session: https://claude.ai/code/session_01Rf3vs7eC8jwF75kohcS3Ye * ♻️ refactor(desktop): synthesize completion sounds at build time Move the chime renderer out of a one-off script into a Vite plugin that writes the WAVs under the renderer public dir and the banner AIFF under the desktop resources on every dev/build start, and stop tracking the audio binaries. Output is byte-identical to the files that were checked in; a test pins the default chime's hash. Claude-Session: https://claude.ai/code/session_01Rf3vs7eC8jwF75kohcS3Ye |
||
|
|
4af3510d81 |
📦 build(desktop): package only target-platform native prebuilds and drop .pdb (#19490)
* 📦 build(desktop): package only target-platform native prebuilds and drop .pdb Claude-Session: https://claude.ai/code/session_01EBaC7yuAyG4Lo4aDg3L43i * 🔥 chore(desktop): drop module-deps config test Claude-Session: https://claude.ai/code/session_01EBaC7yuAyG4Lo4aDg3L43i |
||
|
|
dc67b41643 | 🧹 chore: drop LOBE-XXX issue markers from code comments (2026-09-11) (#19418) | ||
|
|
bf938ff524 |
🐛 fix(desktop): skip the main hash in Vite development mode (#19486)
resolveMainHash walked the full source graph on every dev config load and failed whenever root node_modules drifted from the Desktop lockfile. The runtime already disables renderer OTA when MAIN_HASH is empty, so dev builds now resolve to an empty hash unless MAIN_HASH is set explicitly. Claude-Session: https://claude.ai/code/session_01DjXmqHj7t17UfwNuXMjfn3 |
||
|
|
92c2452d53 | 🐛 fix(desktop): canonicalize main hash graph paths (#19484) | ||
|
|
ab5ec4c81d |
♻️ refactor(agent-share): move the share surface out of the open-source tree (#19467)
* ♻️ refactor(agent-share): move the share surface out of the open-source tree Agent sharing runs a visitor's conversation on the creator's account and bills it to them. `ENABLE_BUSINESS_FEATURES` is `false` here, so every entry point already failed closed and the ~6k lines of surface were unreachable — while the spend cap that makes sharing safe is enforced only where that accounting lives. Its siblings `pageShare` and `artifactShare` already ship that way. Removed: the visitor and creator-settings features, their skeletons, and the `/a/*` route registrations on desktop and mobile. Kept: the table, models, tRPC procedures, runtime authorization, and the locale keys. Only the UI layer moves. Added two slots so the remaining open-source surfaces keep compiling and stay honest about the capability: - `useAgentShareSupported` reports "not supported", which hides the profile tab, the header action, and the settings page. - `AgentShareSettingsPage` renders nothing for the route those entries reach. `SharedAgentData` also gains the fields the moved profile reads — creator, tags, opening questions, reach counts, terms and granted tool identifiers — since the procedure serving them stays here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✅ test(spa): stop asserting the share path as a standalone route `/a/*` is no longer defined here, so it resolves through the main layout's catch-all instead of as a standalone route. The deployment that registers the share surface asserts its own scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🔥 test(e2e): drop the agent-share keyboard scenario with its surface The `@ROUTES-003` scenario tabs to the share visitor top bar's "Go to my LobeHub" link, which left this tree along with the rest of the visitor UI. Its intent — a share link never dead-ends a visitor — is covered in the cloud repo, where the bar now lives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2141e9917b |
💄 style(acceptance): name the author on the record and drop the shared-link banner (#19478)
* 💄 style(acceptance): name the author on the record and drop the shared-link banner The acceptance header now carries who delivered the work, the way a pull request names its author: avatar and name sit right after the status, ahead of the counts. `getBundle` resolves that profile from the acceptance's own owner, so a visitor holding nothing but the shared link still sees whose record they are reading. With the name on the record, the standing "shared with you" banner had nothing left to say that the page did not already show, so it is gone — component, both mount points, and its five locale keys. The per-check entry beside the status was a small button wearing a 44px floor, which drew it as a block twice the height of the row it lives on. Dropping the override lets it sit inline at its own size. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(acceptance): lead the header with the delivery's name, then one bar Review feedback on the first round: the title should come before the status, the pass/uncertain tally beside it should go, and the origin chips sitting on their own row underneath should join the same bar. So the name leads, and one bar under it carries state, author, the origin agent, the originating conversation, the pull request, and the per-check entry. The header goes from three rows to two. The tally and the last-run time are dropped outright. They change every round and read as a verdict while sitting above the checklist that actually shows them, so `acceptance.verdict.latestAt` goes with them. With both avatars now side by side in one bar, an origin agent without a picture drew the component's "UN" placeholder next to a real face. It falls back to its own initial, the way the author's does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(acceptance): trail the title with the per-check entry, on hover Review feedback on the second round: the entry reads cleaner after the name than inside the meta bar, and it only needs to be there when reached for. So it moves out of the bar to sit after the title, and fades in when the row is hovered. It fades rather than hides: an entry that appears by reflow would retitle the row under the pointer, and `visibility: hidden` drops the button from the tab order and the accessibility tree, which was measurable — with it, `btn.focus()` left `document.activeElement` elsewhere and the `:focus-within` rule could never fire. `pointer-events` keeps the faded box from swallowing clicks meant for the title. Touch never hovers, and the button already removes itself below the mobile breakpoint, so nothing is stranded there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f4d21a6e1e |
✨ feat(portal): copy outside-workspace publish assets into the workspace (#19447)
- add a `copyAssetForPublish` transport (device-control default, desktop IPC gated by the external-file approval path, device RPC + gateway with workspace containment on the destination) so the outside-workspace publish guide can relocate assets on every runtime - build `copyFile` / `writeFile` for local, device and sandbox files - let `useBlockedWorkspaceHtmlPublish` hand the confirmed plan to a caller- supplied `runPublish` instead of always publishing inline - show a loader instead of spinning the action icon while a toolbar action is busy Claude-Session: https://claude.ai/code/session_019jVhraxkznG9MtkXaux657 |
||
|
|
6b85e332f1 |
🐛 fix(desktop): pin bundled npm deps through the Desktop lockfile (#19462)
* 🐛 fix(desktop): pin bundled npm deps through the Desktop lockfile The main hash accepted any npm module that happened to resolve inside apps/desktop/node_modules, so a module linked from outside the frozen install (or from a stale store entry) could enter the renderer OTA hash without the lockfile ever pinning it. Resolve each bundled module to its package name@version and require apps/desktop/pnpm-lock.yaml to pin it, keeping the file:/link:/git/tarball escape hatch for specifiers that pin by resolution instead of version. partial-json is imported by packages/utils and packages/context-engine but was never declared, so the Desktop lockfile could not pin it: declare it in both packages and record it in apps/desktop/pnpm-lock.yaml. * 🔊 chore(desktop): name the changed bundles before a dev restart The dev orchestrator logged only that a main/preload bundle changed, which cannot distinguish the debounced restart from an unrelated vite rebuild. Collect every file reported by the watcher during the debounce window and print it with its event type, size and mtime. * 📝 docs(acceptance): publish the round from the session's own observations A delivery verified on the real product earlier in the session still had to re-run the plan and checker stage to obtain a published round. Document that those observations and artifacts are the evidence: write plan[] and cases[] from them, disclose anything never captured, and ingest. |
||
|
|
5aa56d8992 |
🐛 fix(verify): stop the acceptance review from rejecting evidence it never saw (#19468)
* 🐛 fix(verify): share the review evidence budget so one attachment cannot starve the rest The review predictor spent its 60k-character text budget in capture order and broke out of the loop once it ran dry, so a large attachment silently consumed everything behind it. On a real Goal delivery the four rows were 31,853 characters of script source, a 4,307-character rerun log, a 52,226-character score dump and a 1,319-character summary: the summary never reached the prompt at all, and the reviewer rejected the check for missing evidence that had in fact been attached. Allocate by water-filling instead — walk the rows smallest first and grant each an equal share of what is still unspent, so every row is guaranteed `budget / rowCount` characters and small rows stay whole. Two related gaps in the same block: - A text row's own description was dropped, while visual rows had always carried theirs. The caption is often the only thing that says what a payload is ("rerun log, byte-identical to round 1"), so the reviewer was left inferring from raw bytes. - Truncation was applied silently. A payload that stops mid-line reads as a corrupt artifact, which invites exactly the "insufficient evidence" rejection the truncation caused. Truncation and omission are now stated in the text. The allocation moved to its own module so the rule is testable without a database; content resolution for every candidate row now happens before allocation, which is the cost of being able to size them fairly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(verify): let the acceptance review report a criterion it cannot decide The automated review only had `accept` and `reject`, so "this delivery fell short" and "no reader could settle this check" were the same answer. Criteria that ask the reviewer to act on the system — rerun the delivered scripts, build the project, drive something live — are undecidable from captured evidence by construction, and the hardened v3 prompt correctly turns "no affirmative proof" into a reject. The result is a Goal Task that re-delivers the same artifacts against the same unprovable criterion until its attempt budget runs out. Add `unjudgeable` as a third verdict: - The prompt defines it by one test: could some capture the builder is able to produce settle this check? If yes it is a reject and the comment says what to capture. Only if no capture could ever settle it is it `unjudgeable`. Thin, blank, truncated or unconvincing evidence stays a reject, so this is not an escape hatch from the leniency the prompt was hardened against. - `reviewGoalDelivery` keeps it out of the rejected bucket and aggregates by precedence rather than last-writer-wins, since checks are judged concurrently. A genuine rejection outranks it: one short check still makes another attempt worth paying for. - A task whose review could not decide pauses with a new contract string that deliberately has NO recovery branch, so the coordinator routes it to a person on the first occurrence instead of buying another identical attempt. The action vocabulary lives twice — the model's JSON schema and the column it is stored in — so a test now pins the two lists to each other. The column enum is type-level only in drizzle, so no migration is needed. `REVIEW_PREDICT_PROMPT_VERSION` goes to v4 because agreement statistics are grouped by (provider, model, promptVersion); the new cohort covers both this change and the evidence-budget change it ships with. Existing v3 rows stay attributable, and open acceptances re-judge on the next request. Out of scope, stated for the reviewer: the acceptance page still renders no card for an `unjudgeable` prediction (only rejects surface), and nothing yet escalates such a check to a judge that can execute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(verify): name the trigger that makes a criterion undecidable Probing the pinned review model on a criterion that explicitly reads "the reviewer must log in and compare the memory curve themselves", the model still answered `reject` with "no monitoring evidence attached" — it read an action it cannot perform as evidence somebody forgot to capture. Naming the trigger fixes it: a criterion that makes the REVIEWER the actor cannot be settled by any capture, because the reviewer cannot become that actor. Verified against gemini-3.6-flash: that criterion now returns `unjudgeable`, while a check whose evidence is merely absent still returns `reject`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(verify): tell the review which artifacts it was not shown The review request silently carries less than the check does: frames past `MAX_VISUALS` are dropped, audio and video are never read, and a text row whose payload cannot be resolved simply vanishes. The prompt then demands affirmative proof, so the model answers "not observable in the evidence" about artifacts that exist and were withheld from it — and the comment tells the builder to recapture something already captured. Driven against a real check that carries five verification screenshots, with the pinned review model and the real frames: | Withheld declared | Run 1 | Run 2 | | --- | --- | --- | | no (current behaviour) | reject: "缺少单元 U208 和 U223 的截图原图" | reject, same two units | | yes | accept | accept | Both rejections name precisely the two frames the request itself removed. Declare them instead. `describeWithheldEvidence` lists what the request held back — frames past the cap, media this reviewer cannot open, payloads it could not resolve — with each artifact's own caption, because the caption is the only handle the model has on something it cannot open. The prompt gains one rule: a withheld artifact is never cited as missing, and a verdict that turns on one is `unjudgeable` rather than a reject. The caveat is also persisted on the judged row. "Rejected having seen everything" and "rejected while two frames were held back" are otherwise the same verdict in the agreement statistics, which is exactly the distinction the review-quality work needs. `status_reason` is redocumented to cover that. Scope on this account's labelled history: 50 of 1861 checks carried more frames than the request attaches, and 6 carried audio or video the reviewer never reads. One read of the check's evidence now feeds all three decisions — which frames to attach, which payloads to inline, and what was held back — so they cannot disagree about what the check carries. Prompt cohort goes to v5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3e47ca7364 |
🐛 fix: isolate execution settings per topic (#19445)
* 🐛 fix: isolate execution settings per topic * 💄 style: order topic type imports * 🐛 fix: preserve execution metadata in topic creation * ✅ test: align execution fixtures with topic isolation |
||
|
|
ffedec8f6d |
✨ feat(device-control): list one project directory on demand (#19460)
* ✨ feat(device-control): list one project directory on demand The project index asks Git for ignored paths with `ls-files --directory`, which reports a fully ignored folder as a single entry. Its children are never enumerated, so a caller rendering that entry as an expandable row has nothing to show. Mark those entries `collapsed` and add `listProjectDirectory`, which reads one level of a directory from disk. Callers can then fill a subtree only when someone opens it, so an ignored folder still costs nothing until then and `node_modules` is never walked. The flag is only set when the directory truly has no indexed children — a nested `.gitignore` that re-includes part of the subtree leaves those rows complete, and they stay unflagged. Wired for both transports: an Electron IPC method on the desktop controller, and a device RPC plus TRPC procedure for remote devices. Also adds `trashLocalFiles`, which moves paths to the OS trash via `shell.trashItem` rather than unlinking them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🔒 fix(device-control): resolve symlinks before enforcing project containment `path.resolve` is lexical, so a symlink that lives inside the project but points outside it passed the prefix check and then had its target enumerated. Verified against a real temp fixture: listing a link to an outside directory returned that directory's files. Check containment on the real paths of both the root and the target. Entries are still built from the lexical path, so their ids stay anchored to the path the caller expanded rather than wherever the link resolved — a link that stays inside the project keeps working and keeps its row. Also return per-path outcomes from `trashLocalFiles`. A batch is not atomic, so bailing out on the first failure left the caller unable to tell which earlier paths were already in the trash, and therefore unable to refresh its tree or retry only what was left. This matches the shape `moveLocalFiles` already returns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
689f8a3993 |
🔒 fix(bot): stop returning bot credentials from the channel list (#19452)
`lh bot list --json` printed every bot's credentials in the clear. In a workspace that is worse than it sounds: the read path filters on `workspace_id` alone with no `user_id` term, and the list procedure carries no write gate, so any member — viewers included — could dump every colleague's bot token and app secret. That is how a live Feishu app secret ended up pasted into a chat transcript. A list is an inventory. It now omits `credentials` entirely, and the detail read masks each secret instead of returning it. Masking has to survive the edit form, which reads credentials and submits them back. The model replaces the credential blob wholesale, so persisting the mask would destroy the secret and dropping the masked keys would delete it just the same. `update` therefore resolves every mask back to the stored value before anything else inspects it, and `create` rejects a mask outright since a new binding has no secret for it to stand in for. Which fields are secret comes from the form schema's existing `type: 'password'` marking, so Discord's `publicKey` stays readable while its `botToken` does not. WeChat fills credentials from its QR handshake and has no schema to read, so it names its two identifiers through the new `publicCredentialKeys`. Anything unclassified is treated as secret. Channel export dropped credentials too, with a line of copy saying so — exporting masks would have produced a file that looks complete and fails on import. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1eacff997e |
🔒 fix(acceptance): close the holes the open discussion opened (#19448)
* 🔒 fix(acceptance): close the holes the open discussion opened Opening an acceptance's discussion to whoever holds its public link widened three things it should not have. An agent belongs to its workspace, not to its readers. The author-agent gate was scoped by the acceptance's workspace, which answers "is this agent visible there", not "may this caller use it" — and the discussion payload hands a visitor the ids to pick from. It is now scoped to the workspace only for callers who actually belong to it; everyone else falls back to their own agents. Read-only means read-only. A `viewer` of the acceptance's own workspace was swept in by "anyone signed in who can read it" and could write on a private delivery, unlike the same role on topic and document comments. Commenting now needs the public link or a non-viewer seat. An open discussion needs a way to close an unwanted remark. Its author is exactly who will not remove it, so the acceptance's reviewers may now take one down — the same tombstone the author's own deletion leaves — and a visitor gets a per-minute ceiling on how much they can land. Two client reads of `canDelete` as "I wrote this" are replaced by the author id, since that flag now also turns on for a moderator: whether a thread may be closed, and whose approval the reviewer bar is showing. The read-only line no longer flashes while the permission answer is in flight, and the sign-in return URL moves to a shared helper. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🔒 fix(acceptance): do not call an outside reader a departed teammate The `former` tag infers "left the workspace" from "is not a member now". That only holds on a delivery nobody outside could write on. With the link public, most people in the discussion never had a seat, so the tag is dropped there rather than guessed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(acceptance): charge the visitor ceiling where the row is written Codex review of the open-discussion hardening, three findings. The ceiling was counted before the write and enforced after it, so a burst of parallel requests each read the same under-limit count and all landed — which is the shape a flood actually arrives in. It now travels into the write: the transaction takes an advisory lock on (acceptance, author), inserts, counts, and rolls itself back when it is the one over the line. Everybody else still writes in parallel. The same move fixes a retry being punished for a remark it already made. A replay of the tenth comment's idempotency key inserts nothing, so it is no longer counted and answers with the row it wrote the first time, instead of a refusal for a lost response. `canResolveThread` reads the signed-in identity, which arrives after the first paint, but the overlay memo did not depend on it — a thread could keep the answer computed while nobody was signed in until an unrelated comment changed. It is a `useCallback` on what it reads, and the memo depends on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(workbench): let the SSR electron stub answer the gateway device question Five shared store modules read getElectronStoreState().gatewayDeviceInfo to decide whether a working directory is local. They enter the workbench SSR graph through the acceptance check route, where the stub had no such key, so the stub surface guard failed the build. Give the stub the key with no device, which is the truthful answer on the server. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2c5faed6c6 |
🐛 fix(theme): compose ConfigProvider outside ThemeProvider in every shell (#19457)
* 🐛 fix(theme): compose ConfigProvider outside ThemeProvider in every shell Claude-Session: https://claude.ai/code/session_019rSFnGHUE9kq3LMCUxugzR * ♻️ refactor(motion): always render through LazyMotion's m, never the full motion runtime Claude-Session: https://claude.ai/code/session_019rSFnGHUE9kq3LMCUxugzR |
||
|
|
48c61be524 |
🔥 chore(theme): remove AntdStaticMethods now that nothing renders through antd static APIs (#19458)
Claude-Session: https://claude.ai/code/session_019rSFnGHUE9kq3LMCUxugzR |
||
|
|
15c5a6fb05 |
🔖 chore(cli): bump version to 0.0.54 (#19451)
Ships `lh artifact publish` (#19360). Claude-Session: https://claude.ai/code/session_019jVhraxkznG9MtkXaux657 |
||
|
|
85d4f3a79c |
🐛 fix(bot): stop a binding outside the active scope from being undeletable (#19446)
* 🐛 fix(bot): stop a binding outside the active scope from being undeletable `(platform, applicationId)` on `agent_bot_providers` is unique across the whole deployment because it is the webhook routing key, but `list`, `findById` and `delete` all filter by `buildWorkspaceWhere`. A binding that lives in the caller's other scope — a workspace row seen from a personal session, or the reverse — is therefore invisible to `list`, untouched by `delete`, and still holding the application id that every re-bind collides with. Three things then conspired to make it unexplainable. The delete never checked how many rows it removed and the CLI printed its checkmark unconditionally, so a no-op read as done. The create handler rewrote any 23505 into "already registered" without looking up who held it, so the message asserted a cause nobody had verified. Together they point at a phantom: a constraint with seemingly nothing behind it, which reads as a server-side soft-delete leak. The table has no soft-delete columns at all. - `delete` returns the rows it removed; the router falls back to a scope-free lookup and lets the creator reclaim their own binding from either side, and answers NOT_FOUND instead of pretending otherwise. A binding owned by someone else stays NOT_FOUND so the error cannot confirm it exists. - A conflict now resolves the real holder and names the agent and scope when it belongs to the caller, with the exact `lh bot remove` to run. Another account's binding is reported as taken without naming agent or workspace. - `lh bot remove` fails loudly when the server removed nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🔒 fix(bot): re-authorize a cross-scope reclaim against the binding's own workspace Matching only the creator skipped the owning workspace's authorization entirely: `withScopedPermission('agent:update')` is evaluated against the caller's ACTIVE scope, which by definition is not the one holding a stranded row. A user removed from workspace B could reach in from personal scope and delete an integration B still runs. Re-check where it counts — an active, non-viewer seat in the workspace the binding actually lives in. A binding with no workspace stays personal, so the creator check already settles it. Found by Codex review on #19446. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e1d9a716ad |
🐛 fix(hetero): stop a sandbox spawn 504 from killing a live or finished run (#19443)
The cloud sandbox `runCommand` call is issued with `background: true`, but the sandbox gateway can hold the connection and answer `Gateway Timeout` a minute or more after the sandbox actually booted and started streaming events back through `heteroIngest`. The `spawnHeteroSandbox` catch finalized on every rejection, so that late 504 was treated as "the run never started": it blanked the assistant message, stamped an error bubble on a turn the user had already read, marked the op row and its task failed, and closed the UI stream out from under a sandbox that was still producing output. Probe two cheap reads before finalizing — the op row's status, and the topic's `heteroCurrentMsgId` ingest pointer — and skip the finalize when either shows the run reached the sandbox. A run that passes the probe and then dies is still reaped by the agent-gateway inactivity watchdog. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b0eb29b1af |
🐛 fix(tool-execution): sign Market calls with the run identity (#19444)
agent-builder and agent-management built DiscoverService with no options, so they signed no trusted-client token and every server-executed market read came back unauthorized. The model reports that as an ordinary tool failure and works around it, so the operation still ends `done` and the user is left with an agent that has no market tool attached. lobe-skills signed its Market identity without workspaceId, so it acted as the personal account while lobe-creds and lobe-cloud-sandbox acted as the workspace. Under a workspace that split one topic across two sandbox sessions: injected credentials landed in one and every lobe-skills command ran in the other. Claude-Session: https://claude.ai/code/session_01N5sUWhdzDabY67UehXJD3y Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bc5c81f319 |
✨ feat(acceptance): let anyone with the link join the discussion (#19420)
* ✨ feat(acceptance): let anyone with the link join the discussion A public acceptance is passed around as a link so other people can answer the evidence in it, yet only the creator and their workspace could write. Reading and remarking are now the same right: any signed-in reader can comment, reply, annotate a screenshot and react. Speaking for the delivery stays narrow. Approving a round, posting a round's own introduction, and closing someone else's thread still need the creator or a non-viewer member of the acceptance's workspace; the acceptance decision itself is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(acceptance): tell a shared reader what they can actually do The shared-link notice promised the discussion to everyone. A signed-out reader gets no reply box, so it now reads what that viewer may do: review, join the discussion, or only read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(acceptance): answer the review of the shared-link discussion Seven notes from the acceptance review of this delivery, all in what the page shows rather than in who may write: - the add-reaction button leads its row, so it stays put instead of sliding right by one chip each time somebody reacts - an author's colour is the scale's solid step. The bare hue is the tint step: in light mode cyan lands near #95f3d9, and a 2px box in that colour on a white screenshot is a mark nobody can see - a screenshot posted into a remark is shown at reading size, not as a 56px chip. "Here is what I see instead" is unreadable at 56px - a signed-out reader gets a way in where the reply box would have been, rather than a grey line stating the rule - one row closes a check: circling the evidence on the left, the verdict buttons on the right, instead of two half-empty rows - an acceptance that was never shared with this reader says so, instead of blaming the network and offering a retry that cannot work - the timeline rail stops at the composer instead of hanging 14px below it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
bb97d05277 |
🐛 fix(desktop): stabilize renderer OTA compatibility and full-release state (#19410)
* 🐛 fix(desktop): stabilize renderer OTA compatibility and full-release state
* fix: allow sentry cli install build
|
||
|
|
1334209461 |
✨ feat(openapi): expose eval resources and external run lifecycle (#19428)
* ✨ feat(openapi): expose eval resources and external run lifecycle * 🐛 fix(openapi): report and aggregate shared eval dataset cases |
||
|
|
07fb752547 |
🐛 fix(portal): guide outside-workspace HTML publishing (#19338)
* 🐛 fix(portal): guide outside-workspace HTML publishing * 💄 style(portal): keep external-read consent above footer * ✨ feat(portal): confirm external file access * 💄 style(portal): align external-read consent typography * 🐛 fix(portal): harden outside-workspace publishing * 🐛 fix: secure external workspace HTML publishing * 🐛 fix(portal): bound external publish reads and honour local base hrefs - reject an external publish asset by `stat` size before reading it, on both the device-control and Electron transports, instead of letting the renderer find out after a multi-gigabyte base64 round trip - keep a `<base href>` that escapes the workspace, so its refs are reported as real outside-workspace dependencies instead of resolving back inside - drop a local `<base>` from the relocated copy, whose resource hrefs are already rewritten relative to its own directory Claude-Session: https://claude.ai/code/session_017KB2isGJ1RDpSaHiv88Xto * 🔧 chore(deps): register html-artifact in the standalone app workspaces `apps/cli` and `apps/desktop` enumerate their workspace packages instead of globbing, so `packages/device-control`'s new `@lobechat/html-artifact` dependency could not resolve there. Claude-Session: https://claude.ai/code/session_017KB2isGJ1RDpSaHiv88Xto |
||
|
|
2933b9fcc5 |
⚡️ perf(desktop): show the main window on first paint, compile cache, dev progress (#19429)
* ⚡️ perf(desktop): show the main window on the loading screen's first paint `ready-to-show` only fires with the `load` event for the desktop entry, which lands ~250ms after the static loading screen has actually painted. The preload now reports that first paint (double rAF after `#loading-screen` enters the DOM) and the main process shows the window on whichever signal arrives first. Measured on the packaged app (warm start, window creation = 0): window visible at +140ms instead of +370ms, with the loading screen already painted. Claude-Session: https://claude.ai/code/session_012z4cpfrWDXZDtNgT4uJKoS * ⚡️ perf(desktop): enable the V8 compile cache for the main process bundle Injected as an entry banner because rolldown hoists chunk requires above any statement in the entry source, so `main-app` would otherwise load uncached. Main bundle load 55ms → 37ms warm; exec → main.js ~10ms faster on a packaged build. Claude-Session: https://claude.ai/code/session_012z4cpfrWDXZDtNgT4uJKoS * ✨ feat(desktop): show Vite transform progress on the dev loading screens A cold `bun run dev` sits on the loading logo for 10s+ while Vite transforms thousands of modules. A serve-only plugin streams the transform count and the current file over the HMR socket, and an injected client renders it at the bottom of the window for as long as any boot placeholder (`#loading-screen`, `#boot-shell`, `#app-shell-fallback`) is on screen. Claude-Session: https://claude.ai/code/session_012z4cpfrWDXZDtNgT4uJKoS |
||
|
|
f2a0901ef1 |
🐛 fix: harden bot messaging and channel setup (#19405)
* 🐛 fix: harden bot messaging and channel setup * 🐛 fix: retain bot messages across sender and replay races * 🐛 fix: retry deferred bot replay independently * 🐛 fix: preserve command turns and debounce owner lookup |
||
|
|
8171469b80 |
✨ feat: read Feishu documents and preserve rich-text images (#19401)
* ✨ feat: read Feishu documents and preserve rich-text images * 🐛 fix: bound Feishu markdown image scanning * 🌐 chore: refresh translations after rebase |
||
|
|
91a77ae585 |
✨ feat: support member mentions in workspace documents (#19402)
* ✨ feat: support member mentions in workspace documents * 🌐 chore: refresh generated translations * 🐛 fix: enforce knowledge base access for document notifications * ✅ test: include mention plugin in readonly editor mock |
||
|
|
f6660c1666 |
🐛 fix(goal): recover pipeline failures instead of escalating them to a person (#19377)
* 🐛 fix(goal): recover pipeline failures instead of escalating them to a person Supervision only acted on a failure that had already produced an errored operation. Dispatch failures, a dropped device link and a crashed verifier never take that shape — the run either never started or finished cleanly and something around it broke — so the whole class escalated to a human decision gate. In a live run these were the only three interruptions, all of them recorded as `Only a confirmed failed operation can be recovered`, and each one stopped a long-horizon goal until someone clicked retry. - Classify a failure by where it happened. An `agent-run` failure is still judged by its own error text; a `pipeline` failure recovers without one, because retrying re-uses authority the goal already granted. - Keep the human gates that matter: an explicit interruption, a limit stop, a cancelled Task, exhausted attempts, and anything reading as credentials, permission or spend. - Route `VERIFICATION_ERRORED_ERROR` to the verification recovery branch. A verifier that could not run never judged the delivery, so it is infrastructure rather than a verdict; without a branch it fell through to the gate. - Move the three Task error contract strings into `@lobechat/const/goal` so the side that writes one and the coordinator that matches on it cannot drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): apply pipeline recovery on the status the Task actually holds Codex review caught the previous commit widening eligibility without widening the step that acts on it, so the fix was a no-op for its own target case. - The recovery compare-and-set expected `failed`. A pipeline failure leaves the Task `paused`, so the swap never matched, the incident was marked escalated and the goal fell back to the human gate anyway. Compare against the status revalidated inside the transaction instead. - A kickoff that threw before writing a run has no operation to key an incident on, and `reviewFailure` returned early on exactly that shape. Key those on the Task and its attempt so they are recorded, deduplicated and budgeted like any other incident. Both regressions are covered by tests that fail without these changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ♻️ refactor(goal): drop the half-built operation-less supervision path Codex review found two consequences of keying an incident on a synthetic id: `TaskRunnerService` leaves `totalTopics` unchanged when a retry also fails before writing a topic, so a repeated dispatch failure reused the first key and fell through to the gate anyway; and `recordProgress` settles an incident by locating the failed run's sequence, which a synthetic key can never match, so the incident stayed `retrying`, understated recovery metrics and could relabel a later recovery as unsuccessful. Both follow from incident identity and settlement being anchored on a real run. Making the operation-less case correct means redesigning that, which is its own change rather than a rider on a policy fix — and a half-built version is worse than none, because it writes incidents that can never settle. Restore the early return and leave that case on the human gate. What this PR fixes is unchanged and covers every interruption observed live: a settled run whose Task is left `paused` now recovers instead of escalating. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): classify by the operation, not the Task status Review found the origin test read the wrong signal. `TaskLifecycleService` stores an ad-hoc run that fails as `paused` with an errored operation — the same Task shape a pipeline failure leaves — so requiring `failed` labelled real agent errors as pipeline failures and routed them around the credential and transport checks this PR claimed to keep. An errored operation now decides it, whatever the Task holds. Also bound the statuses supervision may restart to the two the coordinator actually routes to recovery. The previous commit compared the swap against whatever status the Task held, which would have accepted a terminal one; a completion recorded during a diagnosis must never be swapped back to `backlog` and rerun. Guarded in the policy and again at the swap itself. The misclassification has a regression test that fails without the fix. The terminal-status guard is defence in depth: it is not reachable through the coordinator, which does not route a settled Task to recovery, so its test pins the invariant rather than reproducing a failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): recognise recoverable dispatch failures instead of inferring them Review found a fifth state that looked like a pipeline failure and was not: a caller marking a Goal-linked Task `failed` after a clean run. Recovery would reopen it and spend more paid work despite an explicit terminal decision. That is a pattern, not a case. Inferring a recoverable failure from the *absence* of a run error is an open set: every state an actor can author arrives looking exactly like a dropped dispatch, so each round found another one. Replace it with one allowlist read off whatever error text exists, applied to both origins. Recoverable now means a recognised transport failure or a gateway code whose own message states the run never started, so a retry cannot duplicate committed work: `DEVICE_OFFLINE`, `DEVICE_CHANNEL_UNAVAILABLE`, `DEVICE_GATEWAY_UNREACHABLE`, `DEVICE_GATEWAY_RATE_LIMITED`. The codes that say retrying will not help (`DEVICE_GATEWAY_UNAUTHORIZED`, `GATEWAY_NOT_CONFIGURED`) or that the outcome is unknown (`DEVICE_RESPONSE_TIMEOUT`) stay with a person, as does an authored failure carrying an arbitrary error or none. `failureOrigin` goes away with the inference it existed to express. The three observed interruptions are still covered: they carried gateway codes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): recognise a dispatch failure in either shape it is stored as Review found the allowlist read only one of the two forms a dispatch failure takes in `task.error`. The coordinator's own dispatch persists the raw gateway payload — confirmed against a live run, where the gate reason was `{"error":"DEVICE_OFFLINE","success":false}` — while the runtime's `finalizeHeteroDispatchError` persists the humanized headline, which contains no code. Matching codes alone therefore still opened the gate this PR exists to remove, on the path that finalizes through the runtime. Match both, derived from the one headline map rather than copied text, so the storage shape does not decide whether a person is needed. `HETERO_DISPATCH_ERROR_HEADLINES` is exported for that. `DEVICE_NOT_FOUND` is also called out as staying with a person: reconnecting or rebinding the device is not something a retry does. The regression asserts the actually persisted value through `humanizeHeteroDispatchError`, not an injected raw code, and fails without the headline match. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): allow the gateway's own 5xx code into transient recovery Review found `DEVICE_GATEWAY_ERROR` missing from the allowlist. The gateway client emits it for any status at or above 500, and its headline states outright that nothing started on the device and the failure is usually temporary — which is the criterion the allowlist was written around. The fallback transport regex only covers 502-504, so an HTTP 500 dispatch failure matched nothing and still opened a human gate. Regression covers both persisted forms, the raw code and the humanized headline, and fails without the entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): never restart a Task an actor already settled Two ways a terminal decision could be overturned and paid work launched. - `TaskService.updateStatus` replaces `error` only when a new one is supplied, so an actor can move a pipeline-paused Task to `failed` and leave the transport text that paused it, which the allowlist then reads as a dropped dispatch. The activity log records assignee changes rather than transitions, so the author is not readable; require the errored run instead. Nothing the system does writes `failed` with a recoverable error — the watchdog's is `Heartbeat timeout`. - `TaskRecoveryCoordinator.recover` rejected only `running`, then swapped whatever status it found to `running` and spawned a run. A person completing or cancelling between the tick's read and the claim would have their decision overwritten. This path bypasses supervision entirely, so the policy guard did not cover it; bound the claim to the statuses that await recovery and report `settled` so neither call site opens a gate over somebody's own decision. The claim race predates this branch on the lease path; routing the verifier error here widened its reach, so both are covered by the regression. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): claim only the paused state recovery was routed for Review found the shared recoverable set was the wrong bound here: it includes `failed`, so a person marking the Task failed between the tick's read and the claim still passed the guard and had their decision swapped to `running`. Both branches that reach this coordinator require `paused`, so that is the only status the claim may take, and the rollback releases to the same one. Coverage now spans a concurrent move to `completed`, `canceled` and `failed` across both the lease and verifier-error routes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ♻️ refactor(goal): give a Task's return to work one gate Three rounds of review each found another entrance through which automatic recovery could overwrite a decision somebody made, because no single place owned the question of who may put a Task back into work. Patching entrances found the next entrance; this collects the rule instead. `claimGoalTask` is now the only way a Goal moves a Task to `running` or `backlog`, and it states the invariant the three sites were getting right or wrong independently: claim against the status the decision was made on, never a fresh re-read. Losing the compare-and-set is the correct outcome — it means the row moved, so the decision is stale and belongs to whoever moved it. Dispatch already worked this way, which is why it was never implicated. The recovery coordinator re-read first and is now pinned to the routed `paused`. Supervision had the widest window of the three: a diagnosis runs for minutes, so the incident records the status it opened on and claims against that, rather than only catching a change since the current tick. Regression covers a Task moved to another recoverable status mid-diagnosis, which the status allowlist accepts and only the claim rule refuses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): escalate a diagnosis that cannot say what it opened on The compatibility fallback restored the race it was added beside: an incident persisted before `taskStatus` existed claimed against a later read, so a person moving the Task during such a diagnosis would have that undone. An incident with no opening status cannot show the row is still the one it decided about, so it escalates instead. A rolling deploy leaves at most the few mid-diagnosis, and a human gate is the right answer for them. Covered by a regression that strips the field the way an older release left it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): ask who wrote a Task's status, not what its error says The previous guard read the error, and the error cannot tell these apart: when a run errors with a recoverable transport failure the lifecycle leaves an errored operation and a paused Task, and a person marking that Task `failed` without supplying a new one keeps the same text. Requiring the errored run therefore passed exactly the case it was meant to stop, and supervision would put an explicit failure back to `backlog` and spend more. An earlier commit asserted the author was not readable. That was wrong, and the comment saying so is gone: `status` is a tracked column, so `updateWithLog` records an actor on the transition, and `TaskService.updateStatus` documents that its absence is how system transitions opt out of the feed. That makes actor presence the discriminator this needed all along. `statusAuthoredByActor` reads the latest tracked transition, requires it to be the one that produced the current status, and both eligibility calls pass it. Regressions cover the helper's edges and the end-to-end shape: errored run, paused by the lifecycle, then failed by a person. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): read the durable actor kind, and honour a pause a person made Two gaps in the provenance check, both confirmed against the code that writes it. - It read only the actor id columns. Those are `ON DELETE SET NULL`, and `taskActivityActor` writes `actorKind` into the payload for exactly that reason: "someone who is gone must not read as the system". An authored failure became recoverable once its author left the workspace. Read the kind first and keep the ids as the fallback for rows written before it. - Verifier-error recovery checked only that the Task was still `paused`. A person moving it `paused → failed → paused` keeps the routing error, because the status update replaces it only when a new one is supplied, so the final pause is theirs and the status alone cannot say so. It now asks the same question the supervisor does and reports `settled`. Covered by the tombstoned-actor shapes and by the manual round trip end to end. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6b608fa53e |
🐛 fix(cli): declare url-join and vitest so the standalone install builds (#19408)
* 🐛 fix(cli): declare url-join and vitest so the standalone install builds `apps/cli` installs against its own workspace, but `src/utils/agentStream.ts` imports `url-join` and the tests import `vitest` while neither is declared there — both resolve only through the root install's hoisting. CI does the root install first, so this stayed invisible until someone builds the CLI on its own: Could not resolve 'url-join' in src/utils/agentStream.ts `src/runtime/*` also imports `@lobechat/agent-runtime` and `@lobechat/conversation-flow` undeclared, and those are deliberately left alone: registering them pulls their workspace closure — 16 more packages including `@lobechat/database` and a dozen builtin tools — into an install kept slim on purpose. Declaring them properly is a bigger decision than this fix. Verified by removing node_modules and running `pnpm install` + `bun run build` inside `apps/cli` with no root install. Claude-Session: https://claude.ai/code/session_01558yVdNvoxocvnWAs11DGT * 🔒 chore(desktop): sync lockfile with the CLI dependency declarations apps/desktop lists ../cli as a workspace member and keeps a lockfile, so declaring url-join and vitest in apps/cli left it outdated and failed Test Desktop App with ERR_PNPM_OUTDATED_LOCKFILE. Claude-Session: https://claude.ai/code/session_01558yVdNvoxocvnWAs11DGT |
||
|
|
b1a9979c5f |
✨ feat(cli): publish a local HTML file as an artifact (#19360)
* ✨ feat(cli): publish a local HTML file as an artifact `lh artifact publish <file>` gathers the page's local references, packs them, and publishes the result. The page's identity is a deployment id, recorded in `.lobehub/artifacts.json` next to the project and committed with it, so a publish from CI updates the same site a developer created locally. Republishing adds a revision and keeps the public URL; `--new` makes a separate site and rebinds; `--deployment <id>` targets one for a single run without touching the manifest. A bound id that no longer resolves is an error — falling back to create would silently strand the URL the caller meant to update. Assets are read straight off the filesystem, so `--root` (default: the working directory) sets how far up a page may reach for them. That replaces the app's sandbox boundary, which does not apply here: the CLI reads with the user's own permissions, and a page under `dist/` referencing `../assets/logo.png` is ordinary rather than an escape attempt. A create carries an idempotency key, written to the manifest *before* the request. If the response is lost, the retry reuses the key and the server returns the deployment it already made instead of minting a second site and burning another quota slot. Updates are addressed by id and deliberately carry no key: a retry costs a spare revision, never a stray site. Claude-Session: https://claude.ai/code/session_01558yVdNvoxocvnWAs11DGT * 📝 docs(acceptance): record what the CLI artifact-publish round had to learn Two judgment rules and two probe recipes from verifying `lh artifact publish` against a local cloud → market → artifact-host chain. L-E21 is the one an acceptance-checker caught mid-round: the case asserting `--new` leaves the original site untouched published two sites with identical bytes and a request line that echoed only the response. It read as a pass while proving nothing, because requesting the wrong site would have printed the same output. The rule generalises — make the two objects distinguishable before capturing, and echo the target in the request. L-S22 covers the quota the round consumes itself: each publish leaves an active deployment, the Free tier caps at three, and the third run fails in a way that reads as a regression. Compounded by clearing the cap for the wrong account — the CLI publishes as the seeded runtime user, not the smoke's default id. P84 records how to authenticate the CLI against lobehub-cloud's dev runtime at all (seed an api_keys row; `dev:runtime:auth` only writes browser cookies), plus the port-override that every runtime subcommand needs. P85 records how to simulate a publish whose response was lost, which cannot be produced by killing the process because the manifest is what carries the recovery state. Claude-Session: https://claude.ai/code/session_01558yVdNvoxocvnWAs11DGT * 🐛 fix(cli): register @lobechat/html-artifact in the CLI's own workspace `apps/cli` installs standalone against its own `pnpm-workspace.yaml`, which enumerates the packages it may link rather than globbing them. The root install resolved the new dependency, so this only showed up as CI's "Install CLI deps" step failing: "@lobechat/html-artifact@workspace:*" is in the dependencies but no package named "@lobechat/html-artifact" is present in the workspace Claude-Session: https://claude.ai/code/session_01558yVdNvoxocvnWAs11DGT * 🐛 fix(desktop): register @lobechat/html-artifact in the desktop workspace too `apps/desktop/pnpm-workspace.yaml` lists `../cli` as a member, so the CLI's new dependency has to be resolvable from the desktop install as well — its "Install deps" step failed the same way the CLI's did. Unlike the CLI workspace this one sets `lockfile: true`, so the regenerated lockfile ships with it. Claude-Session: https://claude.ai/code/session_01558yVdNvoxocvnWAs11DGT |
||
|
|
d2caf84fdd |
✨ feat(acceptance): serve the collaboration table, and let a round introduce itself (#19393)
* ✨ feat(acceptance): serve the collaboration table, and let a round introduce itself The second of three layers. It stacks on the table PR and is called by the UI PR above it; on its own it adds an endpoint nobody invokes yet. `acceptanceComment` covers create, delete, list, resolve and an emoji toggle. Reads are addressed by acceptance id alone and the handler decides visibility, so a public share link works without a session; writes need the creator or a non-viewer member of the acceptance's own workspace. A missing right reads as NOT_FOUND rather than FORBIDDEN, which is what the bundle read already does. The read does the joining the page would otherwise do by hand: author profiles, membership status, round indexes for a comment's context run, attachment ids resolved to display URLs as the aggregate's owner, and reaction rows folded into the comment they answer. `lh acceptance run ingest` gains the round's own note. An agent writes `proposal.md` beside the report; ingest posts it as a comment carrying that round's id and signed by the agent, so a delivery introduces itself the way a pull request does. It is the last step and never fatal — against a server without the endpoint the round still publishes, minus the note. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(acceptance): scope comment agent and run/evidence references A comment carries three references the schema alone does not constrain: the agent it speaks as, the round it was written against, and the evidence tile it is pinned to. Access is resolved from `acceptanceId` only, and the foreign keys prove global existence rather than association, so any participant could name rows from an acceptance they cannot read — or sign a comment as somebody else's agent and expose its name, title and avatar through a public acceptance link. Gate `authorAgentId` through `assertAgentUsableBy` in the acceptance's own workspace, the scope the agent is being presented to, and verify that `contextRunId` and `anchor.evidenceId` resolve back to the target acceptance before insert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(acceptance): categorize the comment namespace for API key scopes The scope guard fails closed on any namespace missing from `TRPC_NAMESPACE_API_KEY_RULES`, and a coverage test makes sure a newly mounted router is categorized on purpose rather than silently denied. `acceptanceComment` follows the acceptance it hangs off: blocked for restricted keys, reachable with a full-access one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
aba9e8150c |
👷 build: migrate the test suite to vitest 5 (#19355)
* 👷 build: upgrade vitest to 5.0.0 Bump vitest and @vitest/coverage-v8 to 5.0.0 and migrate the configs for Vitest 4/5: drop the removed coverage.all option and replace poolOptions.forks.singleFork with maxWorkers/isolate. * 👷 build: shim jest-dom matcher types for vitest 5 Vitest 5 exposes Assertion<R, T> and no longer reads the jest.Matchers augmentation that @testing-library/jest-dom ships, so its matchers vanish from expect(). Re-declare them on the Vitest interface, and replace the removed describe.sequential with { concurrent: false }. * 👷 build: migrate the test suite to vitest 5 Vitest 5 (and @vitest/coverage-v8) replaces the esbuild dep optimizer with Rolldown, removed environmentMatchGlobs/coverage.all/poolOptions, forwards `new` to mock implementations, requires top-level vi.mock, and no longer resets automocks in restoreAllMocks. Migrate the root config to app/server projects with Rolldown optimizer options, shim the jest-dom matchers on Assertion<R, T>, and update the affected tests (constructor mocks, hoisted mocks, awaited assertions, explicit mock types, per-test mock defaults). * 👷 build: make the openapi knowledge-base FileService mock constructable tests/ was outside the per-area sweep; vitest 5 forwards `new` to the mock implementation, so the arrow implementation threw "is not a constructor". * 👷 build: point the CI blob-report artifacts at the vitest 5 output dir Vitest 5 writes blob reports to .vitest/blob instead of .vitest-reports, so the sharded jobs uploaded nothing and the merge jobs failed with ENOENT. Track the new directory in .gitignore too. * 👷 build: refresh the desktop lockfile for vitest 5 apps/desktop/pnpm-lock.yaml is committed and the desktop build action installs with --frozen-lockfile, so the vitest/override change has to be reflected in it. * 👷 build: keep the pre-vitest-4 coverage exclusions explicit Vitest 4+ ships an empty coverageConfigDefaults.exclude, so configs that spread it silently lost the default exclusions (node_modules, dist, *.config.*) and counted more files. Inline the previous list. * 🎨 style: format the migrated vitest configs * 👷 build: keep canary's new class mocks constructable under vitest 5 exploreGraph/dispatchAuthorization/scopedDevices (canary #19375, #19352) mock classes with arrow implementations; vitest 5 constructs the implementation when the mock is newed, so the arrows throw 'is not a constructor'. |
||
|
|
84b089450c |
♻️ refactor(ui): move DraggablePanel to the antd-free base-ui build (#19365)
* ♻️ refactor(ui): move DraggablePanel to the antd-free base-ui build @lobehub/ui@5.41.0 ships a headless DraggablePanel under `@lobehub/ui/base-ui`, rewritten on motion values so a drag never re-renders React. - switch all 10 import sites to `@lobehub/ui/base-ui` - drop `stableLayout`: the new panel holds the content size while the box animates, so the behaviour is unconditional - drop ChatTerminal's `smoothResize` workaround and align its unmount delay with the panel's own 250ms collapse Claude-Session: https://claude.ai/code/session_016Yc2kk25CBo6DXmeGJo1k9 * ♻️ refactor(share): move the share portal to the antd-free DraggablePanel The original sweep only covered `src/`, so this render site in the share app kept importing the antd component. Claude-Session: https://claude.ai/code/session_016Yc2kk25CBo6DXmeGJo1k9 * ⬆️ chore(deps): bump @lobehub/ui to 5.42.1 5.42.1 carries the fix that makes `collapseThreshold` work again behind `RightPanel`'s `expandable={false}`, which this migration depends on. Claude-Session: https://claude.ai/code/session_016Yc2kk25CBo6DXmeGJo1k9 * ⬆️ chore(deps): bump @lobehub/ui to 5.42.2 Picks up the drag-collapse clipping fix (lobehub/ui#651). Claude-Session: https://claude.ai/code/session_012BkEgtdLyHGbfKsHAL4Z5U * 🔧 chore: exclude the whole @lobehub scope from minimumReleaseAge Every release of ours is one we cut ourselves, so the release-age window only costs a version-pinned exclude entry on each bump. Claude-Session: https://claude.ai/code/session_012BkEgtdLyHGbfKsHAL4Z5U |
||
|
|
99fbd13ba0 |
🐛 fix(agent): accept a typed answer in a multi-select intervention question (#19399)
Submitting an AskUserQuestion card failed with a 500 whenever the user wrote their own answer in a multi-select question. Two separate defects stacked up. The answer was genuinely rejected. The client carries a "write your own" entry as one more string in the multi-select array, and the server marks every AskUser question `allowCustomAnswer: true`, but the multi branch of `hasValidAnswers` whitelists the option ids unconditionally — only the single-select branch below it honors the flag. Any typed answer therefore failed the whole submit, and the user had no way to tell which question was at fault. The multi branch now reads the same flag. The rejection then surfaced as an Internal Server Error. A contract failure is a statement about the request, so map the intervention error codes to their tRPC equivalents at the three resolution procedures: invalid payloads become BAD_REQUEST, lost races and stale revisions CONFLICT, and an unavailable review FORBIDDEN. Unrecognized failures are passed through untouched so a genuine server fault still reads as one. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f4a39da7d9 |
✨ feat(goal): let exploration revise a flawed protocol instead of only branching (#19375)
* ✨ feat(goal): let exploration revise a flawed protocol instead of only branching The exploration planner could only `expand` or `verify`, so when an experiment measured the wrong thing its only move was a sibling that inherited the same flawed instrument. It also had no way to tell which experiments a previous turn authored, so it could not see that its last branch had made things worse. - Add a `revise` action that re-runs one experiment with a corrected protocol inside the parent's container, without consuming an experiment slot. - Bound revisions per target via `derived_from` edges so a planner cannot keep polishing one instrument instead of changing the question. - Pass `derivedFromId` to the planner so it can compare a derived experiment against its parent before repeating that direction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): separate a protocol correction from an ordinary branch Codex review found three ways the revision path and the expansion path were conflated, because both write a `derived_from` edge. - The revision cap counted every `derived_from` edge at the target, so an experiment with two ordinary branches was refused its first correction. Count only edges a task sourced, which is what a correction is. - A standalone correction read back as an experiment: it spent an experiment slot and could then be revised as a fresh target with an empty budget, an unbounded chain. Exclude corrections from the experiment selector on both the planner and the apply side so the two see the same graph. - A dispatched correction resolved its provenance through its container, which carries the container's own branch parent, so the run received the results of an older experiment. Prefer the node's own provenance edge. The lookup moves to `provenanceParentId` in `@lobechat/utils/goalGraph` next to the two new predicates, so the rule is unit-testable rather than inline in a 600-line dispatch path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): keep a correction's evidence and stop a spent allowance pausing the goal Two consequences of the previous commit, both found in review. - Excluding corrections from the experiment selector also hid their results. `experimentResults` only walked containment, so a standalone correction's findings were folded into neither itself nor the experiment it corrected, and the next exploration turn evaluated pre-correction evidence only. Gather an `experimentScope` that includes the corrections aimed at a node, and use it for produced Work versions too. - A spent revision allowance threw, and `exploreGraph` treats a throw as a planner crash: it paused the whole goal, and resuming could reach the same choice again because nothing told the planner the allowance was gone. Return it as an outcome that re-plans, and put `revisionsRemaining` in the planner input so the choice is observable before it is made. `MAX_PROTOCOL_REVISIONS` moves next to the revision predicates in `@lobechat/utils/goalGraph`, since both the model and the planner input need it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): give corrections a container, a released lease and a documented contract Four more from review, all consequences of the correction path being bolted onto a graph shaped for branches. - A spent allowance returned without clearing the checkpoint, so the re-plan it asked for was refused by the next claim until the lease timed out. Release it like every other terminal path. - A correction of an uncontained seed was another root task: invisible to the planner, unable to inherit the previous correction's Work, and read back as a slot-consuming experiment by an older release. Require an experiment container and let those goals expand first, which removes all three at once. Both apply paths now gather `experimentScope`, so a correction's Work carries forward. - `revisionsRemaining` reports zero for a node that cannot hold a correction, rather than inviting a choice the apply step would refuse. - `EXPLORATION.md` documented two actions. It now covers the third, the slot-free guarantee, the per-experiment bound and why corrections are serial. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): record corrections on their own edge kind Review found `derived_from` was the wrong carrier. It is generic provenance that predates this feature, `goal.addEdge` accepts it, `createEdge` puts no task-source restriction on it, and an existing dispatch test authors exactly that shape. Treating any task-sourced one as a correction therefore reinterpreted hand-authored provenance: those tasks vanished from planner history and slot counting while their results were attributed to another node. Corrections now use a dedicated `revises` kind, written only by the coordinator and deliberately absent from the public `addEdge` vocabulary, so the discriminator is unambiguous and nothing existing changes meaning. The column is plain text, so no migration is involved. Also stop replanning on a rejected revision. Returning `advanced` re-invoked the planner with the same graph and the same `revisionsRemaining: 0`, which could spend an advance's whole tick budget repeating one question; the advance now ends and the goal stays running for the next wakeup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 📝 docs(goal): note the dedicated revises edge in the exploration contract Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): park a goal whose planner asks past a spent revision allowance Review found ending only the current advance left the goal running with an unchanged graph, so every sweep could buy another identical planning call indefinitely, and enough such goals would crowd the sweep's bounded scan. The previous round's `advanced` had the same defect inside one advance. Park it instead, the way an exhausted experiment budget already does: `pausedBy: 'exploration_revision_limit'`, a recorded status transition naming the target and the count, and `no_progress` carrying that reason. Resuming replans. The new reason is deliberately not treated as budget-stopped, so raising a budget does not silently reopen a goal that needs a different plan. Also documents the rollback shape for a queued correction: the previous coordinator resolves provenance through the containing experiment, so a rolled back deployment dispatches it with that experiment's branch parent instead of the one it corrects. The corrected protocol lives in the Task description and survives, so the loss is context rather than a wrong instruction; the note says to drain queued corrections first if that matters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |