1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
|
import type { ReasoningEffort } from "@dispatch/transport-contract";
/**
* Pure core types for the heartbeat feature — zero DOM, zero effects, zero Svelte.
*
* Heartbeat is a workspace-scoped autonomous agent loop: the backend periodically
* runs a turn in a dedicated conversation using a configured system prompt, task
* prompt, model, reasoning effort, and interval. The FE exposes the config
* (`GET`/`PUT /workspaces/:id/heartbeat`), the run history
* (`GET /workspaces/:id/heartbeat/runs`), and a per-run stop
* (`POST /workspaces/:id/heartbeat/runs/:runId/stop`).
*
* The backend's heartbeat API is a plain REST surface — it is NOT part of the
* shared `@dispatch/transport-contract` / `@dispatch/wire` packages (verified:
* no `heartbeat` symbol in either `dist/`). So, following the consumer-defines-
* port pattern (mirrors `features/mcp` / `features/computer` result types), the
* FE owns these shapes here and adapts the untyped JSON at the network seam in
* the composition root. If the backend later promotes these to a shared contract
* package, swap the local types for the imports (see `backend-handoff.md`).
*/
/** The canonical run lifecycle status (backend-owned enum, verbatim). */
export type HeartbeatRunStatus = "running" | "completed" | "stopped";
/** The workspace's heartbeat configuration (`GET /workspaces/:id/heartbeat`). */
export interface HeartbeatConfig {
/** Whether the autonomous loop is enabled (running on the interval). */
readonly enabled: boolean;
readonly systemPrompt: string;
readonly taskPrompt: string;
/** Minutes between runs. */
readonly intervalMinutes: number;
/** The model name (`<credential>/<model>`) the heartbeat runs with. */
readonly model: string;
/**
* The heartbeat's reasoning effort, or null when never set (the server
* default `"high"` then applies) — mirrors the per-conversation knob's
* resolution chain.
*/
readonly reasoningEffort: ReasoningEffort | null;
}
/**
* A partial config patch for `PUT /workspaces/:id/heartbeat`. Every field is
* optional — the backend merges the patch onto the stored config.
*/
export interface HeartbeatConfigPatch {
readonly enabled?: boolean;
readonly systemPrompt?: string;
readonly taskPrompt?: string;
readonly intervalMinutes?: number;
readonly model?: string;
readonly reasoningEffort?: ReasoningEffort | null;
}
/** One heartbeat run (`GET /workspaces/:id/heartbeat/runs`). */
export interface HeartbeatRun {
readonly id: string;
/** The conversation this run wrote to (watch it live for the chat). */
readonly conversationId: string;
/** ISO timestamp of when the run was triggered. */
readonly triggeredAt: string;
readonly status: HeartbeatRunStatus;
}
// ── Injected ports (consumer-defines-port; the composition root adapts the
// store's HTTP calls to these shapes). ──────────────────────────────────────
/** Outcome of `GET /workspaces/:id/heartbeat` (or the PUT response). */
export type HeartbeatConfigResult =
| { readonly ok: true; readonly config: HeartbeatConfig }
| { readonly ok: false; readonly error: string };
/** Outcome of `GET /workspaces/:id/heartbeat/runs`. */
export type HeartbeatRunsResult =
| { readonly ok: true; readonly runs: readonly HeartbeatRun[] }
| { readonly ok: false; readonly error: string };
/** Outcome of `POST /workspaces/:id/heartbeat/runs/:runId/stop`. */
export type HeartbeatStopResult =
| { readonly ok: true }
| { readonly ok: false; readonly error: string };
export type LoadHeartbeatConfig = () => Promise<HeartbeatConfigResult | null>;
export type SaveHeartbeatConfig = (
patch: HeartbeatConfigPatch,
) => Promise<HeartbeatConfigResult | null>;
export type LoadHeartbeatRuns = () => Promise<HeartbeatRunsResult | null>;
export type StopHeartbeatRun = (runId: string) => Promise<HeartbeatStopResult | null>;
/**
* Outcome of `GET /workspaces/:id/heartbeat/next-run` — the server-authoritative
* timestamp of the next scheduled heartbeat run (ISO 8601 string), or `null`
* when the heartbeat is disabled or no run is scheduled. The FE computes a live
* countdown from this + a 1s clock (see `formatCountdown`). When the endpoint is
* unavailable (404 — backend hasn't shipped it yet), the FE falls back to an
* approximation from the runs + config (see `approximateNextRunEpoch`).
*/
export type HeartbeatNextRunResult =
| { readonly ok: true; readonly nextRunAt: string | null }
| { readonly ok: false; readonly error: string };
export type LoadHeartbeatNextRun = () => Promise<HeartbeatNextRunResult | null>;
|