mirror of
https://github.com/lobehub/lobehub.git
synced 2026-09-20 04:56:13 +08:00
feat/quick-note
134 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c8cb9a1e38 | 🐛 fix(quick-note): serialize proposal input pinning | ||
|
|
41e46f056a | ✨ feat(quick-note): add configurable agentic analysis | ||
|
|
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> |
||
|
|
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 |
||
|
|
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> |
||
|
|
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 |
||
|
|
74a7307df4 |
🗃️ feat(db): add generic metrics time-series tables (#19063)
Two tables split as definition vs data, designed so one query endpoint can
power every future chart:
- `metrics` — one row per series ("one row = one chartable line"). Polymorphic
subjectType/subjectId with no FK (mirroring `goals`), unique on
(subject_type, subject_id, key). Carries what a renderer cannot infer from
the data: aggregation semantics (`kind`: gauge/counter), formatting
(`unit`), and display/evaluation hints (`config`: target, direction,
precision).
- `metric_points` — append-only, strictly numeric (t, v) observations with
`observedAt` distinct from `createdAt` so backfills land on the right spot.
Provenance mirrors goal_events (actorType/actorId/operationId + sourceType);
ownership is denormalized so buildWorkspaceWhere works without a join. Hot
path is the (metric_id, observed_at) index.
Goals are the first intended consumer (numeric acceptance criteria and
observation probes for LOBE-13597 gap 2), but the tables know nothing about
goals — the same schema serves agent cost curves or workspace dashboards.
Schema only; write/read services and TRPC endpoints come in follow-up PRs.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
09cd9edf27 |
✨ feat: add project working directory schema (#18746)
Co-authored-by: Arvin Xu <arvin.x@lobehub.com> |
||
|
|
b4c5ff79ff |
✨ feat(database): add document_likes table (#18947)
* ✨ feat: add document_likes table for workspace document likes Introduce the `document_likes` storage layer so workspace members can like a document: one row per (document, user), unique on that pair, with cascading FKs to documents / users / workspaces and paging-friendly indexes. Ships the Drizzle schema, relations, and an idempotent migration (0155); the model, router, and UI land in a follow-up PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNfPCfAEiWyuvTbXMoqNTZ * 🐛 fix: keep document_likes consistent across document scope transfers DocumentModel.transferTo now rehomes like rows to the target workspace and deletes them on a workspace→personal transfer, mirroring how comments and mention rows are handled. Adds a regression test covering both directions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNfPCfAEiWyuvTbXMoqNTZ * 🐛 fix: count teammates' likes as foreign rows in document transfer guard subtreeHasForeignRows now also detects document_likes rows owned by other users, so a non-owner member cannot transfer a subtree that would rehome or delete teammates' reactions. Covered by a regression test asserting the caller's own like stays transferable while a teammate's blocks it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNfPCfAEiWyuvTbXMoqNTZ * ✨ feat: add DocumentLikeModel with transfer-serialized like creation Ships the model half of the likes storage layer in the same PR as the schema so the serialization contract stays reviewable in one place: like() runs in a transaction that locks the document row FOR UPDATE (mirroring DocumentCommentModel.create), so a like racing a concurrent scope transfer blocks on the transfer's row lock and re-validates the workspace after it commits instead of stamping the stale source workspace_id — the transfer-side foreign-row guard and rehoming from the previous commits therefore see a consistent row set. Includes the DocumentLikeSummary types and model unit tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNfPCfAEiWyuvTbXMoqNTZ * 🐛 fix: recheck foreign rows inside the transfer transaction The non-owner foreign-row guard used to run as a router preflight outside the transfer transaction, so a teammate's like (or comment) committed between the preflight and the transfer could still be rehomed or deleted. transferTo now locks every subtree document row FOR UPDATE and, when called with forbidForeignRows, re-runs the shared foreign-row predicate inside the transaction, throwing DOCUMENT_TRANSFER_FOREIGN_ROWS which the router maps to the existing OwnerOnly error. Concurrent like/comment writers lock the same document row before stamping a workspace, so they either commit before the recheck (and are seen) or block until the transfer commits and then re-validate their scope. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNfPCfAEiWyuvTbXMoqNTZ * 🐛 fix: serialize unlike and summary against document scope transfers unlike() and summary() shared like()'s validate-then-act shape without its serialization, so a transfer committing between the workspace validation and the delete/reads let a stale source-workspace caller mutate or read rehomed rows. All three operations now run through a shared lockDocument helper inside a transaction — writes lock the document row FOR UPDATE, summary takes FOR SHARE so the validation stays true for the duration of its reads while concurrent transfers wait. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNfPCfAEiWyuvTbXMoqNTZ * 🐛 fix: route resource-manager transfers through the in-transaction guard file.transferEntity was the remaining caller still running the foreign-row check as a pre-transaction preflight, leaving the same TOCTOU window the document router just closed. It now passes forbidForeignRows to transferTo and translates DOCUMENT_TRANSFER_FOREIGN_ROWS to the OwnerOnly error exactly like document.transferDocument. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNfPCfAEiWyuvTbXMoqNTZ --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d808d4ef94 |
✨ feat(goal): record and replay a goal's whole trajectory (#18801)
* ✨ feat(agent-tracing): add the goal trajectory trace format A goal is one complete goal execution, the way an operation is one complete agent execution — so it gets one trajectory object with the same shape as an `ExecutionSnapshot`, whose steps are advances instead of LLM calls. The leaf links out rather than copying: an advance records the `operationId`s it put in flight, and `inspect <opId>` continues from there. What the format keeps is the decision *input*, not the result. Every tick carries the graph it read (as a delta against the previously recorded state), the budget it evaluated, the responsible task's state, and every frontier candidate it ranked — including the ones it passed over, which is the only way a trace can answer "why not that node" and the only way a replay can be exact. Deltas sit on ticks rather than advances because an advance runs a loop of ticks and each one re-reads the graph; folding the baseline through them reproduces any tick's decision input. `replayTrajectory` feeds those inputs back to a decider and reports where it now decides differently, which turns a recorded run into regression material for coordinator rule changes. `buildGoalTraceRollup` derives the analytics row — advances bucketed by trigger and by the outcome each stopped on, gates, human waiting time, child operations — leaving per-tick detail in the object, the same split as `agent_operations` and its snapshot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ♻️ refactor(goal): separate the coordinator's decision from its execution `tick` interleaved choosing and doing, so what the coordinator decided existed only for the length of the call: the frontier it ranked, the budget it read and the branch it took were all local variables that a `GoalTickResult` could not carry. Nothing downstream could record them, and nothing could replay them. `decideNextMove` is now a pure function of the graph, the responsible task and the budget, returning the branch and the whole ranked frontier — losers included, because "why not that node" is unanswerable from the winner alone. `tick` calls it once and switches on the result; every arm keeps the code it had, moved into a named method. The existing suite passes unchanged, which is the point: this changes where the decision is made, not what it decides. Two things follow. `onDecision` hands the caller what was read and what was done, on a side channel rather than on `GoalTickResult`, which crosses tRPC to the client — the same reason the context engine snapshot stays off the agent runtime's event array. And the budget is read only when the branch could actually start a run, so a goal waiting on running Work no longer pays for a budget query on every sweep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(goal): record a trajectory for every goal advance A goal now writes what the coordinator did, in the format an operation already uses for what an agent did: one object per goal, accumulated through a partial and finalized when the goal itself is terminal. `advanceGoal` is the only funnel every advance passes through, so the envelope is written there and every caller — create, decide, settle, sweep, resume, budget, the manual nudge — labels its own trigger, which survives the queue hop in the QStash body. That alone answers a question the last run could only answer from HTTP logs: which of those 402 advances came from an event and which from the safety net. Effects sit on ticks rather than on the advance, so an advance that ran several of them keeps the order in which it created the task, opened the gate, or started the run. Where a run or a completed Work carries an operation id, that id goes on the effect — those are the leaves that hand off to `agent-tracing inspect <opId>`. Storage follows the operation switch exactly, so a deployment that keeps one keeps the other and a goal trace never points at operation traces that were dropped. The S3 key is derivable from the goal id alone, so nothing has to carry it. Recording is best-effort throughout: a store that cannot be built, or a write that fails, degrades observability and never stalls a goal. An advance that throws still flushes the ticks it completed with the failure attached — losing that is exactly the blind spot this replaces. A goal that is merely parked keeps its partial, which readers serve: an unfinished long-horizon goal is the normal thing to inspect, not an error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(agent-tracing): read and replay a goal trajectory `agent-tracing goal <goalId>` renders the run: how it ended, which triggers drove its advances, what the graph became, how long it sat on a person, and how many operations it put in flight. `-a <n>` opens one advance in full — the branch each tick took, the budget and responsible task it read, and the whole ranked frontier with the candidates it passed over, which is the question a summary can never answer. `replayGoalAgainstCurrentCoordinator` closes the other half. It lifts each recorded graph back into the shape the coordinator reads and runs the real `decideNextMove` over it — not a model of the coordinator, the function production calls. Divergences come back addressed to the advance and tick that produced them, so a rule change is a diff against history rather than a surprise on the next live goal. Two limits worth stating plainly, because an over-claimed replay gets trusted: this reproduces the coordinator's *decisions*, exactly, and says nothing about whether the dispatched work would have gone the same way. And the trajectory is only as good as what the coordinator actually read — the lift fills descriptions, events and work versions with empty values rather than inventing them, since `decideNextMove` never looks at those and a replay that fabricated them would be testing something the original run never saw. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(goal): write the observation row from each goal's trajectory `goal_traces` landed on canary as schema and migration only (#18852). This is what fills it: `GoalTraceModel` plus the write path from `GoalAdvanceRecorder`, which re-derives the whole row from the accumulated trajectory on every advance rather than incrementing counters — exact however many times a queued advance retries, and queryable while the goal is still running, which is when a goal quietly churning on the sweep is worth finding. Reads join `goals` for ownership; the row deliberately has no `user_id` of its own, because it is 1:1 with a goal and a copy would be a second place for the answer to drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 📝 docs(agent-tracing): document goal trajectories in the skill The skill is how the next agent finds this. Records the format, the storage layout, the CLI, and the two limits of goal-level replay. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(cli): move goal trajectories onto `lh trace goal` `lh trace op` landed the surface for a single operation and reserved sibling scopes for goal and task, so the goal trajectory reader belongs there rather than on the standalone `agent-tracing` bin — which it now leaves entirely, so a brand-new command has one home instead of two that drift. That move brings the thing the standalone bin could not do. An operation's key is derivable from its id, so `agent-tracing` could reach one with a configured `TRACING_BASE_URL`; a goal's key lives on `goal_traces` and is derivable from nothing, so the server is the only way in. `agentTrace.getGoalTrajectoryUrl` signs it against the caller's own scope — a goal belonging to someone else reads as absent rather than forbidden, so this cannot be used to probe ids — and inspecting a production goal needs a LobeHub login and nothing else. Local `.goal-tracing/` files are still read first, so dev needs no session at all. `list` is local-first with `--server` for the rest, matching `lh trace op list`, and the server listing leads with the trigger split: `sweep=38 settle=112` is the number that says how much of a run was the safety net. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ♻️ refactor(goal): say task where the thing is a task A Goal Graph node of kind `work` owns exactly one task — the schema enforces it with a unique index and a check constraint literally named `goal_nodes_task_requires_work_kind`. So "how many attempts does a Work take" is asking about a task, and every metric, branch and line of report copy this branch introduced now says so: `tasksReady` / `tasksBlocked` / `tasksCompleted`, `task_running` / `task_paused`, "tasks 1 ready / 1 blocked / 0 done". The word was already carrying two meanings in one file. `GoalService` imports `WorkModel` — the works/work_versions registry, which is agent *output*, not work to be done — and calls `registerTask` on it inside the method that handles a work node. Adding a third reading of the same word to the reporting surface would have made that permanent. What is deliberately NOT renamed: `goal_nodes.kind = 'work'` and the `goal_traces` columns behind it. Those are one decision, not two — the columns count what the nodes are — and the node kind is shipped with production data and a dozen UI call sites. `writeObservationRow` carries the seam explicitly, with a test pinning it so it cannot drift silently in the meantime. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ♻️ refactor(goal): rename the Goal Graph node kind from work to task A node of this kind owns exactly one task — the schema says so itself, with a unique index on `task_id` and a check constraint named `goal_nodes_task_requires_work_kind`. The UI has been rendering it as "Task" / "任务" since the graph shipped. Only the code still said `work`, and it said it next to `WorkModel` — the works/work_versions artifact registry, a different thing entirely, called from the same methods. After this, `work` in the goal domain means the registry and nothing else. `GoalNodeKind` is now `problem | task | finding | decision`, with the constant, model method, service method and config-field names following it (`GOAL_ACCEPTANCE_TASK_TITLE`, `claimTaskNode`, `consumeCompletedTask`, `maxAttemptsPerTask`, `TaskRecoveryCoordinator`). The i18n key moves with them; its value was already "Task". The migration backfills before it re-adds the constraint, because a row that already has a task and the old kind would violate the new check. The `goal_traces` columns keep the names they shipped with — renaming three columns buys nothing the reporting names do not already give, and the mapping sits in one place in `writeObservationRow`. Two effects were restored on the way through: `opened_decision` and the finding's `created_node` had been silently dropped in an earlier rebase, which would have left `gatesOpened`, `gatesResolved` and `humanWaitingMs` reading zero forever. The unit tests missed it because they feed synthetic effects that always agree with themselves, so the new test drives the real gate and the real finding through `onDecision`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🗃️ chore(db): drop the goal node kind CHECK instead of moving it Renaming the node kind meant either changing this constraint or removing it, and it should be removed: `bindTask` is the only writer of `task_id` and its WHERE already requires the kind, so the CHECK was redundant with the write path while pinning the kind vocabulary into DDL — an ALTER for every future kind, which is exactly what this rename just cost. The unique index on `task_id` stays. One task belonging to one node is an invariant no single write can check for itself; which kinds may own one is a rule the write path can, and does. So the rule moved rather than disappeared, and its test moved with it: the schema test now covers only the uniqueness the database still owns, and a model test covers `bindTask` refusing a node of the wrong kind. Without that second test the rename would have quietly deleted an invariant. The migration drops before it backfills — the old constraint reads `kind = 'work'`, so updating a row that owns a task would violate it on the way past. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(goal): close the four gaps Codex found in the trajectory recorder **Overlapping advances could erase each other's record (P1).** Appending is read-modify-write on one object, and advances for the same goal genuinely overlap — an event hook, a manual nudge and the sweep, which the coordinator handles everywhere else by claiming rows. Two of them read the same partial, picked the same `seq`, and the second write dropped the first; the stale rollup then overwrote the row, so `advancesTotal` could go backwards. The write now runs under a transaction-scoped per-goal advisory lock, held across the object write because reading the partial and writing it back have to be one critical section. **Gate metrics were structurally unreachable.** `gatesResolved` could never be anything but zero: a person answers a gate through `goal.decide`, outside any advance, so no tick is running to emit the effect. Both transitions are derived from the recorded graph now — the decision rows are in every state either way, so this no longer depends on the coordinator remembering to instrument two lifecycle points, one of which it cannot reach. Waiting time is measured between the tick that saw the gate open and the tick that saw it answered, which is the wall time the goal actually stood still. **`lh trace goal list` hid every running goal.** It listed finalized objects only, so a machine with active traces and no finished goals printed "nothing here" while inspecting any of them by id worked. Partials are merged in, newest first, with the finalized copy winning when a stale partial outlived it. **Recovery claimed runs it had not started.** `recover()` returned `continued` both when it spawned a retry and when another advance already owned one, so the trajectory recorded a `started_run` for work it did not do — and discarded the operation id when it had really started something, losing the drill-down link. The outcome now distinguishes `started` from `already-running` and carries the operation id. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✅ test(goal): assert the trigger the `/goal` runtime queues its advance with Adding `trigger` to `scheduleGoalAdvance` broke this exact-argument assertion. Pinning the value rather than loosening the matcher: the label is what lets a recorded run be sliced by what drove it, and a `/goal` kickoff that queued an unlabelled advance would be a real regression. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Arvin Xu <arvin.x@lobehub.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d74e897191 | ✨ feat: add Elasticsearch full-text search provider (#18807) | ||
|
|
d5a16b5202 |
🗃️ chore(db): merge recycle-bin, goal-trace and eval-dataset schema into one migration (#18852)
#18414 (recycle bin), #18801 (goal traces) and #18809 (optional dataset benchmark) each carried their own migration, and all three claimed a number that canary has since taken. They touch disjoint tables, so the three deltas are combined into a single `0152` here and the feature PRs can drop their schema layer and rebase on this. - trash_items + is_deleted/deleted_at on 18 content tables (#18414) - goal_traces (#18801) - agent_eval_datasets.benchmark_id nullable (#18809) The only call sites included are the ones the columns force: null-handling for the now-optional benchmarkId, and fixture padding for the new row columns. |
||
|
|
800b18f4e9 |
✨ feat(db): add workspace document comment schema (#18726)
* ✨ feat(db): add workspace document comment schema * ♻️ refactor(db): use UUIDs for document comments * 🐛 fix(db): sync comments during document transfers * 🐛 fix(db): harden document comment transfers * 🔨 chore(db): regenerate document comment migration |
||
|
|
a89c16e779 |
✨ feat(database): persist generic agent interventions (#18417)
* ✨ feat(database): persist heterogeneous interventions * feat(database): generalize agent intervention approvals * 🐛 fix(database): supersede partial approval batches atomically * 🐛 fix(database): fence custom intervention execution retries * 🐛 fix(database): harden intervention recovery boundaries * 🐛 fix(database): merge plugin state atomically |
||
|
|
bc9b212a21 | ✨ feat(goal): add autonomous work recovery and final acceptance (#18635) | ||
|
|
a3d2937148 |
✨ feat(goal): add durable goal graph schema (#18585)
Co-authored-by: Arvin Xu <arvin.x@lobehub.com> |
||
|
|
a5b8572d32 |
✨ feat(database): subject-aware resource permissions with per-member grants (#18536)
* ✨ feat(database): make resource permissions subject-aware with per-member grants Add a nullable `user_id` subject column to `resource_permissions`, so one table carries both the workspace-wide access level (`user_id IS NULL`) and per-member grants that only ever raise a member above it. The workspace-wide row stays unique via a `NULLS NOT DISTINCT` unique constraint over (workspace_id, resource_type, resource_id, user_id), which replaces the old three-column unique index and lets both row shapes upsert through one conflict target. `ResourcePermissionModel` centralizes the `user_id IS NULL` filter on every workspace-wide read so a per-member grant can never leak into workspace-wide semantics. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🐛 fix(database): use partial unique indexes for the permission subject split `UNIQUE NULLS NOT DISTINCT` requires PostgreSQL 15, but self-hosting documents PostgreSQL 14 as the minimum, so migrations would fail to apply on those installations and the server could not boot. Switch to the pattern the rest of the schema already uses for a nullable discriminator (`agents`, `agent_skills`, `ai_models`, `devices`): one partial unique index per subject — `(workspace_id, resource_type, resource_id) WHERE user_id IS NULL` for the workspace-wide row and the four-column index `WHERE user_id IS NOT NULL` for per-member grants. Both upserts repeat the matching predicate in `targetWhere` so PostgreSQL can infer the arbiter index. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🐛 fix(server): pin knowledge-base permission scans to the workspace-wide subject `getUseLevelKnowledgeBaseIds` and `getRestrictedKnowledgeBaseIds` read `resource_permissions` directly and matched every row of a resource. Now that the table is polymorphic on `user_id`, a per-member collaborator grant would satisfy those scans and mark the knowledge base restricted for the whole workspace — a below-baseline grant is meant to be inert, not a workspace-wide demotion. Both scans now pin `user_id IS NULL`. The regression test records the rendered `where` clause, because the existing fake db ignores predicates and would return the canned rows either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🐛 fix(database): revoke a member's resource grants when they leave the workspace `removeMember` soft-deletes the membership and `addMember` reactivates that same row, so a per-member grant left behind would become effective again the moment the member is re-invited — silently restoring access that removal was supposed to take away. Revoke the member's grants on removal, the same lifecycle the method already applies to their device enrollments. Workspace-wide rows carry no subject and are untouched, as are the member's grants in other workspaces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🐛 fix(database): give collaborator listing a deterministic order `now()` is the transaction timestamp, so every grant inserted by one `upsertCollaborators` statement shares a `created_at`. Ordering by that column alone left the tie to the query plan, which could reorder a collaborator list between reads. Break the tie on `userId` so the order is specified rather than incidental. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ff9aa0ebf6 |
✨ feat: group acceptance deliveries by project (#18448)
* ✨ feat(acceptance): group deliveries by project * ⚡️ perf(acceptance): persist project association * ✨ feat(project): associate durable works |
||
|
|
de269f9a47 |
✨ feat(agent): give agents a character sheet, a society and an extension bag (#18464)
Agent identity is outgrowing the columns it has, and a column per trait means a migration per trait. Three additions instead: - `profile` (jsonb) — the character sheet: MBTI, zodiac, gender, persona traits, the artwork that depicts the character, and the style/direction it was generated from, so reopening the studio resumes where the user left off - `metadata` (jsonb) — the default extension bag for anything with no column and no home on the character sheet. Untyped on purpose - `society_id` (text) — the owning agent org. A real column, not a bag key, because agents get listed and filtered by it; a jsonb key could not take an index or a foreign key later without a rewrite. No foreign key until the societies table exists, no index until the first query needs one `MBTI_TYPES` / `ZODIAC_SIGNS` / `AGENT_GENDERS` are closed literal sets in `@lobechat/types`, so the schema and every consumer read one source; `personaTraits` stays open text because the set is whatever users invent. The artwork studio is the first consumer and ships separately. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7f7330f4af | 🗃️ chore(db): add notifications metadata column (#18434) | ||
|
|
22d64ca104 |
✨ feat(database): add resource transfer requests schema and migration (#18348)
* ✨ feat(database): add resource transfer requests schema and migration * ♻️ refactor(database): move transfer enums to @lobechat/types and drop schema-level enum typing * 📝 docs(skill): add schema conventions (no pg enums for growing domains, no const exports from schemas) to db-migrations * 🐛 fix(database): preserve transfer audit rows when actors are deleted (set null instead of cascade) * 🐛 fix(database): exclude null-recipient rows from the pending-transfer unique index * 📝 docs(skill): scope the schema-constant rule to domain literals in new/modified schemas |
||
|
|
70637b97dd |
✨ feat(expertise): add self-evolving data schema (#18334)
* ✨ feat(expertise): add data schema * ♻️ refactor(expertise): tighten lesson section types * ♻️ refactor(expertise): use UUIDs for internal records * ♻️ refactor(expertise): require domain owners * 🐛 fix(expertise): enforce domain consistency * ♻️ refactor(expertise): regenerate migration after rebase |
||
|
|
f20a924808 | ✨ feat(agents): add Kimi Code runtime (#18228) | ||
|
|
3f1715541a |
✨ feat(acceptance): propose review verdicts and record the reviewer's answer (#18199)
* ✨ feat(acceptance): propose review verdicts and record the reviewer's answer Adds an automated second opinion on acceptance checks: a vision model re-judges a check the verifier already passed, circles the region at fault, and the reviewer answers with one of three verdicts. The shape follows the offline baseline (LOBE-13035, 187 real samples across kimi-k3 / gemini-3.6-flash / gemini-3.1-pro): - Predictions live in their own table, never in `user_decision` — the human decision stays the single ground truth, which is what makes the two comparable at all. - The judging prompt carries an explicit zero-tolerance rule. In the baseline the models perceived defects correctly and then forgave them ("仅约 17px 的 轻微右偏,在可接受容差内"); the rule recovered every one of those. - Adjudication is three-way. `misidentified` carries the OPPOSITE signal on the judgement from `not-an-issue`, and merging them would teach the model that speaking up is risky — the wrong lesson for a reviewer whose measured failure mode is leniency. - A reject now records which of three jobs it is doing. Three models converged on the same "wrong" answer for 24 checks because the reject button is the only outbound channel and carries three unrelated meanings at once. - 20% of checks are deliberately shown with no proposal, so missed defects stay observable instead of silently reading as perfect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 📝 docs(acceptance): record the SSRF gate on server-side local-S3 evidence reads Any feature where the SERVER reads back an uploaded artifact fails silently in the local env: the request never leaves the process, so it reads as the model declining to act rather than as a blocked fetch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 fix(acceptance): quiet the proposal card and stop duplicating its evidence image Review feedback on round 1: the card shouted, repeated the screenshot already below it, and had no visible story for more than one marked region. - Collapsed by default behind a dashed, untinted summary line. A proposal is a suggestion sitting beside real verdicts; a solid tinted panel competed with the check's own status colour for the same glance. - No image of its own. Opening the card draws the regions onto the evidence image that is already in the row, so one screenshot appears once. - Badge numbers run across the whole proposal rather than restarting per image, or "region 2" would name a different box on each screenshot. Also fixes an edit-classification defect the multi-region case exposed: `classifyProposalEdit` paired proposed regions to kept ones by `evidenceId` alone, so several regions on ONE screenshot all matched the first — an untouched two-region proposal reported `region-moved`, feeding a grounding error that never happened into the training signal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🔧 refactor(acceptance): split the prediction's model reference and disable the blind slice Review feedback on #18199. `model_id` was one glued string; split into `provider` + `model` to match how every other model reference in the repo is carried, so an opinion stays filterable by provider on its own when comparing versions. The blind control slice now ships disabled. Its stated purpose was overstated: missed defects are already observable without it, because a proposal is only written when the model says `reject` — a check the model passed shows no card, so a human rejecting it is a visible miss on its own. What a blind slice uniquely measures is whether the labels stayed INDEPENDENT of the model, and that only matters once labels are being assembled for training. Until then it costs a fifth of reviewers their assistance for a guarantee nobody is reading, so the rate defaults to 0 with the mechanism (and its tests) intact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🔥 refactor(acceptance): drop the blind control slice Removed rather than kept-but-disabled. Its stated purpose did not survive scrutiny: missed defects are already observable without it, because a proposal is only written when the model says `reject` — a check the model passed shows no card, so a human rejecting it is a visible miss on its own. What a blind slice uniquely measures is whether the labels stayed INDEPENDENT of the model. That question only exists once labels are being assembled for training, and carrying a hash, a rate, and their tests until then is dead weight in the read path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 fix(acceptance): tighten the proposal card's collapsed copy and its provenance Review feedback on round 4. The collapsed line read as speech — "AI 认为这条可能没做到", "标了 N 处" — where the card is the product stating a review finding. Now "AI review · may not be met" and "{{count}} marked region(s)": source first, conclusion second. The model label moved from the far end of the action row to sit beside the title it qualifies. Parked at the end it read as a stray footnote, and it now appears exactly once whether the card is open or closed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 fix(acceptance): hide the model label while the proposal card is collapsed Review feedback on round 5. Collapsed, the row's job is the finding and its one-line summary; a model id wedged between the title and that summary pushed the summary out of the visible width. It now appears only once the card is open, still beside the title it qualifies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 fix(acceptance): drop the checklist card chrome and align the predict trigger's gate Review feedback: the check list read as a boxed-in panel. Its border, radius and the 16px inline padding on every row are gone — each row reclaims ~32px of width, and the existing row separators carry the structure that the frame was repeating. Also fixes the predict entry point's visibility. It gated on `counts.pending`, which counts any check without a standing review — including ones that never ran. That disagreed with the set the server actually processes, and failed both ways: a list whose only unreviewed item is un-executed showed a button that does nothing, while an acceptance whose executed checks are all reviewed lost the button even though the progress bar still said items remained. It now uses the server's own predicate: has a result, undecided, and carries a frame to look at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(acceptance): bound and defer proposal generation, and close five review findings Requested: the batch was synchronous with no ceiling. Each check is a 10-25s multimodal generation, so a thirty-check acceptance ran past any gateway timeout and lost every proposal it had already paid for, while `Promise.all` opened one generation per check at once — a single click becoming a rate-limit burst. It now dispatches after the response with at most 4 in flight; the client polls the bundle so the button still reflects real progress. Codex review, all five reproduced against the code: - A stale review is last round's rejection kept as history while THIS round's result is undecided. Gating on `Boolean(userReview)` hid the proposal on exactly the repair round that needs it. (P1) - Re-running the same model + prompt version upserted over an adjudicated row and destroyed the reviewer's answer — reachable through ordinary UI, since `not-an-issue` leaves the check pending and the request button available. The predictor now skips answered proposals before spending the call. (P1) - The focused surface rejects straight from its text area, bypassing the annotation modal, so every reject typed there was stored unclassified — the exact conflation the intent split exists to undo. It has its own selector now. (P1) - Predictions were looked up by the union row's id, which is `sourceCriterionId ?? checkItemId`; a plan using distinct snapshot ids generated proposals that could never render. Keyed by the result id instead. (P2) - The decision and the proposal's answer were two writes: a failure between them left the check rejected, its prediction unanswered, and the decision then hid the proposal so nothing could reconcile them. One transaction now. (P2) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🗃️ chore(acceptance): record every prediction attempt, not just the rejections `action` was `notNull`, but the predictor only ever wrote a row when the model rejected — an `accept`, a check with no screenshot to look at, and a provider failure all left the same trace: nothing. So the column presupposed its own value, and the four cases collapsed into "no row", which is the one shape that cannot be counted. Miss rate had no denominator, and a silent provider outage was indistinguishable from the model approving everything. Split it the way `verify_check_results` already splits `status` / `verdict`: a row now records that a review was ATTEMPTED, and only `judged` carries an opinion. `action` is nullable, `status` (judged/skipped/errored) is not, and a CHECK constraint enforces the pairing — the column enums are type-level only in drizzle, so this is the one place it is actually enforced. Nothing changes on screen: `shouldSurfaceProposal` gates on `action === 'reject'` and is now a type predicate, so `CheckProposal.action` narrows to `'reject'` and widening the gate stops the UI compiling. Also regenerates the DBML doc, which the original commit missed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f267232a19 |
✨ feat: add dedicated project coordinator agent (#18054)
✨ feat: add project coordinator agent
|
||
|
|
1d8e1efd4d |
🗃️ chore(db): add agent history job groups junction table (#18081)
* 🗃️ chore(db): add agent history job groups junction table Add `agent_history_job_groups`, the group-side twin of `agent_history_job_agents`, so a chat group can resolve its in-flight history job directly: - `agent_history_jobs.group_ids` is an unindexed jsonb array that completed jobs keep accumulating into, which the progress poll cannot use as a lookup key; - the member-agent junction is not a substitute — a group with an empty roster registers no agent rows at all, so an "already migrating" guard has nothing to match on. Same shape rules as the agents junction: cascade on group delete, unique on (job_id, group_id), and no unique constraint on `group_id` alone since history keeps completed jobs. Also widen the `copy` job payload types for group copies: the job payload gains an optional `group` discriminator, and the queue-row payload's `newAgentId`/`sourceAgentId` become optional, because a group topic remaps through the job-level `agents` map rather than one pair. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🗃️ chore(db): keep copy queue-row coordinates required The queue-row payload's `newAgentId`/`sourceAgentId` were widened to optional in anticipation of group copies, but the drain that resolves a group remap from `job.payload.group` lands in a follow-up. Until then `AgentCopyJobModel.processNextTopic` still treats a unit missing either id as unrecoverable — it skips the copy, then deletes the queue row and advances progress regardless — so a row enqueued against the widened type would complete with no messages copied and an empty target shell. Restore both as required and keep the relaxation with the drain change that makes it safe. The additive `AgentHistoryJobPayload.group` discriminator stays: it has no writer yet and cannot silently drop work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
89ff60f465 |
🗃️ chore(db): add payload column to agent history job tables (#18043)
* 🗃️ chore(db): add payload column to agent history job tables Carries type-specific data for jobs on the agent_history_jobs framework: `transfer` jobs leave it null, while an upcoming `copy` job type stores its source→target agent map on the job row and the per-topic copy coordinates on each queue row. Schema + migration only, so the column is deployed before the code that writes it. Additive and idempotent — no backfill. * 🔨 chore(db): narrow job type + payload shapes Review follow-ups on the schema itself (no SQL change — both are TS-level narrowings; db:generate reports no schema changes): - `type` becomes an enum ('transfer' | 'copy') like `status` above it, so a drain path cannot silently ignore a kind it does not handle. - payload fields become required: the column is nullable and NULL already encodes "no payload for this job type", so a payload that exists must be complete — a copy queue row missing sourceTopicId is now a type error. - corrected two comments: the table header no longer claims transfer is the only kind, and the job payload no longer claims the per-topic copy resolves against it (queue rows are self-contained; the job map serves the pending-copy guard). |
||
|
|
3e489f1b95 | 🗃️ db: add project identifier column (#18034) | ||
|
|
ee7b69d176 |
✨ feat(db): add capability scopes column to api_keys (#18016)
* ✨ feat(db): add capability scopes column to api_keys Adds a nullable `scopes text[]` column to `api_keys` so each key can carry a capability scope list. `NULL` keeps every existing key on full access, `['*']` is an explicit full-access selection, and any other value is a restricted scope list enforced at the API layers (shipped separately). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ✅ test(trpc): carry the new api_keys.scopes column in context fixtures The fixtures pin the row shape with `satisfies`, so the schema-only column addition needs `scopes: null` here to keep the standalone type-check green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ♻️ refactor(db): store api_keys.scopes as jsonb instead of text[] jsonb lets the scope payload evolve beyond a flat string list later (e.g. per-scope status or metadata) without another column migration. Storage semantics are unchanged: NULL = full access, ["*"] = explicit full access, otherwise a restricted scope list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f842a9472e | ✨ feat(database): add project schema (#17996) | ||
|
|
54891b5548 |
✨ feat(db): add agent history job tables for async transfer backfill (#17984)
* ✨ feat(db): add agent history job tables for async transfer backfill Generic per-topic job queue (agent_history_jobs / _job_agents / _job_topics) with a type discriminator, backing the upcoming fast/slow split of agent transfer: heavy message-scope rewrites drain asynchronously topic-by-topic instead of inside the transfer transaction. Schema + migration only; the model/worker/UI land in follow-up PRs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🐛 fix(db): surrogate PKs for job junction tables + idempotent migration DDL Per review: new tables must not use composite primary keys (surrogate uuid id + unique index instead, so the uniqueness scope can evolve without a PK rebuild), and generated DDL is hardened for replay — CREATE TABLE/INDEX IF NOT EXISTS, FKs drop-then-add. Verified by running the migration twice against a dev database (second pass converges as a no-op). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
124de08103 |
🗃️ feat(db): add agent labels schema and migration (uuid ids) (#17847)
🗃️ feat(db): add agent labels schema and migration Reintroduces #17844 with `uuid` primary keys instead of the prefixed nanoid that shipped there. `agent_labels.id` is now `uuid DEFAULT gen_random_uuid()` and `agent_label_assignments.label_id` follows it, since a foreign key must match the referenced column's type. The `albl` prefix is dropped from `idGenerator` — nothing else referenced it. The migration drops both tables before creating them. Its journal tag is unchanged from the reverted revision, so a database that already applied that one would otherwise skip past the guarded CREATE statements and keep text ids. Both tables are new, empty, and back a feature that is not live, so the drop costs nothing. Everything else carries over unchanged: per-scope name uniqueness excluding archived rows, and workspace-shared assignments. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c7ca43869b |
⏪ revert: agent labels schema and migration (#17844) (#17846)
Revert "🗃️ feat(db): add agent labels schema and migration (#17844)"
This reverts commit
|
||
|
|
c0d61e0b26 |
🗃️ feat(db): add agent labels schema and migration (#17844)
* 🗃️ feat(db): add agent labels schema and migration Introduce `agent_labels` and `agent_label_assignments` so agents can be tagged and grouped in the agents list. Labels live at workspace scope (`workspace_id`) or personal scope (`workspace_id IS NULL`) and are shared with every workspace member; `archived` retires a label without touching existing assignments. Schema + migration only — the data-access and UI layers land separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🗃️ feat(db): enforce agent label name uniqueness per scope Two partial unique indexes mirror the sibling registry in `agentSkills`: personal rows key on `(user_id, name)` where `workspace_id IS NULL`, workspace rows on `(workspace_id, name)`. Names are the only thing distinguishing two labels in the grouped agents list, and the workspace index also stops two members racing the same name into one workspace. Archived rows are excluded from both indexes so a retired name becomes available again. The trade-off moves the collision to un-archiving, which callers must surface as a rename prompt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
55ab22f1e6 | ✨ feat(database): add name column to agents table (#17821) | ||
|
|
f2a1ae5048 | ✨ feat: support notification context in inbox (#17624) | ||
|
|
86b0b492fe |
🗃️ feat(db): unique active workspace owner partial index (#17590)
* 🗃️ feat(db): unique active workspace owner partial index Owner is unique per workspace and bound to `workspaces.primary_owner_id`. It is only produced by ownership transfer, which demotes the previous owner in the same transaction. This partial index makes a second active owner unrepresentable rather than relying on that write path staying correct. Soft-deleted rows are excluded so a removed owner does not block the next one, and the transfer's demote-then-promote order stays conflict-free. Data is reconciled out of band: production was converged through the admin console before this ships. * ✅ test(database): align owner-clamp coverage with the unique index `denies owner access for a legacy non-primary owner label` seeded two active owner rows, which the new index makes unrepresentable. The clamp guards a different invariant the index does not constrain — that the surviving owner row belongs to `primary_owner_id` — so the test now moves the label instead of duplicating it. Adds coverage for the index itself: a second active owner is rejected with 23505 on `workspace_members_unique_active_owner_idx`, and a soft-deleted owner does not block a replacement. * 🐛 fix(test): keep the unique-owner rejection assertion type-safe The bare `.catch()` left the value typed as the query result union, so reading `.cause` failed type-check. Collapse the success branch to null first. |
||
|
|
b9158b8b6e |
✨ feat(database): add workspace_id column to notifications (#17565)
* ✨ feat(database): add workspace_id column to notifications * ⚡️ perf(database): add workspace_id-leading index for cascade deletes * 📝 docs(database): document online index build for notifications migration |
||
|
|
7be1bd9c69 | 🗃️ feat(db): add workspace topic comment schema (#17526) | ||
|
|
62689192ac |
✨ feat(agent-quota): account-scoped quota data layer + capacity calibration (#17314)
* ✨ feat(agent-quota): quota accounts upper layer — models, service, tRPC, QuotaMenu DB-first UI Squash of this branch's pre-split history: the DB schema/migration layer was extracted and merged separately as #17378; this base commit carries everything above it — models with credential encryption, AgentQuotaService (ingest / windows / calibration / LB selection), the agentQuota tRPC router, renderer service, and the DB-first QuotaMenu with the account manager modal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ✨ feat(agent-quota): scope accounts, bindings and quota facts by workspace Wire `workspaceId` end to end for the agent quota data layer: the tRPC `quotaProcedure` now passes `ctx.workspaceId` into every model, the six models filter through `buildWorkspaceWhere` and stamp inserts through `buildWorkspacePayload`, and the account identity unique index is split into the personal / workspace pair the rest of the repo uses — so the same external account can be registered once per scope. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🐛 fix(agent-quota): drop NUL-byte bucket keys in recalibrate Two raw NUL bytes had crept in as the map-key separator, which made git treat the whole service file as binary (no diffs, no `git grep`). Key on a JSON tuple and carry the (limitType, scopeKey) pair alongside instead of re-splitting the key, so a scopeKey containing the separator can neither merge two buckets nor be mis-parsed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🚧 chore(agent-quota): hide the account switcher entry until routing lands Switching an account today only moves bindings — `selectForAgent` is not wired into the spawn path yet, so the run still uses whatever credential the local CLI is logged into. Keep the menu read-only until that lands. Also refreshes QuotaMenu tests against the redesigned window row (bare `NN%` + short duration instead of `left:`/`resetsIn:` sentences, exhausted rows grey rather than warning) and the DB-first fetch: a rejected live request now degrades to the unavailable state instead of surfacing a raw transport error. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🌐 chore(agent-quota): sync en-US / zh-CN copy for the quota account keys Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🐛 fix(agent-quota): key the workspace account identity index by workspace only The workspace-mode unique index carried userId, which let two members of one workspace each hold a row for the same real provider account — and the load balancer would then count that account's capacity twice. Drop userId so the index matches what `upsertByIdentity` already does: the workspace-scoped lookup ignores userId, so a teammate observing the same login updates the existing row instead of adding a copy. Covered by a new case in agentQuota.workspace.test.ts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🐛 fix(agent-quota): open the account manager modal via createModal's own mount `createModal()` mounts the modal as it is called and returns a ModalInstance that has no `open()` — the extra call broke the type-check. Renamed the factory to `openQuotaAccountManagerModal` to match the repo's other modal call sites, so the "calling it opens it" contract is visible at the call site. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🐛 fix(agent-quota): calibrate on ingest, accept string epochs, prefer live over an empty panel Three review findings: - `recalibrate` had no caller, so the calibration table could never fill. Ingestion is the only moment new evidence arrives, so trigger it there. It stays self-guarding: without ledger spend every window reads as contaminated and no capacity row is written rather than a fabricated one. - `parseResetsAt` passed every string to `Date.parse`, but the API types `resets_at` as `number | string`, so a quoted epoch parsed to NaN and the reading was dropped. Worse, a short digit string like "1784" was read as a year. Parse digit strings as epochs first. - The live snapshot was only used as a fallback when no account existed. An account with zero persisted windows therefore rendered "unavailable" while a healthy live fetch sat unused. Prefer persisted data, else the live snapshot, else unavailable. Adds a service-level test that drives the real ingest path end to end (readings + ledger in → windows + calibrated capacity out) plus its inverse, and QuotaMenu cases for both fallback routes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🗃️ chore(agent-quota): regenerate the workspace-identity migration as 0126 Canary took 0125 for the stats-activity indexes while this branch was in flight, so the workspace identity-index migration is regenerated after the rebase with the next free number, per the db-migrations convention. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🚚 refactor(agent-quota): gather the quota menu family into a QuotaMenu folder Eight quota files were spread flat across HeterogeneousChatInput. Move the generic menu, both provider menus, the account switcher + manager modal, and the view model into QuotaMenu/, with index.ts exposing just the two menus HeteroControlBar consumes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
16a865b8c8 | ⚡️ perf(db): index activity sources by creation time (#17414) | ||
|
|
a899d62232 |
🗃️ feat(database): add resource_permissions table and messenger credential columns (#17300)
* 🗃️ feat(database): add resource_permissions table Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🗃️ feat(database): fold messenger account link credential columns into 0123 * 🔒 fix(messenger): exclude credentials from default account-link projections Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🐛 fix(messenger): refresh credentials and applicationId on link re-verify Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🗃️ feat(database): enforce uniqueness for messenger credential application ids Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🐛 fix(messenger): surface application-id conflicts from account-link upserts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🐛 fix(messenger): map application-id conflicts on credential refresh updates Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🐛 fix(messenger): read pg constraint diagnostics from both error levels Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0ad9a11163 |
✨ feat(acceptance): per-check user review with feedback loop (#17271)
* ✨ feat(acceptance): per-check user review with feedback loop
- user-level accept/reject on each acceptance check (dual check icon:
verifier verdict + user confirmation; gray circle = awaiting user)
- reject-with-comment per item, incl. circled-region comments drawn on
evidence screenshots; feedback renders under the check's evidence
- consumed feedback (older rounds) folds into the iteration history
- group-level accept-all button
- header rework: origin agent/topic line; branch/commit/PR chips moved
into the latest-report summary card
- acceptances.check_reviews jsonb + reviewChecks trpc + bundle overlay
- CLI acceptance view: USER column + user-feedback section; agent-testing
skill reads acceptance state + user feedback before the next round
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 💄 style(acceptance): polish review UI — all-verified receipt & reject modal layout
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ✨ feat(verify): re-land acceptance visibility as the per-aggregate override
Reverts
|
||
|
|
a06ec65d75 |
👷 build: add work registry tables (#17193)
* 👷 build: add work registry tables * ♻️ refactor: require work tool identifiers * ♻️ refactor: simplify work version agent attribution * 🐛 fix: add work visibility scope * 👷 build: regenerate work registry migration * ♻️ refactor: replace works.rootOperationId with immutable origin provenance works.rootOperationId was a latest-wins single-value slice of an N:M relation (an edit from another conversation stole the summary card from the conversation that created the Work). Replace it with immutable origin columns (originTopicId/originThreadId/originAgentId) stamped once at identity-row insert, powering upcoming "works created in this topic / by this agent" filters. Query/model changes live in the feature PR. |
||
|
|
5cf578c39f |
👷 build(database): add OIDC client governance schema (#17228)
* 👷 build(database): add OIDC client governance schema * 🗃️ chore(db): regenerate OIDC client migration for canary |
||
|
|
7cd62b70cf |
🗃️ feat(db): add acceptance aggregates (#16899)
* 🗃️ feat(db): add acceptance aggregates * ♻️ refactor(db): reuse verify reports for acceptances * 🗃️ feat(db): add acceptance visual render and delivered status Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 📝 docs(db): narrow finalVerifyRunId to write-once closure semantics Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🗃️ refactor(db): drop redundant acceptances.final_verify_run_id A terminal status freezes the chain, so the closing round is always current_verify_run_id; mid-loop rejections are per-round events on verify_runs. The pointer would either lose the rejection trail (overwritten) or duplicate current (write-once). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🗃️ feat(db): add per-round user acceptance decision to verify_runs The user's accept/reject on a round is the human verdict that drives the acceptance loop, so it lives top-level on verify_runs (not in metadata): - user_decision (text, indexed) — the queryable verb; free-form, not an enum column, so a new decision verb needs no migration. Null until decided. - decision_detail (jsonb) — provenance bag: comment, fileIds, decidedAt/By. Per-round because every delivered round can be judged again; the trail lives on each round rather than a single overwritten aggregate pointer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🗃️ fix(db): require round index on acceptance-linked verify runs The verify_runs_acceptance_round_unique index could not order a chain when round_index was null: Postgres treats NULLs as distinct, so several null-round rows could pile onto one acceptance_id. Add a CHECK constraint (acceptance_id IS NULL OR round_index IS NOT NULL) so a linked run always occupies exactly one position; both columns stay nullable for standalone/legacy runs where neither is set. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🗃️ refactor(db): drop derivable acceptance run/report pointers root/current verify run and latest report are all derivable from the round chain via the verify_runs (acceptance_id, round_index) unique index — root = min round, current = max round, latest report = report of the highest round that has one. The denormalized pointers bought no read win at this per-user, bounded list scale and only added a write-time sync burden plus a staleness bug surface, so drop the three columns and their indexes. The composite unique index that backs the derivation stays. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
46dffcb24c |
🗃️ refactor(db): replace workspace_user_settings composite PK with surrogate id (#17042)
* 🗃️ refactor(db): replace workspace_user_settings composite PK with surrogate id * 📝 docs(skills): forbid composite PKs on new tables in drizzle skill * 💄 style: prettier format drizzle skill |
||
|
|
96c29a8d2e |
🗃️ feat(db): add task_topics.trigger and user_connectors.agent_id (combined schema) (#16718)
* 🗃️ feat(db): add task_topics.trigger and user_connectors.agent_id schema Consolidate the DB-layer schema changes from two feature branches into a single migration on canary (both source PRs independently claimed 0115/0116 against different bases, which would collide on canary): - `task_topics.trigger` ('manual' | 'schedule' | 'heartbeat'), nullable, so automation-quota counting can distinguish ad-hoc runs from scheduled/ heartbeat ticks (schema from #16716). - `user_connectors.agent_id` (FK → agents.id, ON DELETE CASCADE), nullable, for the Agent > Workspace > Personal connector dimension, replacing the single (user_id, identifier) unique with two partial unique indexes so an agent-scoped row can coexist with the personal/workspace row of the same identifier (schema from #16562). Both changes ship in one migration `0116_add_task_trigger_and_connector_agent_id`, additive and idempotent. No read/write path consumes the new columns in this PR — the feature logic lands in the respective PRs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 🗃️ chore(db): combine 0116 schema changes --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
74383656b9 |
✨ feat(database): add workspace visibility columns and indexes for pr… (#16598)
* ✨ feat(database): add workspace visibility columns and indexes for private mode Add a `visibility` text column ('public' | 'private', default 'public') and a `(workspace_id, visibility, user_id)` composite index to workspace-scoped tables: agents, chat_groups, documents, files, generation_topics, knowledge_bases, session_groups, tasks, and related task_* tables. Enables per-row private-in-workspace filtering without a join back to a parent table. * 🚨 fix(types): backfill visibility on GenerationTopicItem and ChatGroupItem mocks |
||
|
|
52729750e2 |
✨ feat(verify): make the verify page the final report (scenario scope + conclusion + score) (#16140)
* 📝 docs(agent-testing): add Step 4 — publish the report to LobeHub via the CLI The local `.records/reports/` output is the working artifact; the deliverable is the report opened in LobeHub. Add a mandatory Step 4 to the skill contract: after filling report.md / result.json, run `lh verify ingest-report <dir> --open` to upload the session (cases → check results, evidence files → storage, report.md → report body) and surface the in-app `/verify/<verifyRunId>` link. - SKILL.md: contract gains "Step 4: Publish to LobeHub" + a new section covering the aggregate ingest, the result.json case shape, the atomic commands (run/result/evidence/report), and the storage caveat for stub buckets. - references/report.md: workflow step 5 (publish) + document the case→check-result field mapping (name/status/observation/evidence) in the result.json schema. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ✨ feat(verify): make the verify page the final report (scenario scope + conclusion + score) Turn /verify/<id> into a self-contained report instead of a chat-adjacent summary, and give a verification session a scenario so reports can render per-scenario. Schema (migration 0114, additive + nullable): - verify_runs.scenario — what the session verifies (e.g. `coding`) - verify_runs.context — the scenario's scope bag (branch/commit/surfaces/…) - verify_runs.metadata — generic, reserved for future cross-scenario extension Server / CLI: - createRun accepts scenario + context - ingest-report lifts scope from result.json, maps summary.conclusion → report summary and summary.score (0-100) → overallConfidence, sets evidence caption to the filename (no longer the case title), and no longer aborts the whole ingest when one evidence file upload fails (warn + skip) Viewer (ReportViewer): - scope header (per scenario), conclusion callout under it, and a `score` stat alongside total/passed/failed - compact check cards (title shown once), narrative tail rendered as "Details" - drop the redundant full-markdown dump that duplicated the check table agent-testing skill: result.json is the report (scenario/context/cases/ summary.conclusion+score); report.md is the narrative tail only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 📝 docs(agent-testing): correct the failed-upload recovery note The Step 4 note read "the PUT fails — fall back to inline evidence … the rest of the ingest still succeeds", which both contradicted itself and hid the real outcome. ingest-report catches the upload error and SKIPS that one artifact, so the session/results/report do complete — but the published session is then MISSING that evidence, which is easy to mistake for a complete report. Say that plainly: skipped-with-warning, session still openable, evidence absent; use real storage or inline `--content` if the evidence must appear. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |