@roackb2/heddle 0.0.10 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +136 -6
  2. package/dist/examples/heartbeat-scheduler.d.ts +2 -0
  3. package/dist/examples/heartbeat-scheduler.d.ts.map +1 -0
  4. package/dist/examples/heartbeat-scheduler.js +88 -0
  5. package/dist/examples/heartbeat-scheduler.js.map +1 -0
  6. package/dist/src/cli/chat/App.d.ts.map +1 -1
  7. package/dist/src/cli/chat/App.js +6 -0
  8. package/dist/src/cli/chat/App.js.map +1 -1
  9. package/dist/src/cli/chat/state/local-commands.d.ts +3 -1
  10. package/dist/src/cli/chat/state/local-commands.d.ts.map +1 -1
  11. package/dist/src/cli/chat/state/local-commands.js +178 -3
  12. package/dist/src/cli/chat/state/local-commands.js.map +1 -1
  13. package/dist/src/cli/chat/state/types.d.ts +6 -0
  14. package/dist/src/cli/chat/state/types.d.ts.map +1 -1
  15. package/dist/src/cli/chat/submit.d.ts +2 -0
  16. package/dist/src/cli/chat/submit.d.ts.map +1 -1
  17. package/dist/src/cli/chat/submit.js +10 -1
  18. package/dist/src/cli/chat/submit.js.map +1 -1
  19. package/dist/src/cli/chat/utils/runtime.d.ts +1 -0
  20. package/dist/src/cli/chat/utils/runtime.d.ts.map +1 -1
  21. package/dist/src/cli/chat/utils/runtime.js +1 -0
  22. package/dist/src/cli/chat/utils/runtime.js.map +1 -1
  23. package/dist/src/cli/heartbeat.d.ts +20 -0
  24. package/dist/src/cli/heartbeat.d.ts.map +1 -0
  25. package/dist/src/cli/heartbeat.js +460 -0
  26. package/dist/src/cli/heartbeat.js.map +1 -0
  27. package/dist/src/cli/main.js +15 -0
  28. package/dist/src/cli/main.js.map +1 -1
  29. package/dist/src/index.d.ts +6 -0
  30. package/dist/src/index.d.ts.map +1 -1
  31. package/dist/src/index.js +3 -0
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/runtime/heartbeat-lucid.d.ts +42 -0
  34. package/dist/src/runtime/heartbeat-lucid.d.ts.map +1 -0
  35. package/dist/src/runtime/heartbeat-lucid.js +188 -0
  36. package/dist/src/runtime/heartbeat-lucid.js.map +1 -0
  37. package/dist/src/runtime/heartbeat-scheduler.d.ts +124 -0
  38. package/dist/src/runtime/heartbeat-scheduler.d.ts.map +1 -0
  39. package/dist/src/runtime/heartbeat-scheduler.js +306 -0
  40. package/dist/src/runtime/heartbeat-scheduler.js.map +1 -0
  41. package/dist/src/runtime/heartbeat-views.d.ts +50 -0
  42. package/dist/src/runtime/heartbeat-views.d.ts.map +1 -0
  43. package/dist/src/runtime/heartbeat-views.js +60 -0
  44. package/dist/src/runtime/heartbeat-views.js.map +1 -0
  45. package/dist/src/runtime/heartbeat.js +3 -0
  46. package/dist/src/runtime/heartbeat.js.map +1 -1
  47. package/package.json +4 -2
@@ -0,0 +1,42 @@
1
+ import type { HeartbeatSchedulerEvent, HeartbeatTaskStatus } from './heartbeat-scheduler.js';
2
+ import type { HeartbeatRunView, HeartbeatTaskView } from './heartbeat-views.js';
3
+ export type LucidAgentStatus = 'running' | 'paused' | 'asleep' | 'terminated' | 'blocked' | 'failed';
4
+ export type LucidAgentStatusNotification = {
5
+ agent_id: string;
6
+ status: string;
7
+ timestamp: string;
8
+ };
9
+ export type LucidAgentProgressNotification = {
10
+ agent_id: string;
11
+ progress: string;
12
+ timestamp: string;
13
+ };
14
+ export type LucidAgentResponseNotification = {
15
+ agent_id: string;
16
+ response: string;
17
+ timestamp: string;
18
+ };
19
+ export type LucidAgentMessage = {
20
+ event: 'agent_status';
21
+ data: {
22
+ status: LucidAgentStatusNotification;
23
+ };
24
+ } | {
25
+ event: 'agent_progress';
26
+ data: {
27
+ progress: LucidAgentProgressNotification;
28
+ };
29
+ } | {
30
+ event: 'agent_response';
31
+ data: {
32
+ response: LucidAgentResponseNotification;
33
+ };
34
+ };
35
+ export type LucidAdapterOptions = {
36
+ taskIdToAgentId?: (taskId: string) => string;
37
+ };
38
+ export declare function heartbeatTaskStatusToLucidStatus(status: HeartbeatTaskStatus): LucidAgentStatus;
39
+ export declare function heartbeatTaskViewToLucidMessages(task: HeartbeatTaskView, options?: LucidAdapterOptions): LucidAgentMessage[];
40
+ export declare function heartbeatRunViewToLucidMessages(run: HeartbeatRunView, options?: LucidAdapterOptions): LucidAgentMessage[];
41
+ export declare function heartbeatSchedulerEventToLucidMessages(event: HeartbeatSchedulerEvent, options?: LucidAdapterOptions): LucidAgentMessage[];
42
+ //# sourceMappingURL=heartbeat-lucid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heartbeat-lucid.d.ts","sourceRoot":"","sources":["../../../src/runtime/heartbeat-lucid.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC7F,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEhF,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,YAAY,GAAG,SAAS,GAAG,QAAQ,CAAC;AAErG,MAAM,MAAM,4BAA4B,GAAG;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GACzB;IAAE,KAAK,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE;QAAE,MAAM,EAAE,4BAA4B,CAAA;KAAE,CAAA;CAAE,GACzE;IAAE,KAAK,EAAE,gBAAgB,CAAC;IAAC,IAAI,EAAE;QAAE,QAAQ,EAAE,8BAA8B,CAAA;KAAE,CAAA;CAAE,GAC/E;IAAE,KAAK,EAAE,gBAAgB,CAAC;IAAC,IAAI,EAAE;QAAE,QAAQ,EAAE,8BAA8B,CAAA;KAAE,CAAA;CAAE,CAAC;AAEpF,MAAM,MAAM,mBAAmB,GAAG;IAChC,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;CAC9C,CAAC;AAEF,wBAAgB,gCAAgC,CAAC,MAAM,EAAE,mBAAmB,GAAG,gBAAgB,CAc9F;AAED,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,iBAAiB,EACvB,OAAO,GAAE,mBAAwB,GAChC,iBAAiB,EAAE,CA2CrB;AAED,wBAAgB,+BAA+B,CAC7C,GAAG,EAAE,gBAAgB,EACrB,OAAO,GAAE,mBAAwB,GAChC,iBAAiB,EAAE,CAyCrB;AAED,wBAAgB,sCAAsC,CACpD,KAAK,EAAE,uBAAuB,EAC9B,OAAO,GAAE,mBAAwB,GAChC,iBAAiB,EAAE,CA0FrB"}
@@ -0,0 +1,188 @@
1
+ export function heartbeatTaskStatusToLucidStatus(status) {
2
+ switch (status) {
3
+ case 'running':
4
+ return 'running';
5
+ case 'waiting':
6
+ case 'idle':
7
+ return 'asleep';
8
+ case 'complete':
9
+ return 'terminated';
10
+ case 'blocked':
11
+ return 'blocked';
12
+ case 'failed':
13
+ return 'failed';
14
+ }
15
+ }
16
+ export function heartbeatTaskViewToLucidMessages(task, options = {}) {
17
+ const agentId = resolveAgentId(task.taskId, options);
18
+ const timestamp = task.lastRunAt ?? task.nextRunAt ?? new Date().toISOString();
19
+ const messages = [
20
+ {
21
+ event: 'agent_status',
22
+ data: {
23
+ status: {
24
+ agent_id: agentId,
25
+ status: heartbeatTaskStatusToLucidStatus(task.status),
26
+ timestamp,
27
+ },
28
+ },
29
+ },
30
+ ];
31
+ if (task.progress) {
32
+ messages.push({
33
+ event: 'agent_progress',
34
+ data: {
35
+ progress: {
36
+ agent_id: agentId,
37
+ progress: task.progress,
38
+ timestamp,
39
+ },
40
+ },
41
+ });
42
+ }
43
+ if (task.summary) {
44
+ messages.push({
45
+ event: 'agent_response',
46
+ data: {
47
+ response: {
48
+ agent_id: agentId,
49
+ response: task.summary,
50
+ timestamp,
51
+ },
52
+ },
53
+ });
54
+ }
55
+ return messages;
56
+ }
57
+ export function heartbeatRunViewToLucidMessages(run, options = {}) {
58
+ const agentId = resolveAgentId(run.taskId, options);
59
+ const timestamp = run.createdAt;
60
+ const messages = [
61
+ {
62
+ event: 'agent_status',
63
+ data: {
64
+ status: {
65
+ agent_id: agentId,
66
+ status: heartbeatTaskStatusToLucidStatus(run.status),
67
+ timestamp,
68
+ },
69
+ },
70
+ },
71
+ ];
72
+ if (run.progress) {
73
+ messages.push({
74
+ event: 'agent_progress',
75
+ data: {
76
+ progress: {
77
+ agent_id: agentId,
78
+ progress: run.progress,
79
+ timestamp,
80
+ },
81
+ },
82
+ });
83
+ }
84
+ messages.push({
85
+ event: 'agent_response',
86
+ data: {
87
+ response: {
88
+ agent_id: agentId,
89
+ response: run.summary,
90
+ timestamp,
91
+ },
92
+ },
93
+ });
94
+ return messages;
95
+ }
96
+ export function heartbeatSchedulerEventToLucidMessages(event, options = {}) {
97
+ if (event.type === 'heartbeat.scheduler.started' || event.type === 'heartbeat.scheduler.stopped') {
98
+ return [];
99
+ }
100
+ const agentId = resolveAgentId(event.taskId, options);
101
+ switch (event.type) {
102
+ case 'heartbeat.task.due':
103
+ return [];
104
+ case 'heartbeat.task.started':
105
+ return [
106
+ {
107
+ event: 'agent_status',
108
+ data: {
109
+ status: {
110
+ agent_id: agentId,
111
+ status: heartbeatTaskStatusToLucidStatus(event.status),
112
+ timestamp: event.timestamp,
113
+ },
114
+ },
115
+ },
116
+ {
117
+ event: 'agent_progress',
118
+ data: {
119
+ progress: {
120
+ agent_id: agentId,
121
+ progress: event.progress,
122
+ timestamp: event.timestamp,
123
+ },
124
+ },
125
+ },
126
+ ];
127
+ case 'heartbeat.task.finished':
128
+ return [
129
+ {
130
+ event: 'agent_status',
131
+ data: {
132
+ status: {
133
+ agent_id: agentId,
134
+ status: heartbeatTaskStatusToLucidStatus(event.status),
135
+ timestamp: event.timestamp,
136
+ },
137
+ },
138
+ },
139
+ {
140
+ event: 'agent_progress',
141
+ data: {
142
+ progress: {
143
+ agent_id: agentId,
144
+ progress: event.progress,
145
+ timestamp: event.timestamp,
146
+ },
147
+ },
148
+ },
149
+ {
150
+ event: 'agent_response',
151
+ data: {
152
+ response: {
153
+ agent_id: agentId,
154
+ response: event.summary,
155
+ timestamp: event.timestamp,
156
+ },
157
+ },
158
+ },
159
+ ];
160
+ case 'heartbeat.task.failed':
161
+ return [
162
+ {
163
+ event: 'agent_status',
164
+ data: {
165
+ status: {
166
+ agent_id: agentId,
167
+ status: heartbeatTaskStatusToLucidStatus(event.status),
168
+ timestamp: event.timestamp,
169
+ },
170
+ },
171
+ },
172
+ {
173
+ event: 'agent_progress',
174
+ data: {
175
+ progress: {
176
+ agent_id: agentId,
177
+ progress: `${event.progress}${event.error ? ` Error: ${event.error}` : ''}`,
178
+ timestamp: event.timestamp,
179
+ },
180
+ },
181
+ },
182
+ ];
183
+ }
184
+ }
185
+ function resolveAgentId(taskId, options) {
186
+ return options.taskIdToAgentId?.(taskId) ?? taskId;
187
+ }
188
+ //# sourceMappingURL=heartbeat-lucid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heartbeat-lucid.js","sourceRoot":"","sources":["../../../src/runtime/heartbeat-lucid.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ import type { AgentHeartbeatResult, HeartbeatDecision, RunAgentHeartbeatOptions } from './heartbeat.js';
2
+ import type { AgentLoopCheckpoint, AgentLoopState } from './events.js';
3
+ import type { LlmUsage } from '../llm/types.js';
4
+ export type HeartbeatTask = {
5
+ id: string;
6
+ task: string;
7
+ name?: string;
8
+ enabled: boolean;
9
+ intervalMs: number;
10
+ nextRunAt?: string;
11
+ checkpointPath?: string;
12
+ model?: string;
13
+ maxSteps?: number;
14
+ workspaceRoot?: string;
15
+ stateDir?: string;
16
+ memoryDir?: string;
17
+ searchIgnoreDirs?: string[];
18
+ systemContext?: string;
19
+ lastRunAt?: string;
20
+ status?: HeartbeatTaskStatus;
21
+ lastProgress?: string;
22
+ lastRunId?: string;
23
+ lastLoadedCheckpoint?: boolean;
24
+ resumable?: boolean;
25
+ lastUsage?: LlmUsage;
26
+ lastDecision?: HeartbeatDecision;
27
+ lastOutcome?: string;
28
+ lastSummary?: string;
29
+ lastError?: string;
30
+ updatedAt?: string;
31
+ };
32
+ export type HeartbeatTaskStatus = 'idle' | 'running' | 'waiting' | 'blocked' | 'complete' | 'failed';
33
+ export type HeartbeatTaskRunRecord = {
34
+ task: HeartbeatTask;
35
+ result: AgentHeartbeatResult;
36
+ loadedCheckpoint: boolean;
37
+ };
38
+ export type HeartbeatTaskRunRecordEntry = {
39
+ id: string;
40
+ path: string;
41
+ taskId: string;
42
+ runId: string;
43
+ createdAt: string;
44
+ record: HeartbeatTaskRunRecord;
45
+ };
46
+ export type HeartbeatTaskStore = {
47
+ listTasks: () => Promise<HeartbeatTask[]>;
48
+ saveTask: (task: HeartbeatTask) => Promise<void>;
49
+ loadCheckpoint: (task: HeartbeatTask) => Promise<AgentLoopCheckpoint | undefined>;
50
+ saveCheckpoint: (task: HeartbeatTask, checkpoint: AgentLoopCheckpoint) => Promise<void>;
51
+ saveRunRecord?: (record: HeartbeatTaskRunRecord) => Promise<void>;
52
+ listRunRecords?: (options?: {
53
+ taskId?: string;
54
+ limit?: number;
55
+ }) => Promise<HeartbeatTaskRunRecordEntry[]>;
56
+ loadRunRecord?: (id: string) => Promise<HeartbeatTaskRunRecordEntry | undefined>;
57
+ };
58
+ export type FileHeartbeatTaskStoreOptions = {
59
+ dir: string;
60
+ };
61
+ export type HeartbeatSchedulerEvent = {
62
+ type: 'heartbeat.scheduler.started';
63
+ timestamp: string;
64
+ } | {
65
+ type: 'heartbeat.scheduler.stopped';
66
+ reason: 'aborted' | 'completed' | 'error';
67
+ timestamp: string;
68
+ } | {
69
+ type: 'heartbeat.task.due';
70
+ taskId: string;
71
+ timestamp: string;
72
+ } | {
73
+ type: 'heartbeat.task.started';
74
+ taskId: string;
75
+ loadedCheckpoint: boolean;
76
+ status: HeartbeatTaskStatus;
77
+ progress: string;
78
+ timestamp: string;
79
+ } | {
80
+ type: 'heartbeat.task.finished';
81
+ taskId: string;
82
+ decision: HeartbeatDecision;
83
+ outcome: string;
84
+ status: HeartbeatTaskStatus;
85
+ progress: string;
86
+ summary: string;
87
+ runId: string;
88
+ usage?: LlmUsage;
89
+ nextRunAt?: string;
90
+ enabled: boolean;
91
+ timestamp: string;
92
+ } | {
93
+ type: 'heartbeat.task.failed';
94
+ taskId: string;
95
+ error: string;
96
+ status: HeartbeatTaskStatus;
97
+ progress: string;
98
+ nextRunAt?: string;
99
+ timestamp: string;
100
+ };
101
+ export type HeartbeatTaskRunner = (task: HeartbeatTask, checkpoint: AgentLoopState | AgentLoopCheckpoint | undefined) => Promise<AgentHeartbeatResult>;
102
+ export type RunDueHeartbeatTasksOptions = {
103
+ store: HeartbeatTaskStore;
104
+ runner?: HeartbeatTaskRunner;
105
+ heartbeat?: Omit<RunAgentHeartbeatOptions, 'task' | 'checkpoint'>;
106
+ now?: () => Date;
107
+ onEvent?: (event: HeartbeatSchedulerEvent) => void;
108
+ failureRetryMs?: number;
109
+ };
110
+ export type RunDueHeartbeatTasksResult = {
111
+ checked: number;
112
+ ran: number;
113
+ failed: number;
114
+ records: HeartbeatTaskRunRecord[];
115
+ };
116
+ export type RunHeartbeatSchedulerOptions = RunDueHeartbeatTasksOptions & {
117
+ pollIntervalMs?: number;
118
+ signal?: AbortSignal;
119
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
120
+ };
121
+ export declare function createFileHeartbeatTaskStore(options: FileHeartbeatTaskStoreOptions): HeartbeatTaskStore;
122
+ export declare function runDueHeartbeatTasks(options: RunDueHeartbeatTasksOptions): Promise<RunDueHeartbeatTasksResult>;
123
+ export declare function runHeartbeatScheduler(options: RunHeartbeatSchedulerOptions): Promise<void>;
124
+ //# sourceMappingURL=heartbeat-scheduler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heartbeat-scheduler.d.ts","sourceRoot":"","sources":["../../../src/runtime/heartbeat-scheduler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,306 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { basename, dirname, join } from 'node:path';
3
+ import { runAgentHeartbeat } from './heartbeat.js';
4
+ import { suggestNextHeartbeatDelayMs } from './heartbeat-store.js';
5
+ const DEFAULT_FAILURE_RETRY_MS = 5 * 60_000;
6
+ export function createFileHeartbeatTaskStore(options) {
7
+ const tasksDir = join(options.dir, 'tasks');
8
+ const checkpointsDir = join(options.dir, 'checkpoints');
9
+ const runsDir = join(options.dir, 'runs');
10
+ return {
11
+ async listTasks() {
12
+ if (!existsSync(tasksDir)) {
13
+ return [];
14
+ }
15
+ return readdirSync(tasksDir)
16
+ .filter((entry) => entry.endsWith('.json'))
17
+ .map((entry) => JSON.parse(readFileSync(join(tasksDir, entry), 'utf8')))
18
+ .sort((left, right) => left.id.localeCompare(right.id));
19
+ },
20
+ async saveTask(task) {
21
+ const path = join(tasksDir, `${safeTaskFileName(task.id)}.json`);
22
+ mkdirSync(dirname(path), { recursive: true });
23
+ writeFileSync(path, JSON.stringify(normalizeTaskForSave(task), null, 2));
24
+ },
25
+ async loadCheckpoint(task) {
26
+ const path = checkpointPathForTask(task, checkpointsDir);
27
+ if (!existsSync(path)) {
28
+ return undefined;
29
+ }
30
+ return JSON.parse(readFileSync(path, 'utf8'));
31
+ },
32
+ async saveCheckpoint(task, checkpoint) {
33
+ const path = checkpointPathForTask(task, checkpointsDir);
34
+ mkdirSync(dirname(path), { recursive: true });
35
+ writeFileSync(path, JSON.stringify(checkpoint, null, 2));
36
+ },
37
+ async saveRunRecord(record) {
38
+ const timestamp = new Date().toISOString().replaceAll(':', '-');
39
+ const path = join(runsDir, `${timestamp}-${safeTaskFileName(record.task.id)}.json`);
40
+ mkdirSync(dirname(path), { recursive: true });
41
+ writeFileSync(path, JSON.stringify(record, null, 2));
42
+ },
43
+ async listRunRecords(options = {}) {
44
+ if (!existsSync(runsDir)) {
45
+ return [];
46
+ }
47
+ const entries = readdirSync(runsDir)
48
+ .filter((entry) => entry.endsWith('.json'))
49
+ .flatMap((entry) => {
50
+ const path = join(runsDir, entry);
51
+ try {
52
+ const record = JSON.parse(readFileSync(path, 'utf8'));
53
+ if (options.taskId && record.task.id !== options.taskId) {
54
+ return [];
55
+ }
56
+ return [runRecordEntryFromPath(path, record)];
57
+ }
58
+ catch {
59
+ return [];
60
+ }
61
+ })
62
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt));
63
+ return options.limit ? entries.slice(0, options.limit) : entries;
64
+ },
65
+ async loadRunRecord(id) {
66
+ const entries = await this.listRunRecords?.();
67
+ return entries?.find((entry) => entry.id === id || entry.runId === id);
68
+ },
69
+ };
70
+ }
71
+ export async function runDueHeartbeatTasks(options) {
72
+ const now = options.now?.() ?? new Date();
73
+ const tasks = await options.store.listTasks();
74
+ const dueTasks = tasks.filter((task) => isTaskDue(task, now));
75
+ const records = [];
76
+ let failed = 0;
77
+ for (const task of dueTasks) {
78
+ options.onEvent?.({ type: 'heartbeat.task.due', taskId: task.id, timestamp: now.toISOString() });
79
+ try {
80
+ const checkpoint = await options.store.loadCheckpoint(task);
81
+ const runningTask = normalizeTaskForSave({
82
+ ...task,
83
+ status: 'running',
84
+ lastProgress: checkpoint ?
85
+ 'Resuming heartbeat wake from the last checkpoint.'
86
+ : 'Starting a new heartbeat wake cycle.',
87
+ lastLoadedCheckpoint: Boolean(checkpoint),
88
+ lastError: undefined,
89
+ updatedAt: now.toISOString(),
90
+ });
91
+ await options.store.saveTask(runningTask);
92
+ options.onEvent?.({
93
+ type: 'heartbeat.task.started',
94
+ taskId: task.id,
95
+ loadedCheckpoint: Boolean(checkpoint),
96
+ status: runningTask.status ?? 'running',
97
+ progress: runningTask.lastProgress ?? '',
98
+ timestamp: now.toISOString(),
99
+ });
100
+ const result = await runHeartbeatTask(task, checkpoint, options);
101
+ await options.store.saveCheckpoint(task, result.checkpoint);
102
+ const nextTask = updateTaskAfterResult(task, result, now, Boolean(checkpoint));
103
+ await options.store.saveTask(nextTask);
104
+ const record = { task: nextTask, result, loadedCheckpoint: Boolean(checkpoint) };
105
+ await options.store.saveRunRecord?.(record);
106
+ records.push(record);
107
+ options.onEvent?.({
108
+ type: 'heartbeat.task.finished',
109
+ taskId: task.id,
110
+ decision: result.decision,
111
+ outcome: result.state.outcome,
112
+ status: nextTask.status ?? 'waiting',
113
+ progress: nextTask.lastProgress ?? '',
114
+ summary: result.summary,
115
+ runId: result.state.runId,
116
+ usage: result.state.usage,
117
+ nextRunAt: nextTask.nextRunAt,
118
+ enabled: nextTask.enabled,
119
+ timestamp: now.toISOString(),
120
+ });
121
+ }
122
+ catch (error) {
123
+ failed++;
124
+ const nextTask = updateTaskAfterFailure(task, error, now, options.failureRetryMs ?? DEFAULT_FAILURE_RETRY_MS);
125
+ await options.store.saveTask(nextTask);
126
+ options.onEvent?.({
127
+ type: 'heartbeat.task.failed',
128
+ taskId: task.id,
129
+ error: error instanceof Error ? error.message : String(error),
130
+ status: nextTask.status ?? 'failed',
131
+ progress: nextTask.lastProgress ?? '',
132
+ nextRunAt: nextTask.nextRunAt,
133
+ timestamp: now.toISOString(),
134
+ });
135
+ }
136
+ }
137
+ return {
138
+ checked: tasks.length,
139
+ ran: records.length,
140
+ failed,
141
+ records,
142
+ };
143
+ }
144
+ export async function runHeartbeatScheduler(options) {
145
+ options.onEvent?.({ type: 'heartbeat.scheduler.started', timestamp: (options.now?.() ?? new Date()).toISOString() });
146
+ try {
147
+ while (!options.signal?.aborted) {
148
+ await runDueHeartbeatTasks(options);
149
+ await (options.sleep ?? sleep)(options.pollIntervalMs ?? 60_000, options.signal);
150
+ }
151
+ options.onEvent?.({ type: 'heartbeat.scheduler.stopped', reason: 'aborted', timestamp: (options.now?.() ?? new Date()).toISOString() });
152
+ }
153
+ catch (error) {
154
+ if (options.signal?.aborted) {
155
+ options.onEvent?.({ type: 'heartbeat.scheduler.stopped', reason: 'aborted', timestamp: (options.now?.() ?? new Date()).toISOString() });
156
+ return;
157
+ }
158
+ options.onEvent?.({ type: 'heartbeat.scheduler.stopped', reason: 'error', timestamp: (options.now?.() ?? new Date()).toISOString() });
159
+ throw error;
160
+ }
161
+ }
162
+ function isTaskDue(task, now) {
163
+ if (!task.enabled) {
164
+ return false;
165
+ }
166
+ if (!task.nextRunAt) {
167
+ return true;
168
+ }
169
+ const nextRunAt = Date.parse(task.nextRunAt);
170
+ return Number.isFinite(nextRunAt) && nextRunAt <= now.getTime();
171
+ }
172
+ async function runHeartbeatTask(task, checkpoint, options) {
173
+ if (options.runner) {
174
+ return options.runner(task, checkpoint);
175
+ }
176
+ return runAgentHeartbeat({
177
+ ...options.heartbeat,
178
+ task: task.task,
179
+ checkpoint,
180
+ model: task.model ?? options.heartbeat?.model,
181
+ maxSteps: task.maxSteps ?? options.heartbeat?.maxSteps,
182
+ workspaceRoot: task.workspaceRoot ?? options.heartbeat?.workspaceRoot,
183
+ stateDir: task.stateDir ?? options.heartbeat?.stateDir,
184
+ memoryDir: task.memoryDir ?? options.heartbeat?.memoryDir,
185
+ searchIgnoreDirs: task.searchIgnoreDirs ?? options.heartbeat?.searchIgnoreDirs,
186
+ systemContext: task.systemContext ?? options.heartbeat?.systemContext,
187
+ });
188
+ }
189
+ function updateTaskAfterResult(task, result, now, loadedCheckpoint) {
190
+ const terminal = result.decision === 'complete' || result.decision === 'escalate';
191
+ const delayMs = terminal ? undefined
192
+ : result.decision === 'continue' ? task.intervalMs
193
+ : suggestNextHeartbeatDelayMs(result.decision) ?? task.intervalMs;
194
+ const projection = projectionForResult(result, delayMs);
195
+ return normalizeTaskForSave({
196
+ ...task,
197
+ enabled: terminal ? false : task.enabled,
198
+ status: projection.status,
199
+ lastProgress: projection.progress,
200
+ nextRunAt: delayMs === undefined ? undefined : new Date(now.getTime() + delayMs).toISOString(),
201
+ lastRunAt: now.toISOString(),
202
+ lastRunId: result.state.runId,
203
+ lastLoadedCheckpoint: loadedCheckpoint,
204
+ resumable: result.decision !== 'complete',
205
+ lastUsage: result.state.usage,
206
+ lastDecision: result.decision,
207
+ lastOutcome: result.state.outcome,
208
+ lastSummary: result.summary,
209
+ lastError: undefined,
210
+ updatedAt: now.toISOString(),
211
+ });
212
+ }
213
+ function updateTaskAfterFailure(task, error, now, retryMs) {
214
+ return normalizeTaskForSave({
215
+ ...task,
216
+ status: 'failed',
217
+ lastProgress: 'Heartbeat wake failed and will retry later.',
218
+ nextRunAt: new Date(now.getTime() + retryMs).toISOString(),
219
+ lastRunAt: now.toISOString(),
220
+ lastError: error instanceof Error ? error.message : String(error),
221
+ updatedAt: now.toISOString(),
222
+ });
223
+ }
224
+ function normalizeTaskForSave(task) {
225
+ return {
226
+ ...task,
227
+ intervalMs: Math.max(1, Math.trunc(task.intervalMs)),
228
+ status: task.status ?? 'idle',
229
+ };
230
+ }
231
+ function checkpointPathForTask(task, checkpointsDir) {
232
+ return task.checkpointPath ?? join(checkpointsDir, `${safeTaskFileName(task.id)}.json`);
233
+ }
234
+ function safeTaskFileName(id) {
235
+ if (!/^[a-zA-Z0-9._-]+$/.test(id)) {
236
+ throw new Error(`Invalid heartbeat task id "${id}". Use only letters, numbers, dots, underscores, and hyphens.`);
237
+ }
238
+ return id;
239
+ }
240
+ function projectionForResult(result, delayMs) {
241
+ switch (result.decision) {
242
+ case 'continue':
243
+ return {
244
+ status: 'waiting',
245
+ progress: delayMs === undefined ?
246
+ 'Heartbeat wake finished.'
247
+ : `Heartbeat wake finished. Waiting until the next scheduled run in ${formatDelay(delayMs)}.`,
248
+ };
249
+ case 'pause':
250
+ return {
251
+ status: 'waiting',
252
+ progress: delayMs === undefined ?
253
+ 'Heartbeat paused.'
254
+ : `Heartbeat paused. Waiting ${formatDelay(delayMs)} before the next wake.`,
255
+ };
256
+ case 'complete':
257
+ return {
258
+ status: 'complete',
259
+ progress: 'Heartbeat task completed and will not wake again.',
260
+ };
261
+ case 'escalate':
262
+ return {
263
+ status: 'blocked',
264
+ progress: 'Heartbeat escalated for user input and is waiting for follow-up.',
265
+ };
266
+ }
267
+ }
268
+ function formatDelay(ms) {
269
+ if (ms % (24 * 60 * 60_000) === 0) {
270
+ return `${ms / (24 * 60 * 60_000)}d`;
271
+ }
272
+ if (ms % (60 * 60_000) === 0) {
273
+ return `${ms / (60 * 60_000)}h`;
274
+ }
275
+ if (ms % 60_000 === 0) {
276
+ return `${ms / 60_000}m`;
277
+ }
278
+ if (ms % 1_000 === 0) {
279
+ return `${ms / 1_000}s`;
280
+ }
281
+ return `${ms}ms`;
282
+ }
283
+ function runRecordEntryFromPath(path, record) {
284
+ const id = basename(path, '.json');
285
+ return {
286
+ id,
287
+ path,
288
+ taskId: record.task.id,
289
+ runId: record.result.state.runId,
290
+ createdAt: record.result.state.finishedAt,
291
+ record,
292
+ };
293
+ }
294
+ function sleep(ms, signal) {
295
+ if (signal?.aborted) {
296
+ return Promise.resolve();
297
+ }
298
+ return new Promise((resolve) => {
299
+ const timeout = setTimeout(resolve, ms);
300
+ signal?.addEventListener('abort', () => {
301
+ clearTimeout(timeout);
302
+ resolve();
303
+ }, { once: true });
304
+ });
305
+ }
306
+ //# sourceMappingURL=heartbeat-scheduler.js.map