capacitor-mobile-claw 2.0.0 → 2.0.2

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 (56) hide show
  1. package/dist/esm/engine.d.ts.map +1 -1
  2. package/dist/esm/engine.js +4 -6
  3. package/dist/esm/engine.js.map +1 -1
  4. package/package.json +1 -1
  5. package/dist/esm/agent/agent-runner.d.ts +0 -93
  6. package/dist/esm/agent/agent-runner.d.ts.map +0 -1
  7. package/dist/esm/agent/agent-runner.js +0 -352
  8. package/dist/esm/agent/agent-runner.js.map +0 -1
  9. package/dist/esm/agent/auth-store.d.ts +0 -31
  10. package/dist/esm/agent/auth-store.d.ts.map +0 -1
  11. package/dist/esm/agent/auth-store.js +0 -121
  12. package/dist/esm/agent/auth-store.js.map +0 -1
  13. package/dist/esm/agent/capacitor-fs-adapter.d.ts +0 -65
  14. package/dist/esm/agent/capacitor-fs-adapter.d.ts.map +0 -1
  15. package/dist/esm/agent/capacitor-fs-adapter.js +0 -186
  16. package/dist/esm/agent/capacitor-fs-adapter.js.map +0 -1
  17. package/dist/esm/agent/cron-db-access.d.ts +0 -116
  18. package/dist/esm/agent/cron-db-access.d.ts.map +0 -1
  19. package/dist/esm/agent/cron-db-access.js +0 -741
  20. package/dist/esm/agent/cron-db-access.js.map +0 -1
  21. package/dist/esm/agent/fetch-proxy.d.ts +0 -2
  22. package/dist/esm/agent/fetch-proxy.d.ts.map +0 -1
  23. package/dist/esm/agent/fetch-proxy.js +0 -171
  24. package/dist/esm/agent/fetch-proxy.js.map +0 -1
  25. package/dist/esm/agent/file-tools.d.ts +0 -17
  26. package/dist/esm/agent/file-tools.d.ts.map +0 -1
  27. package/dist/esm/agent/file-tools.js +0 -295
  28. package/dist/esm/agent/file-tools.js.map +0 -1
  29. package/dist/esm/agent/git-tools.d.ts +0 -17
  30. package/dist/esm/agent/git-tools.d.ts.map +0 -1
  31. package/dist/esm/agent/git-tools.js +0 -184
  32. package/dist/esm/agent/git-tools.js.map +0 -1
  33. package/dist/esm/agent/heartbeat-manager.d.ts +0 -63
  34. package/dist/esm/agent/heartbeat-manager.d.ts.map +0 -1
  35. package/dist/esm/agent/heartbeat-manager.js +0 -711
  36. package/dist/esm/agent/heartbeat-manager.js.map +0 -1
  37. package/dist/esm/agent/retry-logic.d.ts +0 -9
  38. package/dist/esm/agent/retry-logic.d.ts.map +0 -1
  39. package/dist/esm/agent/retry-logic.js +0 -30
  40. package/dist/esm/agent/retry-logic.js.map +0 -1
  41. package/dist/esm/agent/session-store.d.ts +0 -31
  42. package/dist/esm/agent/session-store.d.ts.map +0 -1
  43. package/dist/esm/agent/session-store.js +0 -223
  44. package/dist/esm/agent/session-store.js.map +0 -1
  45. package/dist/esm/agent/tool-proxy.d.ts +0 -25
  46. package/dist/esm/agent/tool-proxy.d.ts.map +0 -1
  47. package/dist/esm/agent/tool-proxy.js +0 -58
  48. package/dist/esm/agent/tool-proxy.js.map +0 -1
  49. package/dist/esm/agent/tool-schemas.d.ts +0 -16
  50. package/dist/esm/agent/tool-schemas.d.ts.map +0 -1
  51. package/dist/esm/agent/tool-schemas.js +0 -129
  52. package/dist/esm/agent/tool-schemas.js.map +0 -1
  53. package/dist/esm/agent/wasm-tools.d.ts +0 -11
  54. package/dist/esm/agent/wasm-tools.d.ts.map +0 -1
  55. package/dist/esm/agent/wasm-tools.js +0 -70
  56. package/dist/esm/agent/wasm-tools.js.map +0 -1
