From fdc2d0df8fa5ffca8c1c0957cc8bbbe5f4a5304c Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Mon, 29 Jun 2026 01:51:03 +0900 Subject: feat(heartbeat): add inactiveOnly mode (skip fire while workspace has active agents) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new per-workspace heartbeat setting `inactiveOnly` (default true): when on, the heartbeat SKIPS a scheduled fire whenever the configured workspace has any active agents — a conversation whose persisted status is "active" (driving a turn) or "queued" (waiting on the message queue). The fire is silently skipped (no run recorded); the scheduler re-arms and retries at the next interval. Set false to fire unconditionally (the prior behavior). The check is wired against conversationStore.listConversations({ workspaceId, status: ["active","queued"] }) at fire time — the orchestrator sets "active" on turn start and "idle" on settle, so it is the live busy/idle signal. The spawned heartbeat conversation lives in the dedicated heartbeat workspace, so a heartbeat run never counts as an active agent of the configured workspace (no self-block). - transport-contract: add inactiveOnly to HeartbeatConfig + UpdateHeartbeatRequest - heartbeat config-store: default true, apply/persist, legacy-parse default true - heartbeat service: injectable hasActiveAgents dep + skip logic in fire() - heartbeat extension: wire hasActiveAgents against the conversation store - transport-http: validate inactiveOnly (boolean) on PUT /workspaces/:id/heartbeat - tests: config-store (+4), heartbeat service (+7), transport-http (+3) - notes/heartbeat-setting-handoff.md: API contract for the frontend Verification: typecheck EXIT 0; tests 2013 passed | 6 skipped; biome EXIT 0. --- packages/transport-contract/src/index.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'packages/transport-contract/src') diff --git a/packages/transport-contract/src/index.ts b/packages/transport-contract/src/index.ts index 797ad22..65f7697 100644 --- a/packages/transport-contract/src/index.ts +++ b/packages/transport-contract/src/index.ts @@ -968,10 +968,26 @@ export interface TestComputerResponse { * level. The scheduler resets its timer after each run completes (not a fixed * wall-clock schedule); on backend restart it resumes scheduling for enabled * heartbeats. + * + * `inactiveOnly` (default `true`) gates each fire on the configured workspace + * having NO active agents (no conversation with status `"active"` or `"queued"`): + * the heartbeat stays quiet while the user is actively working, and only fires + * when the workspace is idle. Set `false` to fire unconditionally. */ export interface HeartbeatConfig { /** Whether the heartbeat loop is active for this workspace. */ readonly enabled: boolean; + /** + * When `true` (the default), the heartbeat SKIPS a fire when the configured + * workspace has any active agents — conversations whose persisted status is + * `"active"` (driving a turn) or `"queued"` (waiting on the message queue). + * The fire is silently skipped (no run is recorded); the scheduler re-arms + * and tries again at the next interval. When `false`, the heartbeat fires + * unconditionally regardless of workspace activity. The spawned heartbeat + * conversation lives in the DEDICATED heartbeat workspace, so it never + * counts as an "active agent" of the configured workspace (no self-block). + */ + readonly inactiveOnly: boolean; /** Custom system prompt for the heartbeat AI (empty = no system prompt). */ readonly systemPrompt: string; /** Task prompt sent as the first user message when the heartbeat fires. */ @@ -993,6 +1009,7 @@ export interface HeartbeatConfig { */ export interface UpdateHeartbeatRequest { readonly enabled?: boolean; + readonly inactiveOnly?: boolean; readonly systemPrompt?: string; readonly taskPrompt?: string; readonly intervalMinutes?: number; -- cgit v1.2.3