Fusion uses multiple agent roles for planning, execution, review, and merge workflows.
The dashboard's CLI session banner uses authenticated POST /api/cli-sessions/:id/* routes for task-bound CLI sessions. POST /api/cli-sessions/:id/relaunch is project-scoped, rejects sessions that do not have a taskId, records a relaunch intent, and lets the engine listener clear resume linkage before moving the owning task back to todo for a fresh executor launch. This route backs the resume-exhausted banner's Relaunch fresh action; when a session summary has no cliSessionId, the client does not call the route.
Use fn chat to message an agent from your terminal.
fn chat <agent-id> [message…] [--once] [--non-interactive] [--poll-ms <n>]fn chat <agent-id>opens an interactive REPL.- Each message is stored as a
user-to-agentMessageStore message fromcliwithmetadata.wakeRecipient=true. - Agent replies are polled from your inbox and printed as they arrive.
- Dashboard-created agent chat sessions request the target agent's declared
metadata.skillsplus enabled plugin-contributed skills, so skills such asce-debugare available in chat when the contributing plugin is enabled for the requesting project. Model-only QuickChat sessions request enabled plugin skills, and room responder sessions request the responder agent's skills. - Agent-acting session lanes share the same skill-injection contract as executor sessions: executor, merger, triage, reviewer, heartbeat, step-session, dashboard chat/room responders, CLI agent execution, planning, mission interview, milestone/slice interview, agent-onboarding interview, workflow design, memory dreams/insight extraction, and scheduled cron automation all request agent/fallback skills plus enabled plugin-contributed skills when a plugin runner is available. Utility-only lanes that only summarize/extract/generate JSON (title/PR summaries, memory compaction, subtask breakdown, text refinement, agent generation, PR metadata generation, evaluator/research synthesis, and similar one-shot helpers) intentionally stay exempt to avoid loading skills where no agent-style tool loop can use them.
- In dashboard model-loop chat (main chat, QuickChat, and room responders), typing
/skill:{name}requests that skill for the current AI session and strips the slash token from the prompt sent to the model. Slash and catalog-style names such as/skill:review/pr,/skill:review/pr/SKILL.md, andsource::skills/review/pr/SKILL.mdresolve to the matching discovered bare skill token across chat and agent session lanes. The requested skill is still subject to the normal enabled/disabled execution-skill filters; CLI-agent-backed PTY chat keeps raw terminal input semantics and does not interpret this command. - Dashboard chat and planning sessions with a scoped task store expose
fn_task_document_writeandfn_task_document_read; because neither lane has an ambient task, both tools require an explicittask_id. - Agent workflow-routing tools follow an intent boundary: agents may select or change a task workflow only when the user explicitly requested that workflow or when the agent created the task. Executors must not call
fn_workflow_selectto reroute the task they are executing unless the task instructions or a user steering comment explicitly asks for the workflow change. Lanes without an ambient task, including dashboard chat/planning and published/pi extension calls outside a task, must pass an explicittask_id; task-bound executor paths may default to the current task. - Executor, heartbeat, and dashboard chat sessions expose artifact registry tools so agents can publish and inspect multi-type deliverables without relying on the dashboard gallery. Planning sessions intentionally exclude artifact tools until they can thread the existing
MessageStoredependency. - Permanent/custom heartbeat agents and the published/pi extension receive the broad coordination and work-discovery tool surface instead of a narrowly curated subset: read-only task discovery (
fn_task_list,fn_task_show,fn_task_search) for work discovery and duplicate avoidance, workflow discovery and authoring (fn_workflow_list,fn_workflow_get,fn_workflow_create,fn_workflow_update,fn_workflow_delete,fn_workflow_settings,fn_trait_list), governed research (fn_research_run,fn_research_list,fn_research_get,fn_research_cancel), structured clarification (fn_ask_question), artifact, memory, messaging, goal, evaluation, identity, and delegation tools. Task-scoped heartbeat sessions also expose current-task workflow selection and promotion (fn_workflow_select,fn_task_promote); no-task heartbeats omit those because they have no ambient task, while no-task extension/chat/planning lanes exposefn_workflow_selectbut require explicittask_id. Workflow creation, updates, settings writes, deletion, and selection remain permission-gated task/agent mutations even when the tools are exposed in the lane. Prompt-injectable lanes strip workflow approval-bypass flags duringfn_workflow_create/fn_workflow_update; executor-owner paths are the only authoring path that may preserve those flags. Executor-only worktree/workspace tools such asfn_run_verificationandfn_acquire_repo_worktreeremain out of the ambient heartbeat lane until that lane owns the required worktree/workspace context. The task read tools are store-backed, text-only, and action-gate-recognized as read-only; dangerous actions are controlled at invocation time by each agent'sAgentPermissionPolicythrough the action gate (allow / require approval / block), not by withholding governed tools from the session. agent.taskIdis an active-execution linkage, not durable ownership. It may legitimately point at atodo/triagetask only while the agent has live run or executor-active proof; task-move sync and self-healing clear stale parked, terminal, or unresolved links otherwise.fn_list_agentsandfn_agent_showtherefore include column context in the human-readableCurrent Taskline, such as(triage),(in-progress),(not active — done), or(unresolved), so coordinators can distinguish transient planning ownership from drift.
Artifact tools operate on the shared artifact registry, so artifacts are visible across agents and tasks when the caller has the artifact ID or can discover it through filters.
fn_artifact_registerregisters adocument,image,video,audio, orotherartifact withtitle, optionaldescription, optionalmimeType, and exactly one payload source: inline textcontent, a local filepath(preferred for media the agent saved to disk — screenshots, wireframes, mockups, screen recordings, PDF exports; the file is copied into managed artifact storage with MIME inference and image/video/PDF signature validation), base64dataBase64image bytes, or aurireference for media stored elsewhere. HTML mockups register astype="document"+mimeType="text/html"(inlinecontentorpath) and render as live sandboxed previews in the Artifacts view; PDFs (mimeType="application/pdf",path) open in an embedded viewer; videos stream with range-request seeking. Executor sessions resolve relativepathvalues against the task worktree and defaulttaskIdto the executing task; task-scoped heartbeat sessions resolve relativepathvalues against the acquired heartbeat worktree and defaulttaskIdto the assigned task; dashboard chat uses thedashboard-chatauthor and requirestask_idbecause chat has no ambient task.pathvalues are containment-checked before any file read: the realpath-canonicalized file must stay inside the session's workspace directory (baseDir) or the OS temp directory (where browser/screenshot tooling writes captures); relative paths are rejected outright in lanes without a workspace directory (dashboard chat, no-task heartbeats), which are bounded to tmpdir-only absolute paths.fn_artifact_listlists artifacts across agents and tasks with optionaltype,authorId,taskId,search,limit, andoffsetfilters. Dashboard chat's scoped variant requirestask_idand otherwise supportstype,authorId,search,limit, andoffsetfor that task.fn_artifact_viewfetches one artifact byid, returning registry metadata plus inlinecontentwhen present or the storeduri/path reference for media artifacts.- Successful registration emits a best-effort
system→userinbox notification toDASHBOARD_USER_IDwithartifactId,artifactType,title,authorId, and optionaltaskIdmetadata. Notification delivery failures are logged and must never fail or roll back the artifact registration.
For the user-facing gallery and notification UX, see Artifacts View and Mailbox View. For storage layout and hydration semantics, see Artifact registry.
--oncesend one message and exit after first reply (or timeout)--non-interactiveread full stdin to EOF as message body--poll-ms <n>override poll interval in milliseconds (default1000, orFUSION_CHAT_POLL_MS)
# Interactive chat session
fn chat agent-abc123
# One-shot message (positional content implies --once)
fn chat agent-abc123 "status update?"
# Scripted one-shot from stdin
printf "deploy report" | fn chat agent-abc123 --once --non-interactiveReplies require a running engine for the same project (for example
fndashboard orfn serve).
The fn_agent_update extension tool lets chat/extension callers update existing non-ephemeral agents in place instead of deleting and recreating them. It accepts agent_id plus any editable subset of:
- Identity fields:
name,role(triage,executor,reviewer,merger,engineer,custom),title,icon, andsoul. - Instruction fields:
instructions_text,instructions_path, andheartbeat_procedure_path. - Hierarchy field:
reportsToas a manager agent ID/name; privileged CLI/user calls may passreportsTo: ""to clear the manager. - Heartbeat/runtime fields:
heartbeat_interval_ms,heartbeat_timeout_ms,max_concurrent_runs, andmessage_response_mode(immediateoron-heartbeat). Runtime updates merge into the existingruntimeConfigand preserve unrelated keys.
At least one update field must be provided. The tool rejects missing targets, ephemeral/runtime agents, self-targeting, string/number limit violations (soul 10,000 chars, instructions_text 50,000 chars, path fields 500 chars, heartbeat interval ≥1000ms, heartbeat timeout ≥5000ms, max concurrent runs ≥1), missing managers, self-manager assignments, and hierarchy cycles before mutating storage. Successful calls persist through one AgentStore.updateAgent call, so normal config revision history records the full edit.
Authorization is scoped to the org hierarchy. When the caller is an agent (ctx.agentId is present), the target must already be one of that caller's direct or indirect reports; self-targeting, peer/unrelated targets, and ancestors are rejected. Reparenting must stay inside the caller's subtree: the new manager may be the caller or one of the caller's direct/indirect reports, but not an unrelated agent or ancestor. Direct CLI/user calls that do not carry ctx.agentId are treated as privileged operator actions and may update any non-ephemeral agent, including clearing reportsTo.
The legacy fn_agent_set_instructions extension tool remains available for backward compatibility and narrower instruction-only edits. It accepts:
agent_id— target agent ID or resolvable agent name.instructions_text— optional inline instructions; pass an explicit empty string to clearinstructionsText.instructions_path— optional markdown file path; pass an explicit empty string to clearinstructionsPath.
At least one instruction field must be provided. The legacy tool uses the same direct/indirect-report authorization model for agent callers and persists changes through AgentStore.updateAgent, so instruction edits are captured as normal agent config revisions.
Every first-class editable agent field has a defined create/edit/import/template behavior. This ensures consistent round-tripping across all surfaces.
| Field | Create | Edit | Import | Notes |
|---|---|---|---|---|
name |
✓ | ✓ | ✓ (from manifest) | Unique identifier |
role |
✓ | ✓ | ✓ (mapped from manifest) | Agent capability |
metadata |
✓ | ✓ | ✓ | Arbitrary key-value data |
title |
✓ | ✓ | ✓ (from manifest) | Job title/description |
icon |
✓ | ✓ | ✓ (from manifest) | Emoji or icon identifier |
imageUrl |
✗ (set by avatar upload endpoint) | ✓ | ✗ | Uploaded avatar image URL (/api/agents/:id/avatar) |
reportsTo |
✓ | ✓ | ✓ (from manifest) | Parent agent ID |
runtimeConfig |
✓ | ✓ | ✗ | Heartbeat/budget config |
permissions |
✓ | ✓ | ✗ | Capability flags |
permissionPolicy |
✓ | ✓ | ✗ | Runtime action-gating policy for permanent and ephemeral agents (project default/fallback: unrestricted) |
instructionsPath |
✓ | ✓ | ✗ | File-backed instructions path |
instructionsText |
✓ | ✓ | ✓ (from manifest instructionBody) |
Inline instructions |
soul |
✓ | ✓ | ✗ | Personality/identity description |
memory |
✓ | ✓ | ✓ (from manifest) | Per-agent accumulated knowledge |
bundleConfig |
✓ | ✓ | ✗ | Structured instruction bundle |
| Manifest Field | First-Class Agent Field | Fallback |
|---|---|---|
name |
name |
— (required) |
title |
title |
— |
icon |
icon |
— |
role |
role (mapped to AgentCapability) |
custom |
reportsTo |
reportsTo |
— |
instructionBody |
instructionsText |
— |
memory |
memory |
— |
skills |
metadata.skills |
— |
permissionPolicy is a first-class persisted policy contract for runtime action gating, separate from role/capability authorization and separate from dashboard persona presets.
Built-in preset catalog:
unrestricted(default) — all v1 runtime action categories areallowapproval-required— all v1 runtime action categories arerequire-approvallocked-down— all v1 runtime action categories areblock
V1 runtime action categories:
git_writefile_write_deletecommand_executionnetwork_apitask_agent_mutationreview_gate_bypass(FN-7728; governs thefn_task_bypass_reviewmerge-gate override — theunrestricted/grant-all preset overrides it torequire-approvalinstead ofallow, stricter than the uniform preset disposition)file_scope(FN-7737; governs thefn_task_file_scope_addFile Scope additional-approval action — stays on the UNIFORM per-preset disposition, sounrestricted/grant-all resolves it toallowlike every other plain category)none(classifier-only read-only result; never stored as a policy rule key)
permissionPolicy uses the sensitive categories above (everything except none) plus optional exact toolRules, with the FN-3545 disposition contract:
allowblockrequire-approval
Exact tool overrides are stored as toolRules: { [toolName]: disposition } on either a per-agent permissionPolicy or the project defaultAgentPermissionPolicy. They apply before category rules, so a policy can block one governed tool such as fn_task_create while leaving the broader task_agent_mutation category set to allow for fn_task_update or workflow tools.
The engine classifies tool calls by behavior (not namespace alone):
file_write_delete: built-inwrite/edit, plus direct filesystem attach helpers likefn_task_attach; low-risk coordination/registration writes such asfn_task_document_writeandfn_artifact_registerare handled by the coordination-exempt/read-only allow-lists below rather than this categorycommand_execution: built-inbashwhen not classified as mutating git, plus fn tools that run bounded subprocess/worktree acquisition flows such asfn_run_verificationandfn_acquire_repo_worktreegit_write: mutating git shell commands run viabashnetwork_api: external/network-facing tools (for examplefn_research_run,fn_research_cancel,fn_web_fetch,worktrunk_install;fn_research_retryis permanent-agent network-classified and remains action-gate read-only/exception behavior)task_agent_mutation: task/agent/workflow mutation tools (for examplefn_update_agent_config,fn_task_pause,fn_spawn_agent,fn_task_create,fn_task_update,fn_task_promote,fn_task_refine, and workflow mutators such asfn_workflow_create,fn_workflow_update,fn_workflow_delete,fn_workflow_settings,fn_workflow_select; action-gate-only task coordination tools likefn_delegate_task,fn_task_import_github, andfn_task_import_github_issueuse this category in action-gate evaluation)review_gate_bypass(FN-7728): the operator-onlyfn_task_bypass_reviewmerge-gate override, classified via the sharedREVIEW_GATE_BYPASS_FN_TOOLSset ingating-classifications.tsso both gate paths agree; never falls back totask_agent_mutationor the unrecognized-tool exempt fallback.file_scope(FN-7737):fn_task_file_scope_add, the tool an executing agent uses to extend its task's declared## File Scopebeyond the initial spec at runtime, classified via the sharedFILE_SCOPE_FN_TOOLSset ingating-classifications.tsso both gate paths agree; distinct fromtask_agent_mutation/file_write_deleteand never falls back to the unrecognized-tool exempt fallback.- Dashboard permission editors now show per-category example tools sourced from
AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLESin@fusion/core, exact-tool override controls, plus a read-only exempt-tools panel for coordination/messaging bypass tools. none: positively recognized read-only tools (read,grep,find,ls, list/show/get-stylefn_*tools, plus permanent-agent coordination helpers likefn_delegate_task,fn_task_import_github, andfn_task_import_github_issue). Artifact tools mirrorfn_task_document_writein the shipped allow-lists:fn_artifact_register,fn_artifact_list, andfn_artifact_vieware present inREADONLY_FN_TOOLSandCOORDINATION_EXEMPT_TOOLS, so registration is treated as coordination/registry publication instead of a broad mutation approval.
bash git-write heuristic in v1:
- Mutating git operations include:
git add,commit,merge,rebase,cherry-pick,am,apply,stash,tag,push,reset,rm,mv,clean,worktree add/remove,checkout -b,switch -c,pull --rebase,restore --staged, and branch/remote mutation forms. - Read-only git operations include:
git status,diff,log,show,rev-parse,branch --show-current,branchlisting, andremote -v.
Unknown/unclassified tool fallback:
- In permanent-agent sessions, unknown tools default to
require-approval(fail-safe). - Category
noneonly yieldsallowwhen the tool is positively recognized as read-only. - Internal Fusion runtime coordination tools (heartbeat completion, logs, documents, messaging, structured user questions via
fn_ask_question, evaluations, identity reflection, memory bookkeeping, and read-only discovery) are exempt by design and always allowed so permanent-agent heartbeats can complete. ExacttoolRulesdo not make these heartbeat-critical tools configurable.fn_task_createis governed astask_agent_mutationin both action-gate and permanent-agent evaluation because it creates task rows; delegation/import tools remain governed in action-gate evaluation while the permanent-agent classifier still treats them as positively recognizednonecoordination primitives. Task field/status mutation viafn_task_updateis also governed astask_agent_mutation. - Operators can reload the in-memory exempt-tool registry at runtime via
POST /api/action-gate/reload(optional body{ "tools": string[] }) to apply exemption-list updates without restarting the engine process. - Canonical tool classification/exemption sets live in
packages/engine/src/gating-classifications.tsand are shared by both action-gate paths.
Approval pause/resume lifecycle (FN-3548):
- Permanent-agent gating short-circuits
blockandrequire-approvalactions before tool execution and returns structured non-success tool results. - For
require-approval, the engine creates/reuses a durable approval request and pauses execution with canonicalpauseReason: "awaiting-approval". - If task-backed, the owning task is paused (
Task.paused=true,pausedByAgentId=<requester>); the requesting agent is paused (state="paused",pauseReason="awaiting-approval"). The task-detail Paused by agent indicator is context only: operators may still manually pause or unpause an agent-assigned task, and unpause clears the task pause latch. - FN-7608: for
TaskExecutor-backed sessions, await-for-approvalgate outcome does more than mark the task/agent paused in the store — it actually suspends the in-flight executor session.TaskExecutor.buildActionGateContext()'spauseForApprovalfiresawaitAbortInFlightTaskWork(taskId, "awaiting-approval:...")(fire-and-forget, not awaited inline, to avoid a self-deadlock against the tool call that triggered it) so the running LLM turn is aborted rather than continuing to probe for ungated workarounds.wrapToolsWithActionGate()invokespauseForApprovalon both the newly-created-request path and the reused-pending path (a repeated identical gated call reuses the pending request viaapprovalDedupeKeyand still triggers the pause/suspend). Both canonical executor prompts (engineEXECUTOR_SYSTEM_PROMPT, coreEXECUTOR_PROMPT_TEXT) carry a byte-identical carve-out stating that waiting on a pending approval is a legitimate turn end and that re-issuing the gated call, probing read-only equivalents, or routing around the block via other tools is forbidden. Heartbeat-driven sessions have no persistent in-flight session object to abort (each tick is a short bounded cycle), so pausing the task/agent there is already sufficient. - Dedupe semantics by
approvalDedupeKey:pendingreuses the same request,approvedallows exactly one execution and then marks requestcompleted,deniedstays blocked,completedrequires a fresh request. - HTTP decision endpoint resumes best-effort:
POST /api/approvals/:id/decisionwith{ decision: "approve" | "deny", comment? }unpauses matching task/agent when they are paused forawaiting-approval. - Approval API surface:
GET /api/approvals(supports status/limit/offset and returns{ requests, total, pendingCount }),GET /api/approvals/:id(includes request context + audit/history),POST /api/approvals/:id/decision. - Dashboard mailbox is the primary v1 resolution surface: approvals appear in the mailbox Approvals tab with pending/history views and inline approve/deny controls for pending requests.
- Dashboard mailbox entry points (Header/Mobile nav) display pending-approval indicators so waiting approvals are visible before opening Mailbox.
- Agents list/board cards and Agent Detail summary display per-agent
pendingApprovalCountbadges to show which agents are blocked by waiting approvals.
Agent provisioning approvals (agent_provisioning category):
fn_agent_create/fn_agent_deletecan returnpending_approvalunderprojectSettings.agentProvisioningpolicy (approvalMode, trusted roles/IDs,alwaysApproveDelete).fn_agent_updateis an in-place configuration edit for existing agents, so it uses org-hierarchy authorization andAgentStore.updateAgentrevision auditing rather than the create/delete provisioning approval policy.- Dashboard surface: Project Settings → Agent Permissions → Agent Provisioning Approvals editor (project-scoped only).
- Approval request is persisted with provisioning context (
tool+params) and visible in mailbox/API approval queues. - Dashboard/API decision route
POST /api/approvals/:id/decisionexecutes deferred provisioning onapprovevia engine dispatcher (executeApprovedAgentProvisioning) and never executes ondeny. - Decision handling emits run-audit mutations:
agent:create:{requested,approved,denied}andagent:delete:{requested,approved,denied}using original request task/run/requester linkage. - Malformed provisioning context or failed execution returns 500 from the decision route (no silent approval).
Resolver decision table (resolveAgentProvisioningPolicy):
| matchedRule | decision | Notes |
|---|---|---|
missing-caller |
deny |
Caller context missing. |
privileged-caller |
allow |
Bypasses trust checks and alwaysApproveDelete. |
approval-mode-never |
allow |
Global short-circuit, including deletes. |
delete-always-approve |
require-approval |
Default delete behavior when not short-circuited. |
trusted-agent-id |
allow |
Exact caller ID allowlist match. |
trusted-role |
allow |
Case-insensitive role allowlist match. |
approval-mode-trusted-only |
require-approval |
Untrusted fallback in default mode. |
approval-mode-always |
require-approval |
Approval always required unless privileged/never mode. |
FN-3973 follow-through: spawn_agent evaluation is complete; governance remains in action-gate task_agent_mutation (ephemeral runtime lifecycle), not durable agentProvisioning.
Default and legacy fallback behavior:
- New non-ephemeral/permanent agents persist a normalized
permissionPolicyusing presetunrestrictedwhen not explicitly provided. - New and existing ephemeral/runtime task-worker agents may store an explicit
permissionPolicyand canonicalpermissionsgrants. - Legacy permanent-agent rows missing
permissionPolicyresolve to the same effectiveunrestrictedpolicy at read time (no eager migration required). - Legacy ephemeral/runtime task-worker rows missing
permissionPolicyare not backfilled on disk; runtime sessions inherit the projectdefaultAgentPermissionPolicy(orunrestrictedwhen no project default is configured). - Fallback
executor-FN-*task workers without a stored agent row use a stable synthetic actor and the same project-default policy, so exacttoolRulessuch asfn_task_create: blockapply consistently.
Separation of concerns:
permissionscapability flags (plus role defaults) determine what an agent is conceptually authorized to do (for example,tasks:assign,agents:create).permissionPolicydetermines how sensitive runtime actions are gated (allow,block,require-approval) once the capability path is in play.require-approvalcreates an approval request with the permanent or ephemeral actor identity, pauses the associated task safely, and resumes through the existing approval lifecycle.- Dashboard persona presets (
packages/dashboard/app/components/agent-presets/) are UI templates for identity/behavior and are not the source of truth for permission-policy enforcement.
CLI-agent adapters keep their own autonomy posture and tool-permission handling separate from permanent-agent permissionPolicy. When an adapter reports a permission/input prompt (PermissionRequest, Notification, or a conservative approval-prompt heuristic), the CLI session moves to waitingOnInput; the dashboard shows the session banner, and Fusion dispatches the cli-agent-awaiting-input notification event through enabled ntfy/webhook providers. Repeated waiting events for the same CLI session are de-duplicated before provider delivery, while the in-app banner continues to reflect the live session state.
These fields are managed by the engine and cannot be directly edited:
id— Auto-generated unique identifierstate— Agent lifecycle state (managed by engine). Non-ephemeral agents default toactiveon creation; ephemeral/task-worker agents default toidle.taskId— Current working task (managed by scheduler)totalInputTokens/totalOutputTokens— Token usage totals (managed by engine)createdAt/updatedAt/lastHeartbeatAt— Timestamps (managed by system)lastError— Last error message (managed by engine; cleared after successful recovery runs)pauseReason— Reason for paused state (managed by engine)
The taskId field is suppressed in API responses when the linked task is in a terminal state (done or archived). This prevents stale "working on" UI indicators in the Agents dashboard for agents whose task has already completed.
Terminal task statuses:
done— Task completed successfullyarchived— Task archived
Affected API endpoints:
GET /api/agents—taskIdis omitted from agents with terminal linked tasksGET /api/agents/:id—taskIdis omitted when the linked task is terminalGET /api/agents/stats—assignedTaskCountexcludes agents with terminal linked tasks
Non-terminal task statuses (taskId is preserved):
planningtodoin-progressin-review
Graceful degradation:
- If task lookup fails (e.g., task deleted),
taskIdis preserved in the response to avoid false negatives - The underlying
taskIdis NOT modified in storage — only the API response is sanitized
Performance notes:
- Task-link sanitization now uses
TaskStore.getTaskColumns(ids)for one batched status lookup instead of per-taskgetTask()hydration. GET /api/agents/statsnow usesAgentStore.getRunStatusCounts()to aggregate completed/failed run totals in one grouped query (no per-agentgetRecentRuns()loop).
These fields can only be set during update (not on create):
pauseReason— Why the agent is pausedlastError— Last error message (cleared when the agent successfully recovers)totalInputTokens— Accumulated input token counttotalOutputTokens— Accumulated output token count
When a task sets assignedAgentId to a durable (non-ephemeral) agent, that same agent is used as the active execution owner during runtime execution.
Behavior:
- Fusion links the durable agent's
taskIdto the running task for execution visibility - No synthetic
executor-FN-*task-worker agent is created for that run - On completion/error, the durable agent's execution task link is cleared (the durable record is preserved)
Fallback behavior remains unchanged:
- Unassigned tasks still use runtime-managed
executor-FN-*task-worker agents - Missing assigned agents, or assigned agents that are ephemeral/runtime-managed, fall back to task-worker execution ownership
Execution-ownership sync intentionally avoids assignment-trigger side effects (agent:assigned wakeups) that are intended for control-plane delegation.
Invariant: a durable agent must not remain state="running" while its linked task is outside in-progress (especially todo + status="queued").
Layered enforcement:
- Scheduler rollback (
packages/engine/src/scheduler.ts): when overlap gating requeues a todo task tostatus="queued", scheduler immediately rolls back any running agent linked throughexecutionTaskIdtostate="active"and clears the execution-task link. - Heartbeat reconciliation (
packages/engine/src/agent-heartbeat.ts):reconcileOrphanedRunningAgents()repairs persistedstate="running"drift when no active run exists, or when a persisted active run is untracked and older thanheartbeatTimeoutMs × 3(work-budget grace; see inline FN-4278/FN-4255 comment near that check). - Heartbeat scheduler stale-run reap (
packages/engine/src/agent-heartbeat.ts):HeartbeatTriggerScheduler.maybeReapStaleActiveRun()repairs stale persistedstatus="active"heartbeat runs usingheartbeatTimeoutMs × heartbeatRepairStaleMultiplier(default multiplier2). - Self-healing reconciler (
packages/engine/src/self-healing.ts):recoverAgentsRunningOnInactiveTasks()andrecoverStaleHeartbeatRuns()use task-column mismatch checks plus PID/young-run/age guards (including the 6h stale active-run max age) rather than a simple timeout multiplier. - Cross-process CLI stop/start notification (FN-7723, follow-up from FN-7718):
fn agent stop/startmutate the agent row from a separate short-lived CLI process. The long-lived engineAgentStorenow opts into a boundedfs.watch+poll change-detection fast-path (startWatching()/checkForChanges(), modeled onTaskStore's own watcher — seedocs/architecture.md's FN-7723 note) that re-emits the existingagent:updated/agent:stateChangedevents, soHeartbeatTriggerSchedulertypically observes a CLI-driven stop/start within one poll interval (~2s) instead of waiting on the 60sauditTimerRegistrationssweep, which remains the durable backstop.
Manual recovery for pre-fix stuck rows:
- Open the agent in Dashboard → Agent Detail.
- Set state back to
active. - Clear execution task link (set task link to none).
Programmatic equivalent:
AgentStore.updateAgentState(agentId, "active")AgentStore.syncExecutionTaskLink(agentId, undefined)
When a task is executed by an assigned durable agent, executor session model selection prefers fresh task and settings values before the agent's stored runtime model.
Executor precedence for task runs:
- Task
modelProvider+modelId - Project/global execution lane fallbacks (same resolution as unassigned runs)
- Assigned agent
runtimeConfigmodel pair (combinedruntimeConfig.model = "provider/modelId"or separateruntimeConfig.modelProvider+runtimeConfig.modelId) only when both provider and model ID are present and no task/settings pair is configured
If the assigned agent runtime model is missing or incomplete, Fusion continues to automatic provider/model resolution without mixing partial runtime fields into the selected pair.
Heartbeat sessions for durable agents resolve models with the same fresh-settings-first rule:
- Execution-lane settings fallback (
executionProvider/executionModelId→executionGlobalProvider/executionGlobalModelId→ project/global defaults) - Agent runtime model (
runtimeConfig.modelorruntimeConfig.modelProvider+runtimeConfig.modelId) only when both provider and model ID are present and no execution/default pair is configured
Heartbeat no longer passes a stale runtime model ahead of a saved execution lane or project default override.
Task-scoped heartbeat runs for durable agents execute inside the task's git worktree (same as ephemeral task execution), while no-task heartbeat runs continue to execute from the project root.
Heartbeat and executor system prompts share the same active-goal context injector (buildGoalContextSection), so both lanes receive identical goal preambles when active goals exist.
If a heartbeat cannot create/run a session due to unavailable provider credentials or missing provider registration, Fusion records resultJson.reason = "heartbeat_model_unavailable" with actionable diagnostics in resultJson.detail/stderrExcerpt.
Durable-agent error recovery is coordinated between the heartbeat timer path and the self-healing sweep. Either entry path may auto-recover durable (non-ephemeral) agents stuck in state="error" only when all eligibility checks pass:
- agent is non-ephemeral (
isEphemeralAgent(...) === false) - heartbeat runtime is enabled (
runtimeConfig.enabled !== false) - no active heartbeat execution is already running for the agent
lastErrorclassifies as transient network/infrastructure failurelastErroris not operator-actionable (credentials/model/billing-style failures)- stale worktree/module-resolution failures remain suppressed instead of auto-restarted
Both paths use the same persisted retry budget, agent.metadata.heartbeatErrorRecovery.consecutiveAttempts, with the default cap of 5 attempts (settings-overridable through heartbeatErrorRecoveryAttempts). The timer path provides fast recovery on the agent's own interval; the self-healing sweep is the backstop for stale error agents whose timer was lost, delayed, or did not re-tick. Self-healing still persists agent.metadata.durableErrorRecovery for sweep-specific cooldown and stale-path details:
- exponential cooldown (
30sbase, capped at15m) - persisted
attempts,lastAttemptAt,nextRetryAt,exhausted,lastReason, and stale missing-module path counters
On restart attempts, the runtime triggers the normal heartbeat pipeline with source: "automation" and a structured contextSnapshot.selfHealing payload so operators can audit recovery runs in heartbeat history. The sweep flips error → active before calling executeHeartbeat, so the heartbeat run does not re-enter run-entry error recovery or double-count the same recovery.
Self-healing intentionally leaves agents in error (or parks them paused when the shared retry budget is exhausted) when blockers are operator-actionable or non-transient, when stale worktree/module-resolution suppression applies, when cooldown has not elapsed, when active execution is present, or when retry budget is exhausted.
Manager presence does not gate this sweep (FN-7672/FN-7844): eligibility for durable state="error" recovery does not depend on whether the agent's reportsTo manager is present/active. The timer path is now the fast path for heartbeat-managed error agents, while this recovery sweep remains the maintenance backstop for durable agents that are still stale in error; a present manager does not make the agent any less stuck. (A separate, unrelated managerMissing check still gates recovery of orphaned state="running" agents — a different failure mode where a live process's manager row was deleted.) FN-7672 root-caused a correlated 4-agent error cluster reporting to one active manager (a transient upstream auth/session blip) that could never have self-healed under the old manager-missing-only gate, even once the underlying cause resolved.
- Timer trigger: run completes and the durable agent returns to
state="active"(recoverable soft-fail). - Assignment / on-demand trigger: run completes with
resultJson.actionRequired = true, then the durable agent is paused withpauseReason="heartbeat-model-unavailable"andlastErrorset to actionable credential guidance (including the missing provider name when detectable).
After credentials are fixed, operators should resume the paused durable agent; subsequent heartbeats proceed normally.
When a triage/specification run targets a task with assignedAgentId and that agent is durable, planning now inherits the assigned agent context instead of only generic triage-role defaults.
Triage inheritance behavior:
- The triage system prompt includes assigned-agent identity context and resolves instructions/soul/memory from that agent (including existing rating-aware instruction composition)
- Triage memory tools are created with assigned-agent memory context (
createMemoryTools(..., { agentMemory })), sofn_memory_search/fn_memory_getcan access.fusion/agent-memory/{agentId}/...during planning - Planning model resolution prefers a complete assigned-agent runtime model pair first, then task planning overrides, then normal planning/project/global fallbacks
As with execution, incomplete assigned-agent model configuration falls through cleanly to the existing planning hierarchy.
The Task Detail Activity → Raw Logs model header prefers runtime provenance markers written during execution/review:
Executor using model: <provider>/<modelId>Reviewer using model: <provider>/<modelId>Triage using model: <provider>/<modelId>
When the lane resolves a thinking level, the same row appends (thinking effort: <level>), for example Executor using model: openai/gpt-4o (thinking effort: high). Dashboard parsers ignore parenthesized diagnostics for provider icons/effective-model headers while Raw Logs and Activity rows keep the full text visible.
This makes the header reflect the model that actually ran. For active runs with no runtime marker yet, the UI can use the currently assigned agent runtime model as a temporary fallback before falling back to task/settings resolution.
Runtime-created ephemeral agents are removed immediately after terminal cleanup paths run:
- Task-worker agents created by
InProcessRuntimeare deleted as soon as they reach paused cleanup paths after completion, error, oragent:stateChangedfallback cleanup. - Spawned child agents created by
TaskExecutorare deleted immediately insideterminateChildAgent()after terminal cleanup state update. - User-managed non-ephemeral agents are never auto-deleted by these pathways.
Because deletion is immediate, runtime helper agents should not remain visible in the dashboard or AgentStore after cleanup completes once paused-state cleanup (or run-level termination) finishes.
The agents surface provides:
- Agent-first list and board collections use the desktop split-pane layout (primary collection + detail pane)
- Org Chart is a full-view mode that takes over the full Agents content area; selecting a node opens detail in that same full-width region with back navigation to the chart
- Org chart nodes intentionally stay compact (role/state/health hierarchy signal only) and do not enumerate per-agent skill badges; detailed skills remain in list/board/detail surfaces
- A cross-pane Overview strip above the split layout with summary metrics and a disclosure to expand active/running live cards
- A compact Controls popup for secondary actions (state filter, Show system agents toggle, project-scoped bulk pause/resume, Import, and global Heartbeat Speed)
- Agent import can also be launched from the selected Agent Detail header; this entry opens the import modal directly in the companies.sh browse flow so operators can discover and import packages without leaving the detail context
- Detail/config panels
- Agent Detail includes a Mail tab for inspecting that agent’s inbox/outbox; selecting a message opens full details, and selecting an unread inbox message marks it read
- Agent Detail header utility actions now include a project-scoped Bulk agent actions menu for pause/resume lifecycle transitions; see Agent Detail bulk lifecycle actions
- Split-view synchronization: successful saves plus single-agent and bulk lifecycle actions from the right-side Agent Detail pane immediately refresh the left-side list/selection state (no wait for background polling)
- A per-agent Token Usage panel that summarizes cumulative token consumption for the currently displayed agents
- Run history
- Task assignment context
When an agent card shows the Running control, that control is actionable:
- Clicking Running opens Agent Detail directly on the Runs tab
- If the agent has an active run ID, that run is automatically expanded
- The run detail payload and log stream are loaded immediately so operators can inspect live execution without manually switching tabs
Other entry points (for example, View Details or clicking the agent identity area) continue to open the default Agent Detail Dashboard tab.
The Token Usage panel in Agents view is derived from each agent's persisted cumulative counters:
totalInputTokenstotalOutputTokens
Fusion exposes cache-hit metrics across logs, API, and CLI:
- Structured logs:
token-cache-metricschannel emits per-persist records withtaskId,agentId,role,inputTokens,cachedTokens,cacheWriteTokens, and computedhitRatio. - Agent API:
GET /api/agents/:id/token-usagereturnslast24h,last7d, andallTimewindow summaries for permanent agents. - CLI rollup: run
pnpm fn:cache-stats(orpnpm fn:cache-stats --json) for project-wide role totals plus per-permanent-agent cache-hit summaries.
For the current filtered/visible agent set, the panel shows:
- Aggregate input token total
- Aggregate output token total
- Aggregate combined total (
input + output) - Per-agent rows sorted by descending combined token usage
If either token field is missing for an agent, the dashboard treats it as 0 so the panel stays stable and never crashes on partial/migrating data.
The Agent Detail header includes a kebab-menu button in the utility actions cluster (Bulk agent actions), beside refresh/close controls. This menu runs project-scoped lifecycle changes from the detail view: it fetches agents for the current project and then calls the same per-agent lifecycle API (POST /api/agents/:id/state) used by the single-agent header buttons.
Current bulk transitions are intentionally limited to the two shipped actions:
- Pause All Agents — targets only non-ephemeral agents currently in
activeorrunningstate - Resume All Agents — targets only non-ephemeral agents currently in
pausedstate
Eligibility and UI behavior:
- Ephemeral/system agents are excluded entirely from bulk lifecycle actions
- Agents already outside the target lifecycle state are skipped rather than force-transitioned
- Each menu item shows a live eligibility hint after opening the menu, such as
Pause 2 active/running agentsorResume 1 paused agent - If no agents are currently eligible, the corresponding menu item is disabled and its hint changes to
No active agents eligibleorNo paused agents eligible - While eligibility is loading, the hint reads
Loading eligible agents...
Operator flow and outcomes:
- Open Bulk agent actions from any Agent Detail header
- Review the eligibility hint for the desired action
- Confirm the project-wide action in the confirmation dialog (
Pause/Resume N agents in this project?) - Expect toast feedback after execution plus an Agent Detail refresh/split-view sync
Toast/reporting behavior mirrors the shipped implementation:
- Full success reports a success toast such as
Paused 2 agents; skipped 1 - Partial failure reports an error toast summarizing successes, skipped agents, and failed agents (including up to three per-agent failure details)
- If no agents are eligible at execution time, the dashboard reports
No agents eligible to pauseorNo agents eligible to resume
Agent deletion is available from both the detail header lifecycle controls and the Settings tab's danger zone.
- The Settings-tab delete button reuses the same delete flow as the header action.
- Deletion still requires confirmation before calling
DELETE /api/agents/:id. - On successful deletion, the dashboard shows a success toast and closes the detail view.
- Deletion availability is intentionally restricted to agents in
idleorpausedstate.
When engine sessions include per-agent memory context, the memory tools operate over the full agent-memory workspace under .fusion/agent-memory/{agentId}/, not only the inline agent.memory field.
Runtime behavior:
fn_memory_appendsupports dual scope writes:scope="agent"for private per-agent operating context (personal playbooks/checklists, self-management notes)scope="project"for shared repo-wide durable knowledge (architecture constraints, conventions, pitfalls)
fn_memory_searchcan surface snippets from:.fusion/agent-memory/{agentId}/MEMORY.md(long-term).fusion/agent-memory/{agentId}/DREAMS.md(synthesized patterns).fusion/agent-memory/{agentId}/YYYY-MM-DD.md(daily notes)
fn_memory_getis intentionally bounded to those same files only.- Agent memory resolution order is:
- Inline
agent.memory(highest priority) .fusion/agent-memory/{agentId}/MEMORY.md(fallback when inline is empty, and supplemental long-term section when inline is present)- Additional
.fusion/agent-memory/{agentId}/DREAMS.mdand daily files surfaced viafn_memory_search/fn_memory_get
- Inline
- Empty inline
agent.memorydoes not disable search/read of existing workspace files once the agent-memory workspace exists.
This layered behavior is shared by heartbeat agents and task-scoped sessions that inherit agent identity.
Triage and executor runtime sessions include a bounded research tool surface only when experimentalFeatures.researchView is enabled for the project. Permanent/custom heartbeat sessions always register the governed research discovery/run/cancel tools (fn_research_run, fn_research_list, fn_research_get, fn_research_cancel) so permission policy and runtime setup responses govern use; fn_research_run and fn_research_cancel remain network_api-gated by AgentPermissionPolicy, and disabled or misconfigured research returns an actionable setup result instead of removing the tool.
fn_research_run— create/start a bounded research run for a focused queryfn_research_list— list recent runs and statusesfn_research_get— fetch one run's structured findings payloadfn_research_cancel— cancel an active run
These tools return structured metadata (runId, status, summary, findings, citations, error, setup) in addition to concise text so downstream model steps can consume results deterministically.
Expected behavior and boundaries:
- Agents should use research only when repository/local context is insufficient
- Queries should stay narrow and task-scoped; avoid open-ended exploration
- When
experimentalFeatures.researchViewis disabled, triage/executor sessions do not registerfn_research_*tools and prompts do not advertise research capabilities; heartbeat sessions keep the safe research tools registered but return setup guidance at execution time - If the research surface is enabled but an explicitly selected external provider is misconfigured (or web search is explicitly disabled), tools return actionable
setupresponses instead of crashing - Durable conclusions should be persisted with
fn_task_document_write(for example,key="research") - Research runs require the project engine to be running for processing;
fn_research_runcreates the run but does not block for completion unlesswait_for_completionis set
For the full research workflow, builtin-default behavior, optional external provider setup, CLI commands, and API reference, see the Research guide.
FN-7675: every standard-toolset conversational base prompt — chat/CEO persona, both heartbeat personas (task-bound and no-task), and the executor — is prefixed with a single canonical, cacheable preamble: FUSION_RUNTIME_SELF_AWARENESS, exported from packages/core/src/agent-prompts.ts (re-exported from @fusion/core). The preamble encodes:
- The agent executes as a hosted process inside the running Fusion daemon; its own execution ends the moment that process ends. There is no "wait for Fusion to close, then keep going" capability.
- Shutdown-crossing workflows (updates, installs, patches, migrations, in-place binary swaps) must be handed off as a standalone artifact the user runs themselves — never orchestrated live across the shutdown boundary. This closes a real failure mode: an agent planned a live "close Fusion → back up → swap build → verify → relaunch" sequence, which is impossible because step 1 kills the process running steps 2–5.
- A short, current description of the platform (task board + engine + desktop/CLI/web surfaces).
- A pointer to the maintained docs (
docs/andCONCEPTS.md) so the agent answers "how do I do X in Fusion" from real documentation instead of improvising.
The preamble is prepended to the stable (cacheable) prompt layer — basePrompt in buildPromptLayers() — at each surface, so it is byte-identical across sessions and does not break prompt-cache discounts:
- Chat:
CHAT_SYSTEM_PROMPTinpackages/dashboard/src/chat.ts. - Heartbeat:
HEARTBEAT_SYSTEM_PROMPTandHEARTBEAT_NO_TASK_SYSTEM_PROMPTinpackages/engine/src/agent-heartbeat.ts. - Executor:
EXECUTOR_SYSTEM_PROMPTinpackages/engine/src/executor.ts(mirrored viaEXECUTOR_PROMPT_TEXTinpackages/core/src/agent-prompts.ts).
It is intentionally not wired into merger, reviewer, triage, cron-runner, PR-response, mission-validation, or reflection prompts — those are internal single-purpose lanes, not standard-toolset agents reasoning about the shutdown boundary.
Fusion includes built-in templates for role prompts:
default-executordefault-planningdefault-reviewerdefault-mergersenior-engineerstrict-reviewerconcise-planning
These can be assigned per role using agentPrompts.roleAssignments.
Agents can be configured with:
- Custom instructions
- Heartbeat interval/timeout limits
- Max concurrent heartbeat runs
- Budget governance settings
- Model overrides for heartbeat sessions
In Agent Detail → Settings, configuration fields auto-save after edits (debounced) when validation passes. The inline status indicator shows saving/saved/error state, and no separate Save Settings click is required for settings persistence.
The runtimeConfig field on agents supports the following options:
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
true |
Whether heartbeat triggers are enabled for this agent |
heartbeatIntervalMs |
number |
— | How often the agent should wake up for heartbeat checks (ms) |
autoClaimRelevantTasks |
boolean |
true |
During no-task heartbeats, opportunistically claim unowned relevant todo tasks that align with the agent's role/soul |
engineerBacklogAutoClaim |
boolean |
inherits project (false) |
Opt this engineer-role agent into no-task backlog auto-claim for implementation tasks. Executor-role agents remain eligible by default; explicit routing/delegation is unchanged. |
autoClaimCandidatesInPrompt |
number |
5 |
Per-agent override for no-task candidate lines rendered in prompts. Integer 0-10; 0 suppresses candidate injection. |
heartbeatTimeoutMs |
number |
— | Time without heartbeat before agent is considered unresponsive (ms) |
maxConcurrentRuns |
number |
1 |
Max concurrent heartbeat runs for this agent |
runMissedHeartbeatOnStartup |
boolean |
false |
When enabled, if the server was down across this agent's scheduled heartbeat tick, fire one catch-up heartbeat at startup (only when lastHeartbeatAt is older than the resolved interval) |
allowParallelExecution |
boolean |
true (when unset) |
Permanent agents only. When false, heartbeat and executor paths serialize symmetrically: a heartbeat will not start while the agent's bound task has an active executor session, and an executor session will not start while the agent has an active heartbeat run |
skipHeartbeatWhenIdle |
boolean |
false |
When true, scheduled timer heartbeats are skipped while the agent has no assigned task. Assignment-triggered and on-demand runs still fire |
messageResponseMode |
"immediate" | "on-heartbeat" |
"immediate" |
Whether agent wakes immediately on message (immediate) or processes during heartbeat (on-heartbeat). See Heartbeat Run Mailbox Checking |
heartbeatScopeDiscipline |
"strict" | "lite" | "off" |
inherits project ("strict") |
Per-agent heartbeat procedure template mode. Unset inherits project setting; strict is coordination-focused, lite is pre-2026-05-11 behavior, off is minimal. |
heartbeatPromptTemplate |
"default" | "compact" |
role default (executor→default, others→compact) |
Per-agent heartbeat execution-prompt template override. Unset inherits project heartbeatPromptTemplate (default). |
selfImproveEnabled |
boolean |
true |
Enable periodic self-improvement reflection prompts during heartbeat runs |
selfImproveIntervalMs |
number |
14400000 (4h) |
Minimum delay between self-improvement cycles (minimum enforced: 3600000 ms) |
lastSelfImproveAt |
string (ISO timestamp) |
— | Last recorded self-improvement checkpoint timestamp |
modelProvider |
string |
— | AI provider override for heartbeat session |
modelId |
string |
— | AI model ID override for heartbeat session |
budgetConfig |
AgentBudgetConfig |
— | Token budget governance settings |
Assignment-triggered heartbeats are completion-resilient: if an agent:assigned wake is skipped only because the durable agent already has an active heartbeat run, Fusion records the latest assigned task as a pending assignment and re-fires that assignment wake once the active run completes. This prevents assigned work from being stranded by long heartbeat intervals or skipHeartbeatWhenIdle; disabled agents (enabled === false) and budget-exhausted agents still do not defer assignment wakes.
Self-healing also covers abnormal run/session loss for assigned in-progress work. If the task remains assigned but the durable agent has no active heartbeat run and no active executor session after the orphan grace window, reattach-orphaned-assigned-executions re-dispatches the task forward via executor.resumeTaskForAgent(agentId) without pausing, failing, or moving the task backward.
Agent runtime sessions receive the project/global effective MCP server set when the selected runtime supports MCP (pi/Claude/ACP-compatible runtimes). Mock and unsupported runtimes skip MCP forwarding with content-free structured logs. MCP secret references are materialized only at session creation and are never stored on the agent or written to logs.
Heartbeat values are validated and minimum-clamped to 5 minutes (300,000 ms).
Project setting heartbeatMultiplier (default 1) scales resolved heartbeat timing globally: both the heartbeat interval (pollIntervalMs) and unresponsive timeout base (heartbeatTimeoutMs) are multiplied. Per-agent heartbeatIntervalMs/heartbeatTimeoutMs remain base values before multiplier scaling. This setting is configured from the Agents screen's Controls popup under "Heartbeat Speed".
Project setting heartbeatScopeDiscipline defaults to strict; set per-agent runtimeConfig.heartbeatScopeDiscipline to strict, lite, or off in Agent Detail → Settings → Heartbeat Settings to override.
Project setting heartbeatPromptTemplate defaults to default; per-agent runtimeConfig.heartbeatPromptTemplate overrides it. Role defaults are executor → default, and coordination roles (triage, reviewer, merger) → compact.
runMissedHeartbeatOnStartup defaults to false and is configured in Agent Detail → Settings → Heartbeat Settings → Run Missed Heartbeat On Startup.
allowParallelExecution defaults to true when unset; setting it to false is serialized explicitly so operators can enforce non-parallel heartbeat/executor behavior for that permanent agent. Configure it in Agent Detail → Settings → Heartbeat Settings → Allow Parallel Execution.
No-task heartbeats now consume a project-wide in-memory AutoClaimSnapshotManager cache (TTL 30s) instead of each agent scanning the board independently. Rebuilds occur on TTL expiry and scheduler invalidations (task:created, task:moved when todo edge touched, and task:updated).
Prompt candidate rendering uses:
- project setting
autoClaimCandidatesInPrompt(default5, range0-10) - optional per-agent runtime override
runtimeConfig.autoClaimCandidatesInPrompt
0 suppresses candidate injection in no-task prompts (wake summary shows disabled (prompt-suppressed)). The Agent Detail heartbeat section also includes a Coordination-only agent preset that disables auto-claim and sets candidate injection to 0 for routing/CEO-style agents.
skipHeartbeatWhenIdle defaults to false; when enabled, only scheduled timer ticks are skipped while the agent has no assigned task. Assignment-triggered wakeups and manual/on-demand runs still execute. Configure it in Agent Detail → Settings → Heartbeat Settings → Skip heartbeat when idle.
When an identity-bearing, non-ephemeral agent wakes with no assigned task and runtimeConfig.autoClaimRelevantTasks !== false, the heartbeat monitor scans open todo tasks and may claim one before constructing the prompt run.
Guardrails:
- Only unpaused, unassigned, unchecked-out todo tasks with satisfied dependencies are considered
- Claims are rejected for terminal/paused/owned/conflicting tasks
- Implementation-task backlog pickup is executor-only by default. Engineer-role agents may opt in through Settings → Scheduling & Capacity → "Let engineer agents auto-claim backlog tasks" (
settings.engineerBacklogAutoClaim) or Agents → Agent Detail → Settings → Heartbeat Settings → "Engineer Backlog Auto-Claim" (runtimeConfig.engineerBacklogAutoClaim); the per-agent value overrides the project default in both directions. If a no-task engineer wake shows compatible backlog while this is disabled, delegate the work or create a coordination follow-up instead of treating the board as empty. - Explicit task routing/delegation is not affected by the backlog auto-claim opt-in gate.
- Checkout safety is preserved (
checkout_conflictpaths are non-fatal skips) - On successful claim, the same heartbeat run switches into task-scoped execution (no nested run re-entry)
Operators can disable this per agent in Agent Detail → Settings → Heartbeat Settings → Auto-Claim Relevant Tasks.
When selfImproveEnabled !== false, heartbeat runs periodically enter a self-improvement phase once selfImproveIntervalMs has elapsed since lastSelfImproveAt (or first run with available ratings). During that phase the agent is prompted to:
- Call
fn_read_evaluationsto inspect ratings/reflections - Identify recurring quality issues and trends
- Call
fn_update_identityto adjust its ownsoul,instructionsText, ormemory - Record concise improvement decisions
After a successful run, the monitor records lastSelfImproveAt in runtimeConfig.
The Agent Detail view includes a dedicated Instructions tab for editing agent custom instructions. This replaces the previous embedded instructions editor in the Settings tab, providing a more discoverable and user-friendly experience.
There are two ways to provide custom instructions:
-
Inline Instructions: Direct text entry in the dashboard textarea. Good for simple, short instructions.
-
File-Backed Instructions: A path to a
.mdfile in the project that contains the instructions. Good for:- Longer, more complex instructions
- Version control of instruction changes
- Sharing instruction files across teams
- Open an agent from the Agents view
- Click the Instructions tab
- Enter inline instructions in the Inline Instructions textarea
- Or set a path in Instructions File Path (e.g.,
.fusion/agents/my-agent.md) - When a path is set, a File Content editor appears for direct file editing
- Save instructions using the Save Instructions button
- Save file content separately using the Save File button
- File content loads automatically when an instructions path is set
- Missing files (ENOENT) are treated as new files with empty content
- Non-ENOENT errors (e.g., permission denied) show an error toast
- The editor has an Unsaved changes indicator when file content is modified
- File saves are independent from instruction metadata saves
The Settings tab in the Agent Detail modal includes a Heartbeat Procedure section with an in-modal markdown file viewer/editor.
- The section shows the current
heartbeatProcedurePath. - When a path exists, use View Heartbeat Markdown to load and inspect that file without leaving the modal.
- The editor supports both Edit and Preview modes, with an unsaved-changes indicator and dedicated save action.
- Reads/writes are scoped through the workspace file APIs with
projectIdawareness in multi-project mode.
- Canonical per-agent asset directories now use display name + immutable id suffix (example:
ceo-agent2736).- Canonical heartbeat path example:
.fusion/agents/ceo-agent2736/HEARTBEAT.md - Canonical managed bundle directory example:
.fusion/agents/ceo-agent2736-instructions/
- Canonical heartbeat path example:
- Legacy id-only paths (for example
.fusion/agents/{agent.id}/HEARTBEAT.md) and previously created display-name-based paths remain supported. - Upgrade/create flows preserve existing compatible files and directories in place; Fusion does not auto-rename or delete old paths.
- If the selected default file does not exist yet, the backend seeds it from the built-in template.
- After upgrade completes and the agent refreshes, operators can immediately open the seeded per-agent
HEARTBEAT.mdfrom the same modal section.
The New Agent dialog keeps the existing 3-step flow, and step 0 is split into two tabs:
- Preset personas (default) — quick-start persona cards that prefill the same fields and immediately advance to step 1 when selected
- Custom agent — manual setup for identity, configuration, and the Generate with AI entry point
The custom tab exposes separate fields for:
- Title (
title) — optional role title/description - Soul (
soul) — optional personality and communication style guidance - Heartbeat Procedure Path (
heartbeatProcedurePath) — optional path to the agent heartbeat markdown file, typically.fusion/agents/<display-name>-<agent-id>/HEARTBEAT.md(legacy id-only paths remain valid) - Instructions Path (
instructionsPath) — optional file-backed instructions path - Inline Instructions (
instructionsText) — optional inline behavior instructions
For long-form prompt authoring, Soul, Agent Memory, and Inline Instructions now use the same rich editing affordances as other prompt editors in the dashboard:
- Larger default editing surfaces for easier drafting
- Plain/edit mode and Markdown preview mode
- Fullscreen expand/collapse editing for long content (safe-area-aware on mobile)
In Agent Detail → Agent Memory → Memory Files, selected file content now also supports the same Edit/Preview markdown toggle. Preview renders the current in-memory draft (including unsaved edits), while save/edit controls remain gated by agent read-only state.
These controls are also available on the editable review step, so prompt content can be reviewed and refined with the same markdown and fullscreen behavior before submit.
Before clicking Create, the final review step remains editable for identity/instruction fields so operators can make last-minute corrections without navigating backward. The review step includes edit-in-place controls for:
- Title
- Soul
- Heartbeat Procedure Path
- Instructions Path
- Inline Instructions
The final createAgent(...) call always uses the latest values from these step-2 controls.
After first-project registration, first-run setup asks whether to create a first persistent agent before entering the dashboard. The CEO preset is selected by default because this first agent is framed as an optional coordinator that can help create tasks and keep direction across sessions.
Users can choose a preset, create the project agent, or skip it and create agents later from the Agents view. Agents are optional for task work: Fusion still starts temporary agents to plan, code, review, and merge tasks.
When experimentalFeatures.agentOnboarding is enabled, first-run setup also offers the same draft-first AI Interview path used by the New Agent dialog. Applying the interview draft updates the setup preview, but persistence remains explicit through Create Agent.
The New Agent dialog is the canonical launch point for agent creation.
When Settings → Experimental Features → Planning-style Agent Onboarding (experimentalFeatures.agentOnboarding) is enabled:
- Step 0 of the New Agent dialog includes an AI Interview entry point for create mode.
- Agent detail → Settings includes an AI Interview action for edit mode on existing agents.
- The interview flow asks clarifying questions using repo-aware context (existing agents + preset/template options for create mode, plus current agent configuration for edit mode).
- It generates a draft configuration summary for review, including identity fields,
soul, starterinstructionsText, startermemory, heartbeat guidance (heartbeatProcedurePath,heartbeatIntervalMs,heartbeatEnabled), and draft-only runtime/model suggestions (runtimeHint,modelHint). - In create mode, confirming the summary (Apply draft to agent form) applies the generated draft into
NewAgentDialog's existing editable form fields (step 1 / custom flow) for manual review and edits before save. - In edit mode, Apply draft to settings form updates local editable fields in the settings UI.
- The interview flow does not auto-create or auto-save agents directly; final persistence still happens only through the standard manual Create/Save action.
When experimentalFeatures.agentOnboarding is disabled, the New Agent dialog still opens normally but the AI Interview entry point is hidden.
The dashboard provides quick-start presets for common agent roles. Each preset includes:
- Name, icon, and avatar - Display identification (
imageUrltakes priority overiconin UI rendering) - Professional title - Descriptive role title
- Soul - Personality and operating principles defining how the agent thinks and communicates
- Instructions - Actionable behavioral guidelines
Preset definitions live in packages/dashboard/app/components/agent-presets/:
agent-presets/
├── index.ts # Exports AGENT_PRESETS and helper functions
├── ceo/soul.md # Chief Executive Officer soul
├── cto/soul.md # Chief Technology Officer soul
├── cmo/soul.md # Chief Marketing Officer soul
├── cfo/soul.md # Chief Financial Officer soul
├── engineer/soul.md # Software Engineer soul
├── backend-engineer/soul.md
├── frontend-engineer/soul.md
├── fullstack-engineer/soul.md
├── qa-engineer/soul.md
├── devops-engineer/soul.md
├── ci-engineer/soul.md
├── security-engineer/soul.md
├── data-engineer/soul.md
├── ml-engineer/soul.md
├── product-manager/soul.md
├── designer/soul.md
├── marketing-manager/soul.md
├── technical-writer/soul.md
├── planning/soul.md
└── reviewer/soul.md
Each soul.md file is a Markdown document containing:
# Soul: [Role Name]
[First-person identity statement]
## Operating Principles
[Bullet points describing key behaviors]
## Communication Style
[How the agent communicates]Soul content should be:
- First-person - Written from the agent's perspective ("I am...")
- Role-specific - Defines the unique character of this role
- Actionable - Describes concrete behaviors, not abstract qualities
- Paperclip-inspired - Clear ownership, decision discipline, communication standards
- Create or edit the
soul.mdfile in the appropriate directory - Update
index.tsif adding a new preset (export the imported soul and add toAGENT_PRESETSarray) - Run tests to verify:
pnpm --filter @fusion/dashboard exec vitest run app/components/__tests__/agent-presets.test.ts
Dashboard presets are a UI-only concept that populates the New Agent dialog fields (name, icon, role, soul, instructionsText). They don't map to engine types.
Engine role prompts (in agentPrompts settings) define the actual agent behavior when executing tasks. These are separate from dashboard presets and live in project settings.
This separation means:
- Presets provide starting point personality and instructions for new agents
- Engine templates control actual task execution behavior
- An agent created from a preset can have its engine role prompt customized independently
agentPrompts project setting supports:
templates[]: custom prompt templates by roleroleAssignments: map role → template ID
When no assignment is configured, Fusion falls back to built-in defaults.
The Prompts section in the Settings modal provides a user-friendly interface for customizing specific segments of agent prompts. Unlike agentPrompts which replaces entire role templates, promptOverrides allows surgical customization of individual prompt sections.
| Key | Agent Role | Description |
|---|---|---|
executor-welcome |
executor | Introductory section for the executor agent |
executor-guardrails |
executor | Behavioral guardrails and constraints |
executor-spawning |
executor | Instructions for spawning child agents |
executor-completion |
executor | Completion criteria and signaling |
triage-welcome |
planning | Introductory section for the planning agent |
triage-context |
planning | Context-gathering instructions |
reviewer-verdict |
reviewer | Verdict criteria and format |
merger-conflicts |
merger | Merge conflict resolution instructions |
agent-generation-system |
— | System prompt for AI-assisted agent plan generation |
workflow-step-refine |
— | System prompt for refining workflow step descriptions |
- Navigate to Settings → Prompts in the dashboard
- Each prompt shows its name, key, description, and current value
- Edit the textarea to create a custom override
- Click Reset to restore the built-in default
To clear a specific override, click the Reset button in the UI. This sends null for that prompt key, deleting the override from settings and reverting to the built-in default.
agentPromptsreplaces entire role templatespromptOverridescustomizes individual segments within any template- Both can be used together —
promptOverridesapplies to the segment even within a custom role template
Messaging is available in dashboard mailbox UI and CLI. In dashboard Mailbox → Agents, operators can choose All agents to browse a single combined agent-to-agent stream, or choose a specific agent to keep using per-agent inbox/outbox views.
Agent-backed dashboard chat sessions (including plugin-runtime agents such as Hermes/OpenClaw/Paperclip) also expose mailbox tools (fn_send_message, fn_read_messages) when a MessageStore is wired for that project. Model-only chats without an attached agent do not expose these tools.
fn message inbox
fn message outbox
fn message send AGENT-001 "Please prioritize FN-420"
fn message read MSG-123
fn message delete MSG-123
fn agent mailbox AGENT-001Heartbeat runs are composed from multiple prompt layers so each wake has full identity and operating context:
- System prompt
- Task-scoped runs use the task heartbeat system prompt.
- No-task runs use the ambient/no-task heartbeat system prompt (tool-aligned: no task-scoped tools).
- Workspace tool mode
- Heartbeat sessions are created with coding-capable workspace tools (
read,write,edit,bash,grep,find,ls) inside worktree boundary guards. - Heartbeat behavior still stays lightweight: one concrete action per run, then
fn_heartbeat_done. - Engine-owned heartbeat tools are layered on top for both task-scoped and no-task runs. Permanent/custom agents get the safe coordination/work-discovery surface (task creation/delegation, agent config/provisioning, artifacts, memory, messaging, goals/evaluations/identity/reflection, workflow discovery, bounded research, and
fn_ask_question), while task-only tools such asfn_task_logand task documents stay limited to task-scoped runs.
- Heartbeat sessions are created with coding-capable workspace tools (
- Agent identity and instructions bundle
- Inline instructions (
instructionsText) - File-backed instructions (
instructionsPath) - Soul/personality (
soul) - Agent memory resolved from inline
agent.memoryfirst, then.fusion/agent-memory/{agentId}/MEMORY.mdas fallback/supplement - Optional project memory guidance (when memory is enabled)
- Inline instructions (
- Execution prompt framing
Identity Snapshotblock (agent ID/role + loaded soul/instructions/memory preview;memory: loadedwhen either inline memory or workspaceMEMORY.mdis present)Wake Deltablock (source, trigger detail, wake reason, assignment/comments/messages)- Heartbeat procedure block (task-scoped or no-task variant, plus optional per-agent procedure override file)
This structure ensures every run re-anchors on identity, wake reason, and current context before taking action.
Heartbeat prompts derive wake reason from trigger context plus current inbox snapshot:
message_received— non-forced wake-on-message trigger with unread inbox content still present.message_received_urgent— forced/user-urgent wake-on-message trigger with unread inbox content still present.message_received_already_consumed— wake-on-message trigger fired, but unread inbox/room/comment snapshot is empty at prompt assembly time (already consumed by an earlier serialized run or in-run mailbox read).message_received_urgent_already_consumed— forced wake equivalent of the already-consumed condition.
The Wake Delta block now includes an inbox snapshot line (- inbox snapshot: <N> message(s) or - inbox snapshot: empty (already consumed)) so agents can distinguish a true message payload from a stale wake trigger.
For wake-on-message / wake-on-message-forced runs, Wake Delta also adds:
- wake trigger source: message <messageId> from <fromType>:<fromId> (forced when applicable), <still unread|already consumed at snapshot>
And engine logs emit a structured correlation line keyed by [wake-trigger-diagnostics] with the triggering message metadata plus snapshot counters (inboxUnreadCount, wakeMessageStillUnread, pendingRoomMessages).
Debugging empty-inbox wake noise
grep "\[wake-trigger-diagnostics\]" <engine-log>- Find lines where
triggerDetail=wake-on-message(or forced variant) andinboxUnreadCount=0 wakeMessageStillUnread=false. - Correlate
messageId,from=, andrun=with the run's Wake Delta block; this indicates a false-positive wake where the trigger message had already been consumed by snapshot time.
The shipped default HEARTBEAT_PROCEDURE (in packages/engine/src/agent-heartbeat.ts) now requires bound-task classification on each tick: executor-class, blocked, or coordination-class.
executor-class= implementation work (code/tests/docs prose/build-lint-typecheck)blocked= blockedBy/dependency/peer/external wait statecoordination-class= planning/triage/routing/decision/review work
When the bound task is executor-class or blocked, the default procedure directs the run to pivot toward coordination levers (in-progress risk scan, stale in-review queue, idle direct reports, strategic memory themes) rather than trying to advance implementation from heartbeat. When the task is coordination-class, the heartbeat can engage directly with the bound task.
The manager-facing reports health block in that prompt is populated from AgentStore.getAgentsByReportsTo(agent.id). Engine code must call that store method with its AgentStore instance binding intact because some implementations resolve direct reports through this.listAgents(). If the section disappears unexpectedly, look for logs like Failed to load reports ... Cannot read properties of undefined (reading 'listAgents'), which indicate an unbound method call regressed.
Direct-report staleness in this reports-health block uses each report's configured heartbeat interval, with threshold max(heartbeatIntervalMs × 1.5, 5 minutes). This matches the CEO manual health-check rule and avoids false positives for long-cadence reports.
This behavior is inherited by new non-ephemeral agents because agent creation seeds a per-agent HEARTBEAT.md file from the built-in default. If an agent sets heartbeatProcedurePath, that markdown file fully replaces the built-in default at runtime for task-scoped heartbeats. No-task heartbeats always fall back to the ambient built-in procedure so the prompt never references task-only tools.
For pre-existing agents, use POST /api/agents/:id/upgrade-heartbeat-procedure (also exposed as Upgrade to Default Heartbeat Procedure in the agent detail Config tab) to re-seed from the current built-in constant. When the built-in default changes, running this upgrade propagates the new default to existing agents; direct operator edits to an agent’s existing procedure file are preserved unless this upgrade is run (the upgrade overwrites the per-agent file).
POST /api/agents/:id/runs with source: "on_demand" executes the same autonomous heartbeat flow as timer/assignment triggers. It is not a mailbox-only poll.
Expected behavior for both manual and automatic triggers:
- Re-check identity/instructions context for this tick
- Process wake delta first (including message/comment wakes)
- Re-evaluate assignment state
- Take exactly one concrete next action
- Finish with
fn_heartbeat_done
Messages remain an important input signal, but they do not replace the heartbeat procedure.
For permanent agents, heartbeat runs now continue as an ambient coordination loop even when the currently bound task is blocked from normal task progress.
- Heartbeat path: coordination, wake processing, mailbox/delegation/memory/task-creation actions, and lightweight ambient follow-through.
- Executor path: task-body implementation work from task steps/prompts.
When allowParallelExecution is set to false on a permanent agent, the two paths serialize symmetrically:
- Heartbeat does not start while the bound task has an active executor session.
- Executor does not start while the agent has an active heartbeat run.
When allowParallelExecution is true (default), both paths may run concurrently.
When messaging tools are enabled for an agent, heartbeat runs check for unread mailbox messages during execution regardless of the trigger type. This ensures agents can see and respond to incoming messages without needing an explicit wake-on-message trigger.
Mailbox replies use message.metadata.replyTo.messageId as the stable reply link.
fn_read_messagesincludes each message ID in its human-readable output so agents can target a specific message.- When a message has
metadata.replyTo.messageId,fn_read_messagesnow includes one-level reply-parent context inline (and in structured tool details) so heartbeat/mailbox runs can understand what the message is replying to without expanding full threads. fn_send_messagesupportsreply_to_message_id; when provided, the sent message is stored withmetadata.replyTo.messageId.- Heartbeat prompts explicitly instruct agents to include
reply_to_message_idwhen replying.
The dashboard mailbox UI also uses the same metadata contract when users click Reply, so user and agent replies share one threading model.
For dashboard user messaging, agents should target the canonical user recipient ID dashboard.
When an agent is sending to the dashboard user through fn_send_message, the message must be stored as agent-to-user (agent → dashboard user), not as a user/CLI → agent mailbox message.
Runtime safeguards defensively normalize the legacy alias forms below to the same logical dashboard user:
dashboard(canonical)user:dashboardUser: user:dashboard
If the message type is omitted but the recipient normalizes to the dashboard user alias, routing defaults to the agent-to-user direction to preserve correct inbox semantics.
This normalization applies on send and mailbox reads, so replies from agents still land in the dashboard inbox even when older alias-like recipient strings appear.
Heartbeat runs now surface both direct-message inbox traffic and recent room activity for rooms the agent belongs to. Room traffic is lookback-based (bounded to the prior completed heartbeat / lastHeartbeatAt, capped at 24 hours) and is only shown when there are unread/recent messages worth surfacing.
- Message Prefetch: When
messageStoreis available, heartbeat runs fetch up to 10 unread inbox messages for the agent. - Room Prefetch: When
chatStoreis available, heartbeat runs fetch up to 10 recent room messages per active room (30 total max, self-authored room messages excluded). - Prompt Injection: Pending messages are injected into the execution prompt with message ID, sender, and timestamp information, followed by a Pending Room Messages section grouped by room.
- Reply Guidance: System instructions remind agents to reply with
reply_to_message_idfor direct messages and usefn_post_room_messageonly when room content is relevant to the agent’s role/identity. - Mark as Read: After successful heartbeat completion, direct inbox messages are marked as read.
- Failed Runs: If the heartbeat execution fails, inbox messages remain unread for retry on the next run.
Heartbeat prompts may include a Room Coordination Notices section after Room Ambiguity Notices when both conditions are true:
- A pending room message contains explicit task-filing intent (for example, "file a task" / "create a task").
- The room currently has at least two active agent members.
This notice is advisory prompt-layer routing (not server-side serialization), with two branches:
- claim: the agent should post a one-line claim via
fn_post_room_messagefirst, then callfn_task_create, then post the resultingFN-NNNNtask ID back to the room. - defer-suggested: a peer claim or task announcement was already seen in recent room history; the agent should not call
fn_task_create, and should instead acknowledge the prior claim/announcement viafn_post_room_message.
Deterministic duplicate prevention remains authoritative: FN-4918, FN-4829, FN-5152, and FN-5220 are still the hard intake backstop. The coordination notice reduces upstream duplicate pressure but does not replace those guards.
Each coordination decision emits a run-audit mutationType of room:coordination:branch with metadata:
roomIdagentIdbranch("claim" | "defer-suggested")memberCountintentCuepriorClaimMessageIdpriorTaskId
Layering order is intentional: ambiguity guidance renders first, coordination guidance renders second.
The messageResponseMode runtime configuration controls when agents are triggered by incoming messages:
| Mode | Behavior |
|---|---|
immediate |
Agent wakes immediately when a message arrives (via hook callback) |
on-heartbeat |
Agent processes messages during normal heartbeat runs only |
In the dashboard Agent Settings UI, this is surfaced as Message Response Mode with matching help text.
Important: Both modes include messages in the execution prompt. The immediate mode additionally triggers an immediate heartbeat run when a message arrives, while on-heartbeat relies on the agent's next scheduled heartbeat.
When sending a message to an agent from the dashboard mailbox composer, users can optionally enable Wake agent immediately for that send.
- The checkbox is shown only for agent recipients.
- If the target agent already uses
messageResponseMode: "immediate", the checkbox is shown as checked/locked to reflect that wake behavior is already always-on. - The send-time
wakeImmediatelyflag is transport-level only; it does not change the agent's savedruntimeConfig.messageResponseMode. - On successful send with
wakeImmediately: true, the API best-effort invokes an on-demand heartbeat (triggerDetail: "wake-on-message") in the correct project scope.
- Timer-triggered runs: Check mailbox and include pending messages
- Assignment-triggered runs: Check mailbox and include pending messages
- On-demand runs: Check mailbox and include pending messages
- Wake-on-message triggers: Check mailbox and include pending messages (same as other triggers, but triggered immediately)
This ensures inter-agent and user-to-agent communication is visible to agents on each run, avoiding stale coordination, missed instructions, and delayed responses.
Executor sessions can spawn child agents through spawn_agent.
Behavior:
- Child agents run in separate worktrees
- Parent/child relationship is tracked
- Limits enforced:
maxSpawnedAgentsPerParent(default 5)maxSpawnedAgentsGlobal(default 20)
- Child sessions terminate when parent task ends
spawn_agent is intentionally treated as an ephemeral runtime mutation, not durable provisioning:
fn_spawn_agentstays in action-gatetask_agent_mutationclassification.- Spawned children are created with ephemeral metadata (
metadata.type = "spawned") and task-scoped ownership (reportsTo = parentTaskId). - Parent teardown terminates/deletes spawned children; they are not durable hires.
- Therefore
projectSettings.agentProvisioning(FN-3791 policy for durablefn_agent_create/fn_agent_delete) does not governspawn_agent.
If a deployment config requires approval for task_agent_mutation, spawn_agent uses the standard action-gate approval pause/resume path (awaiting-approval + /api/approvals/:id/decision).
Executor and heartbeat agents can coordinate through six built-in tools: list_agents, delegate_task, agent_create, agent_delete, get_agent_config, and update_agent_config.
Delegation is designed for cross-agent handoff (e.g., an executor handing off to a QA agent). For parallel worktree-based parallelization, use spawn_agent instead.
List all available agents in the system. Shows each agent's name, role, state, personality (soul), and current assignment.
| Parameter | Type | Description |
|---|---|---|
role |
string (optional) |
Filter by agent role/capability (e.g., "executor", "reviewer") |
state |
string (optional) |
Filter by agent state (e.g., "idle", "active", "running") |
includeEphemeral |
boolean (optional) |
Include ephemeral/runtime agents (default: false) |
Create a new task and assign it to a specific agent for execution. The task goes to todo and will be picked up by the target agent on their next heartbeat cycle.
| Parameter | Type | Description |
|---|---|---|
agent_id |
string (required) |
The agent ID to delegate work to |
description |
string (required) |
What needs to be done |
dependencies |
string[] (optional) |
Task IDs this new task depends on |
override |
boolean (optional) |
Set true to bypass executor-role assignment policy |
Error cases:
"ERROR: Agent {agent_id} not found""ERROR: Cannot delegate to ephemeral/runtime agent {agent_id}""ERROR: Agent {agent_id} has role \"...\"; implementation task <new> requires an \"executor\"-role agent by default, with durable \"engineer\" supported only for explicit routing. Pass override=true to bypass.""ERROR: Task ID already exists: {id}"(allocator collision; request fails without mutating the existing task)
Create a new non-ephemeral direct-report agent. By default, the created agent reports to the caller; privileged (CEO-level) callers can set reportsTo to another manager.
| Parameter | Type | Description |
|---|---|---|
name |
string (required) |
Name for the new agent |
role |
"triage" | "executor" | "reviewer" | "merger" | "engineer" | "custom" (required) |
Agent role/capability |
soul |
string (optional) |
Agent personality/identity text |
instructions_text |
string (optional) |
Inline custom instructions |
instructions_path |
string (optional) |
Path to instructions markdown file |
reportsTo |
string (optional) |
Manager agent ID. Defaults to the calling agent |
heartbeat_interval_ms |
number (optional, min 1000) |
Heartbeat polling interval in milliseconds |
heartbeat_timeout_ms |
number (optional, min 5000) |
Heartbeat timeout in milliseconds |
max_concurrent_runs |
number (optional, min 1) |
Maximum concurrent heartbeat runs |
message_response_mode |
"immediate" | "on-heartbeat" (optional) |
How the agent responds to messages |
Authorization rule: Non-privileged callers may only create agents that report to themselves; privileged callers may set any reportsTo target.
Error case:
"ERROR: You can only create agents that report to you"
Delete a non-ephemeral direct-report agent. Deletion is blocked when the target holds a checkout lease unless force: true is provided, and assigned tasks can be reassigned during deletion via reassign_to.
| Parameter | Type | Description |
|---|---|---|
agent_id |
string (required) |
Agent ID to delete |
force |
boolean (optional) |
Force delete even if the agent currently holds a checkout lease |
reassign_to |
string (optional) |
Replacement agent ID for tasks currently assigned to the deleted agent |
Authorization rule: Callers can delete agents where target.reportsTo === caller.id; privileged callers may delete any non-ephemeral agent.
Error cases:
"ERROR: Agent {agent_id} not found""ERROR: You can only delete agents that report to you""ERROR: Cannot delete ephemeral/runtime agent {agent_id}"- Underlying store errors (for example, an active checkout lease) are returned as
"ERROR: {message}"; provideforce: trueto bypass lease-related blocking.
Implementation-task routing distinguishes explicit specialist assignment from generic backlog pickup:
- Explicit assignment/delegation (
PATCH /api/tasks/:id/assign,fn_delegate_task,fn_task_create/fn_task_updatewithagentId):role: "executor"is always supported, and durablerole: "engineer"is also supported without override. - Backlog pickup/auto-claim (unassigned implementation work): remains executor-only by default; durable engineer agents do not auto-claim generic unassigned implementation backlog.
- Other non-executor roles (for example
reviewer,merger,custom) still require an explicit override path on that surface (override: true) when intentional. - Override delegations are persisted with task source metadata (
executorRoleOverride) so inbox selection and heartbeat execution can intentionally run that assigned implementation task on the targeted durable non-executor agent.
Heartbeat/executor ownership now actively renews persisted task lease metadata while work is running (checkoutLeaseRenewedAt plus owner node/run context). Abandonment recovery is fenced by checkoutLeaseEpoch and executed only through MeshLeaseManager.recoverAbandonedLease(...), so stale owners cannot reclaim tasks after recovery.
Fusion's HeartbeatTriggerScheduler supports five trigger types:
timer— periodic wake based on heartbeat intervalassignment— wake when task is assigned to agenton_demand— manual run trigger (POST /api/agents/:id/runs)automation— triggered by scheduled automation jobsroutine— triggered by routine execution
All triggers respect per-agent maxConcurrentRuns and produce structured wake context metadata.
Pause governance for heartbeat execution:
globalPauseis a hard stop: timer, assignment, and on-demand heartbeats are skipped with observable run reasons.enginePausedis a soft stop for heartbeat timers: timer triggers are skipped, while assignment/on-demand triggers remain allowed for critical responsiveness paths.
Heartbeat runs from the Agents panel run on a separate control-plane lane that is independent of task execution concurrency limits. This ensures agent responsiveness is preserved even when task pipelines are saturated.
Key behaviors:
- Heartbeat runs (via
POST /api/agents/:id/runs) execute without gating onmaxConcurrentor in-progress task count - The
HeartbeatTriggerSchedulerandHeartbeatMonitorcomponents do not receive the task-lane semaphore - Trigger scheduling remains responsive regardless of how busy the task pipeline is
- Active-run 409 conflict semantics still apply — a new heartbeat run is rejected if the agent already has an active run
POST /api/agents/:id/stateapplies pause/resume immediately when monitor-bound:- Transitioning to
pausedfirst stops any active run viaHeartbeatMonitor.stopRun(agentId) - Transitioning to
activeimmediately callsHeartbeatMonitor.executeHeartbeat(...)(source:on_demand)
- Transitioning to
Architectural boundary:
| Component | Path | Concurrency |
|---|---|---|
| PlanningProcessor | Task lane | Semaphore-gated |
| TaskExecutor | Task lane | Semaphore-gated |
| Scheduler | Task lane | Semaphore-gated |
| onMerge | Task lane | Semaphore-gated |
| HeartbeatMonitor | Utility/control plane | NOT semaphore-gated |
| HeartbeatTriggerScheduler | Utility/control plane | NOT semaphore-gated |
| CronRunner | Utility/control plane | NOT semaphore-gated |
HeartbeatTriggerScheduler now owns a timer-registration repair sweep so durable agents cannot stay unscheduled when in-memory timer entries are lost without a follow-up lifecycle event.
Cadence:
- Immediate startup audit: runs once in
start()after lifecycle watchers are attached - Periodic audit: runs every 60s while the scheduler is active
- Cleanup: periodic sweep interval is cleared in
stop()
Repair eligibility:
- Durable (non-ephemeral/task-worker) agent
runtimeConfig.enabled !== false- Agent state is tickable:
active,running, oridle - Agent is missing from the scheduler's in-memory
timersmap
Repair outcomes:
- Missing timer, not stale: timer is re-armed and INFO diagnostics are logged (
agentId, resolved interval, elapsed time sincelastHeartbeatAt) - Missing timer, stale: timer is re-armed, WARN diagnostics are logged, and
agent.metadata.heartbeatTimerRepairis updated (repairedAt,staleAtRepair,elapsedMs,staleThresholdMs)
Stale threshold:
- Repair staleness defaults to
2 × heartbeatIntervalMs - This is intentionally separate from dashboard display staleness (
1.5× heartbeatIntervalMswith a 5-minute floor)
Dashboard surfacing path:
- The stale-repair metadata write uses the existing
AgentStore.updateAgent(...)path - That emits
agent:updated, which already flows through SSE (packages/dashboard/src/sse.ts) useAgentsalready refreshes onagent:updated/agent:stateChanged(packages/dashboard/app/hooks/useAgents.ts)- No new SSE event is introduced; stale durable agents become dashboard-visible as
Unresponsivethrough the existing refresh path
Orphaned-timer invalidation on stop (FN-7718): CLI-driven fn agent stop/start mutate the agent row from a separate process, so the in-process agent:updated listener never fires for those transitions — the 60s audit is the only cross-process reconciliation path. The audit now unregisters a lingering timer entry for any agent that fails eligibility (non-tickable state, runtimeConfig.enabled === false, or ephemeral/non-heartbeat-managed) instead of skipping past it, so a stopped agent never keeps an orphaned/"zombie" registration. syncTimerForAgent's in-process start seam mirrors this: an eligible agent with a present-but-stale timer entry is force-cleared and re-armed rather than left as a no-op. This means a stop/start cycle is a durable fix — it no longer relies on the FN-7645 stale-repair path eventually catching the drift minutes later.
Heartbeat timers are armed for agents in valid working states and remain armed across state transitions:
States where timers remain armed:
active— Agent is actively working on a taskrunning— Agent has an active heartbeat run in progressidle— Agent is between tasks, waiting for work
States where timers are cleared:
error— Agent encountered an unrecoverable errorpaused— Agent is paused (e.g., by budget exhaustion, manual stop, or manual pause)
Lifecycle notes:
- Agent lifecycle is
idle | active | running | paused | error(there is noterminatedAgentState). - Stop/termination flows land the agent in
paused;terminatedis reserved for heartbeat run status only.
Key behaviors:
- Timers remain armed when agents transition between
active,running, andidlestates - This ensures heartbeat cadence is maintained even when agents complete tasks and await new assignments
- Ephemeral/task-worker agents are never armed with timers (managed directly by TaskExecutor)
- The
runtimeConfig.enabledflag is respected for disabling heartbeat monitoring entirely
When a tracked agent misses heartbeat for 2 × heartbeatTimeoutMs, the monitor now performs recovery (not termination). The base heartbeatTimeoutMs is already multiplier-scaled (heartbeatMultiplier) before applying this × 2 window:
- Dispose the stuck session and untrack the stale run
pauseAgent(agentId, { pauseReason: "heartbeat-unresponsive", stopActiveRun: false })resumeAgent(agentId, { triggerDetail: "unresponsive-recovery", triggerSource: "heartbeat-unresponsive", clearPauseReason: true })
Effects:
- Agent state transitions
running/active → paused → active - Orphan reconcile uses
3 × heartbeatTimeoutMswhere the timeout is likewise multiplier-scaled first pauseReasonis set toheartbeat-unresponsiveduring recovery and cleared on resume- Assigned tasks are not paused or unpaused by agent sleep/heartbeat recovery; unpaused work stays eligible for scheduler re-dispatch, while tasks already paused by a user retain their existing pause state
- Resume triggers one on-demand heartbeat restart only when
runtimeConfig.enabled !== false onTerminatedis a run-level callback for terminated heartbeat runs and is not used by unresponsive recovery
HeartbeatTriggerScheduler owns a periodic registration audit that reconciles durable-agent truth in AgentStore against the in-memory timer map.
- Audit cadence: once immediately on scheduler start, then every 60 seconds while running
- Repair target: durable, heartbeat-enabled agents in tickable states (
active,running,idle) that are missing a timer entry - Safety guards: skip ephemeral/task-worker agents, skip disabled agents, skip non-tickable states, and skip agents with an active heartbeat run
- Existing timer entries are left untouched (no interval reset/jitter churn)
- Repair metadata: each audit re-arm writes
metadata.heartbeatTimerRepairwithrepairedAtand a stale-at-repair indicator when the agent had already missed its expected cadence - Stale-at-repair threshold: defaults to
2 × heartbeatIntervalMs; override with project settingheartbeatRepairStaleMultiplier(> 0) when you need a different sensitivity - Stale repairs emit a WARN log entry and still flow through the existing
agent:updatedrefresh path for dashboard surfacing
This covers the untracked timer-loss failure mode where no agent:updated event fires after a timer entry disappears. Manual stop/start is no longer required to re-arm the timer in that case.
HeartbeatTriggerScheduler also reaps stale persisted status="active" heartbeat runs before they can block future timer progress forever.
When it fires:
onTimerTick()finds an active run row for a durable, tickable agent- or
auditTimerRegistrations()finds a missing timer plus an active run row for that same durable agent - the persisted run has no fresh heartbeat for longer than
heartbeatTimeoutMs × heartbeatRepairStaleMultiplier - the engine is not globally paused and not timer-paused via
enginePaused
Threshold semantics:
- The reaper reuses the same
heartbeatRepairStaleMultipliersetting that timer-audit repair already uses; no extra stale-run knob exists - The base signal is the agent's
lastHeartbeatAt/recordHeartbeat(...)freshness, not the scheduled timer interval - Default threshold is therefore
2 × heartbeatTimeoutMs(default timeout60s→ default reap threshold120s)
Layering with the existing recovery paths:
HeartbeatMonitor.reconcileOrphanedRunningAgents()handles orphaned persistedstate="running"rows and usesheartbeatTimeoutMs × 3as a run-budget grace threshold when the active run exists but is untracked.HeartbeatTriggerScheduler.onTimerTick()reaps stale persistedstatus="active"runs atheartbeatTimeoutMs × heartbeatRepairStaleMultiplier, logsreason=tick-proceeded-after-reap, and proceeds with the scheduled callback in the same tick.HeartbeatTriggerScheduler.auditTimerRegistrations()applies the same stale-run threshold, then reaps stale active runs before re-arming missing timers and logsreason=timer-audit-rearmed.SelfHealingManager.recoverAgentsRunningOnInactiveTasks()/recoverStaleHeartbeatRuns()are the final backstop using task-state mismatch and PID/max-age guards (not the timeout multiplier formulas above).- Healthy active runs within threshold still keep the old
(active run)skip behavior. - Ephemeral/task-worker agents are never reaped by this path.
Separation of responsibilities:
- HeartbeatMonitor recovery handles tracked stale sessions (stuck in-memory run/session cleanup + pause/resume restart)
- HeartbeatTriggerScheduler audit handles untracked missing-timer registration drift (re-arm scheduling)
- HeartbeatTriggerScheduler stale-run reaper handles orphaned persisted active runs that would otherwise cause both tick and audit to skip forever on
(active run)
The dashboard displays agent health status in AgentsView, AgentListModal, and AgentDetailView using a centralized health evaluation utility (packages/dashboard/app/utils/agentHealth.ts).
| Label | Condition |
|---|---|
| Error | Agent state is "error" (uses lastError if available) |
| Paused | Agent state is "paused" (uses pauseReason if available) |
| Running | Agent state is "running" (task workers with active state also display "Running") |
| Heartbeat Disabled | runtimeConfig.enabled === false |
| Starting... | State is "active" with no lastHeartbeatAt |
| Idle | Non-active state with no lastHeartbeatAt |
| Healthy | Heartbeat is fresh within the resolved interval-based staleness threshold |
| Unresponsive | Heartbeat exceeded the resolved interval-based staleness threshold, or timer-repair metadata indicates scheduler-detected stale drift before the next successful heartbeat |
Health status uses interval-based staleness evaluation:
- Resolve the effective heartbeat interval from
runtimeConfig.heartbeatIntervalMs(or the default 1 hour interval) - Multiply that interval by the dashboard grace multiplier (
1.5×) - Apply a minimum staleness floor of 5 minutes
- Monitoring disabled: Agents with
runtimeConfig.enabled === falsedisplay "Disabled" — they are NOT falsely labeled as "Unresponsive" - Interval-sized gaps are normal: With the default
heartbeatIntervalMs = 3600000(1 hour), an agent can legitimately go tens of minutes without a new heartbeat. Ages like 16–50 minutes are expected and should not be treated as unhealthy on interval age alone. - Consistent across views: All dashboard surfaces use the same centralized utility, ensuring consistent health labels everywhere
- Auto-refresh: Health status is refreshed every 30 seconds while views are open to keep status current
- State-first evaluation: Explicit non-idle states (error, paused, running) take priority over timeout-based evaluation
- Repair-aware surfacing: If scheduler audit repairs a missing timer and marks the agent stale, dashboard surfaces
Unresponsiveimmediately until a newer heartbeat arrives
Agent runs have a defined lifecycle managed by AgentStore:
A heartbeat run can be in one of these states:
active— Run is currently executingcompleted— Run finished successfully (viaendHeartbeatRun(runId, "completed"))terminated— Run was stopped (viaendHeartbeatRun(runId, "terminated"))failed— Run encountered an error
startHeartbeatRun(agentId)— Creates a new run and persists it to structured storageendHeartbeatRun(runId, status)— Ends a run with terminal status, updates persisted stategetActiveHeartbeatRun(agentId)— Returns the current active run (or null)getCompletedHeartbeatRuns(agentId)— Returns all terminal runs (newest first)saveRun(run)— Persists run to structured storagegetRunDetail(agentId, runId)— Gets a specific run by ID
When an agent already has an active run, attempts to start a new run return 409 Conflict:
POST /api/agents/:id/runs → 409 { error: "Agent already has an active run", details: { runId } }
After a run is completed (or terminated at the run level), a new run can be started successfully:
POST /api/agents/:id/runs → 201 { id: "run-xxx", status: "active", ... }
Run records are stored in structured JSON files at .fusion/agents/{agentId}-runs/{runId}.json.
Heartbeat events are also appended to .fusion/agents/{agentId}-heartbeats.jsonl for legacy compatibility. The structured storage is the source of truth; heartbeat events provide a fallback for older run data.
persistAgentThinkingLog is a boolean setting with a default of false (legacy alias for the granular persistence keys). It controls whether thinking/reasoning agent-log rows are persisted across agent roles (executor, reviewer, merger, triage, and step-session). When disabled (the default), only thinking rows are suppressed; normal assistant text output and tool rows are unchanged. See the settings reference for full configuration details.
Use POST /api/agents/:id/runs/stop to terminate an active run:
POST /api/agents/:id/runs/stop → 200 { ok: true, runId: "run-xxx" }
If there's no active run, returns { ok: true, message: "No active run" }.
Per-agent token budget tracking controls costs and prevents runaway AI spending. Budget configuration is stored in runtimeConfig.budgetConfig.
| Field | Type | Description |
|---|---|---|
tokenBudget |
number |
Maximum tokens allowed per budget period |
usageThreshold |
number (0-1) |
Percentage threshold (0.8 = 80%) to trigger warning/warning state |
budgetPeriod |
"daily" | "weekly" | "monthly" | "total" |
Reset interval for budget tracking |
resetDay |
number (0-6) |
Day of week for weekly reset (0=Sunday) |
| Field | Type | Description |
|---|---|---|
isOverBudget |
boolean |
Budget limit exceeded |
isOverThreshold |
boolean |
Usage exceeded warning threshold |
periodStart |
string |
ISO timestamp when current period started |
inputTokens |
number |
Tokens used in current period |
outputTokens |
number |
Tokens generated in current period |
totalTokens |
number |
Combined input + output tokens |
Budget enforcement is centralized in HeartbeatMonitor.executeHeartbeat():
- Timer triggers: Budget is enforced in
executeHeartbeat()which creates explicit run records withbudget_exhaustedorbudget_threshold_exceededreasons. This makes timer budget skips observable rather than silent drops — users see explicit "skipped" run records in the dashboard instead of timer ticks that appear to "not run". - Assignment and on-demand triggers: Budget is enforced in
executeHeartbeat()with the same outcome recording. These triggers are allowed when over threshold (but not over budget) to maintain responsiveness.
When the engine is not paused, the HeartbeatTriggerScheduler dispatches timer callbacks regardless of budget status, delegating budget enforcement to the execution layer. This ensures every eligible timer tick produces a heartbeat run record that is visible in the agent's run history.
Agents can be paused by budget exhaustion. Timer-triggered heartbeats skip when over threshold to avoid runaway costs, but assignment-triggered and on-demand runs may still execute for responsiveness.
| Method | Path | Description |
|---|---|---|
GET |
/api/agents/:id/budget |
Get current budget status |
POST |
/api/agents/:id/budget/reset |
Reset budget counters for current period |
Agent performance ratings allow users and agents to provide feedback that influences future behavior through system prompt injection.
| Method | Path | Description |
|---|---|---|
GET |
/api/agents/:id/ratings |
List all ratings for an agent |
POST |
/api/agents/:id/ratings |
Submit a new rating |
GET |
/api/agents/:id/ratings/summary |
Get aggregated rating summary |
DELETE |
/api/agents/:id/ratings/:ratingId |
Delete a specific rating |
Ratings use a 1-5 scale:
| Value | Meaning |
|---|---|
| 1 | Poor — consistently fails or produces low-quality output |
| 2 | Below average — often needs correction |
| 3 | Average — meets expectations with occasional issues |
| 4 | Good — reliable with minor improvements possible |
| 5 | Excellent — exceeds expectations consistently |
The summary endpoint returns aggregated statistics:
{
"agentId": "AGENT-001",
"averageRating": 4.2,
"totalRatings": 15,
"ratingDistribution": { "1": 0, "2": 1, "3": 2, "4": 8, "5": 4 },
"trend": "improving"
}The trend field indicates rating trajectory: "improving", "declining", or "stable".
To submit a rating:
POST /api/agents/:id/ratings
{
"rating": 4,
"comment": "Agent completed the task efficiently with minimal corrections needed",
"taskId": "FN-123"
}
Permanent-agent sensitive actions are gated across these categories:
git_writefile_write_deletecommand_executionnetwork_apitask_agent_mutationreview_gate_bypass(FN-7728; overridden torequire-approvalunderunrestricted/grant-all)file_scope(FN-7737; uniform disposition, sounrestricted/grant-all resolves it toallow)
Each category can be set to one disposition:
allowrequire-approvalblock
Precedence:
- Per-agent exact
toolRulesoverride (Agent Detail → Settings → Permissions) - Per-agent category rule
- Project default exact
toolRulesoverride (defaultAgentPermissionPolicyin Project Settings → Agent Permissions) - Project default category rule
- Built-in fallback preset (
unrestricted/ allow-all)
For example, toolRules: { "fn_task_create": "block" } with rules.task_agent_mutation: "allow" blocks new task creation while allowing other governed task-agent mutation tools. Per-agent category rows can inherit project defaults category-by-category; per-agent exact-tool rows override project exact-tool rows when present.
The pi extension ships as part of @runfusion/fusion and provides tools + a /fn command for chat agents.
Update when:
- CLI commands change (behavior, flags, output)
- Task store / Agent store API changes
- New user-facing features chat agents should be able to use
Don't add tools for engine-internal operations (move, step updates, logging, merge) — those are owned by the engine's own agents.
The extension has no skills — tool descriptions give the LLM everything it needs.
Published SDK surface: @runfusion/fusion/plugin-sdk now ships as a public subpath export from the CLI package, exposing definePlugin, validatePluginManifest, and the plugin type surface for external plugin authors without depending on private @fusion/* workspace packages.
Lightweight URL read from agent/chat sessions. HTTP GET, follows redirects, extracts readable text (HTML→text and JSON pretty-print), bounded.
Universal baseline: available by default across executor, step-session, reviewer, merger, triage, and heartbeat (including engineer/custom direct-report paths). Gated under the network_api action-gate category (FN-4603).
- Defaults:
timeoutMs=30000,maxBytes=512000(500 KB) - Blocks private/loopback/link-local hosts (including DNS-resolved) unless explicitly overridden in internal/test contexts
- Read-only (no JS rendering, no auth flows, no POST/cookie workflows)
- Use the
agent-browserskill when JS rendering or interactive navigation is required
Agents now emit a durable goal-citation signal whenever reasoning text includes a goal ID.
- Scanned surfaces:
agent_logandtask_document. - Regex contract:
GOAL_ID_PATTERN = /\bG-[0-9A-Z]+(?:-[0-9A-Z]+)*\b/g(uppercaseG-...only). - Snippets are bounded windows (
GOAL_CITATION_SNIPPET_MAX = 200) around the first match per goal ID, with whitespace collapsed. - Query via CLI:
fn goals citations --since <iso> --until <iso>fn goals citations --goal G-XXXX --since <iso>
- Example row:
2026-05-29T08:10:00.000Z G-1ABC-2-XYZ9 agent-ops agent_log agentLog:4821...anchoring this plan to G-1ABC-2-XYZ9 before execution...
- Programmatic consumers can query the same signal through
TaskStore.listGoalCitations(...).
Seven coordination tools support spawning, provisioning, discovery, delegation, and direct-report config.
spawn_agent— Parent-task-scoped ephemeral child in its own worktree. Limits viamaxSpawnedAgentsPerParent(default 5) andmaxSpawnedAgentsGlobal(default 20). Auto-terminated with parent. Gated under generictask_agent_mutation(FN-3973 explicitly excludes it from durableagentProvisioningpolicy).agent_create/agent_delete— Non-ephemeral provisioning of direct reports. Policy-gated viaprojectSettings.agentProvisioning(approvalMode,trustedRoles,trustedAgentIds,alwaysApproveDelete). Tool responses usedetails.outcome:created/deleted/pending_approval/denied. Pending requests resolve viaPOST /api/approvals/:id/decision. Audit events:agent:{create,delete}:{requested,approved,denied}.list_agents— Discovery withrole/state/includeEphemeralfilters.delegate_task— Create + assign task to a specific agent. Implementation tasks require executor-role target unlessoverride: true. Cannot target ephemeral agents (usespawn_agent).get_agent_config/update_agent_config— Read/write soul, instructions, heartbeat interval/timeout, max concurrent runs, message response mode. Authorization: caller can only act on agents wheretarget.reportsTo === caller.id. Cannot operate on ephemeral agents.
- 409 Conflict = ownership contention. Response:
{ error, currentHolder, taskId }. Never auto-retry 409. HeartbeatMonitor.executeHeartbeat()validates checkout before work begins; mismatchedcheckedOutByexits withreason: "checkout_conflict". Heartbeat does not auto-checkout — callers obtain the lease.- With
CentralClaimStorewired, the authoritative owner is the centraltaskClaimsrow; per-project lease fields mirror it.MeshLeaseManager.recoverAbandonedLease()releases central first then local.reconcileLeaseRow(taskId)converges divergent state on the next tick (emitstask:auto-recover-lease-*). Without a claim store, behavior remains single-node per-project.
Per-agent overrides via runtimeConfig:
- Heartbeat:
heartbeatIntervalMs,heartbeatTimeoutMs,maxConcurrentRuns. Triggered by timer, task assignment, or on-demand (POST /api/agents/:id/runs). - Budgets: per-agent token budget tracking;
HeartbeatMonitor.executeHeartbeat()skips whenisOverBudgetorisOverThreshold(timer triggers). Hard caps pause the agent. - Performance ratings: 1–5 scale with trend analysis, injected into system prompts.
