@dotsetlabs/dotclaw 1.1.0

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 (170) hide show
  1. package/.env.example +54 -0
  2. package/LICENSE +21 -0
  3. package/README.md +111 -0
  4. package/config-examples/groups/global/CLAUDE.md +21 -0
  5. package/config-examples/groups/main/CLAUDE.md +47 -0
  6. package/config-examples/mount-allowlist.json +25 -0
  7. package/config-examples/plugin-http.json +18 -0
  8. package/config-examples/runtime.json +30 -0
  9. package/config-examples/tool-budgets.json +24 -0
  10. package/config-examples/tool-policy.json +51 -0
  11. package/container/.dockerignore +6 -0
  12. package/container/Dockerfile +74 -0
  13. package/container/agent-runner/package-lock.json +92 -0
  14. package/container/agent-runner/package.json +20 -0
  15. package/container/agent-runner/src/agent-config.ts +295 -0
  16. package/container/agent-runner/src/container-protocol.ts +73 -0
  17. package/container/agent-runner/src/daemon.ts +91 -0
  18. package/container/agent-runner/src/index.ts +1428 -0
  19. package/container/agent-runner/src/ipc.ts +321 -0
  20. package/container/agent-runner/src/memory.ts +336 -0
  21. package/container/agent-runner/src/prompt-packs.ts +341 -0
  22. package/container/agent-runner/src/tools.ts +1720 -0
  23. package/container/agent-runner/tsconfig.json +19 -0
  24. package/container/build.sh +23 -0
  25. package/container/skills/agent-browser.md +159 -0
  26. package/dist/admin-commands.d.ts +7 -0
  27. package/dist/admin-commands.d.ts.map +1 -0
  28. package/dist/admin-commands.js +87 -0
  29. package/dist/admin-commands.js.map +1 -0
  30. package/dist/agent-context.d.ts +42 -0
  31. package/dist/agent-context.d.ts.map +1 -0
  32. package/dist/agent-context.js +92 -0
  33. package/dist/agent-context.js.map +1 -0
  34. package/dist/agent-execution.d.ts +68 -0
  35. package/dist/agent-execution.d.ts.map +1 -0
  36. package/dist/agent-execution.js +169 -0
  37. package/dist/agent-execution.js.map +1 -0
  38. package/dist/agent-semaphore.d.ts +2 -0
  39. package/dist/agent-semaphore.d.ts.map +1 -0
  40. package/dist/agent-semaphore.js +52 -0
  41. package/dist/agent-semaphore.js.map +1 -0
  42. package/dist/behavior-config.d.ts +14 -0
  43. package/dist/behavior-config.d.ts.map +1 -0
  44. package/dist/behavior-config.js +52 -0
  45. package/dist/behavior-config.js.map +1 -0
  46. package/dist/cli.d.ts +3 -0
  47. package/dist/cli.d.ts.map +1 -0
  48. package/dist/cli.js +626 -0
  49. package/dist/cli.js.map +1 -0
  50. package/dist/config.d.ts +31 -0
  51. package/dist/config.d.ts.map +1 -0
  52. package/dist/config.js +38 -0
  53. package/dist/config.js.map +1 -0
  54. package/dist/container-protocol.d.ts +72 -0
  55. package/dist/container-protocol.d.ts.map +1 -0
  56. package/dist/container-protocol.js +3 -0
  57. package/dist/container-protocol.js.map +1 -0
  58. package/dist/container-runner.d.ts +59 -0
  59. package/dist/container-runner.d.ts.map +1 -0
  60. package/dist/container-runner.js +813 -0
  61. package/dist/container-runner.js.map +1 -0
  62. package/dist/cost.d.ts +9 -0
  63. package/dist/cost.d.ts.map +1 -0
  64. package/dist/cost.js +11 -0
  65. package/dist/cost.js.map +1 -0
  66. package/dist/dashboard.d.ts +58 -0
  67. package/dist/dashboard.d.ts.map +1 -0
  68. package/dist/dashboard.js +471 -0
  69. package/dist/dashboard.js.map +1 -0
  70. package/dist/db.d.ts +99 -0
  71. package/dist/db.d.ts.map +1 -0
  72. package/dist/db.js +423 -0
  73. package/dist/db.js.map +1 -0
  74. package/dist/error-messages.d.ts +17 -0
  75. package/dist/error-messages.d.ts.map +1 -0
  76. package/dist/error-messages.js +109 -0
  77. package/dist/error-messages.js.map +1 -0
  78. package/dist/index.d.ts +2 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +2072 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/locks.d.ts +2 -0
  83. package/dist/locks.d.ts.map +1 -0
  84. package/dist/locks.js +26 -0
  85. package/dist/locks.js.map +1 -0
  86. package/dist/logger.d.ts +4 -0
  87. package/dist/logger.d.ts.map +1 -0
  88. package/dist/logger.js +15 -0
  89. package/dist/logger.js.map +1 -0
  90. package/dist/maintenance.d.ts +13 -0
  91. package/dist/maintenance.d.ts.map +1 -0
  92. package/dist/maintenance.js +151 -0
  93. package/dist/maintenance.js.map +1 -0
  94. package/dist/memory-embeddings.d.ts +13 -0
  95. package/dist/memory-embeddings.d.ts.map +1 -0
  96. package/dist/memory-embeddings.js +126 -0
  97. package/dist/memory-embeddings.js.map +1 -0
  98. package/dist/memory-recall.d.ts +8 -0
  99. package/dist/memory-recall.d.ts.map +1 -0
  100. package/dist/memory-recall.js +127 -0
  101. package/dist/memory-recall.js.map +1 -0
  102. package/dist/memory-store.d.ts +149 -0
  103. package/dist/memory-store.d.ts.map +1 -0
  104. package/dist/memory-store.js +787 -0
  105. package/dist/memory-store.js.map +1 -0
  106. package/dist/metrics.d.ts +12 -0
  107. package/dist/metrics.d.ts.map +1 -0
  108. package/dist/metrics.js +134 -0
  109. package/dist/metrics.js.map +1 -0
  110. package/dist/model-registry.d.ts +67 -0
  111. package/dist/model-registry.d.ts.map +1 -0
  112. package/dist/model-registry.js +230 -0
  113. package/dist/model-registry.js.map +1 -0
  114. package/dist/mount-security.d.ts +37 -0
  115. package/dist/mount-security.d.ts.map +1 -0
  116. package/dist/mount-security.js +284 -0
  117. package/dist/mount-security.js.map +1 -0
  118. package/dist/paths.d.ts +80 -0
  119. package/dist/paths.d.ts.map +1 -0
  120. package/dist/paths.js +149 -0
  121. package/dist/paths.js.map +1 -0
  122. package/dist/personalization.d.ts +6 -0
  123. package/dist/personalization.d.ts.map +1 -0
  124. package/dist/personalization.js +180 -0
  125. package/dist/personalization.js.map +1 -0
  126. package/dist/progress.d.ts +15 -0
  127. package/dist/progress.d.ts.map +1 -0
  128. package/dist/progress.js +92 -0
  129. package/dist/progress.js.map +1 -0
  130. package/dist/runtime-config.d.ts +227 -0
  131. package/dist/runtime-config.d.ts.map +1 -0
  132. package/dist/runtime-config.js +297 -0
  133. package/dist/runtime-config.js.map +1 -0
  134. package/dist/task-scheduler.d.ts +9 -0
  135. package/dist/task-scheduler.d.ts.map +1 -0
  136. package/dist/task-scheduler.js +195 -0
  137. package/dist/task-scheduler.js.map +1 -0
  138. package/dist/telegram-format.d.ts +3 -0
  139. package/dist/telegram-format.d.ts.map +1 -0
  140. package/dist/telegram-format.js +200 -0
  141. package/dist/telegram-format.js.map +1 -0
  142. package/dist/tool-budgets.d.ts +16 -0
  143. package/dist/tool-budgets.d.ts.map +1 -0
  144. package/dist/tool-budgets.js +83 -0
  145. package/dist/tool-budgets.js.map +1 -0
  146. package/dist/tool-policy.d.ts +18 -0
  147. package/dist/tool-policy.d.ts.map +1 -0
  148. package/dist/tool-policy.js +84 -0
  149. package/dist/tool-policy.js.map +1 -0
  150. package/dist/trace-writer.d.ts +39 -0
  151. package/dist/trace-writer.d.ts.map +1 -0
  152. package/dist/trace-writer.js +27 -0
  153. package/dist/trace-writer.js.map +1 -0
  154. package/dist/types.d.ts +81 -0
  155. package/dist/types.d.ts.map +1 -0
  156. package/dist/types.js +2 -0
  157. package/dist/types.js.map +1 -0
  158. package/dist/utils.d.ts +4 -0
  159. package/dist/utils.d.ts.map +1 -0
  160. package/dist/utils.js +30 -0
  161. package/dist/utils.js.map +1 -0
  162. package/launchd/com.dotclaw.plist +32 -0
  163. package/package.json +89 -0
  164. package/scripts/autotune.js +53 -0
  165. package/scripts/bootstrap.js +348 -0
  166. package/scripts/configure.js +200 -0
  167. package/scripts/doctor.js +164 -0
  168. package/scripts/init.js +209 -0
  169. package/scripts/install.sh +219 -0
  170. package/systemd/dotclaw.service +22 -0
