Fork
0
代码
介绍
代码
Issues
Pull Requests
流水线
Actions
讨论
Wiki
项目成员
分析
项目设置
Fork
0
canary
lobe-chat
/
packages
/
builtin-tool-task
下载当前目录
G
GitHub
🐛 fix(task-schedule): enforce maxExecutions cap and block sub-10min heartbeat (
#14865
)
f94e4f46
创建于
7 天前
历史提交
文件
最后提交记录
最后更新时间
src
🐛 fix(task-schedule): enforce maxExecutions cap and block sub-10min heartbeat (#14865) * 🐛 fix(task-schedule): enforce maxExecutions cap and block sub-10min heartbeat The "运行次数限制" input on a scheduled task was accepted by the UI and persisted to `tasks.config.schedule.maxExecutions`, but no execution path ever read it — scheduleDispatch/scheduleTick/runTask had no counter and no cap check, so a "stop after N runs" schedule would loop forever. Separately, the server-side `heartbeatInterval` zod schema was `min(0)`, and the `setTaskSchedule` tool manifest only said "recommend ≥600s". An LLM could pass any positive number and trigger sub-minute heartbeats. Enforcement (no schema migration): - `TaskService.updateStatus` stamps `context.scheduler.scheduleStartedAt` (ISO) when a task transitions into `scheduled` from a non-`running` status. The cron loop's natural `running → scheduled` flips happen via `taskModel.updateStatus` (taskLifecycle), bypassing the service layer, so they don't reset the counter. User-initiated (re)starts do. - `TaskTopicModel.countByTaskSince(taskId, since)` counts task_topics rows created since a timestamp. - `runScheduleTick` reads `config.schedule.maxExecutions`; if the count since `scheduleStartedAt` has reached the cap, it marks the task `completed` (so the next dispatch sweep filters it out) and returns a new `max-executions-reached` skip reason. Heartbeat lower bound: - `updateSchema.heartbeatInterval` on the lambda router now refines to `v === 0 || v >= 600`, matching `MIN_MINUTES = 10` in the UI. - `setTaskSchedule` tool manifest description updated to "Minimum 600s … the server rejects positive values below 600" so the LLM sees the hard limit before the zod refine bounces the call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(task-topic-model): rename countByTaskSince → countByTask, use drizzle count() - Make `since` an optional `options` argument so the helper covers total counts too, not only the since-window the scheduler needed. - Swap `sql<number>\`count(*)::int\`` for drizzle's native `count()` aggregator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(task-schedule): cover countByTask, scheduleStartedAt stamping, and tick max-exec - `TaskTopicModel.countByTask`: total-mode, since-window mode, task scope, user scope (real DB). - `TaskService.updateStatus`: stamps `context.scheduler.scheduleStartedAt` on user-initiated starts/restarts of a schedule task; does NOT stamp on the cron loop's natural `running → scheduled` cycle, on heartbeat-mode tasks, or when the new status isn't `scheduled`. - `runScheduleTick`: cap not configured / under cap → runs; cap reached → marks `completed` and skips with `max-executions-reached`; missing `scheduleStartedAt` → falls through (backwards-compat for tasks created before this PR). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(task-schedule): complete capped schedules at the final allowed run The pre-tick cap check in `runScheduleTick` only sees `runCount` *before* starting the next tick. For low-frequency schedules (e.g. daily, `maxExecutions=1`), this meant the task would consume its final allowed run, get parked back at `scheduled` by `TaskLifecycleService.onTopicComplete`, and then sit in `scheduled` for a full cron period before the next pre-tick check noticed the cap was already consumed — contradicting the "stop after N runs" promise. Move the canonical stop to post-completion: - New `TaskLifecycleService.scheduleCapReached(task)` helper counts `task_topics` rows since `context.scheduler.scheduleStartedAt` and compares against `config.schedule.maxExecutions`. Short-circuits when the task isn't in schedule mode, no cap is configured, or no `scheduleStartedAt` is stamped (pre-PR tasks). - The default post-tick transition in `onTopicComplete` now routes a cap-reached schedule task to `completed` instead of `scheduled`, so the UI/API reflect the cap immediately. The pre-tick check in `runScheduleTick` is kept as defense-in-depth: covers crashed ticks that never reached `onTopicComplete`, users editing `maxExecutions` downward past current count, and stale `scheduled` rows from older code paths. Comment updated to reflect that. Tests: - `onTopicComplete`: schedule task under cap → still `scheduled`; at cap → `completed`; with no `scheduleStartedAt` (pre-PR) → still `scheduled` (helper short-circuits before querying). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 天前
package.json
✨ feat(builtin-tool): add onBeforeCall / onAfterCall lifecycle hooks (#14719) * ✨ feat(builtin-tool): add onBeforeCall / onAfterCall lifecycle hooks Tools that mutate state surfaced in the renderer (e.g. lobe-task) need a way to invalidate UI caches after their own writes — but when the tool runs server-side via a registered server runtime, the renderer never sees the mutation and SWR caches go stale (e.g. delete-all-tasks succeeds on the server but the kanban keeps showing the deleted rows). Adds optional `onBeforeCall` / `onAfterCall` to `IBuiltinToolExecutor`, both taking a single `ToolHookContext` object so the surface stays non-breaking as we add fields. The gateway event handler dispatches them on `tool_start` / `tool_end` regardless of whether the tool actually ran client- or server-side. `TaskExecutor` implements `onAfterCall` to refresh the task list / detail SWR caches for write APIs. Also fills the missing `setTaskSchedule` implementation in the server runtime so cloud-mode users can actually configure schedules through the agent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(tasks): widen empty-tasks hero to 960px Aligns with the default `CONVERSATION_MIN_WIDTH` used elsewhere; the 720px cap was leaving the recommended-template grid feeling cramped on wider monitors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(builtin-tool-task): refresh parent task detail after subtask mutation Deleting a subtask through the agent left the parent's detail view showing the stale child until a manual page reload — `onAfterCall` was only invalidating the mutated task's own detail key, never the parent whose `subtasks[]` array embeds it. Adopt the same multi-target pattern that `updateTask` already uses in the detail slice: walk `taskDetailMap` via `findSubtaskParentId` to locate the embedding parent, and also refresh `activeTaskId` defensively (covers e.g. `createTask` whose new identifier isn't yet in the local map but whose parent the user is viewing). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(builtin-tool): unwrap nested tool_end payload before dispatching hook Real gateway `tool_end` events ship `data.payload` as the `{ parentMessageId, toolCalling }` wrapper (see both publish sites in `src/server/modules/AgentRuntime/RuntimeExecutors.ts`), but `dispatchOnAfterCall` was passing that wrapper straight into `readToolPayload`, which expects `identifier` / `apiName` at the top level. Result: identity always undefined for server-runtime tool completions, `onAfterCall` never fires, and the task cache invalidation from the previous commit was effectively dead code. Add `unwrapToolPayload` that prefers `payload.toolCalling` when present and falls back to the flat shape, plus three regression tests covering the wrapper, flat, and malformed cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(builtin-tool-task): colocate executor under client subpath Aligns with the knowledge-base / lobe-agent precedent: drop the standalone `./executor` subpath and re-export `taskExecutor` from `./client`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(builtin-tool): lazy-load executor registry to break import cycle `gatewayEventHandler.ts` statically imported `getExecutor`, which transitively pulled in tool client barrels (e.g. `@lobechat/builtin-tool-lobe-agent/client` → `PlanCard.tsx` → `@/store/chat`). Loading `gateway.ts` in isolation (as the gateway.test.ts suite does) thus reached the chat-store module while `gateway.ts` was still mid-evaluation, and the eager `useChatStore()` call hit `new GatewayActionImpl(...)` before the class binding was initialized. Dynamic-importing `getExecutor` inside the two async dispatch functions breaks the cycle at module load; runtime behavior is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9 天前
vitest.config.mts
🐛 fix: polish task agent manager (#14569)
14 天前