diff options
| author | Adam Malczewski <[email protected]> | 2026-06-29 01:51:03 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-29 01:51:03 +0900 |
| commit | fdc2d0df8fa5ffca8c1c0957cc8bbbe5f4a5304c (patch) | |
| tree | 27422f665b3c332f0a2a923dc64466dedf16baea /packages/heartbeat/src/extension.ts | |
| parent | 6dd9ea9b935e5011c16faed6c869c976cf5ff172 (diff) | |
| download | dispatch-fdc2d0df8fa5ffca8c1c0957cc8bbbe5f4a5304c.tar.gz dispatch-fdc2d0df8fa5ffca8c1c0957cc8bbbe5f4a5304c.zip | |
feat(heartbeat): add inactiveOnly mode (skip fire while workspace has active agents)
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.
Diffstat (limited to 'packages/heartbeat/src/extension.ts')
| -rw-r--r-- | packages/heartbeat/src/extension.ts | 13 |
1 files changed, 13 insertions, 0 deletions
diff --git a/packages/heartbeat/src/extension.ts b/packages/heartbeat/src/extension.ts index f245b7e..b044619 100644 --- a/packages/heartbeat/src/extension.ts +++ b/packages/heartbeat/src/extension.ts @@ -90,6 +90,19 @@ export const extension: Extension = { // Lazy (resolved at fire time, mirroring resolvePrompt). getWorkspaceCwd: async (wsId) => (await conversationStore.getWorkspace(wsId))?.defaultCwd ?? null, + // inactiveOnly: the configured workspace is "busy" while any of its + // conversations are driving or queued for a turn. The orchestrator + // sets persisted status "active" on turn start and "idle" on settle, + // so a single store read (filtered to the configured workspaceId) is + // the live active-agent check. The heartbeat's own spawned conversation + // lives in the DEDICATED heartbeat workspace, so it never self-blocks. + hasActiveAgents: async (wsId) => { + const active = await conversationStore.listConversations({ + workspaceId: wsId, + status: ["active", "queued"], + }); + return active.length > 0; + }, }); // Reconcile stale runs + arm enabled workspaces on boot. |
