Commit Graph

844 Commits

Author SHA1 Message Date
Neko Ayaka b9917c1894 🐛 fix(quick-note): preserve pinned document history revisions 2026-09-19 23:38:06 +09:00
AmAzing- c1dfecbdd3 🔧 chore(release): refresh weekly candidate with six canary PRs (#19432)
Co-authored-by: Innei <i@innei.in>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2026-09-11 16:30:57 +08:00
Rdmclin2 f2a0901ef1 🐛 fix: harden bot messaging and channel setup (#19405)
* 🐛 fix: harden bot messaging and channel setup

* 🐛 fix: retain bot messages across sender and replay races

* 🐛 fix: retry deferred bot replay independently

* 🐛 fix: preserve command turns and debounce owner lookup
2026-09-11 12:30:23 +08:00
Rdmclin2 8171469b80 ✨ feat: read Feishu documents and preserve rich-text images (#19401)
* ✨ feat: read Feishu documents and preserve rich-text images

* 🐛 fix: bound Feishu markdown image scanning

* 🌐 chore: refresh translations after rebase
2026-09-11 11:18:39 +08:00
Rdmclin2 91a77ae585 ✨ feat: support member mentions in workspace documents (#19402)
* ✨ feat: support member mentions in workspace documents

* 🌐 chore: refresh generated translations

* 🐛 fix: enforce knowledge base access for document notifications

* ✅ test: include mention plugin in readonly editor mock
2026-09-11 09:15:36 +08:00
Arvin Xu f6660c1666 🐛 fix(goal): recover pipeline failures instead of escalating them to a person (#19377)
* 🐛 fix(goal): recover pipeline failures instead of escalating them to a person

Supervision only acted on a failure that had already produced an errored
operation. Dispatch failures, a dropped device link and a crashed verifier
never take that shape — the run either never started or finished cleanly and
something around it broke — so the whole class escalated to a human decision
gate. In a live run these were the only three interruptions, all of them
recorded as `Only a confirmed failed operation can be recovered`, and each one
stopped a long-horizon goal until someone clicked retry.

- Classify a failure by where it happened. An `agent-run` failure is still
  judged by its own error text; a `pipeline` failure recovers without one,
  because retrying re-uses authority the goal already granted.
- Keep the human gates that matter: an explicit interruption, a limit stop, a
  cancelled Task, exhausted attempts, and anything reading as credentials,
  permission or spend.
- Route `VERIFICATION_ERRORED_ERROR` to the verification recovery branch. A
  verifier that could not run never judged the delivery, so it is infrastructure
  rather than a verdict; without a branch it fell through to the gate.
- Move the three Task error contract strings into `@lobechat/const/goal` so the
  side that writes one and the coordinator that matches on it cannot drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(goal): apply pipeline recovery on the status the Task actually holds

Codex review caught the previous commit widening eligibility without widening
the step that acts on it, so the fix was a no-op for its own target case.

- The recovery compare-and-set expected `failed`. A pipeline failure leaves the
  Task `paused`, so the swap never matched, the incident was marked escalated
  and the goal fell back to the human gate anyway. Compare against the status
  revalidated inside the transaction instead.
- A kickoff that threw before writing a run has no operation to key an incident
  on, and `reviewFailure` returned early on exactly that shape. Key those on the
  Task and its attempt so they are recorded, deduplicated and budgeted like any
  other incident.

Both regressions are covered by tests that fail without these changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ♻️ refactor(goal): drop the half-built operation-less supervision path

Codex review found two consequences of keying an incident on a synthetic id:
`TaskRunnerService` leaves `totalTopics` unchanged when a retry also fails before
writing a topic, so a repeated dispatch failure reused the first key and fell
through to the gate anyway; and `recordProgress` settles an incident by locating
the failed run's sequence, which a synthetic key can never match, so the incident
stayed `retrying`, understated recovery metrics and could relabel a later
recovery as unsuccessful.

Both follow from incident identity and settlement being anchored on a real run.
Making the operation-less case correct means redesigning that, which is its own
change rather than a rider on a policy fix — and a half-built version is worse
than none, because it writes incidents that can never settle. Restore the early
return and leave that case on the human gate.

What this PR fixes is unchanged and covers every interruption observed live:
a settled run whose Task is left `paused` now recovers instead of escalating.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(goal): classify by the operation, not the Task status

Review found the origin test read the wrong signal. `TaskLifecycleService`
stores an ad-hoc run that fails as `paused` with an errored operation — the same
Task shape a pipeline failure leaves — so requiring `failed` labelled real agent
errors as pipeline failures and routed them around the credential and transport
checks this PR claimed to keep. An errored operation now decides it, whatever
the Task holds.

Also bound the statuses supervision may restart to the two the coordinator
actually routes to recovery. The previous commit compared the swap against
whatever status the Task held, which would have accepted a terminal one; a
completion recorded during a diagnosis must never be swapped back to `backlog`
and rerun. Guarded in the policy and again at the swap itself.

The misclassification has a regression test that fails without the fix. The
terminal-status guard is defence in depth: it is not reachable through the
coordinator, which does not route a settled Task to recovery, so its test pins
the invariant rather than reproducing a failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(goal): recognise recoverable dispatch failures instead of inferring them

Review found a fifth state that looked like a pipeline failure and was not: a
caller marking a Goal-linked Task `failed` after a clean run. Recovery would
reopen it and spend more paid work despite an explicit terminal decision.

That is a pattern, not a case. Inferring a recoverable failure from the *absence*
of a run error is an open set: every state an actor can author arrives looking
exactly like a dropped dispatch, so each round found another one. Replace it with
one allowlist read off whatever error text exists, applied to both origins.

Recoverable now means a recognised transport failure or a gateway code whose own
message states the run never started, so a retry cannot duplicate committed work:
`DEVICE_OFFLINE`, `DEVICE_CHANNEL_UNAVAILABLE`, `DEVICE_GATEWAY_UNREACHABLE`,
`DEVICE_GATEWAY_RATE_LIMITED`. The codes that say retrying will not help
(`DEVICE_GATEWAY_UNAUTHORIZED`, `GATEWAY_NOT_CONFIGURED`) or that the outcome is
unknown (`DEVICE_RESPONSE_TIMEOUT`) stay with a person, as does an authored
failure carrying an arbitrary error or none.

`failureOrigin` goes away with the inference it existed to express. The three
observed interruptions are still covered: they carried gateway codes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(goal): recognise a dispatch failure in either shape it is stored as

Review found the allowlist read only one of the two forms a dispatch failure
takes in `task.error`. The coordinator's own dispatch persists the raw gateway
payload — confirmed against a live run, where the gate reason was
`{"error":"DEVICE_OFFLINE","success":false}` — while the runtime's
`finalizeHeteroDispatchError` persists the humanized headline, which contains no
code. Matching codes alone therefore still opened the gate this PR exists to
remove, on the path that finalizes through the runtime.

Match both, derived from the one headline map rather than copied text, so the
storage shape does not decide whether a person is needed.
`HETERO_DISPATCH_ERROR_HEADLINES` is exported for that.

`DEVICE_NOT_FOUND` is also called out as staying with a person: reconnecting or
rebinding the device is not something a retry does.

The regression asserts the actually persisted value through
`humanizeHeteroDispatchError`, not an injected raw code, and fails without the
headline match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(goal): allow the gateway's own 5xx code into transient recovery

Review found `DEVICE_GATEWAY_ERROR` missing from the allowlist. The gateway
client emits it for any status at or above 500, and its headline states outright
that nothing started on the device and the failure is usually temporary — which
is the criterion the allowlist was written around. The fallback transport regex
only covers 502-504, so an HTTP 500 dispatch failure matched nothing and still
opened a human gate.

Regression covers both persisted forms, the raw code and the humanized headline,
and fails without the entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(goal): never restart a Task an actor already settled

Two ways a terminal decision could be overturned and paid work launched.

- `TaskService.updateStatus` replaces `error` only when a new one is supplied,
  so an actor can move a pipeline-paused Task to `failed` and leave the transport
  text that paused it, which the allowlist then reads as a dropped dispatch. The
  activity log records assignee changes rather than transitions, so the author is
  not readable; require the errored run instead. Nothing the system does writes
  `failed` with a recoverable error — the watchdog's is `Heartbeat timeout`.
- `TaskRecoveryCoordinator.recover` rejected only `running`, then swapped
  whatever status it found to `running` and spawned a run. A person completing or
  cancelling between the tick's read and the claim would have their decision
  overwritten. This path bypasses supervision entirely, so the policy guard did
  not cover it; bound the claim to the statuses that await recovery and report
  `settled` so neither call site opens a gate over somebody's own decision.

The claim race predates this branch on the lease path; routing the verifier error
here widened its reach, so both are covered by the regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(goal): claim only the paused state recovery was routed for

Review found the shared recoverable set was the wrong bound here: it includes
`failed`, so a person marking the Task failed between the tick's read and the
claim still passed the guard and had their decision swapped to `running`.

Both branches that reach this coordinator require `paused`, so that is the only
status the claim may take, and the rollback releases to the same one. Coverage
now spans a concurrent move to `completed`, `canceled` and `failed` across both
the lease and verifier-error routes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ♻️ refactor(goal): give a Task's return to work one gate

Three rounds of review each found another entrance through which automatic
recovery could overwrite a decision somebody made, because no single place owned
the question of who may put a Task back into work. Patching entrances found the
next entrance; this collects the rule instead.

`claimGoalTask` is now the only way a Goal moves a Task to `running` or
`backlog`, and it states the invariant the three sites were getting right or
wrong independently: claim against the status the decision was made on, never a
fresh re-read. Losing the compare-and-set is the correct outcome — it means the
row moved, so the decision is stale and belongs to whoever moved it.

Dispatch already worked this way, which is why it was never implicated. The
recovery coordinator re-read first and is now pinned to the routed `paused`.
Supervision had the widest window of the three: a diagnosis runs for minutes, so
the incident records the status it opened on and claims against that, rather than
only catching a change since the current tick.

Regression covers a Task moved to another recoverable status mid-diagnosis, which
the status allowlist accepts and only the claim rule refuses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(goal): escalate a diagnosis that cannot say what it opened on

The compatibility fallback restored the race it was added beside: an incident
persisted before `taskStatus` existed claimed against a later read, so a person
moving the Task during such a diagnosis would have that undone.

An incident with no opening status cannot show the row is still the one it
decided about, so it escalates instead. A rolling deploy leaves at most the few
mid-diagnosis, and a human gate is the right answer for them.

Covered by a regression that strips the field the way an older release left it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(goal): ask who wrote a Task's status, not what its error says

The previous guard read the error, and the error cannot tell these apart: when a
run errors with a recoverable transport failure the lifecycle leaves an errored
operation and a paused Task, and a person marking that Task `failed` without
supplying a new one keeps the same text. Requiring the errored run therefore
passed exactly the case it was meant to stop, and supervision would put an
explicit failure back to `backlog` and spend more.

An earlier commit asserted the author was not readable. That was wrong, and the
comment saying so is gone: `status` is a tracked column, so `updateWithLog`
records an actor on the transition, and `TaskService.updateStatus` documents that
its absence is how system transitions opt out of the feed. That makes actor
presence the discriminator this needed all along.

`statusAuthoredByActor` reads the latest tracked transition, requires it to be
the one that produced the current status, and both eligibility calls pass it.

Regressions cover the helper's edges and the end-to-end shape: errored run,
paused by the lifecycle, then failed by a person.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(goal): read the durable actor kind, and honour a pause a person made

Two gaps in the provenance check, both confirmed against the code that writes it.

- It read only the actor id columns. Those are `ON DELETE SET NULL`, and
  `taskActivityActor` writes `actorKind` into the payload for exactly that
  reason: "someone who is gone must not read as the system". An authored failure
  became recoverable once its author left the workspace. Read the kind first and
  keep the ids as the fallback for rows written before it.
- Verifier-error recovery checked only that the Task was still `paused`. A person
  moving it `paused → failed → paused` keeps the routing error, because the status
  update replaces it only when a new one is supplied, so the final pause is
  theirs and the status alone cannot say so. It now asks the same question the
  supervisor does and reports `settled`.

Covered by the tombstoned-actor shapes and by the manual round trip end to end.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 07:35:34 +08:00
Arvin Xu d2caf84fdd ✨ feat(acceptance): serve the collaboration table, and let a round introduce itself (#19393)
* ✨ feat(acceptance): serve the collaboration table, and let a round introduce itself

The second of three layers. It stacks on the table PR and is called by
the UI PR above it; on its own it adds an endpoint nobody invokes yet.

`acceptanceComment` covers create, delete, list, resolve and an emoji
toggle. Reads are addressed by acceptance id alone and the handler
decides visibility, so a public share link works without a session;
writes need the creator or a non-viewer member of the acceptance's own
workspace. A missing right reads as NOT_FOUND rather than FORBIDDEN,
which is what the bundle read already does.

The read does the joining the page would otherwise do by hand: author
profiles, membership status, round indexes for a comment's context run,
attachment ids resolved to display URLs as the aggregate's owner, and
reaction rows folded into the comment they answer.

`lh acceptance run ingest` gains the round's own note. An agent writes
`proposal.md` beside the report; ingest posts it as a comment carrying
that round's id and signed by the agent, so a delivery introduces itself
the way a pull request does. It is the last step and never fatal — against
a server without the endpoint the round still publishes, minus the note.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(acceptance): scope comment agent and run/evidence references

A comment carries three references the schema alone does not constrain: the
agent it speaks as, the round it was written against, and the evidence tile it
is pinned to. Access is resolved from `acceptanceId` only, and the foreign keys
prove global existence rather than association, so any participant could name
rows from an acceptance they cannot read — or sign a comment as somebody else's
agent and expose its name, title and avatar through a public acceptance link.

Gate `authorAgentId` through `assertAgentUsableBy` in the acceptance's own
workspace, the scope the agent is being presented to, and verify that
`contextRunId` and `anchor.evidenceId` resolve back to the target acceptance
before insert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(acceptance): categorize the comment namespace for API key scopes

The scope guard fails closed on any namespace missing from
`TRPC_NAMESPACE_API_KEY_RULES`, and a coverage test makes sure a newly mounted
router is categorized on purpose rather than silently denied. `acceptanceComment`
follows the acceptance it hangs off: blocked for restricted keys, reachable with
a full-access one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 23:13:50 +08:00
Innei 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'.
2026-09-10 22:18:49 +08:00
Arvin Xu 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>
2026-09-10 21:11:08 +08:00
Arvin Xu 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>
2026-09-10 19:12:47 +08:00
Arvin Xu 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>
2026-09-10 18:03:13 +08:00
Coooolfan 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
2026-09-10 16:39:30 +08:00
Neko 3b0d29cd54 🐛 fix: harden device connection presence (#19352) 2026-09-10 16:12:10 +08:00
Neko 30005de789 🐛 fix: prevent stale heterogeneous finish snapshot rollback (#19353) 2026-09-10 15:07:46 +08:00
YuTengjing 7be9722ae1 🐛 fix: restrict agent share rollout checks to creators (#19380) 2026-09-10 14:55:44 +08:00
YuTengjing 2d481065b5 🐛 fix: support Serverless indices without internal UUIDs (#19381) 2026-09-10 13:27:04 +08:00
Arvin Xu 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>
2026-09-10 09:55:15 +08:00
Rdmclin2 4bf05e2499 🐛 fix(bot): surface budget scope and headroom in bot failure replies (#19336)
* 🐛 fix(bot): surface budget scope and headroom in bot failure replies

Carry the budget context of an InsufficientBudgetForModel error from the
runtime lifecycle event through the hook payload into the bot reply, and
render a scope-aware message (member allowance vs. workspace pool) with
the remaining and required credits instead of the generic
"Agent Execution Failed".

LOBE-13726

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKPxmeT4DrAdKP3RUCYzgp

* 🌐 style: update i18n

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKPxmeT4DrAdKP3RUCYzgp

* 🔒 fix(bot): stop quoting the billed allowance in budget replies

Runs mentioned from a shared channel are billed to the bot owner, so a
reply that quoted availableCredits / requiredCredits published the
owner's balance to every channel member. Keep the scope-aware copy and
drop the amounts line; a regression test asserts no figure is rendered.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKPxmeT4DrAdKP3RUCYzgp

* 💬 fix(bot): address the bot owner and split plan-limit copy in budget replies

Runs are billed to the bot owner, not to whoever mentioned the bot, so the
budget copy no longer calls the reader the payer ("your budget") and
points at the bot owner / workspace admin instead. FreePlanLimit and
SubscriptionPlanLimit get their own copy (the allowance itself is
exhausted, so the fix is the plan, not a cheaper model), and the
budgetTypeAtError scope refinement now applies to all three admission
codes since the workspace gate throws SubscriptionPlanLimit when a run
can't be priced upfront.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKPxmeT4DrAdKP3RUCYzgp

* 💬 fix(bot): cover both meanings of SubscriptionPlanLimit in the reply

Per the error taxonomy the code fires when the paid-plan allowance is
reached OR when the plan tier does not cover the requested model, so the
copy no longer claims the credits are spent and offers upgrade / top-up /
custom API as the remedies.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKPxmeT4DrAdKP3RUCYzgp

* 👷 build: serialize build scripts so editor postinstalls stop racing on lexical

Three peer-variant copies of @lobehub/editor patch the same hoisted
lexical files in postinstall; run concurrently they read each other's
half-written output and fail with "Patch context mismatch". Only the
allowBuilds packages run scripts, so childConcurrency: 1 costs seconds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKPxmeT4DrAdKP3RUCYzgp

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 23:30:38 +08:00
YuTengjing dac6eb0fdc 🐛 fix: attach registered Works to the final assistant reply (#19345) 2026-09-09 21:36:49 +08:00
Arvin Xu 20f16ef4cd 🐛 fix(goal): create experiments only for planned hypotheses and fail dead dispatches (#19339)
* 🐛 fix(goal): create experiment containers only for planned hypotheses

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* 🐛 fix(task): fail the kickoff when execAgent reports a dispatch failure

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ✅ test(goal): assert the v5 decomposition prompt cohort

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 20:36:03 +08:00
YuTengjing 6261fa7f84 🐛 fix: surface sandbox recreation in tool results (#19329) 2026-09-09 18:02:25 +08:00
Arvin Xu 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>
2026-09-09 17:42:25 +08:00
Arvin Xu 33ba35e6cb 💄 style(apikey): redesign API Key management (#19328)
- List: description under the title, monospace masked key (prefix + last 4),
  scope summary tag (full access / N scopes), disabled/expired status,
  relative last-used time, and a row "…" menu (view details / delete)
  replacing the trailing chevron
- Create modal: expiration presets (7/30/60/90/180 days, 1 year, never,
  custom date) with a guard against submitting "custom" without a date;
  after creation the modal shows the full key with a copy button
- Server: createApiKey returns the plaintext key once (createWithPlaintext)
  so the create flow can reveal it; list/detail reads are unchanged

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 17:36:40 +08:00
Rdmclin2 d3c25f2a95 🐛 fix(bot): read Feishu chat history and decode WeChat voice (#19323)
* 🐛 fix(bot): read Feishu chat history, decode WeChat voice and gate personal-file visibility toggles

- Feishu/Lark (LOBE-13803): inject the current `{ platform, channelId }` into the
  bot platform prompt so `readMessages` no longer asks the user for a group ID;
  explain `230027` and friends with the exact scope to add (`im:message.group_msg`);
  surface Lark `permission_violations` in API errors; map unicode reactions to
  Feishu `emoji_type` and track `reaction_id` so step swaps replace instead of
  stacking; decide the no-@mention exemption from real chat membership
  (`user_count`/`bot_count`, fail closed) instead of who has spoken.
- WeChat (LOBE-13407): decode SILK voice to WAV on download via `silk-wasm` so
  the attachment is playable in the browser and usable by audio models.
- ResourceManager: only offer "Make private" / "Publish to workspace" for the
  creator's own files inside a workspace; personal-mode rows default to
  `visibility = 'public'` and were wrongly offered the toggle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5Mi6LJCFiMnej3MPAjMpJ

* 🐛 fix(feishu): read the newest history page and bound solo-chat cache staleness

Address the Codex review on #19323:

- `readMessages` and the chat-SDK `fetchMessages` asked Feishu for history
  without `sort_type`, which defaults to ascending — in any chat past the
  page cap the model got the chat's oldest messages instead of what was just
  said. Request `ByCreateTimeDesc` (kept on every cursor page) and flip the
  page so the documented chronological order still holds; `forward` maps to
  ascending.
- A cached "solo" verdict kept the bot answering every unmentioned message for
  10 minutes after someone joined the chat. Positive verdicts now expire after
  60 s; only "shared" verdicts keep the 10-minute TTL.
- Without agent-runtime Redis the verdict was never cached, costing a blocking
  Feishu request per inbound message. Fall back to a bounded process-local
  cache with the same TTLs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5Mi6LJCFiMnej3MPAjMpJ

* 🐛 fix(bot): skip membership lookups for addressed messages and keep the reaction id until deletion succeeds

Second Codex round on #19323:

- `onSubscribedMessage` only consults `isSoloBotConversation` when a DM or
  @mention has not already settled the question, so a lapsed cache never puts
  a platform round-trip in front of a direct message.
- The Feishu final reaction clear forgot the tracked `reaction_id` before the
  remote delete ran; a transient failure left the reaction visible with
  nothing to retry with. Delete the tracker entry only after removal succeeds.
- Fix the `result.messages` possibly-undefined type error CI caught in
  `service.test.ts`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5Mi6LJCFiMnej3MPAjMpJ

* 🐛 fix(bot): keep every placed Feishu reaction id until removal succeeds and scope Slack history injection

Third Codex round on #19323:

- Feishu reaction tracker keeps a list of ids per message, in Redis or a
  bounded process-local cache when agent-runtime Redis is absent (local runs
  used to stack every step reaction). A swap that adds the next reaction but
  fails to remove the previous one keeps both ids; the final clear retries
  removal in place before handing leftovers back to the tracker, since neither
  the bridge nor the queue callback retries it. Legacy bare-id keys still read.
- `chatComposition` shares the same bounded memory cache helper.
- New optional `PlatformClient.extractConversationId`: Slack declines for reply
  threads, so the injected current conversation never points `readMessages`
  (channel history only) at the parent channel of a thread.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5Mi6LJCFiMnej3MPAjMpJ

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 17:25:05 +08:00
Arvin Xu 679380bf56 ⚡️ perf(agent-runtime): stop persisting step events to the reader-less agent_runtime_events list (#19321)
Every step LPUSHed its full event array (all streamed text/reasoning
deltas plus tool events, with finalState snapshots measured at 133KB per
entry) into agent_runtime_events, trimmed to 200 entries with a 2h TTL —
and nothing in the codebase, the closed-source cloud layer, DC, or the
agent-gateway ever read the key back. The same events already reach
clients through the live stream publish and land durably in the
operation trace.

Dropping the write removes a full second copy of every streamed byte
from Redis bandwidth, the largest single contributor to state-store
storage growth, and 3 commands per step. Existing keys expire via TTL
within 2h.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-09 14:50:21 +08:00
YuTengjing e9b1d082f3 🐛 fix: honor user media capabilities in server agent runs (#19296) 2026-09-09 13:30:25 +08:00
Arvin Xu 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
2026-09-09 12:46:15 +08:00
Arvin Xu 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
2026-09-09 11:59:41 +08:00
Rdmclin2 45e923e302 ✨ feat(workspace): mirror account settings, inject workspace context, keep acceptance in scope (#19299)
* ✨ feat(workspace): mirror account settings, inject workspace context and keep acceptance in scope

- LOBE-13896: mirror the account-level settings tabs (profile / appearance /
  hotkey / messenger / advanced / labs / about) under `/:workspaceSlug/settings`
  and regroup both settings sidebars (Account → Personal/Workspace → … →
  System → Developer)
- LOBE-13895: add `WorkspaceContextInjector` so the system prompt carries the
  app origin and the workspace name/slug, with slug-prefixed link rules, on
  both the server runtime and the client context pipeline
- LOBE-13898: open a task's acceptance in the side panel instead of navigating
  to the workspace-less `/acceptance/:id` page, which dropped the slug and
  flipped the app back to personal scope

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YBAZ3KE8AknkvY1ZDKJJy

* 🐛 fix(workspace): keep the workspace name out of the prompt and register the mobile hotkey route

- only the validated slug is injected into `<workspace_context>`; the
  admin-controlled display name never reaches the system prompt
- mirror `/:workspaceSlug/settings/hotkey` in the mobile router so the
  allowlisted path resolves there too

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YBAZ3KE8AknkvY1ZDKJJy

* 🐛 fix(prompts): trim trailing slashes without a polynomial regex

CodeQL flagged `/\/+$/` on the app origin as polynomial on long runs of '/';
a character loop is linear and equivalent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YBAZ3KE8AknkvY1ZDKJJy

* 🐛 fix(prompts): scope the workspace link rules to in-app links

The link rules forbade lobehub.com outright, which also stopped the model from
linking the LobeHub homepage in ordinary chats. They now apply only to
self-composed links into the app.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YBAZ3KE8AknkvY1ZDKJJy

* ✅ test(task): cover the acceptance-row click with a hook regression test

Extracts the panel-opening behaviour into `useOpenAcceptanceInPanel` so the
LOBE-13898 fix is asserted in a hook test: it toggles the task panel and opens
the acceptance, and never routes to the workspace-less `/acceptance/:id` page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YBAZ3KE8AknkvY1ZDKJJy

* ✅ test: repair the two canary-red suites so Test CI can pass

- `ApiKey.test.tsx`: `@lobehub/ui@5.40.2` (published 2026-09-08, bumps
  `@base-ui/react` to 1.8.0) no longer toggles the dialog checkbox on a
  synthetic Space key or a bare click on the `role=checkbox` span; click the
  wrapping label instead, which is what a real pointer hits (LOBE-13941)
- `localization.test.ts`: drop the `content` field that #19293 removed from
  `BuiltinSkillManifest` (LOBE-13942)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YBAZ3KE8AknkvY1ZDKJJy

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 11:02:54 +08:00
YuTengjing 13ada65ad9 ✨ feat(fts-search): safe per-entity Elasticsearch mapping migrations (#19186) 2026-09-09 09:03:40 +08:00
Innei a5e74a9eb4 ⚡️ perf(spa): cut the editor runtime, ajv and database models from the boot path (#19294)
* ⚡️ perf(spa): cut the editor runtime, ajv and database models from the boot path

Four package-level import edges put ~1.5 MB raw on the first screen:

- GlobalProvider/Editor.tsx imported @lobehub/editor/react, and rolldown does
  not shake that entry down to EditorProvider, so lexical, yjs and fuse landed
  in the entry chunk. The provider is reached by path until the package
  exposes a provider-only entry.
- @lobechat/types re-exported the ajv-backed AgentGraphSchema through the
  barrel that every store touches; it now lives on the
  @lobechat/types/agent/graph subpath and the agent barrel re-exports types
  only.
- @lobechat/context-engine's root barrel dragged the database models barrel
  (drizzle) in through AgentDocumentInjector; the app reaches the
  ./agent-document subpath and the injector reads loadPolicy directly.
- packages/locales bundled locales/en-US/*.json on top of the default
  sources it mirrors; the overlay now applies only in development.

Web production build: first-screen gzip 2299 KB -> 1968 KB.

Claude-Session: https://claude.ai/code/session_01QKr8CsFLYmrgqbRTSipTNH

* 🐛 fix(vite): keep one EditorContext in dev for the path-imported provider

Claude-Session: https://claude.ai/code/session_01QKr8CsFLYmrgqbRTSipTNH

* 🐛 fix(locales): refresh consumers after the dev English overlay lands

Claude-Session: https://claude.ai/code/session_01QKr8CsFLYmrgqbRTSipTNH
2026-09-09 02:45:25 +08:00
Neko 29acee69f6 ✨ feat(desktop): added Computer Use with auv (#19051) 2026-09-09 00:01:59 +08:00
YuTengjing 812b39fa8e 🐛 fix(work): let users remove orphaned document works from the card (#19275)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 22:54:54 +08:00
Arvin Xu e7c0a831f7 ✨ feat(goal): add set-agent and restart commands (#19181) 2026-09-08 20:36:49 +08:00
Arvin Xu 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
2026-09-08 20:08:15 +08:00
Rdmclin2 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>
2026-09-08 17:53:00 +08:00
XYenon a19bfe7d23 ✨ feat(telegram): support Guest Mode summons without joining the chat (#18846) 2026-09-08 17:13:08 +08:00
Arvin Xu 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
2026-09-08 17:09:07 +08:00
Arvin Xu 5f8b6a0556 🐛 fix: automatically review and retry Goal deliveries (#19257)
* 🐛 fix: automatically review and return Goal deliveries

* ♻️ refactor: render Goal review feedback in prompts package

* 🐛 fix: resolve configured models for Goal acceptance review

* 🐛 fix: normalize provider config for Goal review model lookup
2026-09-08 11:04:54 +08:00
Arvin Xu a8b902e8da 🐛 fix: preserve reviewed goal content and edit criteria in modals (#19247) 2026-09-08 10:30:25 +08:00
Tsuki b762bbdd7d 🐛 fix(agent-runtime): show question in intervention summary (#19254) 2026-09-07 08:11:25 -07:00
Arvin Xu 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>
2026-09-07 19:46:08 +08:00
AmAzing- 18f6dba56e 🐛 fix(opencode-go): preserve conversation session headers (#19235) 2026-09-07 19:11:52 +08:00
Arvin Xu 5d5d97661d 🐛 fix(task): keep recurring tasks free of delivery acceptance so a failed verify cannot disarm the schedule (#19220)
* 🐛 fix: keep recurring tasks free of delivery acceptance so a failed verify cannot disarm the schedule

A recurring task (schedule / heartbeat) is a loop of ticks, not a delivery:
instantiating a verify plan per tick meant a failed acceptance paused the
task, and the cron query never picks paused tasks up again — the schedule
was permanently disarmed.

- instantiateVerifyPlanOnStart: skip automation tasks entirely (single
  choke point for all run-start callers)
- driveTaskFromVerify: mirror the pass branch on failed/errored — record
  the verdict on the run, never pause a recurring task
- buildTaskPrompt: drop the acceptance section from recurring-task prompts
  so the builder does not chase a plan that will never exist

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 💄 style(task): hide the Verifier / acceptance section for recurring tasks

The server no longer instantiates verify plans for automation tasks, so
offering the config editor or an acceptance state row on a recurring task
would advertise a contract that never runs.

- TaskAcceptance: return null for automationMode tasks (single gate that
  covers the detail page, agent-scoped page, and both portals) and skip
  the acceptance-subject fetch
- TaskProperties: drop the read-only acceptance state row

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-07 10:03:42 +08:00
Tsuki ab22456df5 ✨ feat(device): add remote directory browsing (#19221)
* ✨ feat(device): add remote directory browsing

* 🔒️ fix(device): avoid logging remote paths
2026-09-06 18:37:48 -07:00
Arvin Xu 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>
2026-09-07 01:24:00 +08:00
Arvin Xu 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>
2026-09-07 00:31:38 +08:00
Arvin Xu 4a935c75eb ✨ feat(chat): inject the agent's name and title into the system context (#19214)
* ✨ feat(chat): inject the agent's name and title into the system context

An agent's identity lives on the agent row (`name` / `title`), not in its
prompt text — so an agent with a user-given name but no self-introduction in
its systemRole answered "who are you?" with the product or model name instead
of the name the user gave it.

Add an `<agent_identity>` block (new AgentIdentityInjector, appended right
after the system role) carrying the personal name and role title, wired
through both pipelines: the client contextEngineering (from agent meta) and
the server runtime context builder (from the agent config row). Group chat is
excluded — GroupContextInjector already establishes identity there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix(agent): let the renamed default assistant introduce itself by its own name

The inbox system role hardcoded "You are Lobe" — renaming the default
assistant changed every surface except the model's own self-concept, so it
kept answering "who are you?" as Lobe.

Parametrize the inbox system role with the agent's name/title (RuntimeContext
gains agentName/agentTitle), wired from the agent row at both runtime-config
call sites (client agentConfigResolver + server resolveRunAgentConfig). Falls
back to "Lobe" when the assistant was never renamed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-07 00:24:33 +08:00
YuTengjing 8d852279db ✨ feat(provider): add Unsloth provider support (#18339)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-06 23:28:17 +08:00
Arvin Xu 31401e9ece 🐛 fix: align goal task instructions with implementation outcomes (#19210) 2026-09-06 18:43:17 +08:00