mirror of
https://github.com/lobehub/lobehub.git
synced 2026-09-20 04:56:13 +08:00
feat/quick-note
852 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
13ac4b04b2 | 🐛 fix: allow deletion of pinned Quick Note context documents | ||
|
|
3cae91db04 | 🗃️ chore(database): sync regenerated Quick Note migration and tests | ||
|
|
c8cb9a1e38 | 🐛 fix(quick-note): serialize proposal input pinning | ||
|
|
c15b05da3b | 🗃️ chore(database): regenerate Quick Note migration metadata | ||
|
|
cb6e9fe772 |
♻️ refactor(quick-note): restructure the note page into three columns
List with filter chips lives in the nav panel, the editor uses the shared rich EditorCanvas, and the right column becomes an AI panel with summary, suggestions, related items, comments and a bottom composer. Collapsed panels can be restored by button and hotkey. |
||
|
|
41e46f056a | ✨ feat(quick-note): add configurable agentic analysis | ||
|
|
9e325d3ed2 | ✨ feat(quick-note): add agentic processing pipeline | ||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
e85ae51880 |
✨ feat(openapi): add eval read routes for runs, datasets and topics (#19423)
* 🐛 fix(openapi): exempt heterogeneous relay routes from spec parity `POST /api/v1/anthropic/v1/messages` and `POST /api/v1/openai/v1/responses` are registered without `describeRoute`, so the generator's parity check has been failing since #18558 and `openapi.yml` could no longer be regenerated. Both relays speak the upstream Anthropic/OpenAI wire formats and are gated by `requireHeteroModelInvocation` (operation JWT), so no API key holder can reach them and their schemas are not LobeHub's to publish. Exempt them the same way the documentation-serving routes already are, instead of describing endpoints that are not part of the public API surface. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HmyR6qUiKJX9VteSJR1hiu * ✨ feat(openapi): add eval read routes for runs, datasets and topics `/api/v1/eval` could create a run and read that one run back, but offered no way to discover what already exists, so API consumers had to know a run id out of band. Add the four missing read endpoints: - `GET /eval/runs` (paginated, filterable by dataset and status) - `GET /eval/datasets` - `GET /eval/datasets/:id` - `GET /eval/runs/:id/topics` All four reuse the domain's existing posture: `AGENT_READ` RBAC, mandatory pagination, and public-field allowlists so internal columns never reach the response. Cross-tenant reads stay 404 rather than 403 so the endpoints do not confirm the existence of other tenants' rows. The models gain only the read primitives the routes need (count + paginated list); no schema or migration changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HmyR6qUiKJX9VteSJR1hiu --------- Co-authored-by: Claude Opus 5 (1M context) <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'. |
||
|
|
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> |
||
|
|
ad0b410f15 |
🗃️ feat(acceptance): a table for collaboration on an acceptance (#19392)
* 🗃️ feat(acceptance): a table for collaboration on an acceptance The first of three layers behind commenting on an acceptance: the schema, the model, and the shared types. Nothing calls them yet — the server and the UI arrive in the PRs stacked on this one — so this can land, or be reverted, on its own. One table carries every human row on an acceptance: a remark in the discussion, a region circled on an evidence image, a reply, a reviewer's approval, an emoji, and the note an agent ships a round with. They differ by `kind` and `anchor_type` rather than by table, because they share a thread shape, an author, permissions and deletion, and splitting them would have meant maintaining that four times. Two things the shape buys, both load-bearing: `evidence_id` and `context_run_id` are separate columns. Someone reading round 4 may circle a screenshot round 4 inherited from round 2; both facts decide whether that note is still about something real. `(acceptance_id, author_user_id, client_id)` is unique, so every write is idempotent from a caller-derived key and no path needs a read before it writes — a reaction, an agent's round note and a retried post all rely on it. Ids are twelve characters rather than a uuid: this one is the addressable part of a comment's URL, and 62^12 leaves a one-in-a-billion collision chance at ~2.5M comments, against a primary key that would turn one into a failed insert rather than two rows sharing an address. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(acceptance): regenerate the comment migration metadata and harden deletion Folding three draft migrations into one edited the SQL by hand and left the generated artifacts describing the pre-fold shape: the snapshot and the DBML still recorded a uuid `id` with a database default, a uuid `parent_comment_id`, and no `author_agent_id` / `editor_data` / `attachments`. The next generated migration would have read that as its baseline and tried to convert types and re-add columns on databases where 0161 already created the final shape. Deleted the draft artifacts and regenerated from the schema; the SQL comes back identical apart from index order, so only the metadata was ever wrong. Two fixes in the deletion path: - A tombstoned root cleared only `content`, leaving the editor tree and the attached files readable through the list endpoint. Clear all three. - Deleting the last two replies at once left the empty tombstone standing forever, because each transaction locked only its own reply and counted the other's uncommitted sibling. Lock the thread root first, in a fixed root- before-child order that also rules out a deadlock with a root deletion. The ordering test spaced its rows apart in time: five writes in a loop can share a microsecond, and the tie then breaks on a random id. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- 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> |
||
|
|
d26542b423 |
✨ feat(acceptance): let an author delete a flow the delivery no longer follows (#19388)
A published flow was permanent. Authoring a journey takes a few tries, and every try stayed on the acceptance forever: an abandoned first draft still rendered as its own root graph carrying its own never-executed checks, so the checklist filled with rows nobody would ever verify and the reader could not tell which graph the delivery was actually being checked against. `lh acceptance flow delete <acceptanceId> --flow <flowId>` removes one, and only while it has no verified history. A settled round renders from the snapshot it froze, so deleting a graph it ran would outlive the map explaining its results; that is refused, as is deleting a graph another flow invokes as a subflow. A draft round has nothing to preserve, so the flow's snapshot and plan items are stripped from it and the remaining items re-indexed, leaving the ledger as if the flow had never been planned. The check assets the nodes pointed at are reusable on their own and stay. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
aacb88fba7 |
✨ feat(heterogeneous-agent): support Devin CLI as a heterogeneous agent (#18554)
* ✨ feat(heterogeneous-agent): support Devin CLI * 🐛 fix(heterogeneous-agent): reuse cached Devin authentication * ✨ feat(heterogeneous-agent,cli): default Devin ACP to bypass permission mode - Add `permissionMode` to `DevinAcpSessionOptions` and `SpawnAgentOptions`. - Update `buildDevinAcpArgs` to place `--permission-mode` before the `acp` subcommand, since it is a global `devin` flag. - Default the CLI `lh hetero exec` to start Devin with `permissionMode: 'bypass'` so headless connected-device runs avoid blocking on permission prompts. - Support user-supplied `--permission-mode` in `extraArgs`, which takes precedence and is moved to the correct position. * ✨ feat(heterogeneous-agent): set Devin ACP session mode via session/set_config_option The `--permission-mode` global flag only affects the Devin CLI process; the ACP session's mode still defaults to `accept-edits`, so shell/edit tools still trigger `session/request_permission` and show askUserQuestion. After `session/new` or `session/load`, call `session/set_config_option` with `configId: 'mode'` and the resolved permission mode to actually put the ACP session into bypass (or user-override) mode. The resolved mode is derived from the built argv so an explicit `--permission-mode` in extra args still wins over the default. Add regression tests for new sessions, resumed sessions, and user override. * ✨ feat(heterogeneous-agent): apply Devin ACP initial model via session/set_config_option Previously `initialModel` was only used as a fallback when `session/new` returned no model. Devin's `session/new` always reports a default model (e.g. `swe-1-7-medium`), so a user-selected `glm-5-2` was ignored and the default was used. Add `applyInitialModel()` to resolve the requested model against the `configOptions` catalog and call `session/set_config_option` with `configId: 'model'`. It matches by raw model value and by normalized human-readable name (so 'glm 5.2' maps to 'glm-5-2'). When only a raw `initialModel` is provided and the catalog is unavailable (e.g. resume), it attempts to set the value directly. Also preserve the updated model after setting the permission mode so the final `devin_session` event reports the selected model. Add regression tests for model selection, label matching, default preservation, and combined permission mode + model setup. Update the `spawnAgent` fake to expose a 'sonnet' option for the new behavior. * ♻️ refactor(heterogeneous-agent): stop injecting --permission-mode bypass CLI flag Devin ACP permission mode is now applied through `session/set_config_option` (configId: 'mode'), which is the only reliable way to change the active ACP session mode. The global `--permission-mode` CLI flag before the `acp` subcommand does not change the ACP session's effective mode. - `buildDevinAcpArgs` no longer injects a default `--permission-mode`. - It still moves a user-supplied `--permission-mode` in `extraArgs` before the `acp` subcommand for backwards compatibility. - `DevinAcpSession.resolvedPermissionMode` falls back to `options.permissionMode` so the default CLI `bypass` is still sent via ACP. - Updated tests to expect `acp` without the default flag and assert the `session/set_config_option` mode request. * fix(devin): do not default to bypass permission mode and protect selected model from mode response - Remove the hard-coded `permissionMode: 'bypass'` default for Devin in `lh hetero exec`. Users can still pass `--permission-mode` via `--agent-arg` when needed. - Stop using the `session/set_config_option` mode response to overwrite the model resolved from `applyInitialModel`. Permission-mode negotiation can return config-options whose `model` value is stale/default, causing a freshly selected model (e.g. `glm-5-2`) to be replaced with the Devin default (`swe-1-7`) on `stream_start`. - Update CLI tests to no longer expect the bypass default. Refs LOBE-xxx (feat/devin-cli). * fix(devin): keep default bypass permission mode while protecting selected model Re-introduce the default `permissionMode: 'bypass'` for Devin ACP so headless connected-device runs still do not block on permission prompts. The previous change that removed it was too aggressive; the real bug is that the `session/set_config_option` mode response was overwriting the selected model. That guard in `DevinAcpSession` remains in place. Refs LOBE-xxx (feat/devin-cli). * ✨ feat(heterogeneous-agents): capture and emit Devin ACP usage as turn_metadata - Forward `session/prompt` result `usage` from `DevinAcpSession` into the `devin_prompt_completed` synthetic event. - Add generic ACP `Usage` parsing to `TraeAcpAdapter`, plus `usage_update` handling with a protected `extractUsageFromUsageUpdate` hook. - Override `extractUsageFromUsageUpdate` in `DevinAcpAdapter` to parse `_meta.cognition.ai/{input,output,cachedRead,cachedWrite,thought}Tokens`. - Emit `step_complete` with `phase: 'turn_metadata'` so the frontend reducer writes `usage` (and optional `cost`) onto the assistant message. - Add `UsageData.cost` to mirror `MessageMetadata.usage`. - Add `devinAcp.test.ts` covering prompt-result usage, `usage_update` _meta, and fallback paths. * ✅ test(heterogeneous-agents): add missing request params type in spawnAgent test fixture * 🐛 fix(heterogeneous-agent): cancel device-side CLI process on user stop Previously only remote-task hetero types (openclaw / hermes) triggered a `cancelHeteroTask` dispatch to the device when the user clicked Stop. Local-cli types (claude-code / codex / devin / trae / qoder) that also run on a remote device via `dispatchAgentRun` were silently skipped — the UI showed cancelled but the CLI process kept running. Two fixes: - Server `interruptTask`: drop the `isRemoteHeterogeneousType` guard so any hetero type with a deviceId in runningOperation triggers the cancel dispatch. - Desktop `spawnLhHeteroExec`: register the spawned child process into `platformTasks` via a new `onChildSpawned` callback so `cancelHeteroTask` can find and kill it by operationId. Also add `detached: true` so the process-group kill reaches the CLI without signalling the desktop app. * 🐛 fix(cli): cancel lh connect device CLI process on user stop The previous cancel fix only covered the desktop device path. When a local CLI agent (devin/claude-code/codex/…) is dispatched through `lh connect`, the spawned `lh hetero exec` child was never registered into the task registry, so `cancelHeteroTask` dispatched from the server returned "No task found" and the remote process kept running. - `agentRun.ts`: spawn with `detached: true` and register the child PID into `taskRegistry` on spawn, keyed by operationId. Remove the entry on exit only if the exiting PID still owns the task. - `taskRegistry.ts`: widen `TaskEntry.agentType` from `'hermes' | 'openclaw'` to `string` so local CLI agent types can be persisted. - `heteroTask.ts`: `cancelHeteroTask` now signals the whole process group via negative-PID kill (Unix) or `taskkill /T /F` (Windows), with a 2s SIGINT→SIGKILL escalation — mirroring the desktop path. * 🐛 fix(cli): add cancel-trace logging to diagnose device CLI stop path Add [cancel-trace] logs at every hop of the cancel path: - server interruptTask: logs when dispatching cancelHeteroTask, when condition is not met (with runningOp/targetOperation details), and the dispatch result/error - agentRun.ts: logs when task is registered into taskRegistry, and when it is removed on exit (or skipped due to stale PID) - heteroTask.ts cancelHeteroTask: logs when received, when task is found or not found, and each kill attempt * 🐛 fix(server): seed deviceId/heteroType into runningOperation for local CLI hetero agents Local CLI hetero agents (devin / claude-code / codex / …) dispatch to a device via `dispatchAgentRun`, but `childOperation` only embedded `deviceId`/`heteroType` for remote platform agents (openclaw / hermes) guarded by `isRemoteHetero`. Without these fields in `topic.metadata.runningOperation`, `interruptTask` could not find the target operation and never dispatched `cancelHeteroTask` — so clicking Stop in the web UI did nothing for `lh connect` devices. Patch `runningOperation` with the resolved `dispatchDeviceId` and `heteroType` after a successful local CLI hetero dispatch, handling both root and child (group supervisor) operation shapes. * 🔥 chore: remove [cancel-trace] debug logs Clean up the temporary diagnostic logs added to trace the cancel path. Keep only the production-level `interruptTask` and `execAgent` logs that were already present or are useful for long-term observability. * 🐛 fix(heterogeneous-agent): preserve cancel lifecycle state * ♻️ refactor(heterogeneous-agent): simplify Devin ACP integration * 🐛 fix Devin intervention review flow * 🐛 fix heterogeneous agent cancellation * 🐛 fix heterogeneous agent cancellation confirmation * 🐛 fix(heterogeneous-agent): preserve Devin permission option IDs * 🐛 fix: resolve CI failures after rebase * 🐛 fix: route Devin verification and permissions * 🧪 test(cli): isolate agent run cancellation registry * 🐛 fix(cli): cancel full live agent process group |
||
|
|
c17b9a9388 |
🐛 fix(acceptance): keep flow zoom on node click and stop draft rounds from inflating (#19350)
* 🐛 fix(acceptance): keep flow zoom on node click and stop draft rounds from inflating Clicking a node opens the step panel beside the canvas. The narrower canvas made the flow graph refit, discarding the zoom the user had just set. The viewport is now left alone, and the picked node is panned back at the same zoom only when the panel would hide it. An unexecuted round also forked a new version on every edit: `publish` left the planned round's snapshot behind, `flow plan` inserted another round, and `ingest` chained yet another one, so a ledger could reach round 4 before a single check had run. A round now stays a draft until its first result: `publish` refreshes every draft round in place, `flow plan` reuses the open draft, and `attachRun` folds a detached harness run into it. Both round selectors label a draft "Draft" instead of giving it a number, and the acceptance skill documents the same contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(acceptance): fold only into the newest round and keep a replay pinned Review follow-up. `attachRun` searched the whole ascending chain for a draft, so an older draft left behind by a `--from-run` replay absorbed an incoming run even after a newer round had executed. Only the newest round is the open draft now; anything behind it is an abandoned ledger position and a new round is chained instead. A replay is also excluded from the draft paths. It is pinned to the definition it replays, so refreshing it on the next `publish` would have silently swapped in the live graph and destroyed what the replay exists to re-run. `flow plan --from-run` records `metadata.replayOfRunId`, and the shared `isDraftVerifyRun` predicate treats such a round as numbered rather than open. Also restores the import order in the checklist inventory, which failed lint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
83c5dbadfd |
✨ feat(goal): orchestrate durable goal planning through main agent (#19277)
* ✨ feat: add opt-in goal supervision for interrupted tasks * ✨ feat(goal): give supervisor scoped investigation and recovery tools * 🐛 fix(goal): confirm device execution exit before releasing tasks * ✨ feat: run Goal planning through a durable CLI main Agent * 🐛 fix: carry Task review feedback into Goal planning * 🐛 fix: retain main agent verification handoff in final goal task * 🐛 fix(goal): bind main agent planning to the goal creator * ✨ feat(goal): expose main agent execution history * 💄 style(goal): label main agent entry as supervision progress * 🐛 fix(goal): open supervision in the collapsed conversation panel * 🐛 fix(goal): address operation auth and supervision review findings * 🐛 fix(goal): show read-only supervision in the goal language * 💄 style(goal): remove manager planning summary row * 🚨 fix(test): drop unused userEvent import in ApiKey test Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
065cc132d8 |
🐛 fix(goal): prevent duplicate decomposition with planning leases (#19307)
* 🐛 fix(goal): fence duplicate decomposition with planning leases * 🐛 fix(goal): protect planning leases and guard legacy takeover |
||
|
|
c240cf5943 |
🐛 fix(acceptance): order check occurrences along flow paths (#19319)
🐛 fix(acceptance): order check occurrences by traversed edges
|
||
|
|
e580b9f85a |
🐛 fix(acceptance): preserve check history when regrouping (#19306)
* 🐛 fix(acceptance): group plans by business tasks and follow flow order * 🐛 fix(acceptance): preserve check identity and history when regrouping |
||
|
|
13ada65ad9 | ✨ feat(fts-search): safe per-entity Elasticsearch mapping migrations (#19186) | ||
|
|
9a71b5e0d4 | ♻️ refactor: organize acceptance viewer and improve mobile flow review (#19290) | ||
|
|
812b39fa8e |
🐛 fix(work): let users remove orphaned document works from the card (#19275)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
e7c0a831f7 | ✨ feat(goal): add set-agent and restart commands (#19181) | ||
|
|
2c29ff0e44 |
✨ feat: add reusable check assets and grouped acceptance flows (#19268)
* ✨ feat: add reusable acceptance user journey flows * 📝 docs: define reusable check asset schema * ♻️ refactor: persist acceptance checks as reusable assets * 📝 docs: remove acceptance design markdown * ♻️ refactor: simplify flow definitions and isolate multi-flow navigation * ✨ feat: compose acceptance flows in a grouped canvas * 🐛 fix: address acceptance flow review and CI failures * 📝 docs: clarify flow completion requirements |
||
|
|
8fbb091050 |
✨ feat(task): record assignment, status, priority and automation changes in the activity feed (#19258)
* ✨ feat(task): record assignment changes in the task activity feed Assigning a task to a member or an agent only rewrote a column on `tasks`, so the detail page's activity feed had no way to show who moved it or when — a reader saw "created the task" and nothing else, however many times the task had changed hands. Every write path already funnels through the `task.update` procedure (the member/agent pickers, the `editTask` builtin, the CLI), so the diff between the pre- and post-update assignee columns is taken there and appended to `task_activities`. Only a slot that actually moved is written, so re-saving the same assignee stays silent. Unlike the assignment notification beside it, the write is awaited rather than deferred: the client refetches the detail as soon as the mutation resolves, and a row written after the response would miss that refetch. `updateSchema` gains an `actorAgentId` used solely for attribution, so an agent reassigning through its tool is recorded as the agent rather than the human whose session it borrowed. On the render side `ActivityAuthor` is extracted so the actor and the assignment target share one avatar/name/tag treatment, and `AssignmentRow` reads as "<actor> assigned the task to <target>" / "<actor> removed the agent". A target whose account no longer resolves keeps a stub author and renders as deleted, because collapsing it to null would turn a reassignment into a removal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P * 🌐 chore(i18n): sync generated locales Runs `bun run i18n` so the 16 generated locales pick up the task assignment activity strings instead of falling back to English until the daily workflow catches up. The run also flushes locale drift that had accumulated for other namespaces (auth / device / modelProvider / models / providers) and realigns two hand-edited en-US / zh-CN values with their source in `packages/locales/src/default/chat.ts`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P * 🔒 fix(task): make assignment attribution unforgeable and atomic Addresses three findings from review on the assignment activity log. **Attribution came from the request payload.** `updateSchema.actorAgentId` was public input, so any authorized client could name an arbitrary agent and have the durable feed claim that agent made the reassignment — while suppressing the real user. The field is gone; the agent now rides `AuthContext.actingAgentId`, set only by a server-side `createCaller` from an already-authorized run context, following the contract `spendOrigin` already established. **The diff was read outside the write.** The previous assignee was read by the router before the update and recorded after it, so concurrent reassignments of one task could both observe the same origin: a racing A→B and A→C persist A→B then B→C while the log claims A→B and A→C, losing the middle state, and the rows could be ordered differently from the writes. `updateWithAssignmentLog` now derives the previous value from a `FOR UPDATE` read and appends the events in the same transaction as the update. **One writer bypassed the recorder.** `TaskRunnerService` persists the inbox agent when an unassigned task first runs, straight through `taskModel.update`. The assignee chip visibly changed with nothing in the feed to explain it; that write now goes through the same path, with neither actor column set since nobody requested it — the feed renders those as the system. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P * 🌐 fix(task): make assignment attribution reach the router, and let locales order the sentence Round-2 review follow-ups on the assignment activity log. **The attribution fix was inert.** The caller built with `actingAgentId` in the runtime factory is replaced by `ensureModels()` before any exported method runs, and the replacement omitted it — so every agent-driven reassignment was still recorded as the session user. The previous test missed it by building `createTaskRuntime` directly with a ready-made caller, never exercising the registration lifecycle; the new test drives `taskRuntime.factory` and asserts on the caller the wrapped method actually uses. **The row could not be translated.** Actor, verb and target were three DOM nodes in a fixed order, so a verb-final language could not put the target before the verb — Japanese rendered as a complete sentence with the target stranded after it. The four verb strings are now full sentence templates with `<actor/>` / `<target/>` placeholders rendered through `<Trans>`, so each locale decides where the participants go. The generated locales had to be dropped and retranslated: the keys kept their old fragment values otherwise, since the script only fills in missing ones. **The agent prompt claimed false provenance.** An actorless row is the runner's system fallback, which the feed labels "System" but the prompt rendered as "someone". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P * 🐛 fix(task): tell absent, invisible and unnamed participants apart Round-3 review follow-ups. All three findings come from the same mistake: collapsing distinct identity states onto one fallback. **An invisible agent was credited to the system.** `getAgentAvatarsByIds` filters by `ownership()`, so another member's private agent does not resolve for the current viewer. The actor lookup returned `undefined`, and both the row and the prompt reserve an absent author for the system — so an agent's reassignment was durably shown as a system action. The actor now keeps a stub carrying `unresolved: true`, as the assignment target already did. **A live participant could borrow a deletion label.** `users.full_name` and `agents.title` are both nullable, so a resolved row with an empty name fell through `||` onto "System" or "a deleted agent". Display names now fall back to the other column each query already returns (`title || name`, `fullName || username`), and a resolved-but-nameless participant renders as "Unnamed" rather than as gone. **Two formatters dropped the event.** The `viewTask` tool prompt and the CLI task detail have their own activity branches and neither knew about assignments, so the event vanished on those surfaces. Both render it now, through a shared `assignmentParticipantLabel` on the prompt side; the CLI keeps a three-line local copy rather than take a dependency on `@lobechat/prompts`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P * ⚡️ perf(task): show the assignment activity row in the same beat as the chip Picking a new assignee flipped the chip instantly (that dispatch was already optimistic) but the activity row explaining it only appeared after two serial round-trips: the mutation, then the full task-detail refetch — a fifteen-step aggregation that alone runs one to three seconds against a remote database. The chip said "done" while the feed stayed silent. The row is now synthesized in the same optimistic dispatch. The picker hands over the chosen item's display metadata, since it is the one place that already holds it — the store deliberately does not resolve members itself, that source being a business-layer hook. The actor is the signed-in user; with no known identity no row is invented. Only a slot that actually changes produces a row, mirroring the server's own rule, and the refetch that follows the mutation replaces the whole activity array, which retires the synthetic rows with no reconciliation. A failed mutation is rolled back by that same refetch. Comments share the identical two-round-trip lag and are left as they were. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P * ⚡️ perf(task): show a sent comment in the feed before the round-trip returns Comments had the same two-round-trip lag the assignment row just lost: the post, then the full task-detail refetch, before anything appeared. Sending now synthesizes the row in the same dispatch, authored as the signed-in user and carrying the topic it replies to, so it lands in the feed the moment the button is pressed. Attachments are deliberately left to the refetch — their metadata only exists once the server has resolved the file ids. A comment posted on an agent's behalf through the client-first runtime is also left to the refetch, since the store has no agent identity to attribute it to. While the synthesized row is on screen its edit / delete menu is withheld — the server has never seen that id, so either action would only surface an error. A failed post is rolled back by the same server-truth refetch the assignment path uses, and the failure still reaches the caller. The two builders now share one module and one id prefix, so a renderer has a single predicate for "not yet persisted". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P * ✨ feat(task): log status, priority and automation changes made by a person The activity feed now explains every property a person can change from the task's property bar, not only the assignee. A status pick, a priority pick and turning automation on or off each leave a row: "<actor> changed the status to <tag>", "set the priority to <tag>", "set automation to <summary>". Only person-made changes are recorded — those made from the pickers or by an agent acting through its tool. The runner, lifecycle and watchdog transitions (backlog → running → completed) are deliberately not: the run row already carries them, and a duplicate line per run would be noise. The policy is not a flag; it is which writer a caller uses. Person-made edits go through the new `TaskModel.updateWithLog`, the system transitions keep the plain writers. `updateWithLog` replaces the per-property methods with one transactional diff over a tracked column set (assignees, status, priority and the four automation columns), so adding a tracked field is one list entry. The four automation columns fold into a single event carrying before / after snapshots: switching a schedule on rewrites mode, pattern and timezone in one save, and that is one decision, not three. All three task-detail formatters (page prompt, `viewTask` tool prompt, CLI) render the new event, through a shared `formatPropertyValue` on the prompt side. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P * ✨ feat(task): fold a burst of edits to one property into a single feed entry Fiddling with a property — priority High → Low → High inside a minute — produced one line per hop, where an issue tracker shows "moved from A to C" and shows nothing when the value ended up back where it started. Collapsing happens at read time, in the detail assembly, so the rows in `task_activities` stay append-only: the hop-by-hop history is still there for audit or a "show all" view, and every consumer of the detail (page, `viewTask` prompt, CLI) gets the same folded view. A run is one property by one person with no gap over the window; it keeps the newest row's time (when it settled), the first row's starting value and the last row's ending value. Two people never merge — A moving it and B moving it back are two decisions, not a cancelled one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P * 💄 style(task): say where a property moved from, not only where it landed "changed the status to In Review" hides the half of the story a reader actually wants once a burst of edits has been folded into one entry: an entry can now span several hops, and the starting value is what says how far the property moved. Status, priority and automation rows read "from A to B"; automation keeps its one-sided forms for turning on ("set automation to") and off, since there is no other side to name. The two reworded keys had to be deleted from the generated locales and retranslated — the script only fills in missing keys. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P * 🐛 fix(task): surface every person-made property change in the feed at once Three findings from driving the real UI: **Automation changes never reached the feed.** `setAutomationMode` deliberately does no post-save refetch — an async SWR write could land after the user's next toggle and clobber it — so the row the server appended only showed up on the next unrelated refresh. The row now rides the same optimistic patch as the automation fields, inside the same engine transaction, so a failed save rolls it back with them. **Status and priority rows arrived a round-trip late.** Only assignment and comment rows were synthesized; the other two waited for the detail refetch, which against a remote database is the same lag the assignee row was fixed for. Both now ride the optimistic dispatch that flips their chip. **Priority read as "from ··· to !".** The priority tag alone is an icon; in a sentence it is noise. The level's name now rides along. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P * 🐛 fix(task): attribute client-runtime edits, tighten activity collapse and rollback - update/updateStatus accept an ownership-checked actorAgentId for the client-first runtime; the server-side actingAgentId still wins - collapse only extends a run when each hop starts where the last ended - optimistic property rows fold client-side; schedule edits get a row - addComment drops its synthesized row locally when the send fails - payload.actorKind keeps a deleted actor from reading as the system - task detail reads a bounded window of recent activity rows Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P * 🐛 fix(task): log cascades and rolled-back starts, quieter activity rows - collapse: any edit by someone else ends other actors' open runs - updateStatusCascade logs one status row per family member the confirming person cascaded; the router passes the actor - a person-made start rolled back after the heartbeat tick fails logs the undo too, so no orphan row survives the restore - activity rows read like an issue tracker's: 16px mark, plain names, values as words instead of chips, agent chip dropped Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P * 🐛 fix(task): lock cascade origins, batch their rows, widen the schedule value - cascade reads each task's leaving status under the row lock and writes the family's activity rows in one INSERT - the automation value includes the execution cap and its labels show timezone and cap, so a cap-only or timezone-only edit is visible - a failed status transition drops its synthesized row locally before the best-effort refetch Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P * 💄 style(task): lay the activity feed out as a timeline Compact rows lead with a type mark on a shared rail instead of the actor's avatar, read as one sentence in one tone, and sit tight together; cards still break the rail. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P * 💄 style(task): faces for people rows, property icons for property rows The rail mark is the actor's avatar on created / assignment lines and the property's icon (three-bar priority, status dot, timer) on change lines. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P * 💄 style(task): priority rows use the app's solid priority bars Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P * 💄 style(task): centre the activity rail under the cards' avatar column Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c04405343a |
✨ feat(goal): add nested experiment answers and exploration navigation (#19261)
* ✨ feat(goal): add nested experiment answers and exploration navigation * 📝 docs(goal): correct exploration budget command * 🐛 fix(cli): isolate dispatched agent run identity * 🐛 fix: register CLI notebook reports as operation deliverables * 🐛 fix: avoid signing notebook origins as file URLs * 🐛 fix(cli): retain bounded event backlog across temporary outages |
||
|
|
ad6bb4d58a |
🗃️ feat(db): add task_activities event log for assignment changes (#19217)
Reassigning a task only rewrites a column on `tasks`, so nothing records who moved it or when. Add an append-only event table alongside the other task-child tables: one row per change, carrying the actor (user or agent) and the ids the slot moved between. `type` is a plain text column typed through `.$type<>()`, so onboarding a new event kind — status, priority, … — is a type-only change rather than a migration. `visibility` mirrors the parent task the same way `task_topics` / `task_comments` do, so ownership filters without joining back to `tasks`, and the public → private demotion cascade covers it too. Claude-Session: https://claude.ai/code/session_015wKZMihFpqsChKSb5YR23P Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2f2e939df6 |
⚡️ perf: serve dashboard message totals from a planner estimate instead of a full COUNT (#19223)
* ⚡️ perf: serve dashboard message totals from a planner estimate instead of a full COUNT An exact COUNT over messages heap-fetches every row the user owns — tens of seconds for accounts with 100k+ messages — while the stats cards only need an order-of-magnitude total. MessageModel.countApproximate asks the planner for a row estimate first (EXPLAIN, a few ms); clearly heavy accounts get the estimate rounded to 3 significant digits, everyone else gets an exact count capped at 10k rows. message.count gains an opt-in approximate flag used by the dashboard totals; today's delta stays exact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🐛 fix: address Codex review — log estimate failures, keep growth baseline exact The estimate path's catch no longer swallows errors silently: a driver or EXPLAIN regression would otherwise pin every past-cap account at cap + 1 with no signal. The stats card derives the last-month baseline as total minus an exact count of this month's messages instead of a second planner estimate — a user x date predicate has no multicolumn statistics, so estimating it directly can turn the displayed growth percentage into noise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
14f4cbff28 |
🐛 fix: prevent personal conversations leaking into team recent topics (#19239)
* 🐛 fix: respect agent visibility in recent topics * ✅ test: verify personal topic isolation from team recent feed |
||
|
|
5bf3a37e37 |
✨ feat(goal): north-star metrics strip on the goal detail header (#19206)
* ✨ feat(goal): north-star metrics strip on the goal detail header The goal header's six metrics are all execution metrics — status, tasks, findings, budget, duration, liveness — they say what the run cost, not how far the world is from the declared target. A goal like "grow to 10,000 followers" or "open security issues down to 0" had no surface tracking the number itself. The strip renders one card per measured acceptance clause (config.acceptance.metrics, #19133), joined with its metric series (#19063/#19071): current value vs target, progress normalized from the first observation as the baseline (a count-down clause reads 67% cleared, not a division by its zero target), sparkline trend, and observation freshness — data older than 48h renders as a warning, because a tracking surface is only as trustworthy as its data is fresh. A met clause turns the card green. The strip and the coordinator's measured gate read the same clauses, so they can never disagree about what is being tracked. Declaring and measuring close the loop from the UI: an empty goal shows a guidance row with a declare modal (key / condition / target → goal.setMetricCriteria), and each unmet card offers a record-observation modal (goal.recordObservation) — the server schedules an advance when the observation clears the gate, so recording the finishing number is enough to move the goal. Zero backend changes: clauses, series, points, observation writes and the gate all landed in #19063/#19071/#19133/#19151. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 💄 style(goal): apply north-star review feedback from acceptance r1 Five items from the review round on acceptance 17679ed4: - Move the strip beside the requirement document (C3): the measured clauses are half of the acceptance contract, so they read with it — not squeezed between the title and the execution metrics. - Declare modal gains an optional display name (C2): the key stays the series address, the title is what tracking surfaces show. Carried on the clause (`GoalMetricCriterion.title`, accepted by both router schemas) and threaded into the first observation so the series inherits it. - Card background goes white (C4): `colorBgContainer` over the tinted fill. - Progress reads blue while climbing (C5): bar and sparkline use `colorInfo`; green stays reserved for the met state. - Met state loses the green slab (C7): no tinted background/border — the ✓ tag, value, bar and sparkline carry the success signal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🔥 chore: drop stray local goal-tracing artifact Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🐛 fix(goal): make the north-star strip scale-consistent, bounded, and honest Codex review on #19206 — two P1s and four P2s, all confirmed. Client/server verdict split (P1). The card compared raw values while the server's measured gate compares both operands at the persisted numeric(20, 6) scale, so a target like `eq 0.1234567` could keep a card unmet forever while the goal had already advanced. The card now rounds both operands with the same `toMetricScale` the gate uses. Poll amplification (P1). Every five-second refresh listed every series on the goal and then issued one listPoints RPC per series — 1+N requests and ~3N queries for series that cannot even affect the view. A new `metric.listSeriesWithPoints` returns the whole read in one RPC over K+2 bounded queries, restricted to the declared keys the caller names; the goal store passes exactly those. Windowed baseline (P2). Progress read its baseline from `points[0]` of a 200-point recent window, silently rebasing once history outgrew it (a 0→95/100 trajectory rendering as 50%). The endpoint carries each series' true first observation separately and the card reads its baseline from that. Met cards kept their record entry (P2): the world can regress, and a card whose only refresh path disappeared on success would stay falsely met forever — the ✓ tag and the + button now coexist. First fetch renders skeletons (P2) instead of confidently claiming "never measured" against an empty join before the request settles. Declaring merges server-side (P2): `setMetricCriteria` gains a `merge` mode that upserts by key against the list as it stands on the server, so two editors declaring from the same stale snapshot no longer erase each other; the declare modal uses it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ✅ test: cover the setMetricCriteria mode argument The router now forwards `mode` as a third service argument, so the exact-args assertion needed it; also locks the merge passthrough the declare path depends on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f7103947cf |
🐛 fix(resource): stop agent artifacts from flooding user resource listings (#19215)
Users reported unfamiliar documents piling up in Resources — agent tool outputs (agent-document://) and skill artifacts were surfacing in the All tab and recent-pages sections because KnowledgeRepo's document arm only excluded sourceType 'file'. - KnowledgeRepo.documentArm now hides 'agent' / 'agent-signal' rows by default; knowledge-base listings opt out so explicitly filed agent docs stay visible - lobe-agent plans now carry sourceType 'agent' instead of 'api', with the workspace default-private visibility made explicit since the 'api' fallback in DocumentModel.create no longer applies - hoist the page source/file-type allowlists into @lobechat/const and drop the dead 'editor' value from SQL filters (client-only stamp) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a20880a125 |
🐛 fix(file): reserve storage before upload (#19119)
* 🐛 fix(file): reserve storage before upload * 🐛 fix(file): gate storage quota on upload paths that skip reservations The reservation flow only covers pre-signed tRPC uploads. Three surfaces write `files` rows without ever consulting the quota: the OpenAPI upload endpoint, server-side image generation, and bot-channel attachment ingest. - OpenAPI upload checks the quota once per path that actually adds bytes, and maps the rejection to a 403 instead of a generic 500. Re-requesting a record the caller already owns adds nothing, so it stays exempt. - Image generation admits at submission rather than at persist — blocking after the model call is already paid for is worse than not blocking. - Attachment ingest charges the post-compression buffer, and a quota skip now says so instead of leaving the sender to guess. Claude-Session: https://claude.ai/code/session_014iyxmqssnrPopmKV1xKJ7k * 🐛 fix(file): release reservations on every dataset import failure Claude-Session: https://claude.ai/code/session_01Xmacr8RYT7rZ3zmjZrrENm * 🐛 fix(file): stop reservations from deleting pre-existing objects Reject a reservation whose pathname already holds an object, so aborting one can only ever delete a key it created. Serialize server-owned uploads with the owner-row lock by threading the transaction through the quota check and the file row it authorizes. Claude-Session: https://claude.ai/code/session_01Xmacr8RYT7rZ3zmjZrrENm * 🐛 fix(file): keep the S3 write out of the quota transaction Holding the owner-row lock across the upload blocked every other write to that user for the length of the transfer, and Postgres kills the transaction outright wherever DATABASE_STATEMENT_TIMEOUT is set, orphaning the object it had already written. Check the quota once before spending the transfer, upload outside any transaction, then re-check under the lock the file row is written with. Claude-Session: https://claude.ai/code/session_01Xmacr8RYT7rZ3zmjZrrENm |
||
|
|
7cbe9fdb4d |
✨ feat(topic): pin reasoning effort per topic, add default effort on agent profile (#19190)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
36ca481bf0 |
✨ feat(agent-share): share settings tabs, /a/:slug visitor page with product bar and editor (#19180)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
002a70c41d |
✨ feat(goal): surface each task's verification, and make its result readable (#19171)
* ✨ feat(goal): show each task's own verification on the goal Every dispatched task already owns an Acceptance, but the goal surfaced only the standard it would be judged against — never the judgment. A reader could see that a task finished and still have no way to tell whether it held up, which is what left the page feeling unverifiable. The graph read now carries each task node's acceptance state, and the frontier row wears it as a chip that opens the acceptance — its checks and its evidence — in the side Portal, like every other drill-down on the page. Only statuses a reader acts on get a chip: a settled judgment, a rejection, a delivery waiting on them, or a verification that broke. `pending` / `planned` say nothing yet, and `verifying` / `repairing` are already what the row's own state chip says. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(task): stop the acceptance contract burying the checks The 验收目标 block printed the requirement in full. For a goal-dispatched task that is a generated paragraph, so opening a task result met a wall of contract prose and pushed the checklist — what the reader actually came for — below the fold. Clamp it to two lines, expandable on click. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(task): let the run result be read, and give it a way to ask Opening a task result met the delivery clamped to ~160px behind a "show all", even though the card had already been expanded to reveal it — the content the panel exists to show was the one thing collapsed twice. The newest run in the result view now renders its delivery in full; older runs keep the clamp, since there the list is the point. The run's own conversation was reachable only by clicking the card title, which gave no sign it was a door. Asking a follow-up is the natural next move after reading a delivery, so it gets a real action next to the comment one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(goal): reach the acceptance while it runs, and report the result flat Two review findings. The verification chip skipped `verifying` / `repairing` on the reasoning that the row's own state chip already named them. That was backwards: the label is inert, and while the judgment is running is exactly when a reader wants to look into it — so the one state where the acceptance matters most offered no way to reach it. Those statuses now carry the chip, and it subsumes the plain label rather than sitting beside it. The primary run result was still a boxed activity row: an outline, inner padding, and a grey summary line repeating the title above the body it summarised. It is not one card among many — it is the agent reporting what the task produced, so it now reads as a document. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(task): a dedicated report component, and give it the width to be read The result panel still reused the activity card and only stripped its outline. Everything that made it a list row was still there — avatar, attempt number, trigger chip, duration, status glyph, collapse chevron, overflow menu — and all of it answers "which row is this", which the panel header already answers. `TaskRunReport` replaces it for the newest run: the delivery, an attribution line, and the two actions that follow reading one. Older runs keep the card, collapsed, because there the list IS the point. The goal pane now collapses while a drill-down is open instead of splitting the width with it. Sharing it left neither surface usable — the goal's title wrapped to four lines and its task rows truncated to a few characters, while the report got half a window. The Portal's own close brings the goal straight back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(task): the report leads with its author, and the two verify signals agree Review of the report component: the "运行结果" band above it labels a section of one and eats the top of the reading surface; the author belongs at the head of a report, not as a footnote under the text it wrote; and the lifecycle row ("created the task") is timeline bookkeeping standing between the reader and the delivery. All three are gone, and the acceptance section below is reachable again without them in the way. The goal pane no longer collapses on drill-down. Hiding it took the task's own verification state off screen — the very thing the reader was checking — so the split stays and the width question stays open. While re-verifying, a row rendered a red "lost" badge beside a "verifying" chip: the settle window and the acceptance row were read as independent signals about one fact. A node whose acceptance says it is being judged is never lost. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 📝 docs(task): the report does carry its author The component doc still claimed "no avatar" from before the author moved to the head of the report, where it now belongs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(goal): fold the app rail for a drill-down, not the goal Opened beside the goal, the Portal left both panes cramped — the goal's title wrapping to four lines, its task rows truncated to a few characters. Folding the goal pane was tried and reverted: it hid the task's own verification state, which is exactly what the reader drilled in to check. The rail is the one thing on screen nobody is reading, so it gives up the width instead, and the goal and the report each get a usable half. `showLeftPanel` is a persisted preference, so this only restores what it collapsed: someone who already works with the rail folded is left alone, and the rail is handed back on the way off the page rather than stranded everywhere else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(task): put the verdict under the report, and drop the comment box The acceptance sat last in the result panel, after the artifacts. On any report longer than a screen that put the checks — the reason to trust what you just read — off the bottom, and they read as missing. Report, then the verdict on it, then what it produced. The result panel is for reading what came back, so it no longer parks an empty comment composer under every report. Leaving a note for the next run is already one of the report's own actions, which opens the editor on demand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(task): the result panel's acceptance block is the checklist In the result panel the reader has just read the delivery and wants one thing from this block: the checks and how each came out. It led instead with the round timeline and the 验收目标 contract — "how was this judged", which the full report keeps — and the verdicts sat under both. The result variant now renders the checklist directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(task): reuse acceptance rows in results --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
02e41c2c8c |
✨ feat(tasks): cascade terminal status to subtasks (#19158)
* ✨ feat(tasks): cascade terminal status to subtasks * 🐛 fix(tasks): make subtask status cascade atomic * 🐛 fix(tasks): harden the family status cascade against races and partial failures - freeze the cascade to the snapshotted task ids so a subtask created or started after the confirmation dialog is never rewritten; running topics are canceled by the same frozen id set inside the transaction, with a best-effort interrupt for operations discovered only there - persist successful interrupts (topic canceled) before surfacing a partial interrupt failure so stopped operations are not left recorded as running - surface apply failures in the cascade modal with an error toast and keep it open for retry instead of discarding the rejection - batch completion-side-effect discovery across all completed ids (getUnlockedTasksForMany / cascadeOnCompletionMany) instead of one dependency walk per updated task Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f20ea9602b |
🐛 fix(goal): unstick three panels that could not scroll, and merge spend into the budget metric (#19157)
* 🐛 fix(goal): let the node panel scroll and link the goal breadcrumb Two fixes on the goal detail surface: - The Goal Node portal could not scroll. `Markdown`'s root is `overflow: hidden`, so as a flex item its automatic minimum size collapses to 0 — a long finding handoff was squeezed (and clipped) to fit the panel instead of overflowing it, and the scroll container never grew past its client height. Pinning the Markdown block to `flex-shrink: 0` restores the overflow, and with it the scrollbar. Measured in a browser on the same box model: before, scrollHeight == clientHeight (300/300) with the markdown clipped 434px -> 192px; after, 542/300 and nothing clipped. - The middle "Goals" crumb on the goal detail page was plain text even though the goal title owns the last crumb, leaving no way back to the goal list. It is now a link, built through the new `useAgentRoutePath` hook that AgentBreadcrumb already used internally for the agent home — so the goal list link keeps the active workspace slug and any route prefix the current pathname carries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(topic): stop secondary chat panels from overwriting the sidebar topic list `topicDataMap` is keyed by container (`agent_<id>`) but not by query, so every fetch aimed at a container overwrites whatever the last one left there. The sidebar asks for the user's chat history — no `cron` / `task` / `document` / `eval` triggers, and no completed topics unless the preference says otherwise. The secondary conversation panels that carry their own topic picker asked for everything: `useFetchTopics(true, { agentId })`. Mount one next to the sidebar and its unfiltered response lands last, so task-run topics appear in the sidebar and the count jumps. The goal detail page is the reported case (opening a goal leaks its task-run topics into the left list); `/agent/:aid/task/:taskId` and `/agent/:aid/docs/:docId` have the same shape — all three mount under `agent/_layout`, which owns the sidebar, and all three resolve to the same agent id. The goal toolbar was copied from the task-manager one, which is how the pattern spread. Two changes: - Derive the list query in one place (`useChatTopicListQuery`) and add `useFetchAgentChatTopics(agentId)` for panels that name their agent. Every picker now asks for the same list as the sidebar, so the args match, SWR dedupes them into one request, and the bucket has a single writer. This also fixes the total (the count badge reads the server's total, so client-side filtering alone could not have repaired it). - Generalize the sidebar's client-side `trigger !== 'cron'` guard to the whole system-owned trigger set. The fetch decides the page and the total; this decides what renders, so a future stray fetch degrades the count rather than putting task runs in the user's history. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(acceptance): let the evidence document viewer scroll `DocumentViewer` is the box behind both markdown evidence surfaces: the 320px inline panel in the verify report and the drawer an evidence file card opens. It set `overflow: auto` on a `Flexbox`, which makes the rendered document a flex item — and `Markdown`'s root is `overflow: hidden`, so its automatic minimum size collapses to 0. Flex then shrank the document to the viewer's height and it clipped its own overflow, leaving a scroll container whose content never exceeded it: no scrollbar, and everything past the fold unreachable. The flex context bought nothing here (one child), so the viewer is now a plain block and the document overflows it the way a scroll container expects. Measured in a browser on the shipped box model: before, the viewer reported scrollHeight == clientHeight (344/344) with the document clipped 586px -> 320px; after, 610/344 and nothing clipped. Same root cause as the goal node panel earlier in this branch. The durable fix belongs in @lobehub/ui — a component whose default behaviour is to silently truncate its own content when used as a flex item. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(goal): merge spend into the budget metric and make the cap editable The header showed a budget with nothing to read it against, so `$10` answered "what is the cap" but not "how far in are we" — the question the number is actually there for. Spend now ships with the graph. The goal row never carried it: `evaluateBudget` computed it inline for the coordinator and threw it away, and the panel behind the metric said "per-run spend is recorded on each dispatched task" rather than inventing a total. Both now read one aggregate (`TaskTopicModel.sumRunCostByTaskIds`), so the fraction the page renders is by construction the one the coordinator will stop on. It is deliberately not the goal list's `totalRunCost`, which walks the whole `parent_task_id` subtree and is larger for a goal whose Tasks spawned Tasks — see the note in `resolveSpend`. The aggregate also replaces a full row fetch (`findWithHandoffByTaskIds`, jsonb handoff and all) that existed only to be summed. - Header: one `Spend / Budget` metric reading `$6.4 / $10`. The fraction is always stated in the unit the goal is actually capped in — a rounds-capped goal reads `3 / 5 rounds` rather than `$6.4 / 5 rounds`, which is two units wearing the costume of a ratio. - Panel: the cap IS the input. `spent / [cap]` with the denominator editable in place, committing on blur/Enter against a local draft so the graph poll can't eat half-typed digits. `setGoalBudget` existed on the store with no caller — this is that missing consumer. - Fix: `setBudget` wrote `deadline: null` whenever the goal had any schedule config and the caller omitted the field, so editing only the cost cap silently dropped a calendar budget. Omitted now means untouched, which is what the new one-dimension-at-a-time editor sends. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): make a lost/gate task row openable from its whole body A frontier row is `clickable` — pointer cursor and hover over the entire row — but the expanded body stopped propagation wholesale, so only the ~22px title line actually opened the node. That body is most of a row's height once it carries the "no heartbeat" explanation and the attempt ledger, so a click aimed anywhere natural landed in dead space while the cursor kept promising otherwise. Every stale row gets `rank: 0`, so every lost task in the goal's frontier had this. Measured on the real row: the row spans 88px, the title line 22px of it and the body the other 42px+. A pointer click at the row's centre — where a real click and `agent-browser` both land — fell inside the body and did nothing, while a programmatic `row.click()` on the row element opened the portal. That gap is the bug. The `stopPropagation` existed for the gate form: its note field and decision buttons must not also navigate. It is now scoped to exactly that form. The read-only content — the gate's question, the stale explanation, the attempt ledger — is part of the row's target again. Verified in the running product: clicking a lost row's body now opens its Task Result portal, clicking a gate's question opens the node, and clicking the gate's note field or its button area still resolves to neither. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(goal): rework the cost panel, quiet the budget in the header, and open a broken task directly Three review items from acceptance round 2. **C1 — the header metric names one number.** It read `$6.4 / $10` under the label "Spend / Budget", which gave the cap equal billing with the number the label is about. Spend now leads at full size and the cap trails it at secondary size, under the plain label "Spend" (or "Rounds" when that is the capped unit). **C2 — the budget panel becomes a cost panel.** It could say how much was left but not where the money went, which is the question a user actually arrives with. It is now "Cost": a "Budget control" block on top (the same in-place editable caps) and a "Where it went" list below — one row per dispatched Task with its tokens and dollars, ordered by spend, each row opening its node. `TaskTopicModel.sumRunCostByTaskIds` now groups by `task_id` and folds the per-Task rows into the total, so one round trip serves both the breakdown the panel lists and the total the coordinator enforces the budget against — they cannot drift into disagreeing. A Task whose runs have not settled shows $0 rather than being dropped: it is still consuming rounds. **C7 — a broken task opens the original Task, not its result surface.** A dispatched Task normally lands on its result-focused review surface, with the editable Task one step deeper. A lost or failed one inverts that: there is no result worth reviewing, and the question is what the run did. `select` now routes through `isTroubledTaskNode` — a task node that is stale, rejected, or whose latest attempt failed. Healthy tasks are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d896947d0c |
✨ feat(goal): carry task deliverables on the Goal Graph as Work (#19131)
* ✨ feat(goal): carry task deliverables on the Goal Graph as Work A goal recorded that a task finished but not what came out of it. The `goal_node_work_versions` table was built for exactly this — its `produced` relation included — but its only writers linked the task's own execution container, so every deliverable survived only as a URL buried inside the finding's prose, and the graph could count artifacts without naming one. Settle now harvests the document and external Works the task's runs registered and links them to the node that ordered them, `getGraph` joins the version's display snapshot so a link can be named, and the process-control surface grows a 交付物 section between the acceptance standard and the findings: what counts as done, what was produced, what the goal now believes. `file` and `task` Works stay out — the first is an edit-level event the Work registry itself gates behind an opt-in, the second is the execution container. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(goal): harvest generated files too, and ask tasks to persist deliverables Entity-format files (pptx / xlsx / docx / pdf) exported from an operation are deliverables, not edit noise: the registry gates them behind an opt-in because conversation lists do not want them, which is the opposite of what a goal's deliverable list wants. Harvest them, and read their open target from the version metadata where a file Work keeps it instead of the `url` column. The dispatched Task contract now says so up front: write reports as agent documents and generate files in the operation workspace, because a path under /tmp or a repository directory is a working location, not a delivery — nothing left only there can be reviewed, and nothing left only there is attached here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): address a document deliverable by its document id Clicking a document deliverable landed on the documents index. The row navigated with the agent-document binding id, but the route resolves the document id — and a document Work carries no url to fall back to, so the mis-addressed link degraded silently instead of failing. Carry `works.resourceId` through the graph join and address that; keep `agentDocumentId` only as the gate proving an agent binding exists. Found by driving the real page: the existing tests asserted the field was carried, never that the route resolved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 📝 docs(acceptance): record L-D14, addressing an agent document from a Work Found while verifying PR #19131: a document link built from the Work's agent-document binding id degrades silently to the documents index, and a unit test that only asserts the field was carried cannot see it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(goal): open a document deliverable in the side Portal Review feedback on the deliverables list: clicking should open the document in the sidebar Portal rather than navigating. It is also what the rest of the page already does — ProcessControl drills into a node or a task run through the same Portal, so the deliverable row was the only entry point that took the reader off the goal they were inspecting. Routing a generated file to the FilePreview Portal was tried against a real uploaded file and reverted: that Portal resolves a knowledge-base item, so an ordinary exported file loads forever with no error. Files and external resources keep opening at their canonical target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(goal): right-align the deliverable attribution column Review feedback circled the 来自「…」 column: the title sized itself to its content, so the attribution started at a different x on every row and the list read as a ragged edge rather than a column. Let the title take the slack, and give the relative timestamp a fixed slot — without it the attribution ends wherever the time happens to start, so a list mixing "几秒前" with "8 小时前" loses the alignment again. Verified by measuring both cases in the running app rather than by eye. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): address the Codex review on the deliverables list P1 — a workspace goal is readable by every member (`goals` has no visibility column) while a Work is owner-scoped, and the hydration join carried no ownership predicate: another member opening a shared goal received the owner's private Work title, status, url and document binding. `works` now joins under `workOwnership`, so an unauthorised Work stays an unresolved link that still counts. The regression test fails without the predicate. Harvest completeness: the per-operation cap applies to version EVENTS before the dedupe by Work identity, so a document revised twenty times could push every other deliverable out of its own task's list — read a generous window instead. And harvest from every run via `findByTaskId` rather than the ten rows the finding lookup happens to read, which stranded deliverables from earlier attempts once the attempt budget went above ten. Deliverable rows: resolve a single open target, so a row with nowhere to go no longer carries a pointer cursor, hover and a dead click handler; only proven http(s) urls become destinations (`window.open` reaches `shell.openExternal` on desktop); and an openable row is a real button, giving focus, Enter/Space and semantics that keyboard-only users previously had no way to reach. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ⚡️ perf(goal): hydrate linked Works in a second gated read, and align findings rows The ownership predicate added to the graph join carries correlated EXISTS guards, and evaluating them inside that join cost the graph read ~2.5x — a read the detail page makes every few seconds while it polls. Move the hydration to a small second query keyed by the linked version ids: the guards now run once over a handful of rows, a goal with no links skips the query entirely, and the read comes out faster than it was before the security fix (1.06s vs 2.88s on the event-cap test). The leak test still fails without the predicate. Findings rows adopt the deliverables row layout, so the two adjacent sections share one column edge instead of reading as different layouts of the same row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fe4c8b156a |
🐛 fix(goal): record why the coordinator parked a goal, and bound gate reads (#19151)
* 🐛 fix(goal): record why the coordinator parked a goal, and bound gate reads Follow-up to #19133, from its final review round. A goal the coordinator parks on an unmet measured clause is indistinguishable from one a person paused: same status, same terminal phase, same clauses. The reopen path treated every paused goal as its own, so a later satisfying observation restarted a goal somebody had deliberately stopped — and ran its acceptance work. The coordinator now records `config.pausedBy` when it parks, and only lifts a pause it owns; a user pause or resume clears the marker, so the pause belongs to whoever took it last. The same marker fixes the advertised clear path. `setMetricCriteria(id, [])` dropped the clauses and then asked whether they were satisfied — with none declared the check bailed out, leaving the goal parked on a gate that no longer existed and an advance that ticked straight back out as `goal_paused`. Reopening now asks whether the gate still holds the goal, which covers both ways out: every clause met, or no clauses left. Evaluation is also bounded. Each terminal tick mapped every clause to its own `findByKey` + `latestPoint` pair, so an unbounded criteria list meant an unbounded burst of concurrent queries. It is now two reads for the whole contract (`findBySubject` + a `DISTINCT ON` batch), and both write paths cap the list at `MAX_GOAL_METRIC_CRITERIA`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🐛 fix(goal): make the pause marker atomic, legacy-safe, and narrowly read Second Codex round on this PR. The marker was written by reading the whole JSONB config and writing it back. The coordinator writes it from a tick while the user edits budget and acceptance criteria on the same column from the goal page, so whichever landed second discarded the other's work — the user's new criteria, or the marker a parked goal needs to ever reopen. It is now a `jsonb_set` patch of that one key. Requiring the marker also stranded goals parked before it existed, and any parked by an old worker mid-deploy: no marker meant no reopen, so a satisfying measurement left them paused until somebody pressed Resume. Those rows are recognized from the park the gate recorded on the goal's status trail — matched on the reason it wrote, not merely "the coordinator paused it", since the coordinator also parks goals that are deadlocked or out of budget and an unrelated sample must not restart either. A person's claim is now stored explicitly as `pausedBy: 'user'` rather than as the absence of a marker, because pausing an already-paused goal is a no-op that leaves no other trace of who asked. `setMetricCriteria` takes its verdict from the state preceding its own edit, the way `setBudget` reads what was binding before it wrote. Evaluation reads only the declared keys (`findByKeys`) instead of every series on the goal: criteria are capped, but `recordObservation` can create any number of other series, and none of them can gate acceptance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5512942ba4 |
🐛 fix: clamp message plugin identifier/apiName to 255 chars at every write path (#19002)
* 🐛 fix: clamp message plugin identifier/apiName to 255 chars at every write path Hallucinated tool calls have written multi-KB values (artifact XML, shell commands) into message_plugins.identifier / api_name. With those columns now btree-indexed, any value past the index tuple size limit fails the whole row insert, so every write path clamps before hitting the database. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ✅ test: cover message plugin identifier clamping * ✅ test: include plugin arguments in clamp fixture --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
db920ea162 |
🐛 fix(bot): bind the completion reply to its own operation, and surface failure reasons (#19114)
A Discord thread delivered the same 1509-char reply for five consecutive bot
runs while the topic itself showed each run's real answer (LOBE-13787).
Reading the incident topic's real rows shows a topic can hold more than one
run's assistant messages: a Discord bot run (trigger=bot) and a web chat run
(trigger=chat) executed concurrently off the SAME user message — both carry
`app_context.sourceMessageId = msg_TwLG75ICedVdFhfXdh` — and the reply the bot
delivered belonged to the chat run. Each operation had written its own correct
reply to the database.
- Bind the recovered deliverable to the operation. The DB fallback in
`CompletionLifecycle` now resolves the run's own final assistant row by the
creation-time provenance `call_llm` stamps (`metadata.operationId`) through
the new `MessageModel.findLatestAssistantByOperationId`, instead of trusting
whatever row the event names. Topic-scoped, so the JSONB predicate stays
inside the topic's indexed rows. A concurrent run's row can no longer answer
for this one.
- Always give a failing run a reason. The bot paths that catch a raw throw
(bridge startup, handleMention / router catch-alls) rendered a bare
"**Agent Execution Failed**" — and when the run died before it had an
operation id, nothing else at all, which is what the issue screenshot shows.
`renderThrownAgentError` normalizes the thrown value through the runtime's
own error classifier first, so those paths reach `renderAgentError` with the
same errorType + attribution the completion path already gets and land on
curated copy; an unclassified harness throw defaults to the `harness` tier.
The raw errorMessage still never reaches an IM channel — that invariant is
deliberate (
|
||
|
|
13316907a1 | 🐛 fix(agent-share): clear visitor topic running marks and refuse stale reconnects (#19140) | ||
|
|
0d6a59d67a |
⚡️ perf(home): remove legacy session joins (#19126)
* ⚡️ perf(home): remove legacy session joins * ✅ test(home): pin and group agents via agents table in sidebar tests Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |