Skip to content

🤖 feat: add workspace heartbeat tool#3596

Merged
ThomasK33 merged 7 commits into
mainfrom
heartbeat-nzs9
Jun 19, 2026
Merged

🤖 feat: add workspace heartbeat tool#3596
ThomasK33 merged 7 commits into
mainfrom
heartbeat-nzs9

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary

Adds a model-facing heartbeat tool so an agent can read, set, or unset the scheduled heartbeat for its current workspace without editing config files directly.

Background

Workspace heartbeats already exist as a backend scheduling mechanism, but agents could not adjust their own idle check-in cadence or disable/reset workspace-specific overrides during long-running work. This exposes that capability through the existing tool pipeline while keeping the target scoped to the executing workspace.

Implementation

  • Defines heartbeat tool input/result schemas, bridgeable PTC result typing, generated hook docs, and shared HeartbeatToolArgs/HeartbeatToolResult types.
  • Adds createHeartbeatTool, wired into getToolsForModel only when a heartbeat service is available, the workspace-heartbeats experiment is enabled, and the workspace is not an agent-report child workspace.
  • Injects WorkspaceService into AIService as the heartbeat tool service and propagates the resolved heartbeat experiment flag through preserved send options/workflow task experiment context.
  • Extends WorkspaceService to resolve heartbeat workspace entries, return canonical settings from sparse updates, default sparse persisted heartbeat intervals on read, unset workspace-specific heartbeat settings, and preserve ORPC compatibility by mapping set results back to Result<void, string>.
  • Normalizes heartbeat metadata in Config.getAllWorkspaceMetadata() so sparse persisted heartbeat settings stay schema-valid before the renderer lists workspaces.
  • Maps workspace-heartbeats through mux run --experiment and mux workflow --experiment request-scoped experiment options.
  • Updates hook/context tests to use real providers with injected clients instead of process-global API module mocks, preventing leakage into unrelated hook tests during the unit suite.

Validation

  • bun test src/node/services/tools/heartbeat.test.ts src/node/services/workspaceService.heartbeatSettings.test.ts src/common/utils/tools/tools.test.ts
  • bun test src/common/utils/tools/toolDefinitions.test.ts src/node/services/ptc/typeGenerator.test.ts
  • bun test src/node/services/workspaceService.heartbeatSettings.test.ts src/node/services/tools/heartbeat.test.ts
  • bun test src/node/config.test.ts -t "defaults sparse persisted heartbeat intervals in workspace metadata"
  • bun test src/cli/workflow.test.ts -t "CLI commands work without the dynamic-workflows experiment"
  • bun test src/browser/hooks/useWorkspaceHeartbeat.test.tsx src/browser/hooks/useChatTranscriptFullWidth.test.tsx
  • bun test src/browser/contexts/WorkspaceContext.test.tsx src/browser/hooks/useWorkspaceHeartbeat.test.tsx src/browser/hooks/useChatTranscriptFullWidth.test.tsx
  • bun test src/node/services/workflows/WorkflowTaskServiceAdapter.test.ts
  • make typecheck
  • git diff --check
  • make static-check
  • MUX_ESLINT_CONCURRENCY=1 make static-check after the hook/context test isolation and experiment-gating/sparse-metadata fixes (default local ESLint concurrency was killed by the workspace memory limit)
  • Ran the simplify workflow and committed its actionable cleanup pass.

Risks

Low-to-medium. The change touches model tool assembly, shared tool schemas, workspace config persistence/metadata normalization, CLI experiment option mapping, and focused test fixtures. The heartbeat tool is gated by the same experiment used by the UI/keybind/slash-command paths, only exposed for normal workspaces with the heartbeat service injected, and existing ORPC heartbeat settings behavior is preserved.


Generated with mux • Model: openai:gpt-5.5 • Thinking: xhigh • Cost: $33.49

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@mintlify

mintlify Bot commented Jun 19, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Mux 🟢 Ready View Preview Jun 19, 2026, 8:44 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

Re-requesting after the hook test isolation fix for the unit-suite failure.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dc112a5b46

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/common/utils/tools/tools.ts Outdated
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

Addressed your heartbeat experiment-gating feedback by requiring the resolved workspace-heartbeats experiment before registering the model-facing heartbeat tool, and by propagating that resolved flag through preserved send/workflow task experiment context.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 776d8cff6c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

Re-requesting after the latest unit-suite fix: WorkspaceContext.test.tsx now uses the real APIProvider with an injected client instead of a process-global API module mock, which should stop leaking the mocked APIProvider into the heartbeat/full-width hook tests.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d0162af7f6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/workspaceService.ts Outdated
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

Addressed the sparse heartbeat interval feedback by normalizing persisted heartbeat settings with the resolved global default interval before returning them from WorkspaceService, and added a regression test for missing persisted intervalMs.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4e8e9e3c2d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/workspaceService.ts
Comment thread src/common/orpc/schemas/stream.ts
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

Addressed both latest findings:

  • heartbeat metadata is now normalized in Config.getAllWorkspaceMetadata() so sparse persisted intervals are defaulted before workspace.list/renderer validation
  • workspace-heartbeats now maps through both mux run --experiment and mux workflow --experiment request-scoped experiment objects

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 64ea213142

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ThomasK33 ThomasK33 added this pull request to the merge queue Jun 19, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 19, 2026
@ThomasK33 ThomasK33 added this pull request to the merge queue Jun 19, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 19, 2026
@ThomasK33 ThomasK33 added this pull request to the merge queue Jun 19, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 19, 2026
@ThomasK33 ThomasK33 added this pull request to the merge queue Jun 19, 2026
Merged via the queue into main with commit c89f5e1 Jun 19, 2026
22 of 24 checks passed
@ThomasK33 ThomasK33 deleted the heartbeat-nzs9 branch June 19, 2026 11:16
@mux-bot mux-bot Bot mentioned this pull request Jun 19, 2026
jim-fung pushed a commit to jim-fung/mux that referenced this pull request Jul 1, 2026
## Summary