@@ -1,711 +0,0 @@
1
- import { AgentRunner } from './agent-runner';
2
- import { SessionStore } from './session-store';
3
- const DEFAULT_HEARTBEAT_PROMPT = 'Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.';
4
- const HEARTBEAT_OK_TOKENS = ['HEARTBEAT_OK', 'heartbeat_ok', 'ok', 'OK', '✓', '👍'];
5
- const ERROR_BACKOFF_MS = [30_000, 60_000, 300_000, 900_000, 3_600_000];
6
- const DEDUP_WINDOW_MS = 24 * 60 * 60 * 1000;
7
- const WORKER_BRIDGE_TIMEOUT_MS = 10_000;
8
- export class HeartbeatManager {
9
- config;
10
- wakeInFlight = false;
11
- heartbeatConsecutiveErrors = 0;
12
- sessionStore = new SessionStore();
13
- _cronTimer = null;
14
- _cronApprovalResolvers = new Map();
15
- constructor(config) {
16
- this.config = config;
17
- }
18
- /** Resolve a pending cron approval request (called from engine). */
19
- respondToCronApproval(requestId, approved) {
20
- const pending = this._cronApprovalResolvers.get(requestId);
21
- if (pending) {
22
- pending.resolve(approved);
23
- this._cronApprovalResolvers.delete(requestId);
24
- }
25
- }
26
- /** Kick off the self-rearming cron timer. Call once after init. */
27
- startCronTimer() {
28
- this._armCronTimer();
29
- }
30
- async handleWake(source, opts = {}) {
31
- if (this.wakeInFlight) {
32
- this._emit('heartbeat.skipped', { reason: 'busy' });
33
- return;
34
- }
35
- this.wakeInFlight = true;
36
- const startedAt = Date.now();
37
- try {
38
- const scheduler = await this.config.cronDb.getSchedulerConfig();
39
- const isManual = source === 'manual' || opts.force === true;
40
- if (!isManual && !scheduler.enabled) {
41
- this._emit('heartbeat.skipped', { reason: 'scheduler_disabled' });
42
- await this._emitSchedulerStatus();
43
- return;
44
- }
45
- if (!isManual && this.config.isUserAgentRunning()) {
46
- this._emit('heartbeat.skipped', { reason: 'user_active' });
47
- await this._emitSchedulerStatus();
48
- return;
49
- }
50
- this._emit('heartbeat.started', { source });
51
- const now = Date.now();
52
- const heartbeatResult = await this._runHeartbeatCycle({
53
- source,
54
- now,
55
- force: isManual,
56
- forceSessionKey: opts.forceSessionKey,
57
- });
58
- await this._runDueCronJobs({
59
- source,
60
- now,
61
- forceJobId: opts.forceJobId,
62
- });
63
- const durationMs = Date.now() - startedAt;
64
- if (heartbeatResult.status === 'skipped') {
65
- this._emit('heartbeat.skipped', { reason: heartbeatResult.reason || 'not_due' });
66
- }
67
- else {
68
- this._emit('heartbeat.completed', {
69
- status: heartbeatResult.status,
70
- reason: heartbeatResult.reason,
71
- durationMs,
72
- ...(heartbeatResult.responsePreview ? { responsePreview: heartbeatResult.responsePreview } : {}),
73
- });
74
- }
75
- await this._emitSchedulerStatus();
76
- }
77
- catch (err) {
78
- this._emit('heartbeat.completed', {
79
- status: 'error',
80
- reason: err?.message || 'heartbeat_failed',
81
- durationMs: Date.now() - startedAt,
82
- });
83
- }
84
- finally {
85
- this.wakeInFlight = false;
86
- this._armCronTimer();
87
- }
88
- }
89
- /**
90
- * Self-rearming timer (mirrors OpenClaw's armTimer pattern).
91
- * After each wake completes, check for the next due job and schedule
92
- * a setTimeout to fire it. Clamped to 60s max to prevent drift.
93
- * Native WorkManager handles background wakes; this handles foreground.
94
- */
95
- _armCronTimer() {
96
- if (this._cronTimer) {
97
- clearTimeout(this._cronTimer);
98
- this._cronTimer = null;
99
- }
100
- this.config.cronDb.getDueJobs(Date.now() + 120_000).then((upcoming) => {
101
- if (!upcoming.length)
102
- return;
103
- const now = Date.now();
104
- const earliest = Math.min(...upcoming.map((j) => j.nextRunAt ?? Infinity));
105
- if (!Number.isFinite(earliest))
106
- return;
107
- const delay = Math.max(earliest - now, 1_000);
108
- const clamped = Math.min(delay, 60_000);
109
- this._cronTimer = setTimeout(() => {
110
- this._cronTimer = null;
111
- this.handleWake('timer').catch((err) => {
112
- console.warn('[HeartbeatManager] Timer wake failed:', err?.message);
113
- });
114
- }, clamped);
115
- }).catch(() => {
116
- // DB error — will retry on next native wake
117
- });
118
- }
119
- async _runHeartbeatCycle(params) {
120
- const config = await this.config.cronDb.getHeartbeatConfig();
121
- const startedAt = params.now;
122
- const everyMs = Number(config.everyMs) || 1_800_000;
123
- if (!params.force && !config.enabled) {
124
- return { status: 'skipped', reason: 'heartbeat_disabled' };
125
- }
126
- if (!params.force && config.nextRunAt && startedAt < config.nextRunAt) {
127
- return { status: 'skipped', reason: 'not_due' };
128
- }
129
- if (!isWithinActiveHours(config.activeHours?.start, config.activeHours?.end, config.activeHours?.tz, startedAt)) {
130
- await this.config.cronDb.setHeartbeatConfig({
131
- nextRunAt: startedAt + Math.max(15_000, everyMs),
132
- });
133
- return { status: 'skipped', reason: 'outside_active_hours' };
134
- }
135
- const heartbeatSkill = await this._resolveSkill(config.skillId);
136
- const eventSessionKey = params.forceSessionKey || this.config.getCurrentSessionKey() || 'main';
137
- const pendingEvents = await this.config.cronDb.peekPendingEvents(eventSessionKey);
138
- const prompt = buildHeartbeatPrompt(config.prompt || DEFAULT_HEARTBEAT_PROMPT, pendingEvents);
139
- try {
140
- const sessionKey = params.announceToMain ? eventSessionKey : `heartbeat/${startedAt}`;
141
- const result = await this._runAgentTurn({
142
- prompt,
143
- skill: heartbeatSkill,
144
- sessionKey,
145
- dispatchToEngine: params.announceToMain,
146
- });
147
- const trimmed = result.text.trim();
148
- const hash = fnv1aHash(trimmed);
149
- const isOk = isHeartbeatOk(trimmed);
150
- const isDuplicate = !!trimmed &&
151
- !!config.lastHash &&
152
- config.lastHash === hash &&
153
- !!config.lastSentAt &&
154
- startedAt - config.lastSentAt < DEDUP_WINDOW_MS;
155
- if (!trimmed || isOk || isDuplicate) {
156
- await this._consumePendingEvents(pendingEvents);
157
- await this.config.cronDb.setHeartbeatConfig({
158
- nextRunAt: startedAt + everyMs,
159
- });
160
- this.heartbeatConsecutiveErrors = 0;
161
- if (isDuplicate)
162
- return { status: 'deduped', reason: 'duplicate' };
163
- if (isOk)
164
- return { status: 'suppressed', reason: 'heartbeat_ok' };
165
- return { status: 'suppressed', reason: 'empty' };
166
- }
167
- await this._persistSession(result);
168
- await this._consumePendingEvents(pendingEvents);
169
- await this.config.cronDb.setHeartbeatConfig({
170
- nextRunAt: startedAt + everyMs,
171
- lastHash: hash,
172
- lastSentAt: startedAt,
173
- });
174
- this.heartbeatConsecutiveErrors = 0;
175
- this._emit('cron.notification', {
176
- title: 'Sentinel heartbeat',
177
- body: trimmed,
178
- source: 'heartbeat',
179
- });
180
- return {
181
- status: 'ok',
182
- responsePreview: trimmed.slice(0, 240),
183
- };
184
- }
185
- catch (err) {
186
- this.heartbeatConsecutiveErrors += 1;
187
- const normalNext = startedAt + everyMs;
188
- const backoffNext = startedAt + errorBackoffMs(this.heartbeatConsecutiveErrors);
189
- await this.config.cronDb.setHeartbeatConfig({
190
- nextRunAt: Math.max(normalNext, backoffNext),
191
- });
192
- return {
193
- status: 'error',
194
- reason: err?.message || 'heartbeat_error',
195
- };
196
- }
197
- }
198
- async _runDueCronJobs(params) {
199
- const skills = await this.config.cronDb.listCronSkills();
200
- const skillById = new Map(skills.map((skill) => [skill.id, skill]));
201
- const allJobs = await this.config.cronDb.listCronJobs();
202
- const dueJobs = params.forceJobId
203
- ? allJobs.filter((job) => job.id === params.forceJobId)
204
- : await this.config.cronDb.getDueJobs(params.now);
205
- for (const job of dueJobs) {
206
- const startedAt = Date.now();
207
- const skill = skillById.get(job.skillId) || null;
208
- if (!job.enabled)
209
- continue;
210
- if (!isWithinActiveHours(job.activeHours?.start, job.activeHours?.end, job.activeHours?.tz, startedAt)) {
211
- await this.config.cronDb.updateCronJob(job.id, {
212
- lastRunAt: startedAt,
213
- nextRunAt: computeNextRunAt(job, startedAt),
214
- lastRunStatus: 'skipped',
215
- });
216
- await this.config.cronDb.insertCronRun({
217
- jobId: job.id,
218
- startedAt,
219
- endedAt: Date.now(),
220
- status: 'skipped',
221
- wakeSource: params.source,
222
- });
223
- continue;
224
- }
225
- this._emit('cron.job.started', { jobId: job.id, jobName: job.name });
226
- try {
227
- if (job.sessionTarget === 'main') {
228
- const sessionKey = this.config.getCurrentSessionKey() || 'main';
229
- await this.config.cronDb.enqueueSystemEvent(sessionKey, `cron:${job.id}:${startedAt}`, job.prompt);
230
- if (job.wakeMode === 'now' || params.forceJobId === job.id) {
231
- await this._runHeartbeatCycle({
232
- source: `cron:${job.id}`,
233
- now: Date.now(),
234
- force: true,
235
- forceSessionKey: sessionKey,
236
- });
237
- }
238
- const runEndedAt = Date.now();
239
- await this.config.cronDb.updateCronJob(job.id, {
240
- lastRunAt: startedAt,
241
- nextRunAt: computeNextRunAt(job, startedAt),
242
- lastRunStatus: 'ok',
243
- lastError: null,
244
- lastDurationMs: runEndedAt - startedAt,
245
- consecutiveErrors: 0,
246
- });
247
- await this.config.cronDb.insertCronRun({
248
- jobId: job.id,
249
- startedAt,
250
- endedAt: runEndedAt,
251
- status: 'ok',
252
- responseText: 'Enqueued to main session heartbeat.',
253
- delivered: false,
254
- wakeSource: params.source,
255
- });
256
- this._emit('cron.job.completed', {
257
- jobId: job.id,
258
- status: 'ok',
259
- durationMs: runEndedAt - startedAt,
260
- responsePreview: 'Queued for heartbeat',
261
- });
262
- continue;
263
- }
264
- const result = await this._runAgentTurn({
265
- prompt: job.prompt,
266
- skill,
267
- sessionKey: `cron/${job.id}/${startedAt}`,
268
- cronJob: job,
269
- });
270
- const trimmed = result.text.trim();
271
- const hash = fnv1aHash(trimmed);
272
- const heartbeatOk = isHeartbeatOk(trimmed);
273
- const isDeduped = !!trimmed &&
274
- !!job.lastResponseHash &&
275
- job.lastResponseHash === hash &&
276
- !!job.lastResponseSentAt &&
277
- startedAt - job.lastResponseSentAt < DEDUP_WINDOW_MS;
278
- const status = !trimmed ? 'suppressed' : heartbeatOk ? 'suppressed' : isDeduped ? 'deduped' : 'ok';
279
- let delivered = false;
280
- if (status === 'ok') {
281
- delivered = await this._deliverCronResult(job, trimmed);
282
- await this._persistSession(result);
283
- // Announce: enqueue result as system event in main session
284
- const mainSessionKey = this.config.getCurrentSessionKey() || 'main';
285
- await this.config.cronDb.enqueueSystemEvent(mainSessionKey, `cron:${job.id}:${startedAt}`, `[Scheduled task "${job.name}" completed]\n\n${trimmed}`);
286
- // Surface to main agent if user is idle
287
- if (!this.config.isUserAgentRunning()) {
288
- try {
289
- await this._runHeartbeatCycle({
290
- source: `cron:announce:${job.id}`,
291
- now: Date.now(),
292
- force: true,
293
- forceSessionKey: mainSessionKey,
294
- announceToMain: true,
295
- });
296
- }
297
- catch (announceErr) {
298
- console.warn('[HeartbeatManager] Announce heartbeat failed:', announceErr?.message);
299
- }
300
- }
301
- }
302
- const runEndedAt = Date.now();
303
- await this.config.cronDb.updateCronJob(job.id, {
304
- lastRunAt: startedAt,
305
- nextRunAt: computeNextRunAt(job, startedAt),
306
- lastRunStatus: status,
307
- lastError: null,
308
- lastDurationMs: runEndedAt - startedAt,
309
- lastResponseHash: trimmed ? hash : job.lastResponseHash,
310
- lastResponseSentAt: delivered ? runEndedAt : job.lastResponseSentAt,
311
- consecutiveErrors: 0,
312
- });
313
- await this.config.cronDb.insertCronRun({
314
- jobId: job.id,
315
- startedAt,
316
- endedAt: runEndedAt,
317
- status,
318
- durationMs: runEndedAt - startedAt,
319
- responseText: trimmed || null,
320
- wasHeartbeatOk: heartbeatOk,
321
- wasDeduped: isDeduped,
322
- delivered,
323
- wakeSource: params.source,
324
- });
325
- this._emit('cron.job.completed', {
326
- jobId: job.id,
327
- status,
328
- durationMs: runEndedAt - startedAt,
329
- ...(trimmed ? { responsePreview: trimmed.slice(0, 200) } : {}),
330
- });
331
- }
332
- catch (err) {
333
- const nextConsecutiveErrors = (job.consecutiveErrors || 0) + 1;
334
- const normalNext = computeNextRunAt(job, startedAt);
335
- const backoffNext = startedAt + errorBackoffMs(nextConsecutiveErrors);
336
- const nextRunAt = normalNext ? Math.max(normalNext, backoffNext) : backoffNext;
337
- await this.config.cronDb.updateCronJob(job.id, {
338
- lastRunAt: startedAt,
339
- nextRunAt,
340
- lastRunStatus: 'error',
341
- lastError: err?.message || 'cron_error',
342
- lastDurationMs: Date.now() - startedAt,
343
- consecutiveErrors: nextConsecutiveErrors,
344
- });
345
- await this.config.cronDb.insertCronRun({
346
- jobId: job.id,
347
- startedAt,
348
- endedAt: Date.now(),
349
- status: 'error',
350
- error: err?.message || 'cron_error',
351
- wakeSource: params.source,
352
- });
353
- this._emit('cron.job.error', {
354
- jobId: job.id,
355
- error: err?.message || 'cron_error',
356
- consecutiveErrors: nextConsecutiveErrors,
357
- });
358
- }
359
- }
360
- }
361
- async _runAgentTurn(params) {
362
- const provider = 'anthropic';
363
- const authResult = await withTimeout(this.config.getAuth(provider, 'main'), WORKER_BRIDGE_TIMEOUT_MS, 'auth.getToken timeout');
364
- if (!authResult.apiKey) {
365
- throw new Error('No API provider configured');
366
- }
367
- const promptResult = await withTimeout(this.config.getSystemPrompt('main'), WORKER_BRIDGE_TIMEOUT_MS, 'system_prompt.get timeout');
368
- let agentError = null;
369
- const localApprovals = new Map();
370
- // Timeout pause/resume for approval waits
371
- const model = params.skill?.model || 'claude-sonnet-4-5';
372
- const totalTimeoutMs = Math.max(1_000, Number(params.skill?.timeoutMs) || 60_000);
373
- let remaining = totalTimeoutMs;
374
- let timerStart = Date.now();
375
- let timer = null;
376
- let rejectTimeout = null;
377
- let runner = null;
378
- const clearTimer = () => {
379
- if (timer) {
380
- clearTimeout(timer);
381
- timer = null;
382
- }
383
- };
384
- const startTimer = () => {
385
- clearTimer();
386
- timerStart = Date.now();
387
- timer = setTimeout(() => {
388
- runner?.abort();
389
- rejectTimeout?.(new Error(`Agent run timed out after ${totalTimeoutMs}ms`));
390
- }, remaining);
391
- };
392
- const pauseTimer = () => {
393
- clearTimer();
394
- remaining -= Date.now() - timerStart;
395
- if (remaining < 0)
396
- remaining = 0;
397
- };
398
- const resumeTimer = () => {
399
- timerStart = Date.now();
400
- startTimer();
401
- };
402
- // Build middleware for cron jobs (approval forwarding + _cronContext injection)
403
- const cronJob = params.cronJob;
404
- const middleware = cronJob
405
- ? async (ctx, execute, signal) => {
406
- const requestId = `${cronJob.id}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
407
- const cronContext = { jobId: cronJob.id, jobName: cronJob.name, requestId };
408
- const argsWithContext = { ...ctx.args, _cronContext: cronContext };
409
- const needsApproval = this.config.needsApproval?.(ctx.name) ?? false;
410
- if (!needsApproval) {
411
- return execute(argsWithContext);
412
- }
413
- // Emit approval request to main session UI
414
- this._emit('cron.approval_request', {
415
- requestId,
416
- jobId: cronJob.id,
417
- jobName: cronJob.name,
418
- toolName: ctx.name,
419
- toolCallId: ctx.toolCallId,
420
- args: ctx.args,
421
- _cronContext: cronContext,
422
- });
423
- // Pause timeout while waiting for user
424
- pauseTimer();
425
- try {
426
- const approved = await new Promise((resolve) => {
427
- localApprovals.set(requestId, { resolve });
428
- this._cronApprovalResolvers.set(requestId, { resolve });
429
- signal?.addEventListener('abort', () => {
430
- localApprovals.delete(requestId);
431
- this._cronApprovalResolvers.delete(requestId);
432
- resolve(false);
433
- });
434
- });
435
- if (!approved) {
436
- return {
437
- content: [{ type: 'text', text: `Tool "${ctx.name}" execution was denied by user.` }],
438
- details: { denied: true, reason: 'user_denied' },
439
- };
440
- }
441
- return execute(argsWithContext);
442
- }
443
- finally {
444
- localApprovals.delete(requestId);
445
- this._cronApprovalResolvers.delete(requestId);
446
- resumeTimer();
447
- }
448
- }
449
- : undefined;
450
- runner = new AgentRunner({
451
- dispatch: (msg) => {
452
- if (msg.type === 'agent.error') {
453
- agentError = typeof msg.error === 'string' ? msg.error : 'Agent execution failed';
454
- }
455
- if (params.dispatchToEngine) {
456
- this.config.dispatch(msg);
457
- }
458
- },
459
- toolProxy: this.config.toolProxy,
460
- ...(middleware ? { toolMiddleware: middleware } : {}),
461
- });
462
- try {
463
- await Promise.race([
464
- runner.run({
465
- prompt: params.prompt,
466
- agentId: 'main',
467
- sessionKey: params.sessionKey,
468
- apiKey: authResult.apiKey,
469
- provider,
470
- systemPrompt: params.skill?.systemPrompt || promptResult.systemPrompt,
471
- model,
472
- maxTurns: params.skill?.maxTurns ?? 3,
473
- allowedTools: params.skill?.allowedTools,
474
- extraTools: this.config.extraTools,
475
- }),
476
- new Promise((_, reject) => {
477
- rejectTimeout = reject;
478
- startTimer();
479
- }),
480
- ]);
481
- }
482
- finally {
483
- clearTimer();
484
- // Stale approval cleanup: resolve all pending as denied, notify UI
485
- for (const [requestId] of localApprovals) {
486
- const pending = this._cronApprovalResolvers.get(requestId);
487
- if (pending) {
488
- pending.resolve(false);
489
- this._cronApprovalResolvers.delete(requestId);
490
- }
491
- this._emit('cron.approval_expired', { requestId });
492
- }
493
- localApprovals.clear();
494
- }
495
- if (agentError) {
496
- throw new Error(agentError);
497
- }
498
- const messages = runner.currentAgent?.state.messages || [];
499
- return {
500
- text: extractLastAssistantText(messages),
501
- sessionKey: params.sessionKey,
502
- messages,
503
- model,
504
- };
505
- }
506
- async _persistSession(result) {
507
- if (!result.messages.length)
508
- return;
509
- try {
510
- await this.sessionStore.saveSession({
511
- sessionKey: result.sessionKey,
512
- agentId: 'main',
513
- messages: result.messages,
514
- model: `anthropic/${result.model}`,
515
- startTime: Date.now(),
516
- });
517
- }
518
- catch (err) {
519
- console.warn('[HeartbeatManager] Failed to persist session:', err?.message);
520
- }
521
- }
522
- async _deliverCronResult(job, text) {
523
- if (!text || job.deliveryMode === 'none')
524
- return false;
525
- if (job.deliveryMode === 'webhook' && job.deliveryWebhookUrl) {
526
- try {
527
- const response = await fetch(job.deliveryWebhookUrl, {
528
- method: 'POST',
529
- headers: { 'Content-Type': 'application/json' },
530
- body: JSON.stringify({
531
- jobId: job.id,
532
- jobName: job.name,
533
- text,
534
- sentAt: Date.now(),
535
- }),
536
- });
537
- if (response.ok)
538
- return true;
539
- }
540
- catch {
541
- // Fall back to a local notification event.
542
- }
543
- }
544
- this._emit('cron.notification', {
545
- title: job.deliveryNotificationTitle || job.name,
546
- body: text,
547
- jobId: job.id,
548
- source: 'cron',
549
- });
550
- return true;
551
- }
552
- async _resolveSkill(skillId) {
553
- if (!skillId)
554
- return null;
555
- const skills = await this.config.cronDb.listCronSkills();
556
- return skills.find((skill) => skill.id === skillId) || null;
557
- }
558
- async _emitSchedulerStatus() {
559
- const scheduler = await this.config.cronDb.getSchedulerConfig();
560
- const heartbeat = await this.config.cronDb.getHeartbeatConfig();
561
- const dueJobs = await this.config.cronDb.getDueJobs(Date.now());
562
- this._emit('scheduler.status', {
563
- enabled: scheduler.enabled,
564
- mode: scheduler.schedulingMode || 'balanced',
565
- heartbeatNext: heartbeat.nextRunAt,
566
- nextDueAt: dueJobs[0]?.nextRunAt,
567
- });
568
- }
569
- async _consumePendingEvents(events) {
570
- if (!events.length)
571
- return;
572
- await this.config.cronDb.consumePendingEvents(events.map((event) => event.id));
573
- }
574
- _emit(type, payload = {}) {
575
- this.config.dispatch({ type, ...payload });
576
- }
577
- }
578
- async function withTimeout(promise, timeoutMs, message) {
579
- let timer = null;
580
- try {
581
- return await Promise.race([
582
- promise,
583
- new Promise((_, reject) => {
584
- timer = setTimeout(() => reject(new Error(message)), timeoutMs);
585
- }),
586
- ]);
587
- }
588
- finally {
589
- if (timer)
590
- clearTimeout(timer);
591
- }
592
- }
593
- export function extractLastAssistantText(messages) {
594
- for (let i = messages.length - 1; i >= 0; i -= 1) {
595
- const message = messages[i];
596
- if (message?.role !== 'assistant')
597
- continue;
598
- if (typeof message.content === 'string')
599
- return message.content;
600
- if (!Array.isArray(message.content))
601
- continue;
602
- const text = message.content
603
- .filter((part) => part?.type === 'text')
604
- .map((part) => part?.text || '')
605
- .join('')
606
- .trim();
607
- if (text)
608
- return text;
609
- }
610
- return '';
611
- }
612
- export function buildHeartbeatPrompt(basePrompt, pendingEvents) {
613
- if (!pendingEvents.length)
614
- return basePrompt;
615
- const lines = pendingEvents.map((event) => {
616
- const timestamp = new Date(event.createdAt).toISOString();
617
- const contextKey = event.contextKey ? ` (${event.contextKey})` : '';
618
- return `- [${timestamp}]${contextKey} ${event.text}`;
619
- });
620
- return `${basePrompt}\n\nSystem events:\n${lines.join('\n')}`;
621
- }
622
- export function computeNextRunAt(job, nowMs) {
623
- const schedule = job.schedule || {};
624
- if (schedule.kind === 'every') {
625
- const everyMs = Number(schedule.everyMs) || 0;
626
- return everyMs > 0 ? nowMs + everyMs : null;
627
- }
628
- if (schedule.kind === 'at') {
629
- const atMs = Number(schedule.atMs) || 0;
630
- return atMs > nowMs ? atMs : null;
631
- }
632
- return null;
633
- }
634
- export function isHeartbeatOk(text) {
635
- const trimmed = (text || '').trim();
636
- if (!trimmed)
637
- return true;
638
- return HEARTBEAT_OK_TOKENS.some((token) => trimmed === token || trimmed.startsWith(`${token}\n`));
639
- }
640
- export function fnv1aHash(str) {
641
- let hash = 0x811c9dc5;
642
- for (let i = 0; i < str.length; i += 1) {
643
- hash ^= str.charCodeAt(i);
644
- hash = Math.imul(hash, 0x01000193);
645
- }
646
- return (hash >>> 0).toString(16).padStart(8, '0');
647
- }
648
- export function errorBackoffMs(consecutiveErrors) {
649
- const index = Math.min(consecutiveErrors - 1, ERROR_BACKOFF_MS.length - 1);
650
- return ERROR_BACKOFF_MS[Math.max(0, index)];
651
- }
652
- function parseActiveHoursMinutes(raw, allow24 = false) {
653
- if (typeof raw !== 'string')
654
- return null;
655
- const match = raw.trim().match(/^(\d{2}):(\d{2})$/);
656
- if (!match)
657
- return null;
658
- const hour = Number(match[1]);
659
- const minute = Number(match[2]);
660
- if (!Number.isFinite(hour) || !Number.isFinite(minute))
661
- return null;
662
- if (hour === 24) {
663
- if (!allow24 || minute !== 0)
664
- return null;
665
- return 24 * 60;
666
- }
667
- if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
668
- return null;
669
- return hour * 60 + minute;
670
- }
671
- function resolveMinutesInTimeZone(nowMs, tz) {
672
- try {
673
- const parts = new Intl.DateTimeFormat('en-US', {
674
- timeZone: tz || 'UTC',
675
- hour: '2-digit',
676
- minute: '2-digit',
677
- hourCycle: 'h23',
678
- }).formatToParts(new Date(nowMs));
679
- const values = {};
680
- for (const part of parts) {
681
- if (part.type !== 'literal')
682
- values[part.type] = part.value;
683
- }
684
- const hour = Number(values.hour);
685
- const minute = Number(values.minute);
686
- if (!Number.isFinite(hour) || !Number.isFinite(minute))
687
- return null;
688
- return hour * 60 + minute;
689
- }
690
- catch {
691
- return null;
692
- }
693
- }
694
- export function isWithinActiveHours(start, end, tz, nowMs = Date.now()) {
695
- if (!start || !end)
696
- return true;
697
- const startMin = parseActiveHoursMinutes(start, false);
698
- const endMin = parseActiveHoursMinutes(end, true);
699
- if (startMin === null || endMin === null)
700
- return true;
701
- if (startMin === endMin)
702
- return false;
703
- const currentMin = resolveMinutesInTimeZone(nowMs, tz || 'UTC');
704
- if (currentMin === null)
705
- return true;
706
- if (endMin > startMin) {
707
- return currentMin >= startMin && currentMin < endMin;
708
- }
709
- return currentMin >= startMin || currentMin < endMin;
710
- }
711
- //# sourceMappingURL=heartbeat-manager.js.map