@@ -0,0 +1,295 @@
1
+ import fs from 'fs';
2
+
3
+ export type AgentRuntimeConfig = {
4
+ defaultModel: string;
5
+ daemonPollMs: number;
6
+ agent: {
7
+ assistantName: string;
8
+ openrouter: {
9
+ timeoutMs: number;
10
+ retry: boolean;
11
+ siteUrl: string;
12
+ siteName: string;
13
+ };
14
+ promptPacks: {
15
+ enabled: boolean;
16
+ maxChars: number;
17
+ maxDemos: number;
18
+ canaryRate: number;
19
+ };
20
+ context: {
21
+ maxContextTokens: number;
22
+ compactionTriggerTokens: number;
23
+ recentContextTokens: number;
24
+ summaryUpdateEveryMessages: number;
25
+ maxOutputTokens: number;
26
+ summaryMaxOutputTokens: number;
27
+ temperature: number;
28
+ maxContextMessageTokens: number;
29
+ };
30
+ memory: {
31
+ maxResults: number;
32
+ maxTokens: number;
33
+ extraction: {
34
+ enabled: boolean;
35
+ async: boolean;
36
+ maxMessages: number;
37
+ maxOutputTokens: number;
38
+ };
39
+ archiveSync: boolean;
40
+ extractScheduled: boolean;
41
+ };
42
+ models: {
43
+ summary: string;
44
+ memory: string;
45
+ planner: string;
46
+ responseValidation: string;
47
+ toolSummary: string;
48
+ };
49
+ planner: {
50
+ enabled: boolean;
51
+ mode: string;
52
+ minTokens: number;
53
+ triggerRegex: string;
54
+ maxOutputTokens: number;
55
+ temperature: number;
56
+ };
57
+ responseValidation: {
58
+ enabled: boolean;
59
+ maxOutputTokens: number;
60
+ temperature: number;
61
+ maxRetries: number;
62
+ allowToolCalls: boolean;
63
+ };
64
+ tools: {
65
+ maxToolSteps: number;
66
+ outputLimitBytes: number;
67
+ enableBash: boolean;
68
+ enableWebSearch: boolean;
69
+ enableWebFetch: boolean;
70
+ webfetch: {
71
+ blockPrivate: boolean;
72
+ allowlist: string[];
73
+ blocklist: string[];
74
+ maxBytes: number;
75
+ timeoutMs: number;
76
+ };
77
+ websearch: {
78
+ timeoutMs: number;
79
+ };
80
+ bash: {
81
+ timeoutMs: number;
82
+ outputLimitBytes: number;
83
+ };
84
+ grepMaxFileBytes: number;
85
+ plugin: {
86
+ dirs: string[];
87
+ maxBytes: number;
88
+ httpTimeoutMs: number;
89
+ };
90
+ toolSummary: {
91
+ enabled: boolean;
92
+ maxBytes: number;
93
+ maxOutputTokens: number;
94
+ tools: string[];
95
+ };
96
+ };
97
+ streaming: {
98
+ minIntervalMs: number;
99
+ minChars: number;
100
+ };
101
+ ipc: {
102
+ requestTimeoutMs: number;
103
+ requestPollMs: number;
104
+ };
105
+ tokenEstimate: {
106
+ tokensPerChar: number;
107
+ tokensPerMessage: number;
108
+ tokensPerRequest: number;
109
+ };
110
+ };
111
+ };
112
+
113
+ const CONFIG_PATH = '/workspace/config/runtime.json';
114
+ const DEFAULT_DEFAULT_MODEL = 'moonshotai/kimi-k2.5';
115
+ const DEFAULT_DAEMON_POLL_MS = 200;
116
+
117
+ const DEFAULT_AGENT_CONFIG: AgentRuntimeConfig['agent'] = {
118
+ assistantName: 'Rain',
119
+ openrouter: {
120
+ timeoutMs: 180_000,
121
+ retry: true,
122
+ siteUrl: '',
123
+ siteName: ''
124
+ },
125
+ promptPacks: {
126
+ enabled: true,
127
+ maxChars: 6000,
128
+ maxDemos: 4,
129
+ canaryRate: 0.1
130
+ },
131
+ context: {
132
+ maxContextTokens: 24_000,
133
+ compactionTriggerTokens: 20_000,
134
+ recentContextTokens: 8000,
135
+ summaryUpdateEveryMessages: 20,
136
+ maxOutputTokens: 1024,
137
+ summaryMaxOutputTokens: 600,
138
+ temperature: 0.2,
139
+ maxContextMessageTokens: 3000
140
+ },
141
+ memory: {
142
+ maxResults: 6,
143
+ maxTokens: 2000,
144
+ extraction: {
145
+ enabled: true,
146
+ async: true,
147
+ maxMessages: 4,
148
+ maxOutputTokens: 200
149
+ },
150
+ archiveSync: true,
151
+ extractScheduled: false
152
+ },
153
+ models: {
154
+ summary: 'openai/gpt-5-nano',
155
+ memory: 'openai/gpt-5-mini',
156
+ planner: 'openai/gpt-5-nano',
157
+ responseValidation: 'openai/gpt-5-nano',
158
+ toolSummary: 'openai/gpt-5-nano'
159
+ },
160
+ planner: {
161
+ enabled: true,
162
+ mode: 'auto',
163
+ minTokens: 600,
164
+ triggerRegex: '(plan|steps|roadmap|research|design|architecture|spec|strategy)',
165
+ maxOutputTokens: 200,
166
+ temperature: 0.2
167
+ },
168
+ responseValidation: {
169
+ enabled: true,
170
+ maxOutputTokens: 120,
171
+ temperature: 0,
172
+ maxRetries: 1,
173
+ allowToolCalls: false
174
+ },
175
+ tools: {
176
+ maxToolSteps: 24,
177
+ outputLimitBytes: 400_000,
178
+ enableBash: true,
179
+ enableWebSearch: true,
180
+ enableWebFetch: true,
181
+ webfetch: {
182
+ blockPrivate: true,
183
+ allowlist: [],
184
+ blocklist: ['localhost', '127.0.0.1'],
185
+ maxBytes: 300_000,
186
+ timeoutMs: 20_000
187
+ },
188
+ websearch: {
189
+ timeoutMs: 20_000
190
+ },
191
+ bash: {
192
+ timeoutMs: 120_000,
193
+ outputLimitBytes: 200_000
194
+ },
195
+ grepMaxFileBytes: 1_000_000,
196
+ plugin: {
197
+ dirs: [],
198
+ maxBytes: 800_000,
199
+ httpTimeoutMs: 20_000
200
+ },
201
+ toolSummary: {
202
+ enabled: true,
203
+ maxBytes: 60_000,
204
+ maxOutputTokens: 400,
205
+ tools: ['WebFetch']
206
+ }
207
+ },
208
+ streaming: {
209
+ minIntervalMs: 800,
210
+ minChars: 120
211
+ },
212
+ ipc: {
213
+ requestTimeoutMs: 6000,
214
+ requestPollMs: 150
215
+ },
216
+ tokenEstimate: {
217
+ tokensPerChar: 0.25,
218
+ tokensPerMessage: 3,
219
+ tokensPerRequest: 0
220
+ }
221
+ };
222
+
223
+ let cachedConfig: AgentRuntimeConfig | null = null;
224
+
225
+ function cloneConfig<T>(value: T): T {
226
+ return JSON.parse(JSON.stringify(value)) as T;
227
+ }
228
+
229
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
230
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
231
+ }
232
+
233
+ function mergeDefaults<T>(base: T, overrides: unknown): T {
234
+ if (!isPlainObject(overrides)) return cloneConfig(base);
235
+ const result = cloneConfig(base) as Record<string, unknown>;
236
+ const baseObj = base as Record<string, unknown>;
237
+ for (const [key, value] of Object.entries(overrides)) {
238
+ const current = baseObj[key];
239
+ if (isPlainObject(current) && isPlainObject(value)) {
240
+ result[key] = mergeDefaults(current, value);
241
+ continue;
242
+ }
243
+ if (Array.isArray(current) && Array.isArray(value)) {
244
+ result[key] = value;
245
+ continue;
246
+ }
247
+ if (typeof value === typeof current) {
248
+ result[key] = value as unknown;
249
+ }
250
+ }
251
+ return result as T;
252
+ }
253
+
254
+ function readJson(filePath: string): unknown {
255
+ try {
256
+ if (!fs.existsSync(filePath)) return null;
257
+ const raw = fs.readFileSync(filePath, 'utf-8');
258
+ if (!raw.trim()) return null;
259
+ return JSON.parse(raw);
260
+ } catch {
261
+ return null;
262
+ }
263
+ }
264
+
265
+ export function loadAgentConfig(): AgentRuntimeConfig {
266
+ if (cachedConfig) return cachedConfig;
267
+ const raw = readJson(CONFIG_PATH);
268
+
269
+ let defaultModel = DEFAULT_DEFAULT_MODEL;
270
+ let daemonPollMs = DEFAULT_DAEMON_POLL_MS;
271
+ let agentOverrides: unknown = null;
272
+
273
+ if (isPlainObject(raw)) {
274
+ const host = raw.host;
275
+ if (isPlainObject(host)) {
276
+ if (typeof host.defaultModel === 'string' && host.defaultModel.trim()) {
277
+ defaultModel = host.defaultModel.trim();
278
+ }
279
+ const container = host.container;
280
+ if (isPlainObject(container) && typeof container.daemonPollMs === 'number') {
281
+ daemonPollMs = container.daemonPollMs;
282
+ }
283
+ }
284
+ if (isPlainObject(raw.agent)) {
285
+ agentOverrides = raw.agent;
286
+ }
287
+ }
288
+
289
+ cachedConfig = {
290
+ defaultModel,
291
+ daemonPollMs,
292
+ agent: mergeDefaults(DEFAULT_AGENT_CONFIG, agentOverrides)
293
+ };
294
+ return cachedConfig;
295
+ }
@@ -0,0 +1,73 @@
1
+ export const OUTPUT_START_MARKER = '---DOTCLAW_OUTPUT_START---';
2
+ export const OUTPUT_END_MARKER = '---DOTCLAW_OUTPUT_END---';
3
+
4
+ export interface ContainerInput {
5
+ prompt: string;
6
+ sessionId?: string;
7
+ groupFolder: string;
8
+ chatJid: string;
9
+ isMain: boolean;
10
+ isScheduledTask?: boolean;
11
+ isBackgroundTask?: boolean;
12
+ taskId?: string;
13
+ userId?: string;
14
+ userName?: string;
15
+ memoryRecall?: string[];
16
+ userProfile?: string | null;
17
+ memoryStats?: {
18
+ total: number;
19
+ user: number;
20
+ group: number;
21
+ global: number;
22
+ };
23
+ tokenEstimate?: {
24
+ tokens_per_char: number;
25
+ tokens_per_message: number;
26
+ tokens_per_request: number;
27
+ };
28
+ toolReliability?: Array<{
29
+ name: string;
30
+ success_rate: number;
31
+ count: number;
32
+ avg_duration_ms: number | null;
33
+ }>;
34
+ behaviorConfig?: Record<string, unknown>;
35
+ toolPolicy?: Record<string, unknown>;
36
+ modelOverride?: string;
37
+ modelContextTokens?: number;
38
+ modelMaxOutputTokens?: number;
39
+ modelTemperature?: number;
40
+ streaming?: {
41
+ enabled?: boolean;
42
+ draftId?: number;
43
+ minIntervalMs?: number;
44
+ minChars?: number;
45
+ };
46
+ }
47
+
48
+ export interface ContainerOutput {
49
+ status: 'success' | 'error';
50
+ result: string | null;
51
+ newSessionId?: string;
52
+ error?: string;
53
+ model?: string;
54
+ prompt_pack_versions?: Record<string, string>;
55
+ memory_summary?: string;
56
+ memory_facts?: string[];
57
+ tokens_prompt?: number;
58
+ tokens_completion?: number;
59
+ memory_recall_count?: number;
60
+ session_recall_count?: number;
61
+ memory_items_upserted?: number;
62
+ memory_items_extracted?: number;
63
+ tool_calls?: Array<{
64
+ name: string;
65
+ args?: unknown;
66
+ ok: boolean;
67
+ duration_ms?: number;
68
+ error?: string;
69
+ output_bytes?: number;
70
+ output_truncated?: boolean;
71
+ }>;
72
+ latency_ms?: number;
73
+ }
@@ -0,0 +1,91 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { runAgentOnce } from './index.js';
4
+ import { loadAgentConfig } from './agent-config.js';
5
+ import type { ContainerInput } from './container-protocol.js';
6
+
7
+ const REQUESTS_DIR = '/workspace/ipc/agent_requests';
8
+ const RESPONSES_DIR = '/workspace/ipc/agent_responses';
9
+ const HEARTBEAT_FILE = '/workspace/ipc/heartbeat';
10
+ const POLL_MS = loadAgentConfig().daemonPollMs;
11
+
12
+ function log(message: string): void {
13
+ console.error(`[agent-daemon] ${message}`);
14
+ }
15
+
16
+ function ensureDirs(): void {
17
+ fs.mkdirSync(REQUESTS_DIR, { recursive: true });
18
+ fs.mkdirSync(RESPONSES_DIR, { recursive: true });
19
+ }
20
+
21
+ function writeHeartbeat(): void {
22
+ try {
23
+ fs.writeFileSync(HEARTBEAT_FILE, Date.now().toString());
24
+ } catch {
25
+ // Ignore heartbeat write errors
26
+ }
27
+ }
28
+
29
+ async function processRequests(): Promise<void> {
30
+ const files = fs.readdirSync(REQUESTS_DIR).filter(file => file.endsWith('.json'));
31
+ for (const file of files) {
32
+ const filePath = path.join(REQUESTS_DIR, file);
33
+ let requestId = file.replace('.json', '');
34
+ try {
35
+ const raw = fs.readFileSync(filePath, 'utf-8');
36
+ const payload = JSON.parse(raw) as { id?: string; input?: unknown };
37
+ requestId = payload.id || requestId;
38
+ const input = payload.input || payload;
39
+ if (!isContainerInput(input)) {
40
+ throw new Error('Invalid agent request payload');
41
+ }
42
+ const output = await runAgentOnce(input);
43
+ const responsePath = path.join(RESPONSES_DIR, `${requestId}.json`);
44
+ fs.writeFileSync(responsePath, JSON.stringify(output));
45
+ fs.unlinkSync(filePath);
46
+ } catch (err) {
47
+ log(`Failed processing request ${requestId}: ${err instanceof Error ? err.message : String(err)}`);
48
+ const responsePath = path.join(RESPONSES_DIR, `${requestId}.json`);
49
+ fs.writeFileSync(responsePath, JSON.stringify({
50
+ status: 'error',
51
+ result: null,
52
+ error: err instanceof Error ? err.message : String(err)
53
+ }));
54
+ try {
55
+ fs.unlinkSync(filePath);
56
+ } catch {
57
+ // ignore cleanup failure
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ function isContainerInput(value: unknown): value is ContainerInput {
64
+ if (!value || typeof value !== 'object') return false;
65
+ const record = value as Record<string, unknown>;
66
+ return typeof record.prompt === 'string'
67
+ && typeof record.groupFolder === 'string'
68
+ && typeof record.chatJid === 'string'
69
+ && typeof record.isMain === 'boolean';
70
+ }
71
+
72
+ async function loop(): Promise<void> {
73
+ ensureDirs();
74
+ log('Daemon started');
75
+ while (true) {
76
+ // Write heartbeat at the start of each loop iteration
77
+ writeHeartbeat();
78
+
79
+ try {
80
+ await processRequests();
81
+ } catch (err) {
82
+ log(`Daemon loop error: ${err instanceof Error ? err.message : String(err)}`);
83
+ }
84
+ await new Promise(resolve => setTimeout(resolve, POLL_MS));
85
+ }
86
+ }
87
+
88
+ loop().catch(err => {
89
+ log(`Daemon fatal error: ${err instanceof Error ? err.message : String(err)}`);
90
+ process.exit(1);
91
+ });