Long-lived auto-cleanup PR maintained by the **Auto-Cleanup Agent**.
Each pass lands at most one extremely low-risk, behavior-preserving
cleanup drawn from recently merged `main` activity, then advances the
checkpoint below. The prior long-lived PR (coder#3559) merged into `main`;
this is its successor.

Auto-cleanup checkpoint: 2af29df

## This pass

Considered the one new `main` commit since the previous checkpoint —
coder#3603 (`feat: add built-in loop skill`). That change adds the advertised
`loop` built-in skill (`src/node/builtinSkills/loop.md`), regenerates
`builtInSkillContent.generated.ts` (generated — off-limits), and extends
the agent-skills discovery test expectations. The
skill-content/generated surface offers no safe hand-edit, so the cleanup
was drawn from the production sibling of that area: the skill-discovery
service `agentSkillsService.ts`.

Landed one behavior-preserving cleanup in `agentSkillsService.ts`:
`readSkillDescriptorFromDir` open-coded the same invalid-skill
diagnostic push — `options?.invalidSkills?.push({ directoryName, scope,
displayPath: skillFilePath, message, hint })` — at six failure sites
(missing/unreadable SKILL.md, SKILL.md-is-a-directory, oversized file,
read failure, invalid descriptor, parse failure). Those six sites share
identical identity fields (`directoryName` / `scope` / `displayPath`)
and differ only in `message` + `hint`. All six now delegate to a single
function-local `pushInvalidSkill(message, hint)` closure that captures
the function-constant identity fields. Behavior-preserving: the pushed
object shape and every per-site `message`/`hint` are unchanged, and the
closure short-circuits on `options?.invalidSkills` exactly as the inline
calls did.

## Branch contents (net diff against `main`)

<details>
<summary>Cleanups carried by this PR (not yet merged)</summary>

- **`agentSkillsService.ts` — dedupe invalid-skill diagnostics.**
coder#3603's new built-in skill lives in the agent-skills discovery area;
`readSkillDescriptorFromDir` open-coded the same
`options?.invalidSkills?.push({ directoryName, scope, displayPath:
skillFilePath, message, hint })` at six failure sites that share
identical identity fields and differ only in `message`/`hint`. All six
now delegate to a single function-local `pushInvalidSkill(message,
hint)` closure. Behavior-preserving (identical pushed shape, messages,
hints, and `options?.invalidSkills` short-circuit).
- **`workspaceGoalService.ts` — dedupe session file path resolution.**
The three private accessors `getFilePath` / `getHistoryFilePath` /
`getBoardFilePath` each open-coded the same `assert(...non-empty
workspaceId)` guard and `path.join(getSessionDir(...), <FILE_CONST>)`.
All three now delegate to a single `resolveSessionFilePath(workspaceId,
fileName)` helper. Behavior-preserving (identical guard + join; accessor
names and callers unchanged).
- **`task.ts` — dedupe plan-agent explore-only error.** The plan-agent
restriction error `In the plan agent you may only spawn agentId:
"explore" tasks.` was open-coded twice (the workspace-turn guard and the
per-launch agent-id guard). Both now reference a single module-level
`PLAN_AGENT_EXPLORE_ONLY_ERROR` constant. Behavior-preserving (identical
string output).
- **`task_await.ts` — dedupe workspace-turn error fallback.** coder#3600's
new workspace-turn await paths open-coded the fallback error message
`"Workspace turn failed"` three times (`<record>.error ?? "Workspace
turn failed"`). All three now reference a single module-level
`WORKSPACE_TURN_DEFAULT_ERROR` constant. Behavior-preserving (identical
string output).
- **`goalToolUtils.tsx` / `SetGoalToolCall.tsx` — dedupe turn-count
pluralization.** coder#3595's new goal toolcards open-coded `` `${n} turn${n
=== 1 ? "" : "s"}` `` in both `formatGoalTurns` and the set_goal
toolcard's `formatOptionalTurnCap`. Both now call a shared
`pluralizeTurns(count)` helper. Behavior-preserving (identical string
output).
- **`heartbeat.ts` — dedupe heartbeat tool success result
construction.** coder#3596's new `heartbeat` tool open-coded the same `{
success: true, action, configured, settings, summary }` success object
three times (`get` / `set` / `unset`), each re-calling `summarize({
action, settings })`. All three now route through a single
`buildSuccessResult(action, settings)` helper that derives `configured`
from `settings`. Behavior-preserving: the per-branch `configured` values
(`settings != null`, `false`, `true`) all equal `settings != null` given
the non-null `Ok` payload type for `set`.
- **`taskService.ts` — dedupe startup recovery candidate listing.**
coder#3594's `TaskService.initialize` open-coded the `awaiting_report` /
`running` candidate filters twice (initial computation and
post-interrupt refresh). Both call sites now reuse a single
`listStartupRecoveryCandidates(sourceConfig)` closure returning `{
awaitingReportTasks, runningTasks }`, matching the existing
`listQueuedTasks` pattern. Behavior-preserving.
- **`ChatPane.tsx` — dedupe send-queued-immediately in-flight guard
clear.** coder#3592 introduced `sendQueuedImmediatelyInFlightRef` to block
duplicate send-now attempts; releasing that guard ("clear it only if it
still points at this attempt") was open-coded twice — once after a
failed interrupt result and once in the `catch` arm. Both call sites now
invoke a single callback-local closure `clearInFlightGuardIfCurrent()`.
Behavior-preserving: the failed-result site's combined
`!interruptResult.success && ref === id` becomes `if
(!interruptResult.success) clearInFlightGuardIfCurrent()` (logically
identical); the `catch` site is a verbatim substitution.
- **`WorkflowRunner.ts` — dedupe `parallelAgents` child-failure abort
handling** into a single batch-local closure
`applyChildFailureToBatch(error)`, shared by the per-run task closure
and the bulk-create path.
- **`ChatInput/index.tsx` — dedupe edit-mode attachment toast** string
`"Attachments cannot be added while editing a message."` into a single
module-level constant `EDIT_MODE_ATTACHMENT_ERROR_MESSAGE`, replacing
three inline copies across the add-attachment / paste / drop guards.

</details>

<details>
<summary>Prior passes (landed via coder#3559, merged)</summary>

- Replaced an inline terminal-status check in
`WorkflowService.interruptRunOnAbort` with the
`isTerminalWorkflowRunStatus` helper.
- Deduplicated the `workspaceId`-only metadata JSON Schema repeated by
three id-targeted host actions in `workspaceHostActions.ts` into
`WORKSPACE_ID_ONLY_INPUT_SCHEMA`.
- Extracted a shared `BlockedBadge` component in
`WorkflowDefinitionToolCall.tsx`.
- Deduplicated the repeated `argsSchema.properties must be an object`
error string in `workflowArgs.ts`.
- Deduplicated mocked-module specifier strings in
`useModelsFromSettings.test.ts`.
- Removed a redundant `toggleWorkflowExpanded` wrapper in
`WorkflowRunToolCall.tsx`.
- Deduplicated `SAVE_AUTOMATION_ERROR_MESSAGE` /
`REMOVE_AUTOMATION_ERROR_MESSAGE` in `AutomationModal.tsx`.
- Replaced a duplicated `"wfr_"` literal in `WorkflowRunner.ts` with
`isWorkflowRunTaskId()`.

</details>

## Validation

- `make static-check` — ESLint, both `tsgo` typecheck configs, and
Prettier all pass on the rebased tree. The only failing step is
`fmt-shell-check`, which requires `shfmt` (not installed in this
environment); no shell scripts were touched, so CI (which has `shfmt`)
is unaffected.
- `bun test src/node/services/agentSkills/agentSkillsService.test.ts` —
20 pass, 0 fail (covers skill discovery + the invalid-skill diagnostics
path this pass deduped); `builtInLoopSkill.test.ts` — 1 pass.

---

_Generated with `mux` • Model: `anthropic:claude-opus-4-8` • Thinking:
`xhigh`_

<!-- mux-attribution: model=anthropic:claude-opus-4-8 thinking=xhigh -->

---------

Co-authored-by: mux-bot[bot] <264182336+mux-bot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant