🤖 feat: add per-workspace heartbeat messages#3105
Conversation
Add a workspace-scoped heartbeat message override to the existing heartbeat modal and preserve it across backend prompt composition and slash-command updates. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `$18.31`_ <!-- mux-attribution: model=openai:gpt-5.4 thinking=xhigh costs=18.31 -->
|
@codex review |
Trigger a fresh PR check run for the per-workspace heartbeat message change. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `$18.31`_ <!-- mux-attribution: model=openai:gpt-5.4 thinking=xhigh costs=18.31 -->
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 51a4524150
ℹ️ 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".
Make /heartbeat writes treat reading existing heartbeat settings as best-effort so malformed persisted data does not block interval updates that can repair state. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `$18.31`_ <!-- mux-attribution: model=openai:gpt-5.4 thinking=xhigh costs=18.31 -->
|
@codex review Please take another look. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a038e03162
ℹ️ 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".
Make heartbeat interval updates omit the message field on read failure so the backend can preserve any existing custom message, and add focused tests for the non-destructive server-side merge behavior. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `$18.31`_ <!-- mux-attribution: model=openai:gpt-5.4 thinking=xhigh costs=18.31 -->
|
@codex review Please take another look. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 32772b9c75
ℹ️ 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".
Local static-check passes, so trigger a fresh CI run for the heartbeat PR after the transient static-check failure. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `$18.31`_ <!-- mux-attribution: model=openai:gpt-5.4 thinking=xhigh costs=18.31 -->
|
@codex review Please take another look. |
Avoid materializing an undefined message field in the heartbeat ORPC router so omitted message updates keep the server-side preservation behavior intact. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `$18.31`_ <!-- mux-attribution: model=openai:gpt-5.4 thinking=xhigh costs=18.31 -->
|
@codex review Please take another look. |
Local make static-check-full passes, so trigger a fresh CI run after the transient Static Checks failure. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `$18.31`_ <!-- mux-attribution: model=openai:gpt-5.4 thinking=xhigh costs=18.31 -->
|
@codex review Please take another look. |
Clean up the new heartbeat settings test so CI static checks match the local static-check-full run. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `$18.31`_ <!-- mux-attribution: model=openai:gpt-5.4 thinking=xhigh costs=18.31 -->
|
@codex review Please take another look. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: de26abd4c4
ℹ️ 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".
Return the saved heartbeat message alongside the resolved send options so executeHeartbeat avoids redundant config reloads while still honoring the per-workspace override. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `$18.31`_ <!-- mux-attribution: model=openai:gpt-5.4 thinking=xhigh costs=18.31 -->
|
@codex review Please take another look. |
|
Codex Review: Didn't find any major issues. What shall we delve into next? ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
Summary
Add a per-workspace heartbeat message override to the existing Configure heartbeat modal and use it when backend heartbeats run.
Background
Workspace heartbeats were already persisted per workspace, but they always used a fixed default prompt body. This change keeps the existing Workspace Heartbeats experiment gate while letting each workspace optionally customize the heartbeat instruction body without affecting other workspaces.
Implementation
messageWorkspaceHeartbeatModalanduseWorkspaceHeartbeatto edit, restore, and clear the workspace-scoped message/heartbeatcommands only change cadence or disable the featureValidation
make static-checkmake testbun test src/node/services/heartbeatService.test.ts src/browser/utils/chatCommands.test.ts src/browser/components/WorkspaceHeartbeatModal/WorkspaceHeartbeatModal.test.tsxmake dev-server-sandbox, including persistence across reopen and no cross-workspace bleed; captured screenshots and a local WebM recording during the runRisks
The main regression risk is compatibility between the modal flow, backend heartbeat execution, and slash-command writes. The change stays within the existing workspace heartbeat model and adds tests for fallback/custom prompt composition plus message preservation across
/heartbeatupdates.📋 Implementation Plan
Plan: per-workspace configurable heartbeat message
Goal
Add a per-workspace configurable heartbeat message through the existing Configure heartbeat modal, while keeping the existing Workspace Heartbeats experiment as the feature gate only.
Recommended approach
Option A (selected): extend the existing workspace-scoped heartbeat flow — net +70 to +110 product LoC.
Settings -> Experiments -> Workspace Heartbeatsas the on/off gate.ExperimentsSection; that screen is app-scoped and intentionally lacks selected-workspace context.[Heartbeat]prefixScope decisions
/heartbeatcommand paths.{idleDuration}, scheduling changes, new docs pages.Why this matches current repo patterns
src/common/orpc/schemas/workspace.tsalready stores heartbeat settings as workspace metadata.src/browser/components/WorkspaceHeartbeatModal/WorkspaceHeartbeatModal.tsxis the existing per-workspace edit surface.src/browser/features/Settings/Sections/ExperimentsSection.tsxsupports nested controls for app-scoped experiments (for example, configurable bind URL), but that pattern is a poor fit for workspace-specific data because the settings route does not carry selected-workspace context.src/node/services/workspaceService.ts#executeHeartbeat()currently builds the heartbeat prompt in one place, so prompt selection can stay centralized.Implementation plan
Phase 1 — Extend the heartbeat settings shape and persistence
Files / symbols
src/common/orpc/schemas/workspace.tsWorkspaceHeartbeatSettingsSchemasrc/common/orpc/schemas/api.tsworkspace.heartbeat.getworkspace.heartbeat.setsrc/node/orpc/router.tsworkspace.heartbeat.setsrc/node/services/workspaceService.tssetHeartbeatSettings()getHeartbeatSettings()Changes
messagefield toWorkspaceHeartbeatSettingsSchemawith a conservative max length (recommend 1000 chars for v1).input.messagereachessetHeartbeatSettings().setHeartbeatSettings():messageis either absent or a stringundefinedmessagein the persisted object and in change detectionmessagecontinue to read as default/fallback behavior.Defensive-programming notes
enabled/intervalMsassertions.nextSettingsso changed detection and persistence compare the same shape.Quality gate
make typecheckPhase 2 — Add the per-workspace message field to the existing modal
Files / symbols
src/browser/components/WorkspaceHeartbeatModal/WorkspaceHeartbeatModal.tsxsrc/browser/hooks/useWorkspaceHeartbeat.tsChanges
draftMessage.enabledandintervalMs.<textarea>to the modal instead of introducing a new shared component; keep the diff small.messagethroughsave(); sendundefinedwhen the trimmed draft is empty so clearing the field removes the override.UI behavior choice
Quality gate
Phase 3 — Centralize prompt composition with fallback behavior
Files / symbols
src/node/services/workspaceService.tsexecuteHeartbeat()Changes
executeHeartbeat()using the existing workspace-scoped settings path.[Heartbeat]and the computed idle durationsettings.messagewhen present, otherwise the current built-in default bodymuxMetadata.displayStatus,synthetic: true, andrequireIdle: trueunchanged.Target shape
Why this shape
idleDurationQuality gate
Phase 4 — Preserve custom message across secondary write paths
Files / symbols
src/browser/utils/chatCommands.tsprocessSlashCommand()heartbeat-set branchsrc/browser/utils/chatCommands.test.tsChanges
/heartbeatcommand path so changing cadence or toggling heartbeats off does not accidentally clear the saved custom message.messagethe same way the current code already preserves the stored interval when disabling heartbeats.Why this matters
workspace.heartbeat.set()is called from both the modal hook and the slash-command path/heartbeat 30or/heartbeat offwould overwrite the saved heartbeat messageQuality gate
Test plan
Update existing tests
src/node/services/heartbeatService.test.tssrc/browser/utils/chatCommands.test.ts/heartbeat offpreserves the storedmessage/heartbeat <minutes>preserves the storedmessagewhen only cadence changesAdd focused UI coverage
src/browser/components/WorkspaceHeartbeatModal/WorkspaceHeartbeatModal.test.tsxValidation commands
make typecheckmake lintbun testfor the touched suites abovemake testbefore claiming completion if the targeted runs stay green and runtime budget allowsAcceptance criteria
messagealongsideenabledandintervalMsfor that workspace only./heartbeatcommands do not wipe the saved custom message.Dogfooding and review artifacts
Setup
make dev-server-sandbox.agent-browseragainst that URL and capture screenshots/video from that session.DEV_SERVER_SANDBOX_ARGS="--clean-projects"(and--clean-providerstoo if you want a fully blank environment).Manual dogfood flow
Artifact requirements
Risks / watchpoints
enabledandintervalMs; forgetting to threadmessagethere would silently drop the new field.chatCommands.tsis a compatibility trap because it already writes heartbeat settings outside the modal.Handoff note
If implementation starts later in Exec mode, keep the diff focused on the files above and avoid adding a second settings surface unless product requirements explicitly change toward a global default.
Generated with
mux• Model:openai:gpt-5.4• Thinking:xhigh• Cost:$18